Compare commits

...

106 Commits

Author SHA1 Message Date
Brian J
ff3489a479 MSA-753: Sets mode to home when somebody is home, away when everyone is gone and home / night (optionally) depending on sunrise / sunset. 2015-12-19 15:05:36 -06:00
Vinay Rao
8c3daf5f34 Merge pull request #372 from workingmonk/lock_comment_out
[DVCSMP-1338] commenting out unused variables
2015-12-16 11:38:06 +05:30
Vinay Rao
f3138501e8 commenting out unused variables 2015-12-16 11:34:35 +05:30
Yaima
2dfb43f841 Merge pull request #371 from Yaima/master
Comment out TTS Capability from Speaker SmartApps
2015-12-16 00:24:13 -05:00
Yaima
f9b5e2ba79 Comment out TTS Capability from Speaker SmartApps
https://smartthings.atlassian.net/browse/DVCSMP-1258
2015-12-16 00:22:06 -05:00
Warodom Khamphanchai
1f86001418 Merge pull request #369 from snakedog116/master
Ecobee Update
2015-12-15 10:51:03 -08:00
Kristofer Schaller
e4b010eb46 Merge pull request #367 from rappleg/AddEventBufferBackToAtomicState
Make sure we persist the updated eventBuffer with the new event added back to atomicState
2015-12-15 09:37:25 -08:00
snakedog116
c921121645 Update ecobee-connect.groovy
Replace the licensing information as previously deleted by 1b9d2fe @kwarodom. I believe this was removed in error. The original code was under Apache, so the resulting code should be too.
2015-12-14 23:44:14 -07:00
snakedog116
9be808a3eb Update ecobee-thermostat.groovy
Added some description to the temporary hold.
2015-12-14 23:36:55 -07:00
rappleg
6778f8d58a Make sure we persist the updated eventBuffer with the new event added back to atomicState 2015-12-14 19:50:39 -06:00
Kristofer Schaller
961ee321ac Merge pull request #365 from rappleg/PreventEventBufferInitialStateGrowingUnbounded
Improve performance when streaming to Inital State and prevent buffer growing unbounded
2015-12-14 17:24:46 -08:00
Kristofer Schaller
22fb4e36c6 Merge pull request #216 from SmartThingsCommunity/MSA-640-3
Merged publication request 'PlantLink'
2015-12-14 17:04:38 -08:00
rappleg
18bfa87948 Improve performance when streaming to Inital State and prevent buffer growing unbounded 2015-12-14 18:40:55 -06:00
Steve Vlaminck
7fc4aaa341 Merge pull request #366 from vlaminck/every-element-update
Updated the every element app to better represent what each element can and can't do
2015-12-14 13:57:02 -06:00
vlaminck
d1504e9a3c Updated the ever element app to better represent what each element ca… 2015-12-14 13:27:41 -06:00
Tom Manley
91fefe536d Merge pull request #355 from tpmanley/bugfix/wrong_xyz_values
multi: Fix invalid accelerometer readings during refresh
2015-12-14 12:01:36 -06:00
Tom Manley
fe4a2ed3d0 multi: Fix invalid accelerometer readings during refresh
The code can only correctly handle the accelerometer readings when they
are all received in the same message. Individual reads of the attributes
results in individual read attribute responses which led to events with
incorrect values. The most straightforward fix is to not read the attributes
in the first place and rely on attribute reporting to report the values
at the appropriate time.

Resolves:
https://smartthings.atlassian.net/browse/DVCSMP-1315
2015-12-11 00:24:44 -06:00
Tom Manley
e529624d36 Merge pull request #349 from tpmanley/feature/updated_smartsense_devices
Added fingerprints/support for new version of multi/moisture/motion sensors
2015-12-11 00:23:21 -06:00
Tom Manley
6ba37caa03 Added fingerprints/support for new version of multi/moisture/motion sensors
Partially resolves:
https://smartthings.atlassian.net/browse/DVCSMP-1305
2015-12-10 22:50:15 -06:00
Yaima
8d0fa7f561 Merge pull request #351 from SmartThingsCommunity/DEVC-256-11
DEVC-256: DEVC-256 Bose SoundTouch Control SmartApp - Approval
2015-12-10 15:03:33 -08:00
Donald C. Kirker
6ede225715 DEVC-256: Bose SmartApp - Approval 2015-12-10 16:47:28 -06:00
Yaima
30844676b6 Merge pull request #342 from Yaima/master
Display issue with Wattvision integration
2015-12-08 15:27:44 -08:00
Yaima Valdivia
94aa114ccb Display issue with Wattvision integration
https://smartthings.atlassian.net/browse/DVCSMP-1300
2015-12-08 15:00:35 -08:00
Yaima
a9e68d086c Merge pull request #340 from Yaima/master
Added both() command
2015-12-08 14:56:07 -08:00
Warodom Khamphanchai
d427ab8709 Merge pull request #339 from kwarodom/ecobee
Ecobee: retries to get refresh tokens if exceptions are caught
2015-12-07 15:23:40 -08:00
Yaima Valdivia
a205a94d78 Added both() command
https://smartthings.atlassian.net/browse/DVCSMP-1155
2015-12-07 14:37:10 -08:00
Warodom Khamphanchai
e775752496 Ecobee: retries to get refresh tokens if exceptions are caught in refreshAuthToken 2015-12-07 12:50:29 -08:00
bflorian
9318e9a311 Merge pull request #338 from bflorian/SSQA-65-internation-phone-text
SSQA-65, add text about international phone numbers
2015-12-05 10:25:02 -05:00
bflorian
e1c52454c6 SSQA-65, add text about international phone numbers 2015-12-05 10:24:15 -05:00
Warodom Khamphanchai
145fce2062 Merge pull request #337 from kwarodom/ecobee
Ecobee: round temperature value, add ability to set temperature setpoint temporarily or permanently
2015-12-04 17:07:47 -08:00
Warodom Khamphanchai
e9996b9fd7 Ecobee: round temperature value, add ability to set temperature setpoint temporarily or permanently 2015-12-04 16:16:24 -08:00
Warodom Khamphanchai
8be585e544 Merge pull request #231 from kwarodom/ecobee
DVCSMP-1174 - Fix Ecobee not respond to routines, DVCSMP-604 - Add accessory sensor
2015-12-04 09:22:13 -08:00
Tom Manley
e114fafd56 Merge pull request #308 from tpmanley/bug/threeAxis-events
Generate both Three Axis and Acceleration events when included in same msg
2015-12-03 20:58:21 -06:00
Warodom Khamphanchai
e6367a7832 Fix Ecobee access_token doesn't get refresh after it's expired 2015-12-02 17:37:59 -08:00
Warodom Khamphanchai
c5da3fe4a0 DVCSMP-535
- Fix activity feed displays unformatted text
- update oauth/callback url, use getApiServerUrl() for proxying to corresponding shard
2015-12-02 17:37:59 -08:00
Warodom Khamphanchai
1b9d2fe9ce DVCSMP-1174
FIX - Ecobee - Thermostat isn't responding to routines
2015-12-02 17:37:59 -08:00
Duncan McKee
90dee51255 Merge pull request #297 from mckeed/master
Z-Wave Lock: fix Security Exception DVCSMP-1265 DVCSMP-1059
2015-12-02 12:11:11 -06:00
Yaima
7b7fdd43cd Merge pull request #326 from Yaima/master
Implemented toggle() for locks
2015-12-01 10:52:29 -08:00
Yaima Valdivia
9059718818 Implemented toggle() for locks
https://smartthings.atlassian.net/browse/DVCSMP-672
2015-12-01 10:48:15 -08:00
Yaima
9d7c66c7af Merge pull request #310 from Yaima/master
Device Details > "Which?" Label needs to be removed
2015-11-24 14:02:31 -08:00
Yaima Valdivia
8e81967227 Device Details > "Which?" Label needs to be removed
https://smartthings.atlassian.net/browse/DVCSMP-1250
2015-11-24 14:01:33 -08:00
Tom Manley
290e8e4129 The x,y,z attributes are often sent in a separate Attribute Report from the
accelerometer attribute. Sometimes, however, they are all sent in the same
Attribute Report. When that happens, only the accelerometer attribute was
being handled and the x,y,z attributes were not. Now they are all handled
if they arrive in the same message.

Resolves:
https://smartthings.atlassian.net/browse/DVCSMP-1271
2015-11-24 14:03:29 -06:00
Yaima
8c6c68f102 Merge pull request #303 from Yaima/master
WeMo uninstall not available
2015-11-23 11:26:59 -08:00
Yaima Valdivia
661f8b3bc0 WeMo uninstall not available
https://smartthings.atlassian.net/browse/DVCSMP-1208
2015-11-23 11:22:57 -08:00
Mike Robinet
a62d825f69 Merge pull request #289 from mrobinet/dvcsmp-1262
DVCSMP-1262 Fix reference to Life360 User child device type
2015-11-20 13:52:29 -06:00
Vinay Rao
1890147221 Merge pull request #292 from workingmonk/fingerprint_changes
fingerprint spacing consistency, additional fingerprint color
2015-11-19 08:44:01 -08:00
Vinay Rao
2fb5f8c78c fingerprint spacing consistency, additional fingerprint color
removing sensor from zigbee white color temp, switch and dimmer
2015-11-19 08:41:44 -08:00
Vinay Rao
b44356248c Merge pull request #299 from juano2310/Harmony_Global
Updated - Device Type name
2015-11-19 06:28:00 -08:00
Juan Pablo Risso
c30af84d70 Updated - Device Type name 2015-11-19 09:23:08 -05:00
Yaima Valdivia
5cf72c644c Rename Sonos SmartApps
https://smartthings.atlassian.net/browse/DVCSMP-607
2015-11-18 14:00:57 -06:00
Duncan McKee
8ae9b06022 Z-Wave Lock: fix double updated() commands DVCSMP-1265 2015-11-17 18:08:44 -05:00
Warodom Khamphanchai
5e93bca030 LiFX - change shardUrl param to apiServerUrl 2015-11-16 16:33:07 -08:00
Duncan McKee
71fc8e7f5f Fix Z-Wave Lock SecurityException DVCSMP-1265 2015-11-16 18:52:08 -05:00
Mike Robinet
30dedde0df DVCSMP-1262 Fix reference to Life360 User child device type 2015-11-16 11:49:23 -06:00
Vinay Rao
8b6942525d Merge pull request #280 from workingmonk/lock_raw_command
temporary changes to the lock DTH
2015-11-13 15:18:22 -08:00
Vinay Rao
1a8eab065b Merge pull request #279 from workingmonk/cree_new_api
[DEVC-226] Cree DTH refresh
2015-11-13 12:35:01 -08:00
Vinay Rao
74e3e9f70d Merge pull request #278 from workingmonk/recertified_devices
[DEVC-231] Moving re-certified device to generic zigbee DTH.
2015-11-12 20:41:31 -08:00
Vinay Rao
738d41f5af Merge pull request #276 from workingmonk/osram_new_bulbs
[DEVC-224] Osram color temperature
2015-11-12 20:30:34 -08:00
Vinay Rao
cf69d6127d temporary changes to the lock DTH to account for the upcoming zigbee library changes 2015-11-12 20:08:03 -08:00
Vinay Rao
ef4a32ac2e Cree DTH refresh 2015-11-12 19:28:57 -08:00
Vinay Rao
ac7592d132 Moving re-certified device to generic zigbee DTH. 2015-11-12 19:18:32 -08:00
Vinay Rao
a0a3cd7cf9 Merge pull request #247 from workingmonk/yale_lock
New fingerprints for Yale lock and fixing the battery issue
2015-11-12 19:07:20 -08:00
Vinay Rao
14399f8fc8 Merge pull request #277 from workingmonk/new_bulbs
[DEVC-242] Adding fingerprints for osram and sengled dimmable bulbs
2015-11-12 19:00:55 -08:00
Vinay Rao
1f76b9e960 adding fingerprints for osram and sengled dimmable bulbs 2015-11-12 17:53:21 -08:00
Vinay Rao
5d1630b367 Adding fingerprint for new bulbs and transition old color temperature osram bulbs to new one 2015-11-12 17:31:45 -08:00
Vinay Rao
0af48a5caf new fingerprints for Yale. battery issue resolution for Yale 2015-11-12 17:06:07 -08:00
Juan Pablo Risso
a5aeeed77f Merge pull request #267 from juano2310/HUE_NEW
Reverted to previously working getBridgeIP()
2015-11-09 16:30:35 -05:00
Juan Pablo Risso
0445b415f7 Reverted to previously working getBridgeIP() 2015-11-09 15:55:32 -05:00
Juan Pablo Risso
303219fcc5 Merge pull request #266 from juano2310/Harmony_Global
Replaced atomicState with State
2015-11-09 14:27:46 -05:00
Juan Pablo Risso
4e88a3ac60 Replaced atomicState with State 2015-11-09 12:56:38 -05:00
Juan Pablo Risso
9b8be908e2 Merge pull request #261 from juano2310/Harmony_Global
Harmony Global Oauth
2015-11-05 16:03:09 -05:00
Juan Pablo Risso
ea65ed58dc Added a "," 2015-11-05 14:24:15 -05:00
Juan Pablo Risso
d19ec0b525 removed function ShardUrl() 2015-11-05 14:19:20 -05:00
Juan Pablo Risso
9654c27ca8 added singleInstance: true 2015-11-05 14:15:50 -05:00
juano2310
dec9ff20b0 Harmony Global Oauth 2015-11-05 14:11:38 -05:00
Mike Robinet
c62fd52a26 Merge pull request #253 from mrobinet/crex-1094
CREX-1094 Delete stale device subscriptions on IFTTT app update
2015-11-05 11:28:43 -06:00
Juan Pablo Risso
1195161232 Merge pull request #251 from juano2310/harmony
Jawbone Global Oauth
2015-11-05 10:59:24 -05:00
Juan Pablo Risso
5f1ff8a5c6 Code Cleanup 2015-11-05 10:07:19 -05:00
bflorian
7aabd9bc5f Deleted lights on when door opens after sundown 2015-11-04 14:44:06 -08:00
bflorian
f260e36d54 More name/namespace changes 2015-11-04 14:09:51 -08:00
bflorian
594b61c852 Merge branch 'master' of https://github.com/SmartThingsCommunity/SmartThingsPublic 2015-11-04 13:59:38 -08:00
bflorian
c416560f19 Misc filename and namespace changes. 2015-11-04 13:57:17 -08:00
Juan Pablo Risso
1ee768016f Merge pull request #252 from juano2310/HUE_NEW
Force Level = 1% to 1
2015-11-04 16:11:58 -05:00
Mike Robinet
54e5334cca CREX-1094 Delete stale device subscriptions on IFTTT app update 2015-11-04 14:38:28 -06:00
Juan Pablo Risso
8c55b6314a Force Level = 1% to 1
This ensures that the bulb will be able to dim to it minimum
2015-11-04 15:07:26 -05:00
juano2310
ac5f15efd8 buildActionUrl("hookCallback") 2015-11-04 14:18:46 -05:00
juano2310
bbc680746e Jawbone Global Oauth 2015-11-04 13:05:49 -05:00
bflorian
702f0f87d0 Merge branch 'master' into staging 2015-11-04 09:41:38 -08:00
bflorian
d830c1fae0 Filename corrections 2015-11-03 18:08:51 -08:00
bflorian
f337e8a085 Corrected filename of Z-Wave Device Multichannel 2015-11-03 17:55:11 -08:00
Tom Manley
8f6201507a Merge pull request #246 from tpmanley/feature/lock-battery-fix
Fix 'Low Battery Handler' exception caused by non-integer battery events
2015-11-03 16:47:30 -06:00
Matt Nohr
0171644a77 Merge branch 'master' into staging 2015-11-02 15:45:08 -06:00
Matthew Nohr
58187006b7 Merge pull request #136 from mrobinet/master
CREX-3129 Update service manager apps to be singleton
2015-11-02 15:44:36 -06:00
Donald C. Kirker
5e6bfb5857 Merge pull request #214 from keenhome/update-keen-home-smart-vent-handler
update keen home smart vent device handler
2015-11-02 10:03:18 -08:00
Tom Manley
2d22b5a384 Fix 'Low Battery Handler' exception caused by non-integer battery events
ZigBee locks report battery percentage remaining in .5% increments. However
the Low Battery Handler Smart App in Hello Home expects it to be an integer.
2015-11-02 09:51:03 -06:00
Juan Pablo Risso
10dee44c0a Merge pull request #240 from SmartThingsCommunity/master
Master -> Staging
2015-10-31 00:21:46 -04:00
Juan Pablo Risso
20e5b51aef Merge pull request #239 from juano2310/WEMO_FFinal4
DVCSMP-1189: Wemo refactor final
2015-10-31 00:14:03 -04:00
juano2310
20f1a76889 Wemo refactor final (DVCSMP-1189)
https://smartthings.atlassian.net/browse/DVCSMP-1189

Detect and mark device offline within 5 minutes.
Show Device offline in device tile.
Show Device offline in Recent Activity.
Log the current IP address to Recent Activity.
Log the changed IP address to Recent Activity.
Support 'Turning on' and 'Turning off' (blindly changing the state of
device to ON or OFF without confirming bulb responded correctly)
Turn on / off through Wemo-App reflected timely in SmartThings
App/Ecosystem.
Manual turn on / off of device is reflected timely in SmartThings
App/Ecosystem.

Lower case createEvent

Bug Fixes

Bug fixes

setOffline

Minor cosmetic fixes
2015-10-30 21:56:01 -04:00
Juan Pablo Risso
6db15b12c1 Merge pull request #230 from juano2310/hue_ff
Fix DVCSMP-1227 & DVCSMP-1232
2015-10-30 15:08:26 -04:00
Warodom Khamphanchai
aa93850c62 Merge pull request #209 from kwarodom/aeonMultiSensor6
DVCSMP-668: Aeon Multi Sensor 6
2015-10-28 20:37:36 -07:00
Juan Pablo Risso
47210ca8b4 Fix to Hue reverts dimmer settings (DVCSMP-1227)
if you use the hue native app to adjust the dimmer setting, smartthings will reset the dimmer to previous value when toggling from ST app (and automations)
2015-10-28 14:12:35 -04:00
Mike Cousins
cbd5c91d52 update keen home smart vent device handler 2015-10-28 14:07:09 -04:00
Oso Technologies
dcfc16cf12 MSA-640: Updated version of the deviceType and SmartApp that now uses % moisture instead of fuel %. Improved server api performance. Previously worked with Donald Kirker and Shilpa Mathew.
Needed vars for testing:
client_id = 6479182133460992
clident_secret = a0c318b6-042f-4a91-8f56-654a6cc37c9a
https_plantLinkServer = https://hardware-dot-oso-tech.appspot.com
2015-10-23 10:01:19 -05:00
Warodom Khamphanchai
794ff6b68a DVCSMP-668
- Show batteryStatus tile insteady of battery tile to be able to display both when sensor is USB powered or battery powered
- Remove background for illuminance. This can be added when we have best practice of showing colors for lux.
- Instead of using powerSupply:failed, configurationGet cmd is sent and then the configure() is triggered by configurationReport to determine powerSupply (USB Cable/Battery)
- Instead of querying battery level on wake up, battery report is put in association group 2 that is configured to report every 6 hours by default
- Update configure() to  send both unsecure and secure configuration commands when sensor is joined normally or securely
2015-10-21 15:48:31 -07:00
Warodom Khamphanchai
ef21fd4257 DVCSMP-668
The following changes has been made to the original Aeon Multisensor device type handler to improve and modernize it:
1. Add "powerSupply" attribute to be able to tell power source (USB Cable/Battery)
2. Add preference page for user to customize "motion delay time", "motion sensitivity", and "sensor report interval"
3. Add color backgroud of "illuminance" value tile
4. Add tile for "ultravioletIndex"
5. Add tile for "powerSupply"
6. Modify updated() to be able to send configuration commands to sensor whether it is powered by USB cable or battery
7. When battery operated, send command to get update battery level if it hasn't been reported for a while
8. Report MSR of the sensor
9. Add handle for ConfigurationReport command class to update the "powerSupply" tile and change opetion mode of the sensor accordingly
10. Update configure() to configure parameters changed by user in preference page
11. Take out the "Configure" tile and instead send the configuration commands on every wakeup (sensor is battey powered)
2015-10-21 15:48:31 -07:00
Mike Robinet
8ba5eaf74d CREX-3129 Update parent and service manager apps to be singleton 2015-09-24 15:51:51 -05:00
77 changed files with 4164 additions and 2713 deletions

View File

@@ -1,12 +1,9 @@
/**
* Keen Home Smart Vent
*
* Author: Keen Home
* Date: 2015-06-23
*/
// keen home smart vent
// http://www.keenhome.io
// SmartThings Device Handler v1.0.0
metadata {
definition (name: "Keen Home Smart Vent", namespace: "Keen Home", author: "Gregg Altschul") {
definition (name: "Keen Home Smart Vent", namespace: "Keen Home", author: "Keen Home") {
capability "Switch Level"
capability "Switch"
capability "Configuration"
@@ -21,6 +18,7 @@ metadata {
command "getBattery"
command "getTemperature"
command "setZigBeeIdTile"
command "clearObstruction"
fingerprint endpoint: "1",
profileId: "0104",
@@ -42,9 +40,10 @@ metadata {
// UI tile definitions
tiles {
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
state "on", action:"switch.off", icon:"st.vents.vent-open-text", backgroundColor:"#53a7c0"
state "off", action:"switch.on", icon:"st.vents.vent-closed", backgroundColor:"#ffffff"
state "obstructed", action: "switch.off", icon:"st.vents.vent-closed", backgroundColor:"#ff0000"
state "on", action: "switch.off", icon: "st.vents.vent-open-text", backgroundColor: "#53a7c0"
state "off", action: "switch.on", icon: "st.vents.vent-closed", backgroundColor: "#ffffff"
state "obstructed", action: "clearObstruction", icon: "st.vents.vent-closed", backgroundColor: "#ff0000"
state "clearing", action: "", icon: "st.vents.vent-closed", backgroundColor: "#ffff33"
}
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false) {
state "level", action:"switch level.setLevel"
@@ -206,12 +205,12 @@ private Map makeOnOffResult(rawValue) {
private Map makeLevelResult(rawValue) {
def linkText = getLinkText(device)
// log.debug "rawValue: ${rawValue}"
def value = Integer.parseInt(rawValue, 16)
def rangeMax = 254
// catch obstruction level
if (value == 255) {
log.debug "obstructed"
log.debug "${linkText} is obstructed"
// Just return here. Once the vent is power cycled
// it will go back to the previous level before obstruction.
// Therefore, no need to update level on the display.
@@ -220,24 +219,9 @@ private Map makeLevelResult(rawValue) {
value: "obstructed",
descriptionText: "${linkText} is obstructed. Please power cycle."
]
} else if ( device.currentValue("switch") == "obstructed" &&
value == 254) {
// When the device is reset after an obstruction, the switch
// state will be obstructed and the value coming from the device
// will be 254. Since we're not using heating/cooling mode from
// the device type handler, we need to bump it down to the lower
// (cooling) range
sendEvent(makeOnOffResult(1)) // clear the obstructed switch state
value = rangeMax
}
// else if (device.currentValue("switch") == "off") {
// sendEvent(makeOnOffResult(1)) // turn back on if in off state
// }
// log.debug "pre-value: ${value}"
value = Math.floor(value / rangeMax * 100)
// log.debug "post-value: ${value}"
return [
name: "level",
@@ -327,35 +311,79 @@ private def makeSerialResult(serial) {
value: serial,
descriptionText: "${linkText} has serial ${serial}" ]
}
/**** COMMAND METHODS ****/
// def mfgCode() {
// ["zcl mfg-code 0x115B", "delay 200"]
// }
// takes a level from 0 to 100 and translates it to a ZigBee move to level with on/off command
private def makeLevelCommand(level) {
def rangeMax = 254
def scaledLevel = Math.round(level * rangeMax / 100)
log.debug "scaled level for ${level}%: ${scaledLevel}"
// convert to hex string and pad to two digits
def hexLevel = new BigInteger(scaledLevel.toString()).toString(16).padLeft(2, '0')
"st cmd 0x${device.deviceNetworkId} 1 8 4 {${hexLevel} 0000}"
}
/**** COMMAND METHODS ****/
def on() {
log.debug "on()"
def linkText = getLinkText(device)
log.debug "open ${linkText}"
// only change the state if the vent is not obstructed
if (device.currentValue("switch") == "obstructed") {
log.error("cannot open because ${linkText} is obstructed")
return
}
sendEvent(makeOnOffResult(1))
"st cmd 0x${device.deviceNetworkId} 1 6 1 {}"
}
def off() {
log.debug "off()"
def linkText = getLinkText(device)
log.debug "close ${linkText}"
// only change the state if the vent is not obstructed
if (device.currentValue("switch") == "obstructed") {
log.error("cannot close because ${linkText} is obstructed")
return
}
sendEvent(makeOnOffResult(0))
"st cmd 0x${device.deviceNetworkId} 1 6 0 {}"
}
// does this work?
def toggle() {
log.debug "toggle()"
def clearObstruction() {
def linkText = getLinkText(device)
log.debug "attempting to clear ${linkText} obstruction"
"st cmd 0x${device.deviceNetworkId} 1 6 2 {}"
sendEvent([
name: "switch",
value: "clearing",
descriptionText: "${linkText} is clearing obstruction"
])
// send a move command to ensure level attribute gets reset for old, buggy firmware
// then send a reset to factory defaults
// finally re-configure to ensure reports and binding is still properly set after the rtfd
[
makeLevelCommand(device.currentValue("level")), "delay 500",
"st cmd 0x${device.deviceNetworkId} 1 0 0 {}", "delay 5000"
] + configure()
}
def setLevel(value) {
log.debug "setting level: ${value}"
def linkText = getLinkText(device)
// only change the level if the vent is not obstructed
def currentState = device.currentValue("switch")
if (currentState == "obstructed") {
log.error("cannot set level because ${linkText} is obstructed")
return
}
sendEvent(name: "level", value: value)
if (value > 0) {
sendEvent(name: "switch", value: "on", descriptionText: "${linkText} is on by setting a level")
@@ -363,29 +391,26 @@ def setLevel(value) {
else {
sendEvent(name: "switch", value: "off", descriptionText: "${linkText} is off by setting level to 0")
}
def rangeMax = 254
def computedLevel = Math.round(value * rangeMax / 100)
log.debug "computedLevel: ${computedLevel}"
def level = new BigInteger(computedLevel.toString()).toString(16)
log.debug "level: ${level}"
if (level.size() < 2){
level = '0' + level
}
"st cmd 0x${device.deviceNetworkId} 1 8 4 {${level} 0000}"
makeLevelCommand(value)
}
def getOnOff() {
log.debug "getOnOff()"
// disallow on/off updates while vent is obstructed
if (device.currentValue("switch") == "obstructed") {
log.error("cannot update open/close status because ${getLinkText(device)} is obstructed")
return []
}
["st rattr 0x${device.deviceNetworkId} 1 0x0006 0"]
}
def getPressure() {
log.debug "getPressure()"
// using a Keen Home specific attribute in the pressure measurement cluster
[
"zcl mfg-code 0x115B", "delay 200",
"zcl global read 0x0403 0x20", "delay 200",
@@ -395,12 +420,13 @@ def getPressure() {
def getLevel() {
log.debug "getLevel()"
// rattr = read attribute
// 0x${} = device net id
// 1 = endpoint
// 8 = cluster id (level control, in this case)
// 0 = attribute within cluster
// sendEvent(name: "level", value: value)
// disallow level updates while vent is obstructed
if (device.currentValue("switch") == "obstructed") {
log.error("cannot update level status because ${getLinkText(device)} is obstructed")
return []
}
["st rattr 0x${device.deviceNetworkId} 1 0x0008 0x0000"]
}
@@ -425,78 +451,59 @@ def setZigBeeIdTile() {
name: "zigbeeId",
value: device.zigbeeId,
descriptionText: "${linkText} has zigbeeId ${device.zigbeeId}" ])
return [
return [
name: "zigbeeId",
value: device.zigbeeId,
descriptionText: "${linkText} has zigbeeId ${device.zigbeeId}" ]
}
def refresh() {
getOnOff() +
getOnOff() +
getLevel() +
getTemperature() +
getPressure() +
getBattery()
}
private byte[] reverseArray(byte[] array) {
int i = 0;
int j = array.length - 1;
byte tmp;
while (j > i) {
tmp = array[j];
array[j] = array[i];
array[i] = tmp;
j--;
i++;
}
return array
}
private String swapEndianHex(String hex) {
reverseArray(hex.decodeHex()).encodeHex()
}
def configure() {
log.debug "CONFIGURE"
log.debug "zigbeeId: ${device.hub.zigbeeId}"
// get ZigBee ID by hidden tile because that's the only way we can do it
setZigBeeIdTile()
def configCmds = [
// binding commands
// bind reporting clusters to hub
"zdo bind 0x${device.deviceNetworkId} 1 1 0x0006 {${device.zigbeeId}} {}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} 1 1 0x0008 {${device.zigbeeId}} {}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} 1 1 0x0402 {${device.zigbeeId}} {}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} 1 1 0x0403 {${device.zigbeeId}} {}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} 1 1 0x0001 {${device.zigbeeId}} {}", "delay 500",
// configure report commands
// [cluster] [attr] [type] [min-interval] [max-interval] [min-change]
"zdo bind 0x${device.deviceNetworkId} 1 1 0x0001 {${device.zigbeeId}} {}", "delay 500"
// mike 2015/06/22: preconfigured; see tech spec
// configure report commands
// zcl global send-me-a-report [cluster] [attr] [type] [min-interval] [max-interval] [min-change]
// report with these parameters is preconfigured in firmware, can be overridden here
// vent on/off state - type: boolean, change: 1
// "zcl global send-me-a-report 6 0 0x10 5 60 {01}", "delay 200",
// "send 0x${device.deviceNetworkId} 1 1", "delay 1500",
// mike 2015/06/22: preconfigured; see tech spec
// report with these parameters is preconfigured in firmware, can be overridden here
// vent level - type: int8u, change: 1
// "zcl global send-me-a-report 8 0 0x20 5 60 {01}", "delay 200",
// "send 0x${device.deviceNetworkId} 1 1", "delay 1500",
// mike 2015/06/22: temp and pressure reports are preconfigured, but
// we'd like to override their settings for our own purposes
// report with these parameters is preconfigured in firmware, can be overridden here
// temperature - type: int16s, change: 0xA = 10 = 0.1C
"zcl global send-me-a-report 0x0402 0 0x29 10 60 {0A00}", "delay 200",
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
// "zcl global send-me-a-report 0x0402 0 0x29 60 60 {0A00}", "delay 200",
// "send 0x${device.deviceNetworkId} 1 1", "delay 1500",
// mike 2015/06/22: use new custom pressure attribute
// pressure - type: int32u, change: 1 = 0.1Pa
"zcl mfg-code 0x115B", "delay 200",
"zcl global send-me-a-report 0x0403 0x20 0x22 10 60 {010000}", "delay 200",
"send 0x${device.deviceNetworkId} 1 1", "delay 1500"
// report with these parameters is preconfigured in firmware, can be overridden here
// keen home custom pressure (tenths of Pascals) - type: int32u, change: 1 = 0.1Pa
// "zcl mfg-code 0x115B", "delay 200",
// "zcl global send-me-a-report 0x0403 0x20 0x22 60 60 {010000}", "delay 200",
// "send 0x${device.deviceNetworkId} 1 1", "delay 1500",
// mike 2015/06/22: preconfigured; see tech spec
// report with these parameters is preconfigured in firmware, can be overridden here
// battery - type: int8u, change: 1
// "zcl global send-me-a-report 1 0x21 0x20 60 3600 {01}", "delay 200",
// "send 0x${device.deviceNetworkId} 1 1", "delay 1500",

View File

@@ -0,0 +1,163 @@
/**
* PlantLink
*
* This device type takes sensor data and converts it into a json packet to send to myplantlink.com
* where its values will be computed for soil and plant type to show user readable values of how your
* specific plant is doing.
*
*
* Copyright 2015 Oso Technologies
*
* 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.
*
*/
import groovy.json.JsonBuilder
metadata {
definition (name: "PlantLink", namespace: "OsoTech", author: "Oso Technologies") {
capability "Sensor"
command "setStatusIcon"
command "setPlantFuelLevel"
command "setBatteryLevel"
command "setInstallSmartApp"
attribute "plantStatus","string"
attribute "plantFuelLevel","number"
attribute "linkBatteryLevel","string"
attribute "installSmartApp","string"
fingerprint profileId: "0104", inClusters: "0000,0001,0B04"
}
simulator {
status "battery": "read attr - raw: C0720100010A000021340A, dni: C072, endpoint: 01, cluster: 0001, size: 0A, attrId: 0000, encoding: 21, value: 0a34"
status "moisture": "read attr - raw: C072010B040A0001290000, dni: C072, endpoint: 01, cluster: 0B04, size: 0A, attrId: 0100, encoding: 29, value: 0000"
}
tiles {
standardTile("Title", "device.label") {
state("label", label:'PlantLink ${device.label}')
}
valueTile("plantMoistureTile", "device.plantFuelLevel", width: 1, height: 1) {
state("plantMoisture", label: '${currentValue}% Moisture')
}
valueTile("plantStatusTextTile", "device.plantStatus", decoration: "flat", width: 2, height: 2) {
state("plantStatusTextTile", label:'${currentValue}')
}
valueTile("battery", "device.linkBatteryLevel" ) {
state("battery", label:'${currentValue}% battery')
}
valueTile("installSmartApp","device.installSmartApp", decoration: "flat", width: 3, height: 1) {
state "needSmartApp", label:'Please install SmartApp "Required PlantLink Connector"', defaultState:true
state "connectedToSmartApp", label:'Connected to myplantlink.com'
}
main "plantStatusTextTile"
details(['plantStatusTextTile', "plantMoistureTile", "battery", "installSmartApp"])
}
}
def setStatusIcon(value){
def status = ''
switch (value) {
case '0':
status = 'Needs Water'
break
case '1':
status = 'Dry'
break
case '2':
case '3':
status = 'Good'
break
case '4':
status = 'Too Wet'
break
case 'No Soil':
status = 'Too Dry'
setPlantFuelLevel(0)
break
case 'Recently Watered':
status = 'Watered'
setPlantFuelLevel(100)
break
case 'Low Battery':
status = 'Low Battery'
break
case 'Waiting on First Measurement':
status = 'Calibrating'
break
default:
status = "?"
break
}
sendEvent("name":"plantStatus", "value":status, "description":statusText, displayed: true, isStateChange: true)
}
def setPlantFuelLevel(value){
sendEvent("name":"plantFuelLevel", "value":value, "description":statusText, displayed: true, isStateChange: true)
}
def setBatteryLevel(value){
sendEvent("name":"linkBatteryLevel", "value":value, "description":statusText, displayed: true, isStateChange: true)
}
def setInstallSmartApp(value){
sendEvent("name":"installSmartApp", "value":value)
}
def parse(String description) {
def description_map = parseDescriptionAsMap(description)
def event_name = ""
def measurement_map = [
type: "link",
signal: "0x00",
zigbeedeviceid: device.zigbeeId,
created: new Date().time /1000 as int
]
if (description_map.cluster == "0000"){
/* version number, not used */
} else if (description_map.cluster == "0001"){
/* battery voltage in mV (device needs minimium 2.1v to run) */
log.debug "PlantLink - id ${device.zigbeeId} battery ${description_map.value}"
event_name = "battery_status"
measurement_map["battery"] = "0x${description_map.value}"
} else if (description_map.cluster == "0B04"){
/* raw moisture reading (needs to be sent to plantlink for soil/plant type conversion) */
log.debug "PlantLink - id ${device.zigbeeId} raw moisture ${description_map.value}"
measurement_map["moisture"] = "0x${description_map.value}"
event_name = "moisture_status"
} else{
log.debug "PlantLink - id ${device.zigbeeId} Unknown '${description}'"
return
}
def json_builder = new JsonBuilder(measurement_map)
def result = createEvent(name: event_name, value: json_builder.toString())
return result
}
def parseDescriptionAsMap(description) {
(description - "read attr - ").split(",").inject([:]) { map, param ->
def nameAndValue = param.split(":")
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
}
}

View File

@@ -24,6 +24,8 @@ metadata {
capability "Battery"
attribute "tamper", "enum", ["detected", "clear"]
attribute "batteryStatus", "string"
attribute "powerSupply", "enum", ["USB Cable", "Battery"]
fingerprint deviceId: "0x2101", inClusters: "0x5E,0x86,0x72,0x59,0x85,0x73,0x71,0x84,0x80,0x30,0x31,0x70,0x7A", outClusters: "0x5A"
}
@@ -63,6 +65,19 @@ metadata {
status "wake up" : "command: 8407, payload: "
}
preferences {
input description: "Please consult AEOTEC MULTISENSOR 6 operating manual for advanced setting options. You can skip this configuration to use default settings",
title: "Advanced Configuration", displayDuringSetup: true, type: "paragraph", element: "paragraph"
input "motionDelayTime", "enum", title: "Motion Sensor Delay Time",
options: ["20 seconds", "40 seconds", "1 minute", "2 minutes", "3 minutes", "4 minutes"], defaultValue: "${motionDelayTime}", displayDuringSetup: true
input "motionSensitivity", "enum", title: "Motion Sensor Sensitivity", options: ["normal","maximum","minimum"], defaultValue: "${motionSensitivity}", displayDuringSetup: true
input "reportInterval", "enum", title: "Sensors Report Interval",
options: ["8 minutes", "15 minutes", "30 minutes", "1 hour", "6 hours", "12 hours", "18 hours", "24 hours"], defaultValue: "${reportInterval}", displayDuringSetup: true
}
tiles(scale: 2) {
multiAttributeTile(name:"motion", type: "generic", width: 6, height: 4){
tileAttribute ("device.motion", key: "PRIMARY_CONTROL") {
@@ -85,53 +100,78 @@ metadata {
valueTile("humidity", "device.humidity", inactiveLabel: false, width: 2, height: 2) {
state "humidity", label:'${currentValue}% humidity', unit:""
}
valueTile("illuminance", "device.illuminance", inactiveLabel: false, width: 2, height: 2) {
state "luminosity", label:'${currentValue} ${unit}', unit:"lux"
state "illuminance", label:'${currentValue} ${unit}', unit:"lux"
}
valueTile("ultravioletIndex", "device.ultravioletIndex", inactiveLabel: false, width: 2, height: 2) {
state "ultravioletIndex", label:'${currentValue} UV index', unit:""
}
valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "battery", label:'${currentValue}% battery', unit:""
}
main(["motion", "temperature", "humidity", "illuminance"])
details(["motion", "temperature", "humidity", "illuminance", "battery"])
valueTile("batteryStatus", "device.batteryStatus", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "batteryStatus", label:'${currentValue}', unit:""
}
valueTile("powerSupply", "device.powerSupply", height: 2, width: 2, decoration: "flat") {
state "powerSupply", label:'${currentValue} powered', backgroundColor:"#ffffff"
}
main(["motion", "temperature", "humidity", "illuminance", "ultravioletIndex"])
details(["motion", "temperature", "humidity", "illuminance", "ultravioletIndex", "batteryStatus"])
}
}
def updated()
{
if (state.sec && !isConfigured()) {
// in case we miss the SCSR
def updated() {
log.debug "Updated with settings: ${settings}"
log.debug "${device.displayName} is now ${device.latestValue("powerSupply")}"
if (device.latestValue("powerSupply") == "USB Cable") { //case1: USB powered
response(configure())
} else if (device.latestValue("powerSupply") == "Battery") { //case2: battery powered
// setConfigured("false") is used by WakeUpNotification
setConfigured("false") //wait until the next time device wakeup to send configure command after user change preference
} else { //case3: power source is not identified, ask user to properly pair the sensor again
log.warn "power source is not identified, check it sensor is powered by USB, if so > configure()"
def request = []
request << zwave.configurationV1.configurationGet(parameterNumber: 101)
response(commands(request))
}
}
def parse(String description)
{
def parse(String description) {
log.debug "parse() >> description: $description"
def result = null
if (description.startsWith("Err 106")) {
state.sec = 0
log.debug "parse() >> Err 106"
result = createEvent( name: "secureInclusion", value: "failed", isStateChange: true,
descriptionText: "This sensor failed to complete the network security key exchange. If you are unable to control it via SmartThings, you must remove it from your network and add it again.")
descriptionText: "This sensor failed to complete the network security key exchange. If you are unable to control it via SmartThings, you must remove it from your network and add it again.")
} else if (description != "updated") {
log.debug "parse() >> zwave.parse(description)"
def cmd = zwave.parse(description, [0x31: 5, 0x30: 2, 0x84: 1])
if (cmd) {
result = zwaveEvent(cmd)
}
}
log.debug "Parsed '${description}' to ${result.inspect()}"
log.debug "After zwaveEvent(cmd) >> Parsed '${description}' to ${result.inspect()}"
return result
}
def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd)
{
//this notification will be sent only when device is battery powered
def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) {
def result = [createEvent(descriptionText: "${device.displayName} woke up", isStateChange: false)]
def cmds = []
if (!isConfigured()) {
// we're still in the process of configuring a newly joined device
log.debug("late configure")
result += response(configure())
result << response(configure())
} else {
result += response(zwave.wakeUpV1.wakeUpNoMoreInformation())
log.debug("Device has been configured sending >> wakeUpNoMoreInformation()")
cmds << zwave.wakeUpV1.wakeUpNoMoreInformation().format()
result << response(cmds)
}
result
}
@@ -149,10 +189,29 @@ def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulat
}
def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityCommandsSupportedReport cmd) {
response(configure())
log.info "Executing zwaveEvent 98 (SecurityV1): 03 (SecurityCommandsSupportedReport) with cmd: $cmd"
state.sec = 1
}
def zwaveEvent(physicalgraph.zwave.commands.securityv1.NetworkKeyVerify cmd) {
state.sec = 1
log.info "Executing zwaveEvent 98 (SecurityV1): 07 (NetworkKeyVerify) with cmd: $cmd (node is securely included)"
def result = [createEvent(name:"secureInclusion", value:"success", descriptionText:"Secure inclusion was successful", isStateChange: true)]
result
}
def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) {
log.info "Executing zwaveEvent 72 (ManufacturerSpecificV2) : 05 (ManufacturerSpecificReport) with cmd: $cmd"
log.debug "manufacturerId: ${cmd.manufacturerId}"
log.debug "manufacturerName: ${cmd.manufacturerName}"
log.debug "productId: ${cmd.productId}"
log.debug "productTypeId: ${cmd.productTypeId}"
def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId)
updateDataValue("MSR", msr)
}
def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) {
def result = []
def map = [ name: "battery", unit: "%" ]
if (cmd.batteryLevel == 0xFF) {
map.value = 1
@@ -162,11 +221,14 @@ def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) {
map.value = cmd.batteryLevel
}
state.lastbatt = now()
createEvent(map)
result << createEvent(map)
if (device.latestValue("powerSupply") != "USB Cable"){
result << createEvent(name: "batteryStatus", value: "${map.value} % battery", displayed: false)
}
result
}
def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd)
{
def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd){
def map = [:]
switch (cmd.sensorType) {
case 1:
@@ -208,7 +270,6 @@ def motionEvent(value) {
}
def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv2.SensorBinaryReport cmd) {
setConfigured()
motionEvent(cmd.sensorValue)
}
@@ -225,47 +286,112 @@ def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cm
result << createEvent(name: "tamper", value: "clear", displayed: false)
break
case 3:
result << createEvent(name: "tamper", value: "detected", descriptionText: "$device.displayName was moved")
result << createEvent(name: "tamper", value: "detected", descriptionText: "$device.displayName was tampered")
break
case 7:
result << motionEvent(1)
break
}
} else {
log.warn "Need to handle this cmd.notificationType: ${cmd.notificationType}"
result << createEvent(descriptionText: cmd.toString(), isStateChange: false)
}
result
}
def zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd) {
log.debug "ConfigurationReport: $cmd"
def result = []
def value
if (cmd.parameterNumber == 9 && cmd.configurationValue[0] == 0) {
value = "USB Cable"
if (!isConfigured()) {
log.debug("ConfigurationReport: configuring device")
result << response(configure())
}
result << createEvent(name: "batteryStatus", value: value, displayed: false)
result << createEvent(name: "powerSupply", value: value, displayed: false)
}else if (cmd.parameterNumber == 9 && cmd.configurationValue[0] == 1) {
value = "Battery"
result << createEvent(name: "powerSupply", value: value, displayed: false)
} else if (cmd.parameterNumber == 101){
result << response(configure())
}
result
}
def zwaveEvent(physicalgraph.zwave.Command cmd) {
log.debug "General zwaveEvent cmd: ${cmd}"
createEvent(descriptionText: cmd.toString(), isStateChange: false)
}
def configure() {
// This sensor joins as a secure device if you double-click the button to include it
if (device.device.rawDescription =~ /98/ && !state.sec) {
log.debug "Multi 6 not sending configure until secure"
return []
}
log.debug "Multi 6 configure()"
def request = [
// send no-motion report 20 seconds after motion stops
zwave.configurationV1.configurationSet(parameterNumber: 3, size: 2, scaledConfigurationValue: 20),
log.debug "${device.displayName} is configuring its settings"
def request = []
// report every 8 minutes (threshold reports don't work on battery power)
zwave.configurationV1.configurationSet(parameterNumber: 111, size: 4, scaledConfigurationValue: 8*60),
//1. set association groups for hub
request << zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId)
// report automatically on threshold change
zwave.configurationV1.configurationSet(parameterNumber: 40, size: 1, scaledConfigurationValue: 1),
request << zwave.associationV1.associationSet(groupingIdentifier:2, nodeId:zwaveHubNodeId)
//2. automatic report flags
// param 101 -103 [4 bytes] 128: light sensor, 64 humidity, 32 temperature sensor, 2 ultraviolet sensor, 1 battery sensor -> send command 227 to get all reports
request << zwave.configurationV1.configurationSet(parameterNumber: 101, size: 4, scaledConfigurationValue: 226) //association group 1
request << zwave.configurationV1.configurationSet(parameterNumber: 102, size: 4, scaledConfigurationValue: 1) //association group 2
//3. no-motion report x seconds after motion stops (default 20 secs)
request << zwave.configurationV1.configurationSet(parameterNumber: 3, size: 2, scaledConfigurationValue: timeOptionValueMap[motionDelayTime] ?: 20)
//4. motionSensitivity 3 levels: 64-normal (default), 127-maximum, 0-minimum
request << zwave.configurationV1.configurationSet(parameterNumber: 6, size: 1,
scaledConfigurationValue:
motionSensitivity == "normal" ? 64 :
motionSensitivity == "maximum" ? 127 :
motionSensitivity == "minimum" ? 0 : 64)
//5. report every x minutes (threshold reports don't work on battery power, default 8 mins)
request << zwave.configurationV1.configurationSet(parameterNumber: 111, size: 4, scaledConfigurationValue: timeOptionValueMap[reportInterval] ?: 8*60) //association group 1
request << zwave.configurationV1.configurationSet(parameterNumber: 112, size: 4, scaledConfigurationValue: 6*60*60) //association group 2
//6. report automatically on threshold change
request << zwave.configurationV1.configurationSet(parameterNumber: 40, size: 1, scaledConfigurationValue: 1)
//7. query sensor data
request << zwave.batteryV1.batteryGet()
request << zwave.sensorBinaryV2.sensorBinaryGet(sensorType: 0x0C) //motion
request << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x01) //temperature
request << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x03) //illuminance
request << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x05) //humidity
request << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x1B) //ultravioletIndex
setConfigured("true")
zwave.batteryV1.batteryGet(),
zwave.sensorBinaryV2.sensorBinaryGet(sensorType: 0x0C),
]
commands(request) + ["delay 20000", zwave.wakeUpV1.wakeUpNoMoreInformation().format()]
}
private setConfigured() {
updateDataValue("configured", "true")
private def getTimeOptionValueMap() { [
"20 seconds" : 20,
"40 seconds" : 40,
"1 minute" : 60,
"2 minutes" : 2*60,
"3 minutes" : 3*60,
"4 minutes" : 4*60,
"5 minutes" : 5*60,
"8 minutes" : 8*60,
"15 minutes" : 15*60,
"30 minutes" : 30*60,
"1 hours" : 1*60*60,
"6 hours" : 6*60*60,
"12 hours" : 12*60*60,
"18 hours" : 6*60*60,
"24 hours" : 24*60*60,
]}
private setConfigured(configure) {
updateDataValue("configured", configure)
}
private isConfigured() {
@@ -281,5 +407,6 @@ private command(physicalgraph.zwave.Command cmd) {
}
private commands(commands, delay=200) {
log.info "sending commands: ${commands}"
delayBetween(commands.collect{ command(it) }, delay)
}

View File

@@ -15,6 +15,7 @@
* Author: SmartThings
* Date: 2013-12-04
*/
//DEPRECATED - Using the generic DTH for this device. Users need to be moved before deleting this DTH
metadata {
definition (name: "CentraLite Dimmer", namespace: "smartthings", author: "SmartThings") {
capability "Switch Level"
@@ -25,7 +26,6 @@ metadata {
capability "Refresh"
capability "Sensor"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0B04,0B05", outClusters: "0019"
}
// simulator metadata

View File

@@ -38,168 +38,63 @@ metadata {
}
// UI tile definitions
tiles {
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
state "off", label: '${name}', action: "switch.on", icon: "st.switches.light.off", backgroundColor: "#ffffff"
state "on", label: '${name}', action: "switch.off", icon: "st.switches.light.on", backgroundColor: "#79b821"
}
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
}
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 3, inactiveLabel: false) {
state "level", action:"switch level.setLevel"
}
valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") {
state "level", label: 'Level ${currentValue}%'
}
main(["switch"])
details(["switch", "level", "levelSliderControl", "refresh"])
}
tiles(scale: 2) {
multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
attributeState "on", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff"
attributeState "off", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff"
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
}
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
attributeState "level", action:"switch level.setLevel"
}
}
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
}
main "switch"
details(["switch", "refresh"])
}
}
// Parse incoming device messages to generate events
def parse(String description) {
log.trace description
if (description?.startsWith("catchall:")) {
def msg = zigbee.parse(description)
log.trace msg
log.trace "data: $msg.data"
if(description?.endsWith("0100") ||description?.endsWith("1001"))
{
def result = createEvent(name: "switch", value: "on")
log.debug "Parse returned ${result?.descriptionText}"
return result
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}"
}
if(description?.endsWith("0000") || description?.endsWith("1000"))
{
def result = createEvent(name: "switch", value: "off")
log.debug "Parse returned ${result?.descriptionText}"
return result
else {
sendEvent(name: resultMap.type, value: resultMap.value)
}
}
if (description?.startsWith("read attr")) {
log.debug description[-2..-1]
def i = Math.round(convertHexToInt(description[-2..-1]) / 256 * 100 )
sendEvent( name: "level", value: i )
}
else {
log.warn "DID NOT PARSE MESSAGE for description : $description"
log.debug zigbee.parseDescriptionAsMap(description)
}
}
def off() {
zigbee.off()
}
def on() {
log.debug "on()"
sendEvent(name: "switch", value: "on")
"st cmd 0x${device.deviceNetworkId} ${endpointId} 6 1 {}"
}
def off() {
log.debug "off()"
sendEvent(name: "switch", value: "off")
"st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}"
}
def refresh() {
// Schedule poll every 1 min
//schedule("0 */1 * * * ?", poll)
//poll()
[
"st rattr 0x${device.deviceNetworkId} ${endpointId} 6 0", "delay 500",
"st rattr 0x${device.deviceNetworkId} ${endpointId} 8 0"
]
zigbee.on()
}
def setLevel(value) {
log.trace "setLevel($value)"
def cmds = []
zigbee.setLevel(value)
}
if (value == 0) {
sendEvent(name: "switch", value: "off")
cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {0000 0000}"
}
else if (device.latestValue("switch") == "off") {
sendEvent(name: "switch", value: "on")
}
sendEvent(name: "level", value: value)
def level = hex(value * 255/100)
cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 8 4 {${level} 0000}"
//log.debug cmds
cmds
def refresh() {
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.onOffConfig() + zigbee.levelConfig()
}
def configure() {
log.debug "Configuring Reporting and Bindings."
def configCmds = [
//Switch Reporting
"zcl global send-me-a-report 6 0 0x10 0 3600 {01}", "delay 500",
"send 0x${device.deviceNetworkId} ${endpointId} 1", "delay 1000",
//Level Control Reporting
"zcl global send-me-a-report 8 0 0x20 5 3600 {0010}", "delay 200",
"send 0x${device.deviceNetworkId} ${endpointId} 1", "delay 1500",
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 6 {${device.zigbeeId}} {}", "delay 1000",
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 8 {${device.zigbeeId}} {}", "delay 500",
]
return configCmds + refresh() // send refresh cmds as part of config
}
def uninstalled() {
log.debug "uninstalled()"
response("zcl rftd")
}
private getEndpointId() {
new BigInteger(device.endpointId, 16).toString()
}
private hex(value, width=2) {
def s = new BigInteger(Math.round(value).toString()).toString(16)
while (s.size() < width) {
s = "0" + s
}
s
}
private Integer convertHexToInt(hex) {
Integer.parseInt(hex,16)
}
private String swapEndianHex(String hex) {
reverseArray(hex.decodeHex()).encodeHex()
}
private byte[] reverseArray(byte[] array) {
int i = 0;
int j = array.length - 1;
byte tmp;
while (j > i) {
tmp = array[j];
array[j] = array[i];
array[i] = tmp;
j--;
i++;
}
return array
log.debug "Configuring Reporting and Bindings."
zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh()
}

View File

@@ -0,0 +1,71 @@
/**
* Ecobee Sensor
*
* Copyright 2015 Juan Risso
*
* 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: "Ecobee Sensor", namespace: "smartthings", author: "SmartThings") {
capability "Sensor"
capability "Temperature Measurement"
capability "Motion Sensor"
capability "Refresh"
capability "Polling"
}
simulator {
// TODO: define status and reply messages here
}
tiles {
valueTile("temperature", "device.temperature", width: 2, height: 2) {
state("temperature", label:'${currentValue}°', unit:"F",
backgroundColors:[
[value: 31, color: "#153591"],
[value: 44, color: "#1e9cbb"],
[value: 59, color: "#90d2a7"],
[value: 74, color: "#44b621"],
[value: 84, color: "#f1d801"],
[value: 95, color: "#d04e00"],
[value: 96, color: "#bc2323"]
]
)
}
standardTile("motion", "device.motion") {
state("active", label:'motion', icon:"st.motion.motion.active", backgroundColor:"#53a7c0")
state("inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff")
}
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat") {
state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
}
main (["temperature","motion"])
details(["temperature","motion","refresh"])
}
}
def refresh() {
log.debug "refresh..."
poll()
}
void poll() {
log.debug "Executing 'poll' using parent SmartApp"
parent.pollChildren(this)
}
//generate custom mobile activity feeds event
def generateActivityFeedsEvent(notificationMessage) {
sendEvent(name: "notificationMessage", value: "$device.displayName $notificationMessage", descriptionText: "$device.displayName $notificationMessage", displayed: true)
}

View File

@@ -22,31 +22,31 @@ metadata {
capability "Polling"
capability "Sensor"
capability "Refresh"
command "generateEvent"
command "raiseSetpoint"
command "lowerSetpoint"
command "resumeProgram"
command "switchMode"
attribute "thermostatSetpoint","number"
attribute "thermostatStatus","string"
}
simulator { }
tiles {
tiles {
valueTile("temperature", "device.temperature", width: 2, height: 2) {
state("temperature", label:'${currentValue}°', unit:"F",
backgroundColors:[
[value: 31, color: "#153591"],
[value: 44, color: "#1e9cbb"],
[value: 59, color: "#90d2a7"],
[value: 74, color: "#44b621"],
[value: 84, color: "#f1d801"],
[value: 95, color: "#d04e00"],
[value: 96, color: "#bc2323"]
]
backgroundColors:[
[value: 31, color: "#153591"],
[value: 44, color: "#1e9cbb"],
[value: 59, color: "#90d2a7"],
[value: 74, color: "#44b621"],
[value: 84, color: "#f1d801"],
[value: 95, color: "#d04e00"],
[value: 96, color: "#bc2323"]
]
)
}
standardTile("mode", "device.thermostatMode", inactiveLabel: false, decoration: "flat") {
@@ -54,27 +54,27 @@ metadata {
state "heat", action:"switchMode", nextState: "updating", icon: "st.thermostat.heat"
state "cool", action:"switchMode", nextState: "updating", icon: "st.thermostat.cool"
state "auto", action:"switchMode", nextState: "updating", icon: "st.thermostat.auto"
state "auxHeatOnly", action:"switchMode", icon: "st.thermostat.emergency-heat"
state "updating", label:"Working", icon: "st.secondary.secondary"
state "auxHeatOnly", action:"switchMode", icon: "st.thermostat.emergency-heat"
state "updating", label:"Working", icon: "st.secondary.secondary"
}
standardTile("fanMode", "device.thermostatFanMode", inactiveLabel: false, decoration: "flat") {
state "auto", label:'Fan: ${currentValue}', action:"switchFanMode", nextState: "on"
state "on", label:'Fan: ${currentValue}', action:"switchFanMode", nextState: "off"
state "off", label:'Fan: ${currentValue}', action:"switchFanMode", nextState: "circulate"
state "off", label:'Fan: ${currentValue}', action:"switchFanMode", nextState: "circulate"
state "circulate", label:'Fan: ${currentValue}', action:"switchFanMode", nextState: "auto"
}
standardTile("upButtonControl", "device.thermostatSetpoint", inactiveLabel: false, decoration: "flat") {
state "setpoint", action:"raiseSetpoint", backgroundColor:"#d04e00", icon:"st.thermostat.thermostat-up"
standardTile("upButtonControl", "device.thermostatSetpoint", inactiveLabel: false, decoration: "flat") {
state "setpoint", action:"raiseSetpoint", icon:"st.thermostat.thermostat-up"
}
valueTile("thermostatSetpoint", "device.thermostatSetpoint", width: 1, height: 1, decoration: "flat") {
state "thermostatSetpoint", label:'${currentValue}'
valueTile("thermostatSetpoint", "device.thermostatSetpoint", width: 1, height: 1, decoration: "flat") {
state "thermostatSetpoint", label:'${currentValue}°'
}
valueTile("currentStatus", "device.thermostatStatus", height: 1, width: 2, decoration: "flat") {
state "thermostatStatus", label:'${currentValue}', backgroundColor:"#ffffff"
}
}
standardTile("downButtonControl", "device.thermostatSetpoint", inactiveLabel: false, decoration: "flat") {
state "setpoint", action:"lowerSetpoint", backgroundColor:"#d04e00", icon:"st.thermostat.thermostat-down"
}
state "setpoint", action:"lowerSetpoint", icon:"st.thermostat.thermostat-down"
}
controlTile("heatSliderControl", "device.heatingSetpoint", "slider", height: 1, width: 2, inactiveLabel: false) {
state "setHeatingSetpoint", action:"thermostat.setHeatingSetpoint", backgroundColor:"#d04e00"
}
@@ -91,218 +91,196 @@ metadata {
state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
}
standardTile("resumeProgram", "device.resumeProgram", inactiveLabel: false, decoration: "flat") {
state "resume", label:'Resume Program', action:"device.resumeProgram", icon:"st.sonos.play-icon"
state "resume", action:"resumeProgram", nextState: "updating", label:'Resume Schedule', icon:"st.samsung.da.oven_ic_send"
state "updating", label:"Working", icon: "st.secondary.secondary"
}
main "temperature"
details(["temperature", "upButtonControl", "thermostatSetpoint", "currentStatus", "downButtonControl", "mode", "resumeProgram", "refresh"])
details(["temperature", "upButtonControl", "thermostatSetpoint", "currentStatus", "downButtonControl", "mode", "resumeProgram", "refresh"])
}
preferences {
input "holdType", "enum", title: "Hold Type", description: "When changing temperature, use Temporary (Until next transition) or Permanent hold (default)", required: false, options:["Temporary", "Permanent"]
}
}
/*
preferences {
input "highTemperature", "number", title: "Auto Mode High Temperature:", defaultValue: 80
input "lowTemperature", "number", title: "Auto Mode Low Temperature:", defaultValue: 70
input name: "holdType", type: "enum", title: "Hold Type", description: "When changing temperature, use Temporary or Permanent hold", required: true, options:["Temporary", "Permanent"]
}
*/
// parse events into attributes
def parse(String description) {
log.debug "Parsing '${description}'"
// TODO: handle '' attribute
}
def refresh()
{
log.debug "refresh called"
poll()
log.debug "refresh ended"
def refresh() {
log.debug "refresh called"
poll()
log.debug "refresh ended"
}
def go()
{
log.debug "before:go tile tapped"
poll()
log.debug "after"
}
void poll() {
log.debug "Executing 'poll' using parent SmartApp"
def results = parent.pollChild(this)
parseEventData(results)
generateStatusEvent()
generateEvent(results) //parse received message from parent
}
def parseEventData(Map results)
{
def generateEvent(Map results) {
log.debug "parsing data $results"
if(results)
{
results.each { name, value ->
if(results) {
results.each { name, value ->
def linkText = getLinkText(device)
def isChange = false
def isDisplayed = true
if (name=="temperature" || name=="heatingSetpoint" || name=="coolingSetpoint") {
isChange = isTemperatureStateChange(device, name, value.toString())
isDisplayed = isChange
sendEvent(
name: name,
value: value,
unit: "F",
linkText: linkText,
descriptionText: getThermostatDescriptionText(name, value, linkText),
handlerName: name,
isStateChange: isChange,
displayed: isDisplayed)
}
else {
isChange = isStateChange(device, name, value.toString())
isDisplayed = isChange
sendEvent(
name: name,
value: value.toString(),
linkText: linkText,
descriptionText: getThermostatDescriptionText(name, value, linkText),
handlerName: name,
isStateChange: isChange,
displayed: isDisplayed)
}
}
generateSetpointEvent ()
generateStatusEvent ()
}
}
def isChange = false
def isDisplayed = true
def event = [name: name, linkText: linkText, descriptionText: getThermostatDescriptionText(name, value, linkText),
handlerName: name]
void generateEvent(Map results)
{
log.debug "parsing data $results"
if(results)
{
results.each { name, value ->
def linkText = getLinkText(device)
def isChange = false
def isDisplayed = true
if (name=="temperature" || name=="heatingSetpoint" || name=="coolingSetpoint") {
if (name=="temperature" || name=="heatingSetpoint" || name=="coolingSetpoint") {
def sendValue = value? convertTemperatureIfNeeded(value.toDouble(), "F", 1): value //API return temperature value in F
isChange = isTemperatureStateChange(device, name, value.toString())
isDisplayed = isChange
sendEvent(
name: name,
value: value,
unit: "F",
linkText: linkText,
descriptionText: getThermostatDescriptionText(name, value, linkText),
handlerName: name,
isStateChange: isChange,
displayed: isDisplayed)
}
else {
isChange = isStateChange(device, name, value.toString())
isDisplayed = isChange
sendEvent(
name: name,
value: value.toString(),
linkText: linkText,
descriptionText: getThermostatDescriptionText(name, value, linkText),
handlerName: name,
isStateChange: isChange,
displayed: isDisplayed)
}
isDisplayed = isChange
event << [value: sendValue, isStateChange: isChange, displayed: isDisplayed]
} else if (name=="heatMode" || name=="coolMode" || name=="autoMode" || name=="auxHeatMode"){
isChange = isStateChange(device, name, value.toString())
event << [value: value.toString(), isStateChange: isChange, displayed: false]
} else {
isChange = isStateChange(device, name, value.toString())
isDisplayed = isChange
event << [value: value.toString(), isStateChange: isChange, displayed: isDisplayed]
}
sendEvent(event)
}
generateSetpointEvent ()
generateStatusEvent()
generateSetpointEvent ()
generateStatusEvent ()
}
}
private getThermostatDescriptionText(name, value, linkText)
{
if(name == "temperature")
{
return "$linkText was $value°F"
//return descriptionText to be shown on mobile activity feed
private getThermostatDescriptionText(name, value, linkText) {
if(name == "temperature") {
return "$linkText temperature is $value°F"
} else if(name == "heatingSetpoint") {
return "heating setpoint is $value°F"
} else if(name == "coolingSetpoint"){
return "cooling setpoint is $value°F"
} else if (name == "thermostatMode") {
return "thermostat mode is ${value}"
} else if (name == "thermostatFanMode") {
return "thermostat fan mode is ${value}"
} else {
return "${name} = ${value}"
}
else if(name == "heatingSetpoint")
{
return "latest heating setpoint was $value°F"
}
void setHeatingSetpoint(setpoint) {
setHeatingSetpoint(setpoint.toDouble())
}
void setHeatingSetpoint(Double setpoint) {
// def mode = device.currentValue("thermostatMode")
def heatingSetpoint = setpoint
def coolingSetpoint = device.currentValue("coolingSetpoint").toDouble()
def deviceId = device.deviceNetworkId.split(/\./).last()
//enforce limits of heatingSetpoint
if (heatingSetpoint > 79) {
heatingSetpoint = 79
} else if (heatingSetpoint < 45) {
heatingSetpoint = 45
}
else if(name == "coolingSetpoint")
{
return "latest cooling setpoint was $value°F"
//enforce limits of heatingSetpoint vs coolingSetpoint
if (heatingSetpoint >= coolingSetpoint) {
coolingSetpoint = heatingSetpoint
}
log.debug "Sending setHeatingSetpoint> coolingSetpoint: ${coolingSetpoint}, heatingSetpoint: ${heatingSetpoint}"
def sendHoldType = holdType ? (holdType=="Temporary")? "nextTransition" : (holdType=="Permanent")? "indefinite" : "indefinite" : "indefinite"
if (parent.setHold (this, heatingSetpoint, coolingSetpoint, deviceId, sendHoldType)) {
sendEvent("name":"heatingSetpoint", "value":heatingSetpoint)
sendEvent("name":"coolingSetpoint", "value":coolingSetpoint)
log.debug "Done setHeatingSetpoint> coolingSetpoint: ${coolingSetpoint}, heatingSetpoint: ${heatingSetpoint}"
generateSetpointEvent()
generateStatusEvent()
} else {
log.error "Error setHeatingSetpoint(setpoint)" //This error is handled by the connect app
}
else if (name == "thermostatMode")
{
return "thermostat mode is ${value}"
}
else
{
return "${name} = ${value}"
}
}
void setHeatingSetpoint(degreesF) {
setHeatingSetpoint(degreesF.toDouble())
void setCoolingSetpoint(setpoint) {
setCoolingSetpoint(setpoint.toDouble())
}
void setHeatingSetpoint(Double degreesF) {
log.debug "setHeatingSetpoint({$degreesF})"
sendEvent("name":"heatingSetpoint", "value":degreesF)
Double coolingSetpoint = device.currentValue("coolingSetpoint")
log.debug "coolingSetpoint: $coolingSetpoint"
parent.setHold(this, degreesF, coolingSetpoint)
void setCoolingSetpoint(Double setpoint) {
// def mode = device.currentValue("thermostatMode")
def heatingSetpoint = device.currentValue("heatingSetpoint").toDouble()
def coolingSetpoint = setpoint
def deviceId = device.deviceNetworkId.split(/\./).last()
if (coolingSetpoint > 92) {
coolingSetpoint = 92
} else if (coolingSetpoint < 65) {
coolingSetpoint = 65
}
//enforce limits of heatingSetpoint vs coolingSetpoint
if (heatingSetpoint >= coolingSetpoint) {
heatingSetpoint = coolingSetpoint
}
log.debug "Sending setCoolingSetpoint> coolingSetpoint: ${coolingSetpoint}, heatingSetpoint: ${heatingSetpoint}"
def sendHoldType = holdType ? (holdType=="Temporary")? "nextTransition" : (holdType=="Permanent")? "indefinite" : "indefinite" : "indefinite"
if (parent.setHold (this, heatingSetpoint, coolingSetpoint, deviceId, sendHoldType)) {
sendEvent("name":"heatingSetpoint", "value":heatingSetpoint)
sendEvent("name":"coolingSetpoint", "value":coolingSetpoint)
log.debug "Done setCoolingSetpoint>> coolingSetpoint = ${coolingSetpoint}, heatingSetpoint = ${heatingSetpoint}"
generateSetpointEvent()
generateStatusEvent()
} else {
log.error "Error setCoolingSetpoint(setpoint)" //This error is handled by the connect app
}
}
void setCoolingSetpoint(degreesF) {
setCoolingSetpoint(degreesF.toDouble())
}
void resumeProgram() {
void setCoolingSetpoint(Double degreesF) {
log.debug "setCoolingSetpoint({$degreesF})"
sendEvent("name":"coolingSetpoint", "value":degreesF)
Double heatingSetpoint = device.currentValue("heatingSetpoint")
parent.setHold(this, heatingSetpoint, degreesF)
}
log.debug "resumeProgram() is called"
sendEvent("name":"thermostatStatus", "value":"resuming schedule", "description":statusText, displayed: false)
def deviceId = device.deviceNetworkId.split(/\./).last()
if (parent.resumeProgram(this, deviceId)) {
sendEvent("name":"thermostatStatus", "value":"setpoint is updating", "description":statusText, displayed: false)
runIn(5, "poll")
log.debug "resumeProgram() is done"
sendEvent("name":"resumeProgram", "value":"resume", descriptionText: "resumeProgram is done", displayed: false, isStateChange: true)
} else {
sendEvent("name":"thermostatStatus", "value":"failed resume click refresh", "description":statusText, displayed: false)
log.error "Error resumeProgram() check parent.resumeProgram(this, deviceId)"
}
def configure() {
}
def resumeProgram() {
parent.resumeProgram(this)
}
def modes() {
if (state.modes) {
log.debug "Modes = ${state.modes}"
return state.modes
}
else {
state.modes = parent.availableModes(this)
log.debug "Modes = ${state.modes}"
return state.modes
}
log.debug "Modes = ${state.modes}"
return state.modes
}
else {
state.modes = parent.availableModes(this)
log.debug "Modes = ${state.modes}"
return state.modes
}
}
def fanModes() {
["off", "on", "auto", "circulate"]
}
def switchMode() {
log.debug "in switchMode"
def currentMode = device.currentState("thermostatMode")?.value
@@ -314,7 +292,7 @@ def switchMode() {
}
def switchToMode(nextMode) {
log.debug "In switchToMode = ${nextMode}"
log.debug "In switchToMode = ${nextMode}"
if (nextMode in modes()) {
state.lastTriedMode = nextMode
"$nextMode"()
@@ -376,300 +354,326 @@ def getDataByName(String name) {
def setThermostatMode(String value) {
log.debug "setThermostatMode({$value})"
}
def setThermostatFanMode(String value) {
log.debug "setThermostatFanMode({$value})"
}
def generateModeEvent(mode) {
sendEvent(name: "thermostatMode", value: mode, descriptionText: "$device.displayName is in ${mode} mode", displayed: true, isStateChange: true)
sendEvent(name: "thermostatMode", value: mode, descriptionText: "$device.displayName is in ${mode} mode", displayed: true)
}
def generateFanModeEvent(fanMode) {
sendEvent(name: "thermostatFanMode", value: fanMode, descriptionText: "$device.displayName fan is in ${mode} mode", displayed: true, isStateChange: true)
sendEvent(name: "thermostatFanMode", value: fanMode, descriptionText: "$device.displayName fan is in ${mode} mode", displayed: true)
}
def generateOperatingStateEvent(operatingState) {
sendEvent(name: "thermostatOperatingState", value: operatingState, descriptionText: "$device.displayName is ${operatingState}", displayed: true, isStateChange: true)
sendEvent(name: "thermostatOperatingState", value: operatingState, descriptionText: "$device.displayName is ${operatingState}", displayed: true)
}
def off() {
log.debug "off"
generateModeEvent("off")
if (parent.setMode (this,"off"))
generateModeEvent("off")
else {
log.debug "Error setting new mode."
def currentMode = device.currentState("thermostatMode")?.value
generateModeEvent(currentMode) // reset the tile back
}
generateSetpointEvent()
generateStatusEvent()
def deviceId = device.deviceNetworkId.split(/\./).last()
if (parent.setMode (this,"off", deviceId))
generateModeEvent("off")
else {
log.debug "Error setting new mode."
def currentMode = device.currentState("thermostatMode")?.value
generateModeEvent(currentMode) // reset the tile back
}
generateSetpointEvent()
generateStatusEvent()
}
def heat() {
log.debug "heat"
generateModeEvent("heat")
if (parent.setMode (this,"heat"))
generateModeEvent("heat")
else {
log.debug "Error setting new mode."
def currentMode = device.currentState("thermostatMode")?.value
generateModeEvent(currentMode) // reset the tile back
}
generateSetpointEvent()
generateStatusEvent()
def deviceId = device.deviceNetworkId.split(/\./).last()
if (parent.setMode (this,"heat", deviceId))
generateModeEvent("heat")
else {
log.debug "Error setting new mode."
def currentMode = device.currentState("thermostatMode")?.value
generateModeEvent(currentMode) // reset the tile back
}
generateSetpointEvent()
generateStatusEvent()
}
def auxHeatOnly() {
log.debug "auxHeatOnly"
generateModeEvent("auxHeatOnly")
if (parent.setMode (this,"auxHeatOnly"))
generateModeEvent("auxHeatOnly")
else {
log.debug "Error setting new mode."
def currentMode = device.currentState("thermostatMode")?.value
generateModeEvent(currentMode) // reset the tile back
}
generateSetpointEvent()
generateStatusEvent()
def deviceId = device.deviceNetworkId.split(/\./).last()
if (parent.setMode (this,"auxHeatOnly", deviceId))
generateModeEvent("auxHeatOnly")
else {
log.debug "Error setting new mode."
def currentMode = device.currentState("thermostatMode")?.value
generateModeEvent(currentMode) // reset the tile back
}
generateSetpointEvent()
generateStatusEvent()
}
def cool() {
log.debug "cool"
generateModeEvent("cool")
if (parent.setMode (this,"cool"))
generateModeEvent("cool")
else {
log.debug "Error setting new mode."
def currentMode = device.currentState("thermostatMode")?.value
generateModeEvent(currentMode) // reset the tile back
}
generateSetpointEvent()
generateStatusEvent()
def deviceId = device.deviceNetworkId.split(/\./).last()
if (parent.setMode (this,"cool", deviceId))
generateModeEvent("cool")
else {
log.debug "Error setting new mode."
def currentMode = device.currentState("thermostatMode")?.value
generateModeEvent(currentMode) // reset the tile back
}
generateSetpointEvent()
generateStatusEvent()
}
def auto() {
log.debug "auto"
generateModeEvent("auto")
if (parent.setMode (this,"auto"))
generateModeEvent("auto")
else {
log.debug "Error setting new mode."
def currentMode = device.currentState("thermostatMode")?.value
generateModeEvent(currentMode) // reset the tile back
}
generateSetpointEvent()
generateStatusEvent()
def deviceId = device.deviceNetworkId.split(/\./).last()
if (parent.setMode (this,"auto", deviceId))
generateModeEvent("auto")
else {
log.debug "Error setting new mode."
def currentMode = device.currentState("thermostatMode")?.value
generateModeEvent(currentMode) // reset the tile back
}
generateSetpointEvent()
generateStatusEvent()
}
def fanOn() {
log.debug "fanOn"
parent.setFanMode (this,"on")
// parent.setFanMode (this,"on")
}
def fanAuto() {
log.debug "fanAuto"
parent.setFanMode (this,"auto")
// parent.setFanMode (this,"auto")
}
def fanCirculate() {
log.debug "fanCirculate"
parent.setFanMode (this,"circulate")
// parent.setFanMode (this,"circulate")
}
def fanOff() {
log.debug "fanOff"
parent.setFanMode (this,"off")
// parent.setFanMode (this,"off")
}
def generateSetpointEvent() {
log.debug "Generate SetPoint Event"
log.debug "Generate SetPoint Event"
def mode = device.currentValue("thermostatMode")
log.debug "Current Mode = ${mode}"
log.debug "Current Mode = ${mode}"
def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger()
log.debug "Heating Setpoint = ${heatingSetpoint}"
def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger()
log.debug "Heating Setpoint = ${heatingSetpoint}"
def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger()
log.debug "Cooling Setpoint = ${coolingSetpoint}"
if (mode == "heat") {
sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint.toString()+"°")
}
else if (mode == "cool") {
sendEvent("name":"thermostatSetpoint", "value":coolingSetpoint.toString()+"°")
log.debug "Cooling Setpoint = ${coolingSetpoint}"
} else if (mode == "auto") {
sendEvent("name":"thermostatSetpoint", "value":"Auto")
if (mode == "heat") {
} else if (mode == "off") {
sendEvent("name":"thermostatSetpoint", "value":"Off")
sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint.toString())
} else if (mode == "emergencyHeat") {
sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint.toString()+"°")
}
else if (mode == "cool") {
}
sendEvent("name":"thermostatSetpoint", "value":coolingSetpoint.toString())
} else if (mode == "auto") {
sendEvent("name":"thermostatSetpoint", "value":"Auto")
} else if (mode == "off") {
sendEvent("name":"thermostatSetpoint", "value":"Off")
} else if (mode == "emergencyHeat") {
sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint.toString())
}
}
void raiseSetpoint() {
log.debug "Raise SetPoint"
def mode = device.currentValue("thermostatMode")
def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger()
def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger()
log.debug "Current Mode = ${mode}"
if (mode == "heat") {
heatingSetpoint++
if (heatingSetpoint > 99)
heatingSetpoint = 99
sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint.toString()+"°")
sendEvent("name":"heatingSetpoint", "value":heatingSetpoint)
parent.setHold (this, heatingSetpoint, coolingSetpoint)
log.debug "New Heating Setpoint = ${heatingSetpoint}"
}
else if (mode == "cool") {
coolingSetpoint++
if (coolingSetpoint > 99)
coolingSetpoint = 99
sendEvent("name":"thermostatSetpoint", "value":coolingSetpoint.toString()+"°")
sendEvent("name":"coolingSetpoint", "value":coolingSetpoint)
parent.setHold (this, heatingSetpoint, coolingSetpoint)
log.debug "New Cooling Setpoint = ${coolingSetpoint}"
}
generateStatusEvent()
def targetvalue
if (mode == "off" || mode == "auto") {
log.warn "this mode: $mode does not allow raiseSetpoint"
} else {
def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger()
def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger()
def thermostatSetpoint = device.currentValue("thermostatSetpoint").toInteger()
log.debug "raiseSetpoint() mode = ${mode}, heatingSetpoint: ${heatingSetpoint}, coolingSetpoint:${coolingSetpoint}, thermostatSetpoint:${thermostatSetpoint}"
if (device.latestState('thermostatSetpoint')) {
targetvalue = device.latestState('thermostatSetpoint').value as Integer
} else {
targetvalue = 0
}
targetvalue = targetvalue + 1
if (mode == "heat" && targetvalue > 79) {
targetvalue = 79
} else if (mode == "cool" && targetvalue > 92) {
targetvalue = 92
}
sendEvent("name":"thermostatSetpoint", "value":targetvalue, displayed: false)
log.info "In mode $mode raiseSetpoint() to $targetvalue"
runIn(3, "alterSetpoint", [data: [value:targetvalue], overwrite: true]) //when user click button this runIn will be overwrite
}
}
//called by tile when user hit raise temperature button on UI
void lowerSetpoint() {
log.debug "Lower SetPoint"
def mode = device.currentValue("thermostatMode")
def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger()
def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger()
log.debug "Current Mode = ${mode}, Current Heating Setpoint = ${heatingSetpoint}, Current Cooling Setpoint = ${coolingSetpoint}"
if (mode == "heat" || mode == "emergencyHeat") {
heatingSetpoint--
if (heatingSetpoint < 32)
heatingSetpoint = 32
def targetvalue
sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint.toString()+"°")
sendEvent("name":"heatingSetpoint", "value":heatingSetpoint)
parent.setHold (this, heatingSetpoint, coolingSetpoint)
log.debug "New Heating Setpoint = ${heatingSetpoint}"
}
else if (mode == "cool") {
coolingSetpoint--
if (coolingSetpoint < 32)
coolingSetpoint = 32
sendEvent("name":"thermostatSetpoint", "value":coolingSetpoint.toString()+"°")
sendEvent("name":"coolingSetpoint", "value":coolingSetpoint)
parent.setHold (this, heatingSetpoint, coolingSetpoint)
log.debug "New Cooling Setpoint = ${coolingSetpoint}"
}
generateStatusEvent()
if (mode == "off" || mode == "auto") {
log.warn "this mode: $mode does not allow lowerSetpoint"
} else {
def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger()
def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger()
def thermostatSetpoint = device.currentValue("thermostatSetpoint").toInteger()
log.debug "lowerSetpoint() mode = ${mode}, heatingSetpoint: ${heatingSetpoint}, coolingSetpoint:${coolingSetpoint}, thermostatSetpoint:${thermostatSetpoint}"
if (device.latestState('thermostatSetpoint')) {
targetvalue = device.latestState('thermostatSetpoint').value as Integer
} else {
targetvalue = 0
}
targetvalue = targetvalue - 1
if (mode == "heat" && targetvalue.toInteger() < 45) {
targetvalue = 45
} else if (mode == "cool" && targetvalue.toInteger() < 65) {
targetvalue = 65
}
sendEvent("name":"thermostatSetpoint", "value":targetvalue, displayed: false)
log.info "In mode $mode lowerSetpoint() to $targetvalue"
runIn(3, "alterSetpoint", [data: [value:targetvalue], overwrite: true]) //when user click button this runIn will be overwrite
}
}
//called by raiseSetpoint() and lowerSetpoint()
void alterSetpoint(temp) {
def mode = device.currentValue("thermostatMode")
def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger()
def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger()
def deviceId = device.deviceNetworkId.split(/\./).last()
def targetHeatingSetpoint
def targetCoolingSetpoint
//step1: check thermostatMode, enforce limits before sending request to cloud
if (mode == "heat"){
if (temp.value > coolingSetpoint){
targetHeatingSetpoint = temp.value
targetCoolingSetpoint = temp.value
} else {
targetHeatingSetpoint = temp.value
targetCoolingSetpoint = coolingSetpoint
}
} else if (mode == "cool") {
//enforce limits before sending request to cloud
if (temp.value < heatingSetpoint){
targetHeatingSetpoint = temp.value
targetCoolingSetpoint = temp.value
} else {
targetHeatingSetpoint = heatingSetpoint
targetCoolingSetpoint = temp.value
}
}
log.debug "alterSetpoint >> in mode ${mode} trying to change heatingSetpoint to ${targetHeatingSetpoint} " +
"coolingSetpoint to ${targetCoolingSetpoint} with holdType : ${holdType}"
def sendHoldType = holdType ? (holdType=="Temporary")? "nextTransition" : (holdType=="Permanent")? "indefinite" : "indefinite" : "indefinite"
//step2: call parent.setHold to send http request to 3rd party cloud
if (parent.setHold(this, targetHeatingSetpoint, targetCoolingSetpoint, deviceId, sendHoldType)) {
sendEvent("name": "thermostatSetpoint", "value": temp.value.toString(), displayed: false)
sendEvent("name": "heatingSetpoint", "value": targetHeatingSetpoint)
sendEvent("name": "coolingSetpoint", "value": targetCoolingSetpoint)
log.debug "alterSetpoint in mode $mode succeed change setpoint to= ${temp.value}"
} else {
log.error "Error alterSetpoint()"
if (mode == "heat"){
sendEvent("name": "thermostatSetpoint", "value": heatingSetpoint.toString(), displayed: false)
} else if (mode == "cool") {
sendEvent("name": "thermostatSetpoint", "value": coolingSetpoint.toString(), displayed: false)
}
}
generateStatusEvent()
}
def generateStatusEvent() {
def mode = device.currentValue("thermostatMode")
def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger()
def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger()
def temperature = device.currentValue("temperature").toInteger()
def statusText
log.debug "Generate Status Event for Mode = ${mode}"
log.debug "Temperature = ${temperature}"
log.debug "Heating set point = ${heatingSetpoint}"
log.debug "Cooling set point = ${coolingSetpoint}"
log.debug "HVAC Mode = ${mode}"
if (mode == "heat") {
if (temperature >= heatingSetpoint)
statusText = "Right Now: Idle"
else
statusText = "Heating to ${heatingSetpoint}° F"
} else if (mode == "cool") {
if (temperature <= coolingSetpoint)
statusText = "Right Now: Idle"
else
statusText = "Cooling to ${coolingSetpoint}° F"
} else if (mode == "auto") {
statusText = "Right Now: Auto"
} else if (mode == "off") {
statusText = "Right Now: Off"
} else if (mode == "emergencyHeat") {
statusText = "Emergency Heat"
} else {
statusText = "?"
}
log.debug "Generate Status Event = ${statusText}"
sendEvent("name":"thermostatStatus", "value":statusText, "description":statusText, displayed: true, isStateChange: true)
def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger()
def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger()
def temperature = device.currentValue("temperature").toInteger()
def statusText
log.debug "Generate Status Event for Mode = ${mode}"
log.debug "Temperature = ${temperature}"
log.debug "Heating set point = ${heatingSetpoint}"
log.debug "Cooling set point = ${coolingSetpoint}"
log.debug "HVAC Mode = ${mode}"
if (mode == "heat") {
if (temperature >= heatingSetpoint)
statusText = "Right Now: Idle"
else
statusText = "Heating to ${heatingSetpoint}° F"
} else if (mode == "cool") {
if (temperature <= coolingSetpoint)
statusText = "Right Now: Idle"
else
statusText = "Cooling to ${coolingSetpoint}° F"
} else if (mode == "auto") {
statusText = "Right Now: Auto"
} else if (mode == "off") {
statusText = "Right Now: Off"
} else if (mode == "emergencyHeat") {
statusText = "Emergency Heat"
} else {
statusText = "?"
}
log.debug "Generate Status Event = ${statusText}"
sendEvent("name":"thermostatStatus", "value":statusText, "description":statusText, displayed: true)
}
//generate custom mobile activity feeds event
def generateActivityFeedsEvent(notificationMessage) {
sendEvent(name: "notificationMessage", value: "$device.displayName $notificationMessage", descriptionText: "$device.displayName $notificationMessage", displayed: true)
}

View File

@@ -50,7 +50,7 @@ metadata {
capability "Switch Level"
capability "Polling"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,1000", outClusters: "0019"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,1000", outClusters: "0019", manufacturer: "GE_Appliances", model: "ZLL Light", deviceJoinName: "GE Link Bulb"
}
// UI tile definitions

View File

@@ -26,8 +26,6 @@ metadata {
capability "Actuator"
capability "Sensor"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0B05,0702", outClusters: "000A,0019", manufacturer: "Jasco Products", model: "45852"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0B05,0702", outClusters: "000A,0019", manufacturer: "Jasco Products", model: "45857"
}
// simulator metadata

View File

@@ -26,8 +26,6 @@ metadata {
capability "Actuator"
capability "Sensor"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B05,0702", outClusters: "0003, 000A,0019", manufacturer: "Jasco Products", model: "45853"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B05,0702", outClusters: "000A,0019", manufacturer: "Jasco Products", model: "45856"
}
// simulator metadata

View File

@@ -0,0 +1,81 @@
/**
* Logitech Harmony Activity
*
* Copyright 2015 Juan Risso
*
* 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: "Harmony Activity", namespace: "smartthings", author: "Juan Risso") {
capability "Switch"
capability "Actuator"
capability "Refresh"
command "huboff"
command "alloff"
command "refresh"
}
// simulator metadata
simulator {
}
// UI tile definitions
tiles {
standardTile("button", "device.switch", width: 2, height: 2, canChangeIcon: true) {
state "off", label: 'Off', action: "switch.on", icon: "st.harmony.harmony-hub-icon", backgroundColor: "#ffffff", nextState: "on"
state "on", label: 'On', action: "switch.off", icon: "st.harmony.harmony-hub-icon", backgroundColor: "#79b821", nextState: "off"
}
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
}
standardTile("forceoff", "device.switch", inactiveLabel: false, decoration: "flat") {
state "default", label:'Force End', action:"switch.off", icon:"st.secondary.off"
}
standardTile("huboff", "device.switch", inactiveLabel: false, decoration: "flat") {
state "default", label:'End Hub Action', action:"huboff", icon:"st.harmony.harmony-hub-icon"
}
standardTile("alloff", "device.switch", inactiveLabel: false, decoration: "flat") {
state "default", label:'All Actions', action:"alloff", icon:"st.secondary.off"
}
main "button"
details(["button", "refresh", "forceoff", "huboff", "alloff"])
}
}
def parse(String description) {
}
def on() {
sendEvent(name: "switch", value: "on")
log.trace parent.activity(device.deviceNetworkId,"start")
}
def off() {
sendEvent(name: "switch", value: "off")
log.trace parent.activity(device.deviceNetworkId,"end")
}
def huboff() {
sendEvent(name: "switch", value: "off")
log.trace parent.activity(device.deviceNetworkId,"hub")
}
def alloff() {
sendEvent(name: "switch", value: "off")
log.trace parent.activity("all","end")
}
def refresh() {
log.debug "Executing 'refresh'"
log.trace parent.poll()
}

View File

@@ -17,44 +17,50 @@ metadata {
}
simulator {
// TODO: define status and reply messages here
}
tiles {
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
state "unreachable", label: "?", action:"refresh.refresh", icon:"st.switches.light.off", backgroundColor:"#666666"
state "on", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff"
state "off", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
state "turningOn", label:'Turning on', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff"
state "turningOff", label:'Turning off', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
tiles(scale: 2) {
multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
attributeState "unreachable", label: "?", action:"refresh.refresh", icon:"http://hosted.lifx.co/smartthings/v1/196xUnreachable.png", backgroundColor:"#666666"
attributeState "on", label:'${name}', action:"switch.off", icon:"http://hosted.lifx.co/smartthings/v1/196xOn.png", backgroundColor:"#79b821", nextState:"turningOff"
attributeState "off", label:'${name}', action:"switch.on", icon:"http://hosted.lifx.co/smartthings/v1/196xOff.png", backgroundColor:"#ffffff", nextState:"turningOn"
attributeState "turningOn", label:'Turning on', action:"switch.off", icon:"http://hosted.lifx.co/smartthings/v1/196xOn.png", backgroundColor:"#79b821", nextState:"turningOff"
attributeState "turningOff", label:'Turning off', action:"switch.on", icon:"http://hosted.lifx.co/smartthings/v1/196xOff.png", backgroundColor:"#ffffff", nextState:"turningOn"
}
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
attributeState "level", action:"switch level.setLevel"
}
tileAttribute ("device.color", key: "COLOR_CONTROL") {
attributeState "color", action:"setColor"
}
tileAttribute ("device.model", key: "SECONDARY_CONTROL") {
attributeState "model", label: '${currentValue}'
}
}
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
}
valueTile("null", "device.switch", inactiveLabel: false, decoration: "flat") {
state "default", label:''
}
controlTile("rgbSelector", "device.color", "color", height: 3, width: 3, inactiveLabel: false) {
state "color", action:"setColor"
}
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 3, inactiveLabel: false, range:"(0..100)") {
state "level", action:"switch level.setLevel"
}
valueTile("level", "device.level", inactiveLabel: false, icon: "st.illuminance.illuminance.light", decoration: "flat") {
state "level", label: '${currentValue}%'
}
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", height: 1, width: 2, inactiveLabel: false, range:"(2700..9000)") {
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", height: 2, width: 4, inactiveLabel: false, range:"(2700..9000)") {
state "colorTemp", action:"color temperature.setColorTemperature"
}
valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat") {
valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat", height: 2, width: 2) {
state "colorTemp", label: '${currentValue}K'
}
main(["switch"])
details(["switch", "refresh", "level", "levelSliderControl", "rgbSelector", "colorTempSliderControl", "colorTemp"])
main "switch"
details(["switch", "colorTempSliderControl", "colorTemp", "refresh"])
}
}
@@ -70,7 +76,7 @@ def parse(String description) {
def setHue(percentage) {
log.debug "setHue ${percentage}"
parent.logErrors(logObject: log) {
def resp = parent.apiPUT("/lights/${device.deviceNetworkId}/color", [color: "hue:${percentage * 3.6}"])
def resp = parent.apiPUT("/lights/${selector()}/state", [color: "hue:${percentage * 3.6}", power: "on"])
if (resp.status < 300) {
sendEvent(name: "hue", value: percentage)
sendEvent(name: "switch", value: "on")
@@ -83,7 +89,7 @@ def setHue(percentage) {
def setSaturation(percentage) {
log.debug "setSaturation ${percentage}"
parent.logErrors(logObject: log) {
def resp = parent.apiPUT("/lights/${device.deviceNetworkId}/color", [color: "saturation:${percentage / 100}"])
def resp = parent.apiPUT("/lights/${selector()}/state", [color: "saturation:${percentage / 100}", power: "on"])
if (resp.status < 300) {
sendEvent(name: "saturation", value: percentage)
sendEvent(name: "switch", value: "on")
@@ -114,7 +120,7 @@ def setColor(Map color) {
}
}
parent.logErrors(logObject:log) {
def resp = parent.apiPUT("/lights/${device.deviceNetworkId}/color", [color: attrs.join(" ")])
def resp = parent.apiPUT("/lights/${selector()}/state", [color: attrs.join(" "), power: "on"])
if (resp.status < 300) {
sendEvent(name: "color", value: color.hex)
sendEvent(name: "switch", value: "on")
@@ -135,9 +141,10 @@ def setLevel(percentage) {
return off() // if the brightness is set to 0, just turn it off
}
parent.logErrors(logObject:log) {
def resp = parent.apiPUT("/lights/${device.deviceNetworkId}/color", ["color": "brightness:${percentage / 100}"])
def resp = parent.apiPUT("/lights/${selector()}/state", ["brightness": percentage / 100, "power": "on"])
if (resp.status < 300) {
sendEvent(name: "level", value: percentage)
sendEvent(name: "switch.setLevel", value: percentage)
sendEvent(name: "switch", value: "on")
} else {
log.error("Bad setLevel result: [${resp.status}] ${resp.data}")
@@ -148,7 +155,7 @@ def setLevel(percentage) {
def setColorTemperature(kelvin) {
log.debug "Executing 'setColorTemperature' to ${kelvin}"
parent.logErrors() {
def resp = parent.apiPUT("/lights/${device.deviceNetworkId}/color", [color: "kelvin:${kelvin}"])
def resp = parent.apiPUT("/lights/${selector()}/state", [color: "kelvin:${kelvin}", power: "on"])
if (resp.status < 300) {
sendEvent(name: "colorTemperature", value: kelvin)
sendEvent(name: "color", value: "#ffffff")
@@ -163,7 +170,7 @@ def setColorTemperature(kelvin) {
def on() {
log.debug "Device setOn"
parent.logErrors() {
if (parent.apiPUT("/lights/${device.deviceNetworkId}/power", [state: "on"]) != null) {
if (parent.apiPUT("/lights/${selector()}/state", [power: "on"]) != null) {
sendEvent(name: "switch", value: "on")
}
}
@@ -172,7 +179,7 @@ def on() {
def off() {
log.debug "Device setOff"
parent.logErrors() {
if (parent.apiPUT("/lights/${device.deviceNetworkId}/power", [state: "off"]) != null) {
if (parent.apiPUT("/lights/${selector()}/state", [power: "off"]) != null) {
sendEvent(name: "switch", value: "off")
}
}
@@ -180,19 +187,26 @@ def off() {
def poll() {
log.debug "Executing 'poll' for ${device} ${this} ${device.deviceNetworkId}"
def resp = parent.apiGET("/lights/${device.deviceNetworkId}")
if (resp.status != 200) {
def resp = parent.apiGET("/lights/${selector()}")
if (resp.status == 404) {
sendEvent(name: "switch", value: "unreachable")
return []
} else if (resp.status != 200) {
log.error("Unexpected result in poll(): [${resp.status}] ${resp.data}")
return []
}
def data = resp.data
def data = resp.data[0]
log.debug("Data: ${data}")
sendEvent(name: "level", value: sprintf("%.1f", (data.brightness ?: 1) * 100))
sendEvent(name: "label", value: data.label)
sendEvent(name: "level", value: Math.round((data.brightness ?: 1) * 100))
sendEvent(name: "switch.setLevel", value: Math.round((data.brightness ?: 1) * 100))
sendEvent(name: "switch", value: data.connected ? data.power : "unreachable")
sendEvent(name: "color", value: colorUtil.hslToHex((data.color.hue / 3.6) as int, (data.color.saturation * 100) as int))
sendEvent(name: "hue", value: data.color.hue / 3.6)
sendEvent(name: "hue", value: data.color.hue / 3.6)
sendEvent(name: "saturation", value: data.color.saturation * 100)
sendEvent(name: "colorTemperature", value: data.color.kelvin)
sendEvent(name: "model", value: "${data.product.company} ${data.product.name}")
return []
}
@@ -201,3 +215,11 @@ def refresh() {
log.debug "Executing 'refresh'"
poll()
}
def selector() {
if (device.deviceNetworkId.contains(":")) {
return device.deviceNetworkId
} else {
return "id:${device.deviceNetworkId}"
}
}

View File

@@ -16,41 +16,44 @@ metadata {
}
simulator {
// TODO: define status and reply messages here
}
tiles {
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
state "on", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff"
state "off", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
state "turningOn", label:'Turning on', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff"
state "turningOff", label:'Turning off', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
state "unreachable", label: "?", action:"refresh.refresh", icon:"st.switches.light.off", backgroundColor:"#666666"
tiles(scale: 2) {
multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
attributeState "unreachable", label: "?", action:"refresh.refresh", icon:"http://hosted.lifx.co/smartthings/v1/196xUnreachable.png", backgroundColor:"#666666"
attributeState "on", label:'${name}', action:"switch.off", icon:"http://hosted.lifx.co/smartthings/v1/196xOn.png", backgroundColor:"#79b821", nextState:"turningOff"
attributeState "off", label:'${name}', action:"switch.on", icon:"http://hosted.lifx.co/smartthings/v1/196xOff.png", backgroundColor:"#ffffff", nextState:"turningOn"
attributeState "turningOn", label:'Turning on', action:"switch.off", icon:"http://hosted.lifx.co/smartthings/v1/196xOn.png", backgroundColor:"#79b821", nextState:"turningOff"
attributeState "turningOff", label:'Turning off', action:"switch.on", icon:"http://hosted.lifx.co/smartthings/v1/196xOff.png", backgroundColor:"#ffffff", nextState:"turningOn"
}
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
attributeState "level", action:"switch level.setLevel"
}
}
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
}
valueTile("null", "device.switch", inactiveLabel: false, decoration: "flat") {
state "default", label:''
}
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 3, inactiveLabel: false, range:"(0..100)") {
state "level", action:"switch level.setLevel"
}
valueTile("level", "device.level", inactiveLabel: false, icon: "st.illuminance.illuminance.light", decoration: "flat") {
state "level", label: '${currentValue}%'
}
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", height: 1, width: 2, inactiveLabel: false, range:"(2700..6500)") {
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", height: 2, width: 4, inactiveLabel: false, range:"(2700..9000)") {
state "colorTemp", action:"color temperature.setColorTemperature"
}
valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat") {
valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat", height: 2, width: 2) {
state "colorTemp", label: '${currentValue}K'
}
main(["switch"])
details(["switch", "refresh", "level", "levelSliderControl", "colorTempSliderControl", "colorTemp"])
main "switch"
details(["switch", "colorTempSliderControl", "colorTemp", "refresh"])
}
}
// parse events into attributes
@@ -72,9 +75,10 @@ def setLevel(percentage) {
return off() // if the brightness is set to 0, just turn it off
}
parent.logErrors(logObject:log) {
def resp = parent.apiPUT("/lights/${device.deviceNetworkId}/color", ["color": "brightness:${percentage / 100}"])
def resp = parent.apiPUT("/lights/${selector()}/state", [brightness: percentage / 100, power: "on"])
if (resp.status < 300) {
sendEvent(name: "level", value: percentage)
sendEvent(name: "switch.setLevel", value: percentage)
sendEvent(name: "switch", value: "on")
} else {
log.error("Bad setLevel result: [${resp.status}] ${resp.data}")
@@ -85,7 +89,7 @@ def setLevel(percentage) {
def setColorTemperature(kelvin) {
log.debug "Executing 'setColorTemperature' to ${kelvin}"
parent.logErrors() {
def resp = parent.apiPUT("/lights/${device.deviceNetworkId}/color", [color: "kelvin:${kelvin}"])
def resp = parent.apiPUT("/lights/${selector()}/state", [color: "kelvin:${kelvin}", power: "on"])
if (resp.status < 300) {
sendEvent(name: "colorTemperature", value: kelvin)
sendEvent(name: "color", value: "#ffffff")
@@ -100,7 +104,7 @@ def setColorTemperature(kelvin) {
def on() {
log.debug "Device setOn"
parent.logErrors() {
if (parent.apiPUT("/lights/${device.deviceNetworkId}/power", [state: "on"]) != null) {
if (parent.apiPUT("/lights/${selector()}/state", [power: "on"]) != null) {
sendEvent(name: "switch", value: "on")
}
}
@@ -109,7 +113,7 @@ def on() {
def off() {
log.debug "Device setOff"
parent.logErrors() {
if (parent.apiPUT("/lights/${device.deviceNetworkId}/power", [state: "off"]) != null) {
if (parent.apiPUT("/lights/${selector()}/state", [power: "off"]) != null) {
sendEvent(name: "switch", value: "off")
}
}
@@ -117,16 +121,22 @@ def off() {
def poll() {
log.debug "Executing 'poll' for ${device} ${this} ${device.deviceNetworkId}"
def resp = parent.apiGET("/lights/${device.deviceNetworkId}")
if (resp.status != 200) {
def resp = parent.apiGET("/lights/${selector()}")
if (resp.status == 404) {
sendEvent(name: "switch", value: "unreachable")
return []
} else if (resp.status != 200) {
log.error("Unexpected result in poll(): [${resp.status}] ${resp.data}")
return []
}
def data = resp.data
def data = resp.data[0]
sendEvent(name: "level", value: sprintf("%f", (data.brightness ?: 1) * 100))
sendEvent(name: "label", value: data.label)
sendEvent(name: "level", value: Math.round((data.brightness ?: 1) * 100))
sendEvent(name: "switch.setLevel", value: Math.round((data.brightness ?: 1) * 100))
sendEvent(name: "switch", value: data.connected ? data.power : "unreachable")
sendEvent(name: "colorTemperature", value: data.color.kelvin)
sendEvent(name: "model", value: data.product.name)
return []
}
@@ -135,3 +145,11 @@ def refresh() {
log.debug "Executing 'refresh'"
poll()
}
def selector() {
if (device.deviceNetworkId.contains(":")) {
return device.deviceNetworkId
} else {
return "id:${device.deviceNetworkId}"
}
}

View File

@@ -5,6 +5,8 @@
that issue by using state variables
*/
//DEPRECATED - Using the generic DTH for this device. Users need to be moved before deleting this DTH
metadata {
definition (name: "OSRAM LIGHTIFY LED Tunable White 60W", namespace: "smartthings", author: "SmartThings") {
@@ -20,10 +22,7 @@ metadata {
// indicates that device keeps track of heartbeat (in state.heartbeat)
attribute "heartbeat", "string"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Classic A60 TW"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY A19 Tunable White"
}

View File

@@ -79,8 +79,8 @@ metadata {
}
preferences {
input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
input title: "Temperature Offset", description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
}
}

View File

@@ -88,8 +88,8 @@ metadata {
}
preferences {
input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
input title: "Temperature Offset", description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
}
}

View File

@@ -27,7 +27,7 @@ metadata {
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3315-S", deviceJoinName: "Water Leak Sensor"
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3315"
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3315-Seu", deviceJoinName: "Water Leak Sensor"
fingerprint inClusters: "0000,0001,0003,000F,0020,0402,0500", outClusters: "0019", manufacturer: "SmartThings", model: "moisturev4", deviceJoinName: "Water Leak Sensor"
}
simulator {
@@ -43,8 +43,8 @@ metadata {
])
}
section {
input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
input title: "Temperature Offset", description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
}
}

View File

@@ -29,6 +29,7 @@ metadata {
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3305"
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3325"
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3326"
fingerprint inClusters: "0000,0001,0003,000F,0020,0402,0500", outClusters: "0019", manufacturer: "SmartThings", model: "motionv4", deviceJoinName: "Motion Sensor"
}
simulator {
@@ -45,8 +46,8 @@ metadata {
])
}
section {
input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
input title: "Temperature Offset", description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
}
}
@@ -352,4 +353,4 @@ private byte[] reverseArray(byte[] array) {
i++;
}
return array
}
}

View File

@@ -37,8 +37,8 @@ metadata {
}
preferences {
input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
input title: "Temperature Offset", description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
}
tiles(scale: 2) {

View File

@@ -30,6 +30,7 @@
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3320"
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3321"
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3321-S", deviceJoinName: "Multipurpose Sensor"
fingerprint inClusters: "0000,0001,0003,000F,0020,0402,0500,FC02", outClusters: "0019", manufacturer: "SmartThings", model: "multiv4", deviceJoinName: "Multipurpose Sensor"
attribute "status", "string"
}
@@ -61,8 +62,8 @@
])
}
section {
input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
input title: "Temperature Offset", description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
}
section {
input("garageSensor", "enum", title: "Do you want to use this sensor on a garage door?", options: ["Yes", "No"], defaultValue: "No", required: false, displayDuringSetup: false)
@@ -115,31 +116,30 @@
}
}
def parse(String description) {
Map map = [:]
if (description?.startsWith('catchall:')) {
map = parseCatchAllMessage(description)
}
else if (description?.startsWith('read attr -')) {
map = parseReportAttributeMessage(description)
}
def parse(String description) {
Map map = [:]
if (description?.startsWith('catchall:')) {
map = parseCatchAllMessage(description)
}
else if (description?.startsWith('temperature: ')) {
map = parseCustomMessage(description)
}
else if (description?.startsWith('zone status')) {
map = parseIasMessage(description)
}
map = parseCustomMessage(description)
}
else if (description?.startsWith('zone status')) {
map = parseIasMessage(description)
}
def result = map ? createEvent(map) : null
if (description?.startsWith('enroll request')) {
List cmds = enrollResponse()
log.debug "enroll response: ${cmds}"
result = cmds?.collect { new physicalgraph.device.HubAction(it) }
}
return result
}
if (description?.startsWith('enroll request')) {
List cmds = enrollResponse()
log.debug "enroll response: ${cmds}"
result = cmds?.collect { new physicalgraph.device.HubAction(it) }
}
else if (description?.startsWith('read attr -')) {
result = parseReportAttributeMessage(description).each { createEvent(it) }
}
return result
}
private Map parseCatchAllMessage(String description) {
Map resultMap = [:]
@@ -178,28 +178,40 @@ private boolean shouldProcessMessage(cluster) {
return !ignoredMessage
}
private Map parseReportAttributeMessage(String description) {
private List parseReportAttributeMessage(String description) {
Map descMap = (description - "read attr - ").split(",").inject([:]) { map, param ->
def nameAndValue = param.split(":")
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
}
Map resultMap = [:]
List result = []
if (descMap.cluster == "0402" && descMap.attrId == "0000") {
def value = getTemperature(descMap.value)
resultMap = getTemperatureResult(value)
result << getTemperatureResult(value)
}
else if (descMap.cluster == "FC02" && descMap.attrId == "0010") {
resultMap = getAccelerationResult(descMap.value)
if (descMap.value.size() == 32) {
// value will look like 00ae29001403e2290013001629001201
// breaking this apart and swapping byte order where appropriate, this breaks down to:
// X (0x0012) = 0x0016
// Y (0x0013) = 0x03E2
// Z (0x0014) = 0x00AE
// note that there is a known bug in that the x,y,z attributes are interpreted in the wrong order
// this will be fixed in a future update
def threeAxisAttributes = descMap.value[0..-9]
result << parseAxis(threeAxisAttributes)
descMap.value = descMap.value[-2..-1]
}
result << getAccelerationResult(descMap.value)
}
else if (descMap.cluster == "FC02" && descMap.attrId == "0012") {
resultMap = parseAxis(descMap.value)
else if (descMap.cluster == "FC02" && descMap.attrId == "0012") {
result << parseAxis(descMap.value)
}
else if (descMap.cluster == "0001" && descMap.attrId == "0020") {
resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16))
result << getBatteryResult(Integer.parseInt(descMap.value, 16))
}
return resultMap
return result
}
private Map parseCustomMessage(String description) {
@@ -363,29 +375,17 @@ def getTemperature(value) {
/* sensitivity - default value (8) */
"zcl mfg-code 0x104E", "delay 200",
"zcl mfg-code ${manufacturerCode}", "delay 200",
"zcl global write 0xFC02 0 0x20 {02}", "delay 200",
"send 0x${device.deviceNetworkId} 1 1", "delay 400",
"st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200",
"st rattr 0x${device.deviceNetworkId} 1 1 0x20", "delay 200",
"zcl mfg-code 0x104E", "delay 200",
"zcl mfg-code ${manufacturerCode}", "delay 200",
"zcl global read 0xFC02 0x0010",
"send 0x${device.deviceNetworkId} 1 1","delay 400",
"zcl mfg-code 0x104E", "delay 200",
"zcl global read 0xFC02 0x0012",
"send 0x${device.deviceNetworkId} 1 1","delay 400",
"zcl mfg-code 0x104E", "delay 200",
"zcl global read 0xFC02 0x0013",
"send 0x${device.deviceNetworkId} 1 1","delay 400",
"zcl mfg-code 0x104E", "delay 200",
"zcl global read 0xFC02 0x0014",
"send 0x${device.deviceNetworkId} 1 1", "delay 400"
]
"send 0x${device.deviceNetworkId} 1 1","delay 400"
]
return refreshCmds + enrollResponse()
}
@@ -409,19 +409,19 @@ def getTemperature(value) {
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0xFC02 {${device.zigbeeId}} {}", "delay 200",
"zcl mfg-code 0x104E",
"zcl mfg-code ${manufacturerCode}",
"zcl global send-me-a-report 0xFC02 0x0010 0x18 10 3600 {01}",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
"zcl mfg-code 0x104E",
"zcl mfg-code ${manufacturerCode}",
"zcl global send-me-a-report 0xFC02 0x0012 0x29 1 3600 {01}",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
"zcl mfg-code 0x104E",
"zcl mfg-code ${manufacturerCode}",
"zcl global send-me-a-report 0xFC02 0x0013 0x29 1 3600 {01}",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
"zcl mfg-code 0x104E",
"zcl mfg-code ${manufacturerCode}",
"zcl global send-me-a-report 0xFC02 0x0014 0x29 1 3600 {01}",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500"
@@ -519,6 +519,14 @@ private Map getXyzResult(results, description) {
]
}
private getManufacturerCode() {
if (device.getDataValue("manufacturer") == "SmartThings") {
return "0x110A"
} else {
return "0x104E"
}
}
private hexToInt(value) {
new BigInteger(value, 16)
}

View File

@@ -43,8 +43,8 @@ metadata {
}
preferences {
input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
input title: "Temperature Offset", description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
}
tiles(scale: 2) {

View File

@@ -32,8 +32,8 @@
}
preferences {
input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
input title: "Temperature Offset", description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
}
tiles(scale: 2) {

View File

@@ -34,8 +34,8 @@ metadata {
}
preferences {
input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
input title: "Temperature Offset", description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
}
tiles(scale: 2) {

View File

@@ -33,8 +33,8 @@ metadata {
}
preferences {
input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
input title: "Temperature Offset", description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
}
tiles(scale: 2) {

View File

@@ -45,8 +45,8 @@ metadata {
}
preferences {
input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
input title: "Temperature Offset", description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
}
tiles {

View File

@@ -11,6 +11,9 @@
* for the specific language governing permissions and limitations under the License.
*
*/
//DEPRECATED - Using the generic DTH for this device. Users need to be moved before deleting this DTH
metadata {
definition (name: "Sylvania Ultra iQ", namespace:"smartthings", author: "SmartThings") {
capability "Switch Level"

View File

@@ -1,5 +1,5 @@
metadata {
definition (name: "Color Control Capability", namespace: "capabilities", author: "SmartThings") {
definition (name: "Simulated Color Control", namespace: "smartthings/testing", author: "SmartThings") {
capability "Color Control"
}

View File

@@ -33,8 +33,8 @@ metadata {
}
preferences {
input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
input title: "Temperature Offset", description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
}
tiles {

View File

@@ -33,14 +33,14 @@ metadata {
state "power", label: '${currentValue} W'
}
tile(name: "powerChart", attribute: "powerContent", type: "HTML", url: '${currentValue}', width: 3, height: 2) { }
htmlTile(name: "powerContent", attribute: "powerContent", type: "HTML", whitelist: "www.wattvision.com" , url: '${currentValue}', width: 3, height: 2)
standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat") {
state "default", label: '', action: "refresh.refresh", icon: "st.secondary.refresh"
}
main "power"
details(["powerChart", "power", "refresh"])
details(["powerContent", "power", "refresh"])
}
}
@@ -74,10 +74,10 @@ public addWattvisionData(json) {
log.trace "Adding data from Wattvision"
def data = json.data
def data = parseJson(json.data.toString())
def units = json.units ?: "watts"
if (data) {
if (data.size() > 0) {
def latestData = data[-1]
data.each {
sendPowerEvent(it.t, it.v, units, (latestData == it))
@@ -103,3 +103,7 @@ private sendPowerEvent(time, value, units, isLatest = false) {
sendEvent(eventData)
}
def parseJson(String s) {
new groovy.json.JsonSlurper().parseText(s)
}

View File

@@ -15,6 +15,8 @@
* Thanks to Chad Monroe @cmonroe and Patrick Stuart @pstuart
*
*/
//DEPRECATED - Using the generic DTH for this device. Users need to be moved before deleting this DTH
metadata {
definition (name: "WeMo Bulb", namespace: "smartthings", author: "SmartThings") {
@@ -25,7 +27,6 @@ metadata {
capability "Switch"
capability "Switch Level"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,FF00", outClusters: "0019"
}
// simulator metadata

View File

@@ -25,6 +25,8 @@ metadata {
capability "Refresh"
capability "Sensor"
attribute "currentIP", "string"
command "subscribe"
command "resubscribe"
command "unsubscribe"
@@ -34,21 +36,36 @@ metadata {
// simulator metadata
simulator {}
// UI tile definitions
tiles {
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
state "turningOn", label:'${name}', icon:"st.switches.switch.on", backgroundColor:"#79b821"
state "turningOff", label:'${name}', icon:"st.switches.switch.off", backgroundColor:"#ffffff"
}
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
}
// UI tile definitions
tiles(scale: 2) {
multiAttributeTile(name:"rich-control", type: "switch", canChangeIcon: true){
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
attributeState "on", label:'${name}', action:"switch.off", icon:"st.Home.home30", backgroundColor:"#79b821", nextState:"turningOff"
attributeState "off", label:'${name}', action:"switch.on", icon:"st.Home.home30", backgroundColor:"#ffffff", nextState:"turningOn"
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.Home.home30", backgroundColor:"#79b821", nextState:"turningOff"
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.Home.home30", backgroundColor:"#ffffff", nextState:"turningOn"
attributeState "offline", label:'${name}', icon:"st.Home.home30", backgroundColor:"#ff0000"
}
tileAttribute ("currentIP", key: "SECONDARY_CONTROL") {
attributeState "currentIP", label: ''
}
}
main "switch"
details (["switch", "refresh"])
}
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
state "on", label:'${name}', action:"switch.off", icon:"st.Home.home30", backgroundColor:"#79b821", nextState:"turningOff"
state "off", label:'${name}', action:"switch.on", icon:"st.Home.home30", backgroundColor:"#ffffff", nextState:"turningOn"
state "turningOn", label:'${name}', action:"switch.off", icon:"st.Home.home30", backgroundColor:"#79b821", nextState:"turningOff"
state "turningOff", label:'${name}', action:"switch.on", icon:"st.Home.home30", backgroundColor:"#ffffff", nextState:"turningOn"
state "offline", label:'${name}', icon:"st.Home.home30", backgroundColor:"#ff0000"
}
standardTile("refresh", "device.switch", inactiveLabel: false, height: 2, width: 2, decoration: "flat") {
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
}
main(["switch"])
details(["rich-control", "refresh"])
}
}
// parse events into attributes
@@ -68,6 +85,7 @@ def parse(String description) {
def result = []
def bodyString = msg.body
if (bodyString) {
unschedule("setOffline")
def body = new XmlSlurper().parseText(bodyString)
if (body?.property?.TimeSyncRequest?.text()) {
@@ -78,13 +96,14 @@ def parse(String description) {
} else if (body?.property?.BinaryState?.text()) {
def value = body?.property?.BinaryState?.text().toInteger() == 1 ? "on" : "off"
log.trace "Notify: BinaryState = ${value}"
result << createEvent(name: "switch", value: value)
result << createEvent(name: "switch", value: value, descriptionText: "Switch is ${value}")
} else if (body?.property?.TimeZoneNotification?.text()) {
log.debug "Notify: TimeZoneNotification = ${body?.property?.TimeZoneNotification?.text()}"
} else if (body?.Body?.GetBinaryStateResponse?.BinaryState?.text()) {
def value = body?.Body?.GetBinaryStateResponse?.BinaryState?.text().toInteger() == 1 ? "on" : "off"
log.trace "GetBinaryResponse: BinaryState = ${value}"
result << createEvent(name: "switch", value: value)
def dispaux = device.currentValue("switch") != value
result << createEvent(name: "switch", value: value, descriptionText: "Switch is ${value}", displayed: dispaux)
}
}
@@ -101,14 +120,6 @@ private getCallBackAddress() {
device.hub.getDataValue("localIP") + ":" + device.hub.getDataValue("localSrvPortTCP")
}
private Integer convertHexToInt(hex) {
Integer.parseInt(hex,16)
}
private String convertHexToIP(hex) {
[convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
}
private getHostAddress() {
def ip = getDataValue("ip")
def port = getDataValue("port")
@@ -195,6 +206,8 @@ def subscribe(ip, port) {
if (ip && ip != existingIp) {
log.debug "Updating ip from $existingIp to $ip"
updateDataValue("ip", ip)
def ipvalue = convertHexToIP(getDataValue("ip"))
sendEvent(name: "currentIP", value: ipvalue, descriptionText: "IP changed to ${ipvalue}")
}
if (port && port != existingPort) {
log.debug "Updating port from $existingPort to $port"
@@ -259,6 +272,8 @@ User-Agent: CyberGarage-HTTP/1.0
def poll() {
log.debug "Executing 'poll'"
if (device.currentValue("currentIP") != "Offline")
runIn(10, setOffline)
new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1
SOAPACTION: "urn:Belkin:service:basicevent:1#GetBinaryState"
Content-Length: 277
@@ -274,3 +289,15 @@ User-Agent: CyberGarage-HTTP/1.0
</s:Body>
</s:Envelope>""", physicalgraph.device.Protocol.LAN)
}
def setOffline() {
sendEvent(name: "switch", value: "offline", descriptionText: "The device is offline")
}
private Integer convertHexToInt(hex) {
Integer.parseInt(hex,16)
}
private String convertHexToIP(hex) {
[convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
}

View File

@@ -21,6 +21,8 @@
capability "Refresh"
capability "Sensor"
attribute "currentIP", "string"
command "subscribe"
command "resubscribe"
command "unsubscribe"
@@ -31,17 +33,30 @@
}
// UI tile definitions
tiles {
tiles(scale: 2) {
multiAttributeTile(name:"rich-control", type: "motion", canChangeIcon: true){
tileAttribute ("device.motion", key: "PRIMARY_CONTROL") {
attributeState "active", label:'motion', icon:"st.motion.motion.active", backgroundColor:"#53a7c0"
attributeState "inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff"
attributeState "offline", label:'${name}', icon:"st.motion.motion.active", backgroundColor:"#ff0000"
}
tileAttribute ("currentIP", key: "SECONDARY_CONTROL") {
attributeState "currentIP", label: ''
}
}
standardTile("motion", "device.motion", width: 2, height: 2) {
state("active", label:'motion', icon:"st.motion.motion.active", backgroundColor:"#53a7c0")
state("inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff")
}
standardTile("refresh", "device.motion", inactiveLabel: false, decoration: "flat") {
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
state("offline", label:'${name}', icon:"st.motion.motion.inactive", backgroundColor:"#ff0000")
}
standardTile("refresh", "device.switch", inactiveLabel: false, height: 2, width: 2, decoration: "flat") {
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
}
main "motion"
details (["motion", "refresh"])
details (["rich-control", "refresh"])
}
}
@@ -62,6 +77,7 @@ def parse(String description) {
def result = []
def bodyString = msg.body
if (bodyString) {
unschedule("setOffline")
def body = new XmlSlurper().parseText(bodyString)
if (body?.property?.TimeSyncRequest?.text()) {
@@ -72,7 +88,7 @@ def parse(String description) {
} else if (body?.property?.BinaryState?.text()) {
def value = body?.property?.BinaryState?.text().toInteger() == 1 ? "active" : "inactive"
log.debug "Notify - BinaryState = ${value}"
result << createEvent(name: "motion", value: value)
result << createEvent(name: "motion", value: value, descriptionText: "Motion is ${value}")
} else if (body?.property?.TimeZoneNotification?.text()) {
log.debug "Notify: TimeZoneNotification = ${body?.property?.TimeZoneNotification?.text()}"
}
@@ -91,14 +107,6 @@ private getCallBackAddress() {
device.hub.getDataValue("localIP") + ":" + device.hub.getDataValue("localSrvPortTCP")
}
private Integer convertHexToInt(hex) {
Integer.parseInt(hex,16)
}
private String convertHexToIP(hex) {
[convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
}
private getHostAddress() {
def ip = getDataValue("ip")
def port = getDataValue("port")
@@ -125,6 +133,8 @@ def refresh() {
////////////////////////////
def getStatus() {
log.debug "Executing WeMo Motion 'getStatus'"
if (device.currentValue("currentIP") != "Offline")
runIn(10, setOffline)
new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1
SOAPACTION: "urn:Belkin:service:basicevent:1#GetBinaryState"
Content-Length: 277
@@ -165,7 +175,9 @@ def subscribe(ip, port) {
def existingPort = getDataValue("port")
if (ip && ip != existingIp) {
log.debug "Updating ip from $existingIp to $ip"
updateDataValue("ip", ip)
updateDataValue("ip", ip)
def ipvalue = convertHexToIP(getDataValue("ip"))
sendEvent(name: "currentIP", value: ipvalue, descriptionText: "IP changed to ${ipvalue}")
}
if (port && port != existingPort) {
log.debug "Updating port from $existingPort to $port"
@@ -226,3 +238,15 @@ User-Agent: CyberGarage-HTTP/1.0
</s:Envelope>
""", physicalgraph.device.Protocol.LAN)
}
def setOffline() {
sendEvent(name: "motion", value: "offline", descriptionText: "The device is offline")
}
private Integer convertHexToInt(hex) {
Integer.parseInt(hex,16)
}
private String convertHexToIP(hex) {
[convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
}

View File

@@ -10,120 +10,142 @@
* 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.
*
* Wemo Switch
* Wemo Switch
*
* Author: superuser
* Date: 2013-10-11
* Author: Juan Risso (SmartThings)
* Date: 2015-10-11
*/
metadata {
definition (name: "Wemo Switch", namespace: "smartthings", author: "SmartThings") {
capability "Actuator"
capability "Switch"
capability "Polling"
capability "Refresh"
capability "Sensor"
definition (name: "Wemo Switch", namespace: "smartthings", author: "SmartThings") {
capability "Actuator"
capability "Switch"
capability "Polling"
capability "Refresh"
capability "Sensor"
command "subscribe"
command "resubscribe"
command "unsubscribe"
}
attribute "currentIP", "string"
// simulator metadata
simulator {}
command "subscribe"
command "resubscribe"
command "unsubscribe"
}
// UI tile definitions
tiles {
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821"
state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff"
}
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
}
// simulator metadata
simulator {}
main "switch"
details (["switch", "refresh"])
}
// UI tile definitions
tiles(scale: 2) {
multiAttributeTile(name:"rich-control", type: "switch", canChangeIcon: true){
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
attributeState "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.off", backgroundColor:"#79b821", nextState:"turningOff"
attributeState "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.on", backgroundColor:"#ffffff", nextState:"turningOn"
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.off", backgroundColor:"#79b821", nextState:"turningOff"
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.on", backgroundColor:"#ffffff", nextState:"turningOn"
attributeState "offline", label:'${name}', icon:"st.switches.switch.off", backgroundColor:"#ff0000"
}
tileAttribute ("currentIP", key: "SECONDARY_CONTROL") {
attributeState "currentIP", label: ''
}
}
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.off", backgroundColor:"#79b821", nextState:"turningOff"
state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.on", backgroundColor:"#ffffff", nextState:"turningOn"
state "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.off", backgroundColor:"#79b821", nextState:"turningOff"
state "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.on", backgroundColor:"#ffffff", nextState:"turningOn"
state "offline", label:'${name}', icon:"st.switches.switch.off", backgroundColor:"#ff0000"
}
standardTile("refresh", "device.switch", inactiveLabel: false, height: 2, width: 2, decoration: "flat") {
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
}
main(["switch"])
details(["rich-control", "refresh"])
}
}
// parse events into attributes
def parse(String description) {
log.debug "Parsing '${description}'"
log.debug "Parsing '${description}'"
def msg = parseLanMessage(description)
def headerString = msg.header
def msg = parseLanMessage(description)
def headerString = msg.header
if (headerString?.contains("SID: uuid:")) {
def sid = (headerString =~ /SID: uuid:.*/) ? ( headerString =~ /SID: uuid:.*/)[0] : "0"
sid -= "SID: uuid:".trim()
if (headerString?.contains("SID: uuid:")) {
def sid = (headerString =~ /SID: uuid:.*/) ? ( headerString =~ /SID: uuid:.*/)[0] : "0"
sid -= "SID: uuid:".trim()
updateDataValue("subscriptionId", sid)
}
updateDataValue("subscriptionId", sid)
}
def result = []
def bodyString = msg.body
if (bodyString) {
def body = new XmlSlurper().parseText(bodyString)
if (body?.property?.TimeSyncRequest?.text()) {
log.trace "Got TimeSyncRequest"
result << timeSyncResponse()
} else if (body?.Body?.SetBinaryStateResponse?.BinaryState?.text()) {
log.trace "Got SetBinaryStateResponse = ${body?.Body?.SetBinaryStateResponse?.BinaryState?.text()}"
} else if (body?.property?.BinaryState?.text()) {
def value = body?.property?.BinaryState?.text().toInteger() == 1 ? "on" : "off"
log.trace "Notify: BinaryState = ${value}"
result << createEvent(name: "switch", value: value)
} else if (body?.property?.TimeZoneNotification?.text()) {
log.debug "Notify: TimeZoneNotification = ${body?.property?.TimeZoneNotification?.text()}"
} else if (body?.Body?.GetBinaryStateResponse?.BinaryState?.text()) {
def value = body?.Body?.GetBinaryStateResponse?.BinaryState?.text().toInteger() == 1 ? "on" : "off"
log.trace "GetBinaryResponse: BinaryState = ${value}"
result << createEvent(name: "switch", value: value)
}
}
result
def result = []
def bodyString = msg.body
if (bodyString) {
unschedule("setOffline")
def body = new XmlSlurper().parseText(bodyString)
if (body?.property?.TimeSyncRequest?.text()) {
log.trace "Got TimeSyncRequest"
result << timeSyncResponse()
} else if (body?.Body?.SetBinaryStateResponse?.BinaryState?.text()) {
log.trace "Got SetBinaryStateResponse = ${body?.Body?.SetBinaryStateResponse?.BinaryState?.text()}"
} else if (body?.property?.BinaryState?.text()) {
def value = body?.property?.BinaryState?.text().substring(0, 1).toInteger() == 0 ? "off" : "on"
log.trace "Notify: BinaryState = ${value}, ${body.property.BinaryState}"
def dispaux = device.currentValue("switch") != value
result << createEvent(name: "switch", value: value, descriptionText: "Switch is ${value}", displayed: dispaux)
} else if (body?.property?.TimeZoneNotification?.text()) {
log.debug "Notify: TimeZoneNotification = ${body?.property?.TimeZoneNotification?.text()}"
} else if (body?.Body?.GetBinaryStateResponse?.BinaryState?.text()) {
def value = body?.Body?.GetBinaryStateResponse?.BinaryState?.text().substring(0, 1).toInteger() == 0 ? "off" : "on"
log.trace "GetBinaryResponse: BinaryState = ${value}, ${body.property.BinaryState}"
log.info "Connection: ${device.currentValue("connection")}"
if (device.currentValue("currentIP") == "Offline") {
def ipvalue = convertHexToIP(getDataValue("ip"))
sendEvent(name: "IP", value: ipvalue, descriptionText: "IP is ${ipvalue}")
}
def dispaux2 = device.currentValue("switch") != value
result << createEvent(name: "switch", value: value, descriptionText: "Switch is ${value}", displayed: dispaux2)
}
}
result
}
private getTime() {
// This is essentially System.currentTimeMillis()/1000, but System is disallowed by the sandbox.
((new GregorianCalendar().time.time / 1000l).toInteger()).toString()
// This is essentially System.currentTimeMillis()/1000, but System is disallowed by the sandbox.
((new GregorianCalendar().time.time / 1000l).toInteger()).toString()
}
private getCallBackAddress() {
device.hub.getDataValue("localIP") + ":" + device.hub.getDataValue("localSrvPortTCP")
device.hub.getDataValue("localIP") + ":" + device.hub.getDataValue("localSrvPortTCP")
}
private Integer convertHexToInt(hex) {
Integer.parseInt(hex,16)
Integer.parseInt(hex,16)
}
private String convertHexToIP(hex) {
[convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
[convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
}
private getHostAddress() {
def ip = getDataValue("ip")
def port = getDataValue("port")
if (!ip || !port) {
def parts = device.deviceNetworkId.split(":")
if (parts.length == 2) {
ip = parts[0]
port = parts[1]
} else {
log.warn "Can't figure out ip and port for device: ${device.id}"
}
}
log.debug "Using ip: ${ip} and port: ${port} for device: ${device.id}"
return convertHexToIP(ip) + ":" + convertHexToInt(port)
def ip = getDataValue("ip")
def port = getDataValue("port")
if (!ip || !port) {
def parts = device.deviceNetworkId.split(":")
if (parts.length == 2) {
ip = parts[0]
port = parts[1]
} else {
log.warn "Can't figure out ip and port for device: ${device.id}"
}
}
log.debug "Using ip: ${ip} and port: ${port} for device: ${device.id}"
return convertHexToIP(ip) + ":" + convertHexToInt(port)
}
def on() {
log.debug "Executing 'on'"
sendEvent(name: "switch", value: "on")
log.debug "Executing 'on'"
def turnOn = new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1
SOAPAction: "urn:Belkin:service:basicevent:1#SetBinaryState"
Host: ${getHostAddress()}
@@ -133,17 +155,16 @@ Content-Length: 333
<?xml version="1.0"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<SOAP-ENV:Body>
<m:SetBinaryState xmlns:m="urn:Belkin:service:basicevent:1">
<m:SetBinaryState xmlns:m="urn:Belkin:service:basicevent:1">
<BinaryState>1</BinaryState>
</m:SetBinaryState>
</m:SetBinaryState>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>""", physicalgraph.device.Protocol.LAN)
}
def off() {
log.debug "Executing 'off'"
sendEvent(name: "switch", value: "off")
def turnOff = new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1
log.debug "Executing 'off'"
def turnOff = new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1
SOAPAction: "urn:Belkin:service:basicevent:1#SetBinaryState"
Host: ${getHostAddress()}
Content-Type: text/xml
@@ -152,36 +173,13 @@ Content-Length: 333
<?xml version="1.0"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<SOAP-ENV:Body>
<m:SetBinaryState xmlns:m="urn:Belkin:service:basicevent:1">
<m:SetBinaryState xmlns:m="urn:Belkin:service:basicevent:1">
<BinaryState>0</BinaryState>
</m:SetBinaryState>
</m:SetBinaryState>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>""", physicalgraph.device.Protocol.LAN)
}
/*def refresh() {
log.debug "Executing 'refresh'"
new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1
SOAPACTION: "urn:Belkin:service:basicevent:1#GetBinaryState"
Content-Length: 277
Content-Type: text/xml; charset="utf-8"
HOST: ${getHostAddress()}
User-Agent: CyberGarage-HTTP/1.0
<?xml version="1.0" encoding="utf-8"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:GetBinaryState xmlns:u="urn:Belkin:service:basicevent:1">
</u:GetBinaryState>
</s:Body>
</s:Envelope>""", physicalgraph.device.Protocol.LAN)
}*/
def refresh() {
log.debug "Executing WeMo Switch 'subscribe', then 'timeSyncResponse', then 'poll'"
[subscribe(), timeSyncResponse(), poll()]
}
def subscribe(hostAddress) {
log.debug "Executing 'subscribe()'"
def address = getCallBackAddress()
@@ -200,27 +198,30 @@ def subscribe() {
subscribe(getHostAddress())
}
def subscribe(ip, port) {
def existingIp = getDataValue("ip")
def existingPort = getDataValue("port")
if (ip && ip != existingIp) {
log.debug "Updating ip from $existingIp to $ip"
updateDataValue("ip", ip)
}
if (port && port != existingPort) {
log.debug "Updating port from $existingPort to $port"
updateDataValue("port", port)
}
def refresh() {
log.debug "Executing WeMo Switch 'subscribe', then 'timeSyncResponse', then 'poll'"
[subscribe(), timeSyncResponse(), poll()]
}
def subscribe(ip, port) {
def existingIp = getDataValue("ip")
def existingPort = getDataValue("port")
if (ip && ip != existingIp) {
log.debug "Updating ip from $existingIp to $ip"
updateDataValue("ip", ip)
def ipvalue = convertHexToIP(getDataValue("ip"))
sendEvent(name: "currentIP", value: ipvalue, descriptionText: "IP changed to ${ipvalue}")
}
if (port && port != existingPort) {
log.debug "Updating port from $existingPort to $port"
updateDataValue("port", port)
}
subscribe("${ip}:${port}")
}
////////////////////////////
def resubscribe() {
log.debug "Executing 'resubscribe()'"
def sid = getDeviceDataByName("subscriptionId")
log.debug "Executing 'resubscribe()'"
def sid = getDeviceDataByName("subscriptionId")
new physicalgraph.device.HubAction("""SUBSCRIBE /upnp/event/basicevent1 HTTP/1.1
HOST: ${getHostAddress()}
SID: uuid:${sid}
@@ -228,12 +229,11 @@ TIMEOUT: Second-5400
""", physicalgraph.device.Protocol.LAN)
}
////////////////////////////
def unsubscribe() {
def sid = getDeviceDataByName("subscriptionId")
def sid = getDeviceDataByName("subscriptionId")
new physicalgraph.device.HubAction("""UNSUBSCRIBE publisher path HTTP/1.1
HOST: ${getHostAddress()}
SID: uuid:${sid}
@@ -242,7 +242,7 @@ SID: uuid:${sid}
""", physicalgraph.device.Protocol.LAN)
}
////////////////////////////
//TODO: Use UTC Timezone
def timeSyncResponse() {
log.debug "Executing 'timeSyncResponse()'"
@@ -267,9 +267,15 @@ User-Agent: CyberGarage-HTTP/1.0
""", physicalgraph.device.Protocol.LAN)
}
def setOffline() {
//sendEvent(name: "currentIP", value: "Offline", displayed: false)
sendEvent(name: "switch", value: "offline", descriptionText: "The device is offline")
}
def poll() {
log.debug "Executing 'poll'"
if (device.currentValue("currentIP") != "Offline")
runIn(10, setOffline)
new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1
SOAPACTION: "urn:Belkin:service:basicevent:1#GetBinaryState"
Content-Length: 277

View File

@@ -24,6 +24,9 @@ metadata {
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B04"
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702"
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702, 0B05", outClusters: "0019", manufacturer: "sengled", model: "Z01-CIA19NAE26", deviceJoinName: "Sengled Element touch"
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702, 0B05", outClusters: "000A, 0019", manufacturer: "Jasco Products", model: "45852", deviceJoinName: "GE Zigbee Plug-In Dimmer"
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702, 0B05", outClusters: "000A, 0019", manufacturer: "Jasco Products", model: "45857", deviceJoinName: "GE Zigbee In-Wall Dimmer"
}
tiles(scale: 2) {

View File

@@ -17,12 +17,14 @@ metadata {
capability "Actuator"
capability "Configuration"
capability "Refresh"
capability "Sensor"
capability "Switch"
capability "Switch Level"
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008"
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY A19 ON/OFF/DIM", deviceJoinName: "OSRAM LIGHTIFY LED Smart Connected Light"
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, FF00", outClusters: "0019", manufacturer: "MRVL", model: "MZ100", deviceJoinName: "Wemo Bulb"
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B05", outClusters: "0019", manufacturer: "OSRAM SYLVANIA", model: "iQBR30", deviceJoinName: "Sylvania Ultra iQ"
}
tiles(scale: 2) {

View File

@@ -32,7 +32,13 @@
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0004,0005,0009,0020,0101,0402,0B05,FDBD", outClusters: "000A,0019",
manufacturer: "Kwikset", model: "SMARTCODE_DEADBOLT_10T", deviceJoinName: "Kwikset 10-Button Touch Deadbolt"
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,000A,0101,0020", outClusters: "000A,0019",
manufacturer: "Yale", model: "YRL220 TS LL", deviceJoinName: "Yale YRL220 Lock"
manufacturer: "Yale", model: "YRL220 TS LL", deviceJoinName: "Yale Touch Screen Lever Lock"
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,000A,0101,0020", outClusters: "000A,0019",
manufacturer: "Yale", model: "YRD210 PB DB", deviceJoinName: "Yale Push Button Deadbolt Lock"
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,000A,0101,0020", outClusters: "000A,0019",
manufacturer: "Yale", model: "YRD220/240 TSDB", deviceJoinName: "Yale Touch Screen Deadbolt Lock"
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,000A,0101,0020", outClusters: "000A,0019",
manufacturer: "Yale", model: "YRL210 PB LL", deviceJoinName: "Yale Push Button Lever Lock"
}
tiles(scale: 2) {
@@ -85,11 +91,24 @@ 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}", 3600, 3600, "{01}")
"${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",
]
log.info "configure() --- cmds: $cmds"
return cmds + refresh() // send refresh cmds as part of config
}
@@ -117,15 +136,17 @@ def parse(String description) {
// Lock capability commands
def lock() {
def cmds = zigbee.zigbeeCommand("${CLUSTER_DOORLOCK}", "${DOORLOCK_CMD_LOCK_DOOR}", "{}")
log.info "lock() -- cmds: $cmds"
return cmds
//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 unlock() {
def cmds = zigbee.zigbeeCommand("${CLUSTER_DOORLOCK}", "${DOORLOCK_CMD_UNLOCK_DOOR}", "{}")
log.info "unlock() -- cmds: $cmds"
return cmds
//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} {}"
}
// Private methods
@@ -139,8 +160,10 @@ private Map parseReportAttributeMessage(String description) {
Map resultMap = [:]
if (descMap.clusterInt == CLUSTER_POWER && descMap.attrInt == POWER_ATTR_BATTERY_PERCENTAGE_REMAINING) {
resultMap.name = "battery"
// BatteryPercentageRemaining is specified in .5% increments
resultMap.value = Integer.parseInt(descMap.value, 16) / 2
resultMap.value = Math.round(Integer.parseInt(descMap.value, 16) / 2)
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) {

View File

@@ -23,6 +23,8 @@ metadata {
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0B04"
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0702"
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0702, 0B05", outClusters: "0003, 000A, 0019", manufacturer: "Jasco Products", model: "45853", deviceJoinName: "GE ZigBee Plug-In Switch"
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0702, 0B05", outClusters: "000A, 0019", manufacturer: "Jasco Products", model: "45856", deviceJoinName: "GE ZigBee In-Wall Switch"
}
tiles(scale: 2) {

View File

@@ -17,7 +17,6 @@ metadata {
capability "Actuator"
capability "Configuration"
capability "Refresh"
capability "Sensor"
capability "Switch"
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006"

View File

@@ -23,14 +23,18 @@ metadata {
capability "Color Temperature"
capability "Configuration"
capability "Refresh"
capability "Sensor"
capability "Switch"
capability "Switch Level"
attribute "colorName", "string"
command "setGenericName"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04", outClusters: "0019"
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300", outClusters: "0019"
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B04", outClusters: "0019"
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY BR Tunable White", deviceJoinName: "OSRAM LIGHTIFY LED Flood BR30 Tunable White"
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY RT Tunable White", deviceJoinName: "OSRAM LIGHTIFY LED Recessed Kit RT 5/6 Tunable White"
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Classic A60 TW", deviceJoinName: "OSRAM LIGHTIFY LED Tunable White 60W"
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY A19 Tunable White", deviceJoinName: "OSRAM LIGHTIFY LED Tunable White 60W"
}
// UI tile definitions

View File

@@ -66,9 +66,20 @@ metadata {
import physicalgraph.zwave.commands.doorlockv1.*
import physicalgraph.zwave.commands.usercodev1.*
def updated() {
try {
if (!state.init) {
state.init = true
response(secureSequence([zwave.doorLockV1.doorLockOperationGet(), zwave.batteryV1.batteryGet()]))
}
} catch (e) {
log.warn "updated() threw $e"
}
}
def parse(String description) {
def result = null
if (description.startsWith("Err")) {
if (description.startsWith("Err 106")) {
if (state.sec) {
result = createEvent(descriptionText:description, displayed:false)
} else {
@@ -80,6 +91,8 @@ def parse(String description) {
displayed: true,
)
}
} else if (description == "updated") {
return null
} else {
def cmd = zwave.parse(description, [ 0x98: 1, 0x72: 2, 0x85: 2, 0x86: 1 ])
if (cmd) {
@@ -286,7 +299,7 @@ def zwaveEvent(physicalgraph.zwave.commands.alarmv2.AlarmReport cmd) {
}
break
case 167:
if (!state.lastbatt || (new Date().time) - state.lastbatt > 12*60*60*1000) {
if (!state.lastbatt || now() - state.lastbatt > 12*60*60*1000) {
map = [ descriptionText: "$device.displayName: battery low", isStateChange: true ]
result << response(secure(zwave.batteryV1.batteryGet()))
} else {
@@ -431,7 +444,7 @@ def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) {
} else {
map.value = cmd.batteryLevel
}
state.lastbatt = new Date().time
state.lastbatt = now()
createEvent(map)
}
@@ -499,15 +512,14 @@ def refresh() {
cmds << "delay 4200"
cmds << zwave.associationV1.associationGet(groupingIdentifier:2).format() // old Schlage locks use group 2 and don't secure the Association CC
cmds << secure(zwave.associationV1.associationGet(groupingIdentifier:1))
state.associationQuery = new Date().time
} else if (new Date().time - state.associationQuery.toLong() > 9000) {
log.debug "setting association"
state.associationQuery = now()
} else if (secondsPast(state.associationQuery, 9)) {
cmds << "delay 6000"
cmds << zwave.associationV1.associationSet(groupingIdentifier:2, nodeId:zwaveHubNodeId).format()
cmds << secure(zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId))
cmds << zwave.associationV1.associationGet(groupingIdentifier:2).format()
cmds << secure(zwave.associationV1.associationGet(groupingIdentifier:1))
state.associationQuery = new Date().time
state.associationQuery = now()
}
log.debug "refresh sending ${cmds.inspect()}"
cmds
@@ -515,55 +527,22 @@ def refresh() {
def poll() {
def cmds = []
if (state.assoc != zwaveHubNodeId && secondsPast(state.associationQuery, 19 * 60)) {
log.debug "setting association"
cmds << zwave.associationV1.associationSet(groupingIdentifier:2, nodeId:zwaveHubNodeId).format()
cmds << secure(zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId))
cmds << zwave.associationV1.associationGet(groupingIdentifier:2).format()
cmds << "delay 6000"
cmds << secure(zwave.associationV1.associationGet(groupingIdentifier:1))
cmds << "delay 6000"
state.associationQuery = new Date().time
} else {
// Only check lock state if it changed recently or we haven't had an update in an hour
def latest = device.currentState("lock")?.date?.time
if (!latest || !secondsPast(latest, 6 * 60) || secondsPast(state.lastPoll, 55 * 60)) {
cmds << secure(zwave.doorLockV1.doorLockOperationGet())
state.lastPoll = (new Date()).time
} else if (!state.MSR) {
cmds << zwave.manufacturerSpecificV1.manufacturerSpecificGet().format()
} else if (!state.fw) {
cmds << zwave.versionV1.versionGet().format()
} else if (!state.codes) {
state.pollCode = 1
cmds << secure(zwave.userCodeV1.usersNumberGet())
} else if (state.pollCode && state.pollCode <= state.codes) {
cmds << requestCode(state.pollCode)
} else if (!state.lastbatt || (new Date().time) - state.lastbatt > 53*60*60*1000) {
cmds << secure(zwave.batteryV1.batteryGet())
} else if (!state.enc) {
encryptCodes()
state.enc = 1
}
// Only check lock state if it changed recently or we haven't had an update in an hour
def latest = device.currentState("lock")?.date?.time
if (!latest || !secondsPast(latest, 6 * 60) || secondsPast(state.lastPoll, 55 * 60)) {
cmds << secure(zwave.doorLockV1.doorLockOperationGet())
state.lastPoll = now()
} else if (!state.lastbatt || now() - state.lastbatt > 53*60*60*1000) {
cmds << secure(zwave.batteryV1.batteryGet())
state.lastbatt = now() //inside-214
}
log.debug "poll is sending ${cmds.inspect()}"
device.activity()
cmds ?: null
}
private def encryptCodes() {
def keys = new ArrayList(state.keySet().findAll { it.startsWith("code") })
keys.each { key ->
def match = (key =~ /^code(\d+)$/)
if (match) try {
def keynum = match[0][1].toInteger()
if (keynum > 30 && !state[key]) {
state.remove(key)
} else if (state[key] && !state[key].startsWith("~")) {
log.debug "encrypting $key: ${state[key].inspect()}"
state[key] = encrypt(state[key])
}
} catch (java.lang.NumberFormatException e) { }
if (cmds) {
log.debug "poll is sending ${cmds.inspect()}"
cmds
} else {
// workaround to keep polling from stopping due to lack of activity
sendEvent(descriptionText: "skipping poll", isStateChange: true, displayed: false)
null
}
}
@@ -672,7 +651,7 @@ private Boolean secondsPast(timestamp, seconds) {
return true
}
}
return (new Date().time - timestamp) > (seconds * 1000)
return (now() - timestamp) > (seconds * 1000)
}
private allCodesDeleted() {

View File

@@ -115,6 +115,10 @@ def strobe() {
]
}
def both() {
on()
}
def refresh() {
log.debug "sending battery refresh command"
zwave.batteryV1.batteryGet().format()

View File

@@ -20,7 +20,8 @@ definition(
description: "Use this free SmartApp in conjunction with the ObyThing Music app for your Mac to control and automate music and more with iTunes and SmartThings.",
category: "SmartThings Labs",
iconUrl: "http://obycode.com/obything/ObyThingSTLogo.png",
iconX2Url: "http://obycode.com/obything/ObyThingSTLogo@2x.png")
iconX2Url: "http://obycode.com/obything/ObyThingSTLogo@2x.png",
singleInstance: true)
preferences {

View File

@@ -22,7 +22,8 @@ definition(
category: "SmartThings Labs",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/netamo-icon-1.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/netamo-icon-1%402x.png",
oauth: true
oauth: true,
singleInstance: true
){
appSetting "clientId"
appSetting "clientSecret"

View File

@@ -1,164 +1,427 @@
/**
* Nobody Home
*
* Author: brian@bevey.org
* Date: 12/19/14
* Author: brian@bevey.org, raychi@gmail.com
* Date: 12/02/2015
*
* Monitors a set of presence detectors and triggers a mode change when everyone has left.
* When everyone has left, sets mode to a new defined mode.
* When at least one person returns home, set the mode back to a new defined mode.
* When someone is home - or upon entering the home, their mode may change dependent on sunrise / sunset.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an "AS
* IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
definition(
name: "Nobody Home",
namespace: "imbrianj",
author: "brian@bevey.org",
description: "When everyone leaves, change mode. If at least one person home, switch mode based on sun position.",
category: "Mode Magic",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience%402x.png"
/**
* Monitors a set of presence sensors and trigger appropriate mode
* based on configured modes and sunrise/sunset time.
*
* - When everyone is away [Away]
* - When someone is home during the day [Home]
* - When someone is home at the night [Night]
*/
// ********** App related functions **********
// The definition provides metadata about the App to SmartThings.
definition (
name: "Nobody Home",
namespace: "imbrianj",
author: "brian@bevey.org",
description: "Automatically set Away/Home/Night mode based on a set of presence sensors and sunrise/sunset time.",
category: "Mode Magic",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png"
)
// The preferences defines information the App needs from the user.
preferences {
section("When all of these people leave home") {
input "people", "capability.presenceSensor", multiple: true
}
section("Change to this mode to...") {
input "newAwayMode", "mode", title: "Everyone is away"
input "newSunsetMode", "mode", title: "At least one person home and nightfall"
input "newSunriseMode", "mode", title: "At least one person home and sunrise"
}
section("Away threshold (defaults to 10 min)") {
input "awayThreshold", "decimal", title: "Number of minutes", required: false
}
section("Notifications") {
input "sendPushMessage", "enum", title: "Send a push notification?", metadata:[values:["Yes","No"]], required:false
}
}
def installed() {
init()
}
def updated() {
unsubscribe()
init()
}
def init() {
subscribe(people, "presence", presence)
subscribe(location, "sunrise", setSunrise)
subscribe(location, "sunset", setSunset)
state.sunMode = location.mode
}
def setSunrise(evt) {
changeSunMode(newSunriseMode)
}
def setSunset(evt) {
changeSunMode(newSunsetMode)
}
def changeSunMode(newMode) {
state.sunMode = newMode
if(everyoneIsAway() && (location.mode == newAwayMode)) {
log.debug("Mode is away, not evaluating")
}
else if(location.mode != newMode) {
def message = "${app.label} changed your mode to '${newMode}'"
send(message)
setLocationMode(newMode)
}
else {
log.debug("Mode is the same, not evaluating")
}
}
def presence(evt) {
if(evt.value == "not present") {
log.debug("Checking if everyone is away")
if(everyoneIsAway()) {
log.info("Starting ${newAwayMode} sequence")
def delay = (awayThreshold != null && awayThreshold != "") ? awayThreshold * 60 : 10 * 60
runIn(delay, "setAway")
}
}
else {
if(location.mode != state.sunMode) {
log.debug("Checking if anyone is home")
if(anyoneIsHome()) {
log.info("Starting ${state.sunMode} sequence")
changeSunMode(state.sunMode)
}
section("Presence sensors to monitor") {
input "people", "capability.presenceSensor", multiple: true
}
else {
log.debug("Mode is the same, not evaluating")
}
}
}
def setAway() {
if(everyoneIsAway()) {
if(location.mode != newAwayMode) {
def message = "${app.label} changed your mode to '${newAwayMode}' because everyone left home"
log.info(message)
send(message)
setLocationMode(newAwayMode)
section("Mode setting") {
input "newAwayMode", "mode", title: "Everyone is away"
input "newSunriseMode", "mode", title: "Someone is home during the day"
input "newSunsetMode", "mode", title: "Someone is home at night"
}
else {
log.debug("Mode is the same, not evaluating")
section("Mode change delay (minutes)") {
input "awayThreshold", "decimal", title: "Away delay [5m]", required: false
input "arrivalThreshold", "decimal", title: "Arrival delay [2m]", required: false
}
}
else {
log.info("Somebody returned home before we set to '${newAwayMode}'")
}
section("Notifications") {
input "sendPushMessage", "bool", title: "Push notification", required:false
}
}
private everyoneIsAway() {
def result = true
if(people.findAll { it?.currentPresence == "present" }) {
result = false
}
log.debug("everyoneIsAway: ${result}")
return result
// called when the user installs the App
def installed()
{
log.debug("installed() @${location.name}: ${settings}")
initialize(true)
}
private anyoneIsHome() {
def result = false
if(people.findAll { it?.currentPresence == "present" }) {
result = true
}
log.debug("anyoneIsHome: ${result}")
return result
// called when the user installs the app, or changes the App
// preference
def updated()
{
log.debug("updated() @${location.name}: ${settings}")
unsubscribe()
initialize(false)
}
private send(msg) {
if(sendPushMessage != "No") {
log.debug("Sending push message")
sendPush(msg)
}
def initialize(isInstall)
{
// subscribe to all the events we care about
log.debug("Subscribing to events ...")
log.debug(msg)
// thing to subscribe, attribute/state we care about, and callback fn
subscribe(people, "presence", presenceHandler)
subscribe(location, "sunrise", sunriseHandler)
subscribe(location, "sunset", sunsetHandler)
// set the optional parameter values. these are not available
// directly until the app has initialized (that is,
// installed/updated has returned). so here we access them through
// the settings object, as otherwise will get an exception.
// store information we need in state object so we can get access
// to it later in our event handlers.
// calculate the away threshold in seconds. can't use the simpler
// default falsy value, as value of 0 (no delay) is evaluated to
// false (not specified), but we want 0 to represent no delay. so
// we compare against null explicitly to see if the user has set a
// value or not.
if (settings.awayThreshold == null) {
settings.awayThreshold = 5 // default away 5 minute
}
state.awayDelay = (int) settings.awayThreshold * 60
log.debug("awayThreshold set to " + state.awayDelay + " second(s)")
if (settings.arrivalThreshold == null) {
settings.arrivalThreshold = 2 // default arrival 2 minute
}
state.arrivalDelay = (int) settings.arrivalThreshold * 60
log.debug("arrivalThreshold set to " + state.arrivalDelay + " second(s)")
// get push notification setting
state.isPush = settings.sendPushMessage ? true : false
log.debug("sendPushMessage set to " + state.isPush)
// on install (not update), figure out what mode we should be in
// IF someone's home. This value is needed so that when a presence
// sensor is triggered, we know what mode to set the system to, as
// the sunrise/sunset event handler may not be triggered yet after
// a fresh install.
if (isInstall) {
// TODO: for now, we simply assume daytime. a better approach
// would be to figure out whether current time is day or
// night, and set it appropriately. However there
// doesn't seem to be a way to query this directly
// without a zip code. This will become the correct
// value at the next sunrise/sunset event.
log.debug("No sun info yet, assuming daytime")
state.modeIfHome = newSunriseMode
// set keep a separate sun mode state so we can show a more
// helpful message when sun events are triggered.
state.currentSunMode = "sunUnknown"
state.eventDevice = "" // last event device
// device that triggered timer. This is not necessarily the
// eventDevice. For example, if A arrives, kick off timer,
// then b arrives before timer elapsed, we want the
// notification message to reference A, not B.
state.timerDevice = null
// anything in flight? We use this to avoid scheduling
// duplicate timers (so we don't extend the timer).
state.pendingOp = "init"
// now set the correct mode for the location. This way, we
// don't need to wait for the next sun/presence event.
// we schedule this action to run after the app has fully
// initialized. This way, the app install is faster and the
// user customized app name is used in the notification.
runIn(7, "setInitialMode")
}
// On update, we don't change state.modeIfHome. This is so that we
// preserve the current sun rise/set state we obtained in earlier
// sunset/sunrise handler. This way the app remains in the correct
// sun state when the user reconfigures it.
}
def setInitialMode()
{
changeSunMode(state.modeIfHome)
state.pendingOp = null
}
// ********** sunrise/sunset handling **********
// event handler when the sunrise time is reached
def sunriseHandler(evt)
{
// we store the mode we should be in, IF someone's home
state.modeIfHome = newSunriseMode
state.currentSunMode = "sunRise"
// change mode if someone's home, otherwise set to away
changeSunMode(newSunriseMode)
}
// event handler when the sunset time is reached
def sunsetHandler(evt)
{
// we store the mode we should be in, IF someone's home
state.modeIfHome = newSunsetMode
state.currentSunMode = "sunSet"
// change mode if someone's home, otherwise set to away
changeSunMode(newSunsetMode)
}
def changeSunMode(newMode)
{
// if everyone is away, we need to check and ensure the system is
// in away mode.
if (isEveryoneAway()) {
// this shouldn not happen normally as the mode should already
// be changed during presenceHandler, but in case this is not
// done, such as when app is initially installed while away,
// and system is not in away mode, then we toggle it to away
// at the sun rise/set event.
changeMode(newAwayMode, " because no one is present")
} else {
// someone is home, we update the mode depending on
// sunrise/sunset.
if (state.currentSunMode == "sunRise") {
changeMode(newMode, " because it's sunrise")
} else if (state.currentSunMode == "sunSet") {
changeMode(newMode, " because it's sunset")
} else {
changeMode(newMode)
}
}
}
// ********** presence handling **********
// event handler when presence sensor changes state
def presenceHandler(evt)
{
// get the device name that resulted in the change
state.eventDevice= evt.device?.displayName
// is setInitialMode() still pending?
if (state.pendingOp == "init") {
log.debug("Pending ${state.pendingOp} op still in progress, ignoring presence event")
return
}
if (evt.value == "not present") {
handleDeparture()
} else {
handleArrival()
}
}
def handleDeparture()
{
log.info("${state.eventDevice} left ${location.name}")
// do nothing if someone's still home
if (!isEveryoneAway()) {
log.info("Someone is still present, no actions needed")
return
}
// Now we set away mode. We perform the following actions even if
// home is already in away mode because an arrival timer may be
// pending, and scheduling delaySetMode() has the nice effect of
// canceling any previous pending timer, which is what we want to
// do. So we do this even if delay is 0.
log.info("Scheduling ${newAwayMode} mode in " + state.awayDelay + "s")
state.pendingOp = "away"
state.timerDevice = state.eventDevice
// we always use runIn(). This has the benefit of automatically
// replacing any pending arrival/away timer. if any arrival timer
// is active, it will be clobbered with this away timer. If any
// away timer is active, it will be extended with this new timeout
// (though normally it should not happen)
runIn(state.awayDelay, "delaySetMode")
}
def handleArrival()
{
// someone returned home, set home/night mode after delay
log.info("${state.eventDevice} arrived at ${location.name}")
def numHome = isAnyoneHome()
if (!numHome) {
// no one home, do nothing for now (should NOT happen)
log.warn("${deviceName} arrived, but isAnyoneHome() returned false!")
return
}
if (numHome > 1) {
// not the first one home, do nothing, as any action that
// should happen would've happened when the first sensor
// arrived. this is the opposite of isEveryoneAway() where we
// don't do anything if someone's still home.
log.debug("Someone is already present, no actions needed")
return
}
// check if any pending arrival timer is already active. we want
// the timer to trigger when the 1st person arrives, but not
// extended when the 2nd person arrives later. this should not
// happen because of the >1 check above, but just in case.
if (state.pendingOp == "arrive") {
log.debug("Pending ${state.pendingOp} op already in progress, do nothing")
return
}
// now we set home/night mode
log.info("Scheduling ${state.modeIfHome} mode in " + state.arrivalDelay + "s")
state.pendingOp = "arrive"
state.timerDevice = state.eventDevice
// if any away timer is active, it will be clobbered with
// this arrival timer
runIn(state.arrivalDelay, "delaySetMode")
}
// ********** helper functions **********
// change the system to the new mode, unless its already in that mode.
def changeMode(newMode, reason="")
{
if (location.mode != newMode) {
// notification message
def message = "${location.name} changed mode from '${location.mode}' to '${newMode}'" + reason
setLocationMode(newMode)
send(message) // send message after changing mode
} else {
log.debug("${location.name} is already in ${newMode} mode, no actions needed")
}
}
// create a useful departure/arrival reason string
def reasonStr(isAway, delaySec, delayMin)
{
def reason
// if we are invoked by timer, use the stored timer trigger
// device, otherwise use the last event device
if (state.timerDevice) {
reason = " because ${state.timerDevice} "
} else {
reason = " because ${state.eventDevice} "
}
if (isAway) {
reason += "left"
} else {
reason += "arrived"
}
if (delaySec) {
if (delaySec > 60) {
if (delayMin == null) {
delayMin = (int) delaySec / 60
}
reason += " ${delayMin} minutes ago"
} else {
reason += " ${delaySec}s ago"
}
}
return reason
}
// http://docs.smartthings.com/en/latest/smartapp-developers-guide/scheduling.html#schedule-from-now
//
// By default, if a method is scheduled to run in the future, and then
// another call to runIn with the same method is made, the last one
// overwrites the previously scheduled method.
//
// We use the above property to schedule our arrval/departure delay
// using the same function so we don't have to worry about
// arrival/departure timer firing independently and complicating code.
def delaySetMode()
{
def newMode = null
def reason = ""
// timer has elapsed, check presence status to figure out what we
// need to do
if (isEveryoneAway()) {
reason = reasonStr(true, state.awayDelay, awayThreshold)
newMode = newAwayMode
if (state.pendingOp) {
log.debug("${state.pendingOp} timer elapsed: everyone is away")
}
} else {
reason = reasonStr(false, state.arrivalDelay, arrivalThreshold)
newMode = state.modeIfHome
if (state.pendingOp) {
log.debug("${state.pendingOp} timer elapsed: someone is home")
}
}
// now change the mode
changeMode(newMode, reason);
state.pendingOp = null
state.timerDevice = null
}
private isEveryoneAway()
{
def result = true
if (people.findAll { it?.currentPresence == "present" }) {
result = false
}
// log.debug("isEveryoneAway: ${result}")
return result
}
// return the number of people that are home
private isAnyoneHome()
{
def result = 0
// iterate over our people variable that we defined
// in the preferences method
for (person in people) {
if (person.currentPresence == "present") {
result++
}
}
return result
}
private send(msg)
{
if (state.isPush) {
log.debug("Sending push notification")
sendPush(msg)
} else {
log.debug("Sending notification")
sendNotificationEvent(msg)
}
log.info(msg)
}

View File

@@ -11,9 +11,9 @@
* 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.
*
*
* SmartThings data is sent from this SmartApp to Initial State. This is event data only for
* devices for which the user has authorized. Likewise, Initial State's services call this
* devices for which the user has authorized. Likewise, Initial State's services call this
* SmartApp on the user's behalf to configure Initial State specific parameters. The ToS and
* Privacy Policy for Initial State can be found here: https://www.initialstate.com/terms
*/
@@ -90,7 +90,7 @@ def subscribeToEvents() {
if (beacons != null) {
subscribe(beacons, "presence", genericHandler)
}
if (cos != null) {
subscribe(cos, "carbonMonoxide", genericHandler)
}
@@ -218,7 +218,7 @@ def setAccessKey() {
atomicState.grokerSubdomain = "$newGrokerSubdomain"
atomicState.isBucketCreated = false
}
if (newAccessKey && newAccessKey != atomicState.accessKey) {
atomicState.accessKey = "$newAccessKey"
atomicState.isBucketCreated = false
@@ -262,7 +262,7 @@ def uninstalled() {
}
def tryCreateBucket() {
// can't ship events if there is no grokerSubdomain
if (atomicState.grokerSubdomain == null || atomicState.grokerSubdomain == "") {
log.error "streaming url is currently null"
@@ -330,57 +330,57 @@ def genericHandler(evt) {
// This is a handler function for flushing the event buffer
// after a specified amount of time to reduce the load on ST servers
def flushBuffer() {
def eventBuffer = atomicState.eventBuffer
log.trace "About to flush the buffer on schedule"
if (atomicState.eventBuffer != null && atomicState.eventBuffer.size() > 0) {
tryShipEvents()
if (eventBuffer != null && eventBuffer.size() > 0) {
atomicState.eventBuffer = []
tryShipEvents(eventBuffer)
}
}
def eventHandler(name, value) {
log.debug atomicState.eventBuffer
def eventBuffer = atomicState.eventBuffer
def epoch = now() / 1000
// if for some reason this code block is being run
// but the SmartApp wasn't propery setup during install
// we need to set initialize the eventBuffer.
if (!atomicState.eventBuffer) {
atomicState.eventBuffer = []
}
def eventBuffer = atomicState.eventBuffer ?: []
eventBuffer << [key: "$name", value: "$value", epoch: "$epoch"]
log.debug eventBuffer
atomicState.eventBuffer = eventBuffer
if (eventBuffer.size() >= 10) {
tryShipEvents()
// Clear eventBuffer right away since we've already pulled it off of atomicState to reduce the risk of missing
// events. This assumes the grokerSubdomain, accessKey, and bucketKey are set correctly to avoid the eventBuffer
// from growing unbounded.
atomicState.eventBuffer = []
tryShipEvents(eventBuffer)
} else {
// Make sure we persist the updated eventBuffer with the new event added back to atomicState
atomicState.eventBuffer = eventBuffer
}
log.debug "Event added to buffer: " + eventBuffer
}
// a helper function for shipping the atomicState.eventBuffer to Initial State
def tryShipEvents() {
def tryShipEvents(eventBuffer) {
def grokerSubdomain = atomicState.grokerSubdomain
// can't ship events if there is no grokerSubdomain
if (atomicState.grokerSubdomain == null || atomicState.grokerSubdomain == "") {
if (grokerSubdomain == null || grokerSubdomain == "") {
log.error "streaming url is currently null"
return
}
def accessKey = atomicState.accessKey
def bucketKey = atomicState.bucketKey
// can't ship if access key and bucket key are null, so finish trying
if (atomicState.accessKey == null || atomicState.bucketKey == null) {
if (accessKey == null || bucketKey == null) {
return
}
def eventPost = [
uri: "https://${atomicState.grokerSubdomain}.initialstate.com/api/events",
uri: "https://${grokerSubdomain}.initialstate.com/api/events",
headers: [
"Content-Type": "application/json",
"X-IS-BucketKey": "${atomicState.bucketKey}",
"X-IS-AccessKey": "${atomicState.accessKey}",
"X-IS-BucketKey": "${bucketKey}",
"X-IS-AccessKey": "${accessKey}",
"Accept-Version": "0.0.2"
],
body: atomicState.eventBuffer
body: eventBuffer
]
try {
@@ -389,13 +389,10 @@ def tryShipEvents() {
log.debug "shipped events and got ${resp.status}"
if (resp.status >= 400) {
log.error "shipping failed... ${resp.data}"
} else {
// clear the buffer
atomicState.eventBuffer = []
}
}
} catch (e) {
log.error "shipping events failed: $e"
}
}

View File

@@ -13,11 +13,11 @@ definition(
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/jawbone-up.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/jawbone-up@2x.png",
oauth: true,
usePreferencesForAuthorization: false
usePreferencesForAuthorization: false,
singleInstance: true
) {
appSetting "clientId"
appSetting "clientSecret"
appSetting "serverUrl"
}
preferences {
@@ -28,16 +28,13 @@ mappings {
path("/receivedToken") { action: [ POST: "receivedToken", GET: "receivedToken"] }
path("/receiveToken") { action: [ POST: "receiveToken", GET: "receiveToken"] }
path("/hookCallback") { action: [ POST: "hookEventHandler", GET: "hookEventHandler"] }
path("/oauth/initialize") {action: [GET: "oauthInitUrl"]}
path("/oauth/callback") { action: [ GET: "callback" ] }
}
def getSmartThingsClientId() {
return appSettings.clientId
}
def getSmartThingsClientSecret() {
return appSettings.clientSecret
}
def getServerUrl() { return "https://graph.api.smartthings.com" }
def getBuildRedirectUrl() { "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${state.accessToken}&apiServerUrl=${apiServerUrl}" }
def buildRedirectUrl(page) { return buildActionUrl(page) }
def callback() {
def redirectUrl = null
@@ -63,9 +60,8 @@ def callback() {
// SmartThings code, which we ignore, as we don't need to exchange for an access token.
// Instead, go initiate the Jawbone OAuth flow.
log.debug "Executing callback redirect to auth page"
def stcid = getSmartThingsClientId()
state.oauthInitState = UUID.randomUUID().toString()
def oauthParams = [response_type: "code", client_id: stcid, scope: "move_read sleep_read", redirect_uri: "${serverUrl}/oauth/callback"]
def oauthParams = [response_type: "code", client_id: appSettings.clientId, scope: "move_read sleep_read", redirect_uri: "${serverUrl}/oauth/callback"]
redirect(location: "https://jawbone.com/auth/oauth2/auth?${toQueryString(oauthParams)}")
}
} else {
@@ -84,10 +80,11 @@ def authPage() {
createAccessToken()
}
description = "Click to enter Jawbone Credentials"
def redirectUrl = oauthInitUrl()
// log.debug "RedirectURL = ${redirectUrl}"
return dynamicPage(name: "Credentials", title: "Jawbone UP", nextPage: null, uninstall: true, install:false) {
section { href url:redirectUrl, style:"embedded", required:true, title:"Jawbone UP", description:description }
def redirectUrl = buildRedirectUrl
log.debug "RedirectURL = ${redirectUrl}"
def donebutton= state.JawboneAccessToken != null
return dynamicPage(name: "Credentials", title: "Jawbone UP", nextPage: null, uninstall: true, install: donebutton) {
section { href url:redirectUrl, style:"embedded", required:true, title:"Jawbone UP", state: hast ,description:description }
}
} else {
description = "Jawbone Credentials Already Entered."
@@ -99,17 +96,14 @@ def authPage() {
def oauthInitUrl() {
log.debug "oauthInitUrl"
def stcid = getSmartThingsClientId()
state.oauthInitState = UUID.randomUUID().toString()
def oauthParams = [ response_type: "code", client_id: stcid, scope: "move_read sleep_read", redirect_uri: buildRedirectUrl("receiveToken") ]
return "https://jawbone.com/auth/oauth2/auth?${toQueryString(oauthParams)}"
def oauthParams = [ response_type: "code", client_id: appSettings.clientId, scope: "move_read sleep_read", redirect_uri: "${serverUrl}/oauth/callback" ]
redirect(location: "https://jawbone.com/auth/oauth2/auth?${toQueryString(oauthParams)}")
}
def receiveToken(redirectUrl = null) {
log.debug "receiveToken"
def stcid = getSmartThingsClientId()
def oauthClientSecret = getSmartThingsClientSecret()
def oauthParams = [ client_id: stcid, client_secret: oauthClientSecret, grant_type: "authorization_code", code: params.code ]
def oauthParams = [ client_id: appSettings.clientId, client_secret: appSettings.clientSecret, grant_type: "authorization_code", code: params.code ]
def params = [
uri: "https://jawbone.com/auth/oauth2/token?${toQueryString(oauthParams)}",
]
@@ -231,18 +225,10 @@ String toQueryString(Map m) {
return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&")
}
def getServerUrl() { return appSettings.serverUrl ?: "https://graph.api.smartthings.com" }
def buildRedirectUrl(page) {
// log.debug "buildRedirectUrl"
// /api/token/:st_token/smartapps/installations/:id/something
return "${serverUrl}/api/token/${state.accessToken}/smartapps/installations/${app.id}/${page}"
}
def validateCurrentToken() {
log.debug "validateCurrentToken"
def url = "https://jawbone.com/nudge/api/v.1.1/users/@me/refreshToken"
def requestBody = "secret=${getSmartThingsClientSecret()}"
def requestBody = "secret=${appSettings.clientSecret}"
try {
httpPost(uri: url, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ], body: requestBody) {response ->
@@ -256,9 +242,7 @@ def validateCurrentToken() {
if (e.statusCode == 401) { // token is expired
log.debug "Access token is expired"
if (state.refreshToken) { // if we have this we are okay
def stcid = getSmartThingsClientId()
def oauthClientSecret = getSmartThingsClientSecret()
def oauthParams = [client_id: stcid, client_secret: oauthClientSecret, grant_type: "refresh_token", refresh_token: state.refreshToken]
def oauthParams = [client_id: appSettings.clientId, client_secret: appSettings.clientSecret, grant_type: "refresh_token", refresh_token: state.refreshToken]
def tokenUrl = "https://jawbone.com/auth/oauth2/token?${toQueryString(oauthParams)}"
def params = [
uri: tokenUrl
@@ -287,9 +271,10 @@ def validateCurrentToken() {
}
def initialize() {
def hookUrl = "${serverUrl}/api/token/${state.accessToken}/smartapps/installations/${app.id}/hookCallback"
def webhook = "https://jawbone.com/nudge/api/v.1.1/users/@me/pubsub?webhook=$hookUrl"
log.debug "Callback URL: $webhook"
log.debug "Callback URL - Webhook"
def localServerUrl = getApiServerUrl()
def hookUrl = "${localServerUrl}/api/token/${state.accessToken}/smartapps/installations/${app.id}/hookCallback"
def webhook = "https://jawbone.com/nudge/api/v.1.1/users/@me/pubsub?webhook=$hookUrl"
httpPost(uri: webhook, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ])
}
@@ -327,7 +312,6 @@ def setup() {
}
def installed() {
enableCallback()
if (!state.accessToken) {
log.debug "About to create access token"
@@ -340,7 +324,6 @@ def installed() {
}
def updated() {
enableCallback()
if (!state.accessToken) {
log.debug "About to create access token"
@@ -499,4 +482,4 @@ def hookEventHandler() {
def html = """{"code":200,"message":"OK"}"""
render contentType: 'application/json', data: html
}
}

View File

@@ -1,53 +0,0 @@
/**
*
* Lights On When Door Open After Sundown
*
* Based on "Turn It On When It Opens" by SmartThings
*
* Author: Aaron Crocco
*/
preferences {
section("When the door opens..."){
input "contact1", "capability.contactSensor", title: "Where?"
}
section("Turn on these lights..."){
input "switches", "capability.switch", multiple: true
}
section("and change mode to...") {
input "HomeAfterDarkMode", "mode", title: "Mode?"
}
}
def installed()
{
subscribe(contact1, "contact.open", contactOpenHandler)
}
def updated()
{
unsubscribe()
subscribe(contact1, "contact.open", contactOpenHandler)
}
def contactOpenHandler(evt) {
log.debug "$evt.value: $evt, $settings"
//Check current time to see if it's after sundown.
def s = getSunriseAndSunset(zipCode: zipCode, sunriseOffset: sunriseOffset, sunsetOffset: sunsetOffset)
def now = new Date()
def setTime = s.sunset
log.debug "Sunset is at $setTime. Current time is $now"
if (setTime.before(now)) { //Executes only if it's after sundown.
log.trace "Turning on switches: $switches"
switches.on()
log.trace "Changing house mode to $HomeAfterDarkMode"
setLocationMode(HomeAfterDarkMode)
sendPush("Welcome home! Changing mode to $HomeAfterDarkMode.")
}
}

View File

@@ -26,7 +26,8 @@ definition(
iconUrl: "http://i.imgur.com/HU0ANBp.png",
iconX2Url: "http://i.imgur.com/HU0ANBp.png",
iconX3Url: "http://i.imgur.com/HU0ANBp.png",
oauth: true)
oauth: true,
singleInstance: true)
preferences {

View File

@@ -0,0 +1,389 @@
/**
* Required PlantLink Connector
* This SmartApp forwards the raw data of the deviceType to myplantlink.com
* and returns it back to your device after calculating soil and plant type.
*
* Copyright 2015 Oso Technologies
*
* 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.
*
*/
import groovy.json.JsonBuilder
import java.util.regex.Matcher
import java.util.regex.Pattern
definition(
name: "PlantLink Connector",
namespace: "Osotech",
author: "Oso Technologies",
description: "This SmartApp connects to myplantlink.com and forwards the device data to it so it can calculate easy to read plant status for your specific plant's needs.",
category: "Convenience",
iconUrl: "https://dashboard.myplantlink.com/images/apple-touch-icon-76x76-precomposed.png",
iconX2Url: "https://dashboard.myplantlink.com/images/apple-touch-icon-120x120-precomposed.png",
iconX3Url: "https://dashboard.myplantlink.com/images/apple-touch-icon-152x152-precomposed.png"
) {
appSetting "client_id"
appSetting "client_secret"
appSetting "https_plantLinkServer"
}
preferences {
page(name: "auth", title: "Step 1 of 2", nextPage:"deviceList", content:"authPage")
page(name: "deviceList", title: "Step 2 of 2", install:true, uninstall:false){
section {
input "plantlinksensors", "capability.sensor", title: "Select PlantLink sensors", multiple: true
}
}
}
mappings {
path("/swapToken") {
action: [
GET: "swapToken"
]
}
}
def authPage(){
if(!atomicState.accessToken){
createAccessToken()
atomicState.accessToken = state.accessToken
}
def redirectUrl = oauthInitUrl()
def uninstallAllowed = false
def oauthTokenProvided = false
if(atomicState.authToken){
uninstallAllowed = true
oauthTokenProvided = true
}
if (!oauthTokenProvided) {
return dynamicPage(name: "auth", title: "Step 1 of 2", nextPage:null, uninstall:uninstallAllowed) {
section(){
href(name:"login",
url:redirectUrl,
style:"embedded",
title:"PlantLink",
image:"https://dashboard.myplantlink.com/images/PLlogo.png",
description:"Tap to login to myplantlink.com")
}
}
}else{
return dynamicPage(name: "auth", title: "Step 1 of 2 - Completed", nextPage:"deviceList", uninstall:uninstallAllowed) {
section(){
paragraph "You are logged in to myplantlink.com, tap next to continue", image: iconUrl
href(url:redirectUrl, title:"Or", description:"tap to switch accounts")
}
}
}
}
def installed() {
log.debug "Installed with settings: ${settings}"
initialize()
}
def updated() {
log.debug "Updated with settings: ${settings}"
unsubscribe()
initialize()
}
def uninstalled() {
if (plantlinksensors){
plantlinksensors.each{ sensor_device ->
sensor_device.setInstallSmartApp("needSmartApp")
}
}
}
def initialize() {
atomicState.attached_sensors = [:]
if (plantlinksensors){
subscribe(plantlinksensors, "moisture_status", moistureHandler)
subscribe(plantlinksensors, "battery_status", batteryHandler)
plantlinksensors.each{ sensor_device ->
sensor_device.setStatusIcon("Waiting on First Measurement")
sensor_device.setInstallSmartApp("connectedToSmartApp")
}
}
}
def dock_sensor(device_serial, expected_plant_name) {
def docking_body_json_builder = new JsonBuilder([version: '1c', smartthings_device_id: device_serial])
def docking_params = [
uri : appSettings.https_plantLinkServer,
path : "/api/v1/smartthings/links",
headers : ["Content-Type": "application/json", "Authorization": "Bearer ${atomicState.authToken}"],
contentType: "application/json",
body: docking_body_json_builder.toString()
]
def plant_post_body_map = [
plant_type_key: 999999,
soil_type_key : 1000004
]
def plant_post_params = [
uri : appSettings.https_plantLinkServer,
path : "/api/v1/plants",
headers : ["Content-Type": "application/json", "Authorization": "Bearer ${atomicState.authToken}"],
contentType: "application/json",
]
log.debug "Creating new plant on myplantlink.com - ${expected_plant_name}"
httpPost(docking_params) { docking_response ->
if (parse_api_response(docking_response, "Docking a link")) {
if (docking_response.data.plants.size() == 0) {
log.debug "creating plant for - ${expected_plant_name}"
plant_post_body_map["name"] = expected_plant_name
plant_post_body_map['links_key'] = [docking_response.data.key]
def plant_post_body_json_builder = new JsonBuilder(plant_post_body_map)
plant_post_params["body"] = plant_post_body_json_builder.toString()
httpPost(plant_post_params) { plant_post_response ->
if(parse_api_response(plant_post_response, 'creating plant')){
def attached_map = atomicState.attached_sensors
attached_map[device_serial] = plant_post_response.data
atomicState.attached_sensors = attached_map
}
}
} else {
def plant = docking_response.data.plants[0]
def attached_map = atomicState.attached_sensors
attached_map[device_serial] = plant
atomicState.attached_sensors = attached_map
checkAndUpdatePlantIfNeeded(plant, expected_plant_name)
}
}
}
return true
}
def checkAndUpdatePlantIfNeeded(plant, expected_plant_name){
def plant_put_params = [
uri : appSettings.https_plantLinkServer,
headers : ["Content-Type": "application/json", "Authorization": "Bearer ${atomicState.authToken}"],
contentType : "application/json"
]
if (plant.name != expected_plant_name) {
log.debug "updating plant for - ${expected_plant_name}"
plant_put_params["path"] = "/api/v1/plants/${plant.key}"
def plant_put_body_map = [
name: expected_plant_name
]
def plant_put_body_json_builder = new JsonBuilder(plant_put_body_map)
plant_put_params["body"] = plant_put_body_json_builder.toString()
httpPut(plant_put_params) { plant_put_response ->
parse_api_response(plant_put_response, 'updating plant name')
}
}
}
def moistureHandler(event){
def expected_plant_name = "SmartThings - ${event.displayName}"
def device_serial = getDeviceSerialFromEvent(event)
if (!atomicState.attached_sensors.containsKey(device_serial)){
dock_sensor(device_serial, expected_plant_name)
}else{
def measurement_post_params = [
uri: appSettings.https_plantLinkServer,
path: "/api/v1/smartthings/links/${device_serial}/measurements",
headers: ["Content-Type": "application/json", "Authorization": "Bearer ${atomicState.authToken}"],
contentType: "application/json",
body: event.value
]
httpPost(measurement_post_params) { measurement_post_response ->
if (parse_api_response(measurement_post_response, 'creating moisture measurement') &&
measurement_post_response.data.size() >0){
def measurement = measurement_post_response.data[0]
def plant = measurement.plant
log.debug plant
checkAndUpdatePlantIfNeeded(plant, expected_plant_name)
plantlinksensors.each{ sensor_device ->
if (sensor_device.id == event.deviceId){
sensor_device.setStatusIcon(plant.status)
if (plant.last_measurements && plant.last_measurements[0].moisture){
sensor_device.setPlantFuelLevel(plant.last_measurements[0].moisture * 100 as int)
}
if (plant.last_measurements && plant.last_measurements[0].battery){
sensor_device.setBatteryLevel(plant.last_measurements[0].battery * 100 as int)
}
}
}
}
}
}
}
def batteryHandler(event){
def expected_plant_name = "SmartThings - ${event.displayName}"
def device_serial = getDeviceSerialFromEvent(event)
if (!atomicState.attached_sensors.containsKey(device_serial)){
dock_sensor(device_serial, expected_plant_name)
}else{
def measurement_post_params = [
uri: appSettings.https_plantLinkServer,
path: "/api/v1/smartthings/links/${device_serial}/measurements",
headers: ["Content-Type": "application/json", "Authorization": "Bearer ${atomicState.authToken}"],
contentType: "application/json",
body: event.value
]
httpPost(measurement_post_params) { measurement_post_response ->
parse_api_response(measurement_post_response, 'creating battery measurement')
}
}
}
def getDeviceSerialFromEvent(event){
def pattern = /.*"zigbeedeviceid"\s*:\s*"(\w+)".*/
def match_result = (event.value =~ pattern)
return match_result[0][1]
}
def oauthInitUrl(){
atomicState.oauthInitState = UUID.randomUUID().toString()
def oauthParams = [
response_type: "code",
client_id: appSettings.client_id,
state: atomicState.oauthInitState,
redirect_uri: buildRedirectUrl()
]
return appSettings.https_plantLinkServer + "/oauth/oauth2/authorize?" + toQueryString(oauthParams)
}
def buildRedirectUrl(){
return getServerUrl() + "/api/token/${atomicState.accessToken}/smartapps/installations/${app.id}/swapToken"
}
def swapToken(){
def code = params.code
def oauthState = params.state
def stcid = appSettings.client_id
def postParams = [
method: 'POST',
uri: "https://oso-tech.appspot.com",
path: "/api/v1/oauth-token",
query: [grant_type:'authorization_code', code:params.code, client_id:stcid,
client_secret:appSettings.client_secret, redirect_uri: buildRedirectUrl()],
]
def jsonMap
httpPost(postParams) { resp ->
jsonMap = resp.data
}
atomicState.refreshToken = jsonMap.refresh_token
atomicState.authToken = jsonMap.access_token
def html = """
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
.container {
padding:25px;
}
.flex1 {
width:33%;
float:left;
text-align: center;
}
p {
font-size: 2em;
font-family: Verdana, Geneva, sans-serif;
text-align: center;
color: #777;
}
</style>
</head>
<body>
<div class="container">
<div class="flex1"><img src="https://dashboard.myplantlink.com/images/PLlogo.png" alt="PlantLink" height="75"/></div>
<div class="flex1"><img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/connected-device-icn%402x.png" alt="connected to" height="25" style="padding-top:25px;" /></div>
<div class="flex1"><img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/st-logo%402x.png" alt="SmartThings" height="75"/></div>
<br clear="all">
</div>
<div class="container">
<p>Your PlantLink Account is now connected to SmartThings!</p>
<p style="color:green;">Click <strong>Done</strong> at the top right to finish setup.</p>
</div>
</body>
</html>
"""
render contentType: 'text/html', data: html
}
private refreshAuthToken() {
def stcid = appSettings.client_id
def refreshParams = [
method: 'POST',
uri: "https://hardware-dot-oso-tech.appspot.com",
path: "/api/v1/oauth-token",
query: [grant_type:'refresh_token', code:"${atomicState.refreshToken}", client_id:stcid,
client_secret:appSettings.client_secret],
]
try{
def jsonMap
httpPost(refreshParams) { resp ->
if(resp.status == 200){
log.debug "OAuth Token refreshed"
jsonMap = resp.data
if (resp.data) {
atomicState.refreshToken = resp?.data?.refresh_token
atomicState.authToken = resp?.data?.access_token
if (data?.action && data?.action != "") {
log.debug data.action
"{data.action}"()
data.action = ""
}
}
data.action = ""
}else{
log.debug "refresh failed ${resp.status} : ${resp.status.code}"
}
}
}
catch(Exception e){
log.debug "caught exception refreshing auth token: " + e
}
}
def parse_api_response(resp, message) {
if (resp.status == 200) {
return true
} else {
log.error "sent ${message} Json & got http status ${resp.status} - ${resp.status.code}"
if (resp.status == 401) {
refreshAuthToken()
return false
} else {
debugEvent("Authentication error, invalid authentication method, lack of credentials, etc.", true)
return false
}
}
}
def getServerUrl() {
return "https://graph.api.smartthings.com"
}
def debugEvent(message, displayEvent) {
def results = [
name: "appdebug",
descriptionText: message,
displayed: displayEvent
]
log.debug "Generating AppDebug Event: ${results}"
sendEvent (results)
}
def toQueryString(Map m){
return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&")
}

View File

@@ -14,8 +14,8 @@
*
*/
definition(
name: "Smart Lock / Unlock",
namespace: "",
name: "Smart Auto Lock / Unlock",
namespace: "smart-auto-lock-unlock",
author: "Arnaud",
description: "Automatically locks door X minutes after being closed and keeps door unlocked if door is open.",
category: "Safety & Security",

View File

@@ -21,7 +21,8 @@
category: "SmartThings Labs",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png"
iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
singleInstance: true
)
preferences {

View File

@@ -0,0 +1,341 @@
/**
* Copyright 2015 SmartThings
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
* for the specific language governing permissions and limitations under the License.
*
* Bose® SoundTouch® Control
*
* Author: SmartThings & Joe Geiger
*
* Date: 2015-30-09
*/
definition(
name: "Bose® SoundTouch® Control",
namespace: "smartthings",
author: "SmartThings & Joe Geiger",
description: "Control your Bose® SoundTouch® when certain actions take place in your home.",
category: "SmartThings Labs",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience%402x.png",
iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience%402x.png"
)
preferences {
page(name: "mainPage", title: "Control your Bose® SoundTouch® when something happens", install: true, uninstall: true)
page(name: "timeIntervalInput", title: "Only during a certain time") {
section {
input "starting", "time", title: "Starting", required: false
input "ending", "time", title: "Ending", required: false
}
}
}
def mainPage() {
dynamicPage(name: "mainPage") {
def anythingSet = anythingSet()
if (anythingSet) {
section("When..."){
ifSet "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true
ifSet "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true
ifSet "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true
ifSet "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true
ifSet "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true
ifSet "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true
ifSet "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true
ifSet "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true
ifSet "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true
ifSet "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true
ifSet "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production
ifSet "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true
ifSet "timeOfDay", "time", title: "At a Scheduled Time", required: false
}
}
section(anythingSet ? "Select additional triggers" : "When...", hideable: anythingSet, hidden: true){
ifUnset "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true
ifUnset "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true
ifUnset "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true
ifUnset "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true
ifUnset "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true
ifUnset "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true
ifUnset "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true
ifUnset "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true
ifUnset "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true
ifUnset "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true
ifUnset "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production
ifUnset "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true
ifUnset "timeOfDay", "time", title: "At a Scheduled Time", required: false
}
section("Perform this action"){
input "actionType", "enum", title: "Action?", required: true, defaultValue: "play", options: [
"Turn On & Play",
"Turn Off",
"Toggle Play/Pause",
"Skip to Next Track",
"Skip to Beginning/Previous Track",
"Play Preset 1",
"Play Preset 2",
"Play Preset 3",
"Play Preset 4",
"Play Preset 5",
"Play Preset 6"
]
}
section {
input "bose", "capability.musicPlayer", title: "Bose® SoundTouch® music player", required: true
}
section("More options", hideable: true, hidden: true) {
input "volume", "number", title: "Set the volume volume", description: "0-100%", required: false
input "frequency", "decimal", title: "Minimum time between actions (defaults to every event)", description: "Minutes", required: false
href "timeIntervalInput", title: "Only during a certain time", description: timeLabel ?: "Tap to set", state: timeLabel ? "complete" : "incomplete"
input "days", "enum", title: "Only on certain days of the week", multiple: true, required: false,
options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
if (settings.modes) {
input "modes", "mode", title: "Only when mode is", multiple: true, required: false
}
input "oncePerDay", "bool", title: "Only once per day", required: false, defaultValue: false
}
section([mobileOnly:true]) {
label title: "Assign a name", required: false
mode title: "Set for specific mode(s)"
}
}
}
private anythingSet() {
for (name in ["motion","contact","contactClosed","acceleration","mySwitch","mySwitchOff","arrivalPresence","departurePresence","smoke","water","button1","triggerModes","timeOfDay"]) {
if (settings[name]) {
return true
}
}
return false
}
private ifUnset(Map options, String name, String capability) {
if (!settings[name]) {
input(options, name, capability)
}
}
private ifSet(Map options, String name, String capability) {
if (settings[name]) {
input(options, name, capability)
}
}
def installed() {
log.debug "Installed with settings: ${settings}"
subscribeToEvents()
}
def updated() {
log.debug "Updated with settings: ${settings}"
unsubscribe()
unschedule()
subscribeToEvents()
}
def subscribeToEvents() {
log.trace "subscribeToEvents()"
subscribe(app, appTouchHandler)
subscribe(contact, "contact.open", eventHandler)
subscribe(contactClosed, "contact.closed", eventHandler)
subscribe(acceleration, "acceleration.active", eventHandler)
subscribe(motion, "motion.active", eventHandler)
subscribe(mySwitch, "switch.on", eventHandler)
subscribe(mySwitchOff, "switch.off", eventHandler)
subscribe(arrivalPresence, "presence.present", eventHandler)
subscribe(departurePresence, "presence.not present", eventHandler)
subscribe(smoke, "smoke.detected", eventHandler)
subscribe(smoke, "smoke.tested", eventHandler)
subscribe(smoke, "carbonMonoxide.detected", eventHandler)
subscribe(water, "water.wet", eventHandler)
subscribe(button1, "button.pushed", eventHandler)
if (triggerModes) {
subscribe(location, modeChangeHandler)
}
if (timeOfDay) {
schedule(timeOfDay, scheduledTimeHandler)
}
}
def eventHandler(evt) {
if (allOk) {
def lastTime = state[frequencyKey(evt)]
if (oncePerDayOk(lastTime)) {
if (frequency) {
if (lastTime == null || now() - lastTime >= frequency * 60000) {
takeAction(evt)
}
else {
log.debug "Not taking action because $frequency minutes have not elapsed since last action"
}
}
else {
takeAction(evt)
}
}
else {
log.debug "Not taking action because it was already taken today"
}
}
}
def modeChangeHandler(evt) {
log.trace "modeChangeHandler $evt.name: $evt.value ($triggerModes)"
if (evt.value in triggerModes) {
eventHandler(evt)
}
}
def scheduledTimeHandler() {
eventHandler(null)
}
def appTouchHandler(evt) {
takeAction(evt)
}
private takeAction(evt) {
log.debug "takeAction($actionType)"
def options = [:]
if (volume) {
bose.setLevel(volume as Integer)
options.delay = 1000
}
switch (actionType) {
case "Turn On & Play":
options ? bose.on(options) : bose.on()
break
case "Turn Off":
options ? bose.off(options) : bose.off()
break
case "Toggle Play/Pause":
def currentStatus = bose.currentValue("playpause")
if (currentStatus == "play") {
options ? bose.pause(options) : bose.pause()
}
else if (currentStatus == "pause") {
options ? bose.play(options) : bose.play()
}
break
case "Skip to Next Track":
options ? bose.nextTrack(options) : bose.nextTrack()
break
case "Skip to Beginning/Previous Track":
options ? bose.previousTrack(options) : bose.previousTrack()
break
case "Play Preset 1":
options ? bose.preset1(options) : bose.preset1()
break
case "Play Preset 2":
options ? bose.preset2(options) : bose.preset2()
break
case "Play Preset 3":
options ? bose.preset3(options) : bose.preset3()
break
case "Play Preset 4":
options ? bose.preset4(options) : bose.preset4()
break
case "Play Preset 5":
options ? bose.preset5(options) : bose.preset5()
break
case "Play Preset 6":
options ? bose.preset6(options) : bose.preset6()
break
default:
log.error "Action type '$actionType' not defined"
}
if (frequency) {
state.lastActionTimeStamp = now()
}
}
private frequencyKey(evt) {
//evt.deviceId ?: evt.value
"lastActionTimeStamp"
}
private dayString(Date date) {
def df = new java.text.SimpleDateFormat("yyyy-MM-dd")
if (location.timeZone) {
df.setTimeZone(location.timeZone)
}
else {
df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
}
df.format(date)
}
private oncePerDayOk(Long lastTime) {
def result = true
if (oncePerDay) {
result = lastTime ? dayString(new Date()) != dayString(new Date(lastTime)) : true
log.trace "oncePerDayOk = $result"
}
result
}
// TODO - centralize somehow
private getAllOk() {
modeOk && daysOk && timeOk
}
private getModeOk() {
def result = !modes || modes.contains(location.mode)
log.trace "modeOk = $result"
result
}
private getDaysOk() {
def result = true
if (days) {
def df = new java.text.SimpleDateFormat("EEEE")
if (location.timeZone) {
df.setTimeZone(location.timeZone)
}
else {
df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
}
def day = df.format(new Date())
result = days.contains(day)
}
log.trace "daysOk = $result"
result
}
private getTimeOk() {
def result = true
if (starting && ending) {
def currTime = now()
def start = timeToday(starting, location?.timeZone).time
def stop = timeToday(ending, location?.timeZone).time
result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start
}
log.trace "timeOk = $result"
result
}
private hhmm(time, fmt = "h:mm a")
{
def t = timeToday(time, location.timeZone)
def f = new java.text.SimpleDateFormat(fmt)
f.setTimeZone(location.timeZone ?: timeZone(time))
f.format(t)
}
private timeIntervalLabel()
{
(starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z") : ""
}
// TODO - End Centralize

View File

@@ -246,6 +246,9 @@ def toggle(devices) {
else if (devices*.currentValue('lock').contains('locked')) {
devices.unlock()
}
else if (devices*.currentValue('lock').contains('unlocked')) {
devices.lock()
}
else if (devices*.currentValue('alarm').contains('off')) {
devices.siren()
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
/**
* Every Element
*
* Copyright 2014 SmartThings
* Copyright 2015 SmartThings
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at:
@@ -14,349 +14,555 @@
*
*/
definition(
name: "Every Element",
namespace: "smartthings/examples",
author: "SmartThings",
description: "Every element demonstration app",
category: "SmartThings Internal",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png"
name: "Every Element",
namespace: "smartthings/examples",
author: "SmartThings",
description: "Every element demonstration app",
category: "SmartThings Internal",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png"
)
preferences {
page(name: "firstPage")
page(name: "inputPage")
page(name: "appPage")
page(name: "labelPage")
page(name: "modePage")
page(name: "paragraphPage")
page(name: "iconPage")
page(name: "hrefPage")
page(name: "buttonsPage")
page(name: "imagePage")
page(name: "videoPage")
page(name: "deadEnd", title: "Nothing to see here, move along.", content: "foo")
page(name: "flattenedPage")
// landing page
page(name: "firstPage")
// PageKit
page(name: "buttonsPage")
page(name: "imagePage")
page(name: "inputPage")
page(name: "inputBooleanPage")
page(name: "inputIconPage")
page(name: "inputImagePage")
page(name: "inputDevicePage")
page(name: "inputCapabilityPage")
page(name: "inputRoomPage")
page(name: "inputModePage")
page(name: "inputSelectionPage")
page(name: "inputHubPage")
page(name: "inputContactBookPage")
page(name: "inputTextPage")
page(name: "inputTimePage")
page(name: "appPage")
page(name: "hrefPage")
page(name: "paragraphPage")
page(name: "videoPage")
page(name: "labelPage")
page(name: "modePage")
// Every element helper pages
page(name: "deadEnd", title: "Nothing to see here, move along.", content: "foo")
page(name: "flattenedPage")
}
def firstPage() {
dynamicPage(name: "firstPage", title: "Where to first?", install: true, uninstall: true) {
section() {
href(page: "inputPage", title: "Element: 'input'")
href(page: "appPage", title: "Element: 'app'")
href(page: "labelPage", title: "Element: 'label'")
href(page: "modePage", title: "Element: 'mode'")
href(page: "paragraphPage", title: "Element: 'paragraph'")
href(page: "iconPage", title: "Element: 'icon'")
href(page: "hrefPage", title: "Element: 'href'")
href(page: "buttonsPage", title: "Element: 'buttons'")
href(page: "imagePage", title: "Element: 'image'")
href(page: "videoPage", title: "Element: 'video'")
}
section() {
href(page: "flattenedPage", title: "All of the above elements on a single page")
}
}
dynamicPage(name: "firstPage", title: "Where to first?", install: true, uninstall: true) {
section {
href(page: "appPage", title: "Element: 'app'")
href(page: "buttonsPage", title: "Element: 'buttons'")
href(page: "hrefPage", title: "Element: 'href'")
href(page: "imagePage", title: "Element: 'image'")
href(page: "inputPage", title: "Element: 'input'")
href(page: "labelPage", title: "Element: 'label'")
href(page: "modePage", title: "Element: 'mode'")
href(page: "paragraphPage", title: "Element: 'paragraph'")
href(page: "videoPage", title: "Element: 'video'")
}
section {
href(page: "flattenedPage", title: "All of the above elements on a single page")
}
}
}
def inputPage() {
dynamicPage(name: "inputPage", title: "Every 'input' type") {
section("enum") {
input(type: "enum", name: "enumRefresh", title: "submitOnChange:true", required: false, multiple: true, options: ["one", "two", "three"], submitOnChange: true)
if (enumRefresh) {
paragraph "${enumRefresh}"
}
input(type: "enum", name: "enumSegmented", title: "style:segmented", required: false, multiple: true, options: ["one", "two", "three"], style: "segmented")
input(type: "enum", name: "enum", title: "required:false, multiple:false", required: false, multiple: false, options: ["one", "two", "three"])
input(type: "enum", name: "enumRequired", title: "required:true", required: true, multiple: false, options: ["one", "two", "three"])
input(type: "enum", name: "enumMultiple", title: "multiple:true", required: false, multiple: true, options: ["one", "two", "three"])
input(type: "enum", name: "enumWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, multiple: true, options: ["one", "two", "three"], image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
input(type: "enum", name: "enumWithGroupedOptions", title: "groupedOptions", description: "This enum has grouped options", required: false, multiple: true, groupedOptions: [
[
title : "the group title that is displayed",
order : 0, // the order of the group; 0-based
image : "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", // not yet supported
values: [
[
key : "the value that will be placed in SmartApp settings.", // such as a device id
value: "the title of the selectable option that is displayed", // such as a device name
order: 0 // the order of the option
]
]
],
[
title : "the second group title that is displayed",
order : 1, // the order of the group; 0-based
image : null, // not yet supported
values: [
[
key : "some_device_id",
value: "some_device_name",
order: 1 // the order of the option. This option will appear second in the list even though it is the first option defined in this map
],
[
key : "some_other_device_id",
value: "some_other_device_name",
order: 0 // the order of the option. This option will appear first in the list even though it is not the first option defined in this map
]
]
]
])
}
section("text") {
input(type: "text", name: "text", title: "required:false, multiple:false", required: false, multiple: false)
input(type: "text", name: "textRequired", title: "required:true", required: true, multiple: false)
input(type: "text", name: "textWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
}
section("number") {
input(type: "number", name: "number", title: "required:false, multiple:false", required: false, multiple: false)
input(type: "number", name: "numberRequired", title: "required:true", required: true, multiple: false)
input(type: "number", name: "numberWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
}
section("boolean") {
input(type: "boolean", name: "boolean", title: "required:false, multiple:false", required: false, multiple: false)
input(type: "boolean", name: "booleanWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
}
section("password") {
input(type: "password", name: "password", title: "required:false, multiple:false", required: false, multiple: false)
input(type: "password", name: "passwordRequired", title: "required:true", required: true, multiple: false)
input(type: "password", name: "passwordWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
}
section("phone") {
input(type: "phone", name: "phone", title: "required:false, multiple:false", required: false, multiple: false)
input(type: "phone", name: "phoneRequired", title: "required:true", required: true, multiple: false)
input(type: "phone", name: "phoneWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
}
section("email") {
input(type: "email", name: "email", title: "required:false, multiple:false", required: false, multiple: false)
input(type: "email", name: "emailRequired", title: "required:true", required: true, multiple: false)
input(type: "email", name: "emailWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
}
section("decimal") {
input(type: "decimal", name: "decimal", title: "required:false, multiple:false", required: false, multiple: false)
input(type: "decimal", name: "decimalRequired", title: "required:true", required: true, multiple: false)
input(type: "decimal", name: "decimalWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
}
section("mode") {
input(type: "mode", name: "mode", title: "required:false, multiple:false", required: false, multiple: false)
input(type: "mode", name: "modeRequired", title: "required:true", required: true, multiple: false)
input(type: "mode", name: "modeMultiple", title: "multiple:true", required: false, multiple: true)
input(type: "mode", name: "iconWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, multiple: true, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
}
section("icon") {
input(type: "icon", name: "icon", title: "required:false, multiple:false", required: false, multiple: false)
input(type: "icon", name: "iconRequired", title: "required:true", required: true, multiple: false)
input(type: "icon", name: "iconWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
}
section("capability") {
input(type: "capability.switch", name: "capability", title: "required:false, multiple:false", required: false, multiple: false)
input(type: "capability.switch", name: "capabilityRequired", title: "required:true", required: true, multiple: false)
input(type: "capability.switch", name: "capabilityMultiple", title: "multiple:true", required: false, multiple: true)
input(type: "capability.switch", name: "capabilityWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, multiple: true, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
}
section("hub") {
input(type: "hub", name: "hub", title: "required:false, multiple:false", required: false, multiple: false)
input(type: "hub", name: "hubRequired", title: "required:true", required: true, multiple: false)
input(type: "hub", name: "hubMultiple", title: "multiple:true", required: false, multiple: true)
input(type: "hub", name: "hubWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, multiple: true, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
}
section("device") {
input(type: "device.switch", name: "device", title: "required:false, multiple:false", required: false, multiple: false)
input(type: "device.switch", name: "deviceRequired", title: "required:true", required: true, multiple: false)
input(type: "device.switch", name: "deviceMultiple", title: "multiple:true", required: false, multiple: true)
input(type: "device.switch", name: "deviceWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, multiple: true, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
}
section("time") {
input(type: "time", name: "time", title: "required:false, multiple:false", required: false, multiple: false)
input(type: "time", name: "timeRequired", title: "required:true", required: true, multiple: false)
input(type: "time", name: "timeWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
}
section("contact-book") {
input("recipients", "contact", title: "Notify", description: "Send notifications to") {
input(type: "phone", name: "phone", title: "Send text message to", required: false, multiple: false)
input(type: "boolean", name: "boolean", title: "Send push notification", required: false, multiple: false)
}
}
}
dynamicPage(name: "inputPage", title: "Links to every 'input' element") {
section {
href(page: "inputBooleanPage", title: "to boolean page")
href(page: "inputIconPage", title: "to icon page")
href(page: "inputImagePage", title: "to image page")
href(page: "inputSelectionPage", title: "to selection page")
href(page: "inputTextPage", title: "to text page")
href(page: "inputTimePage", title: "to time page")
}
section("subsets of selection input") {
href(page: "inputDevicePage", title: "to device selection page")
href(page: "inputCapabilityPage", title: "to capability selection page")
href(page: "inputRoomPage", title: "to room selection page")
href(page: "inputModePage", title: "to mode selection page")
href(page: "inputHubPage", title: "to hub selection page")
href(page: "inputContactBookPage", title: "to contact-book selection page")
}
}
}
def inputBooleanPage() {
dynamicPage(name: "inputBooleanPage") {
section {
paragraph "The `required` and `multiple` attributes have no effect because the value will always be either `true` or `false`"
}
section {
input(type: "boolean", name: "booleanWithoutDescription", title: "without description", description: null)
input(type: "boolean", name: "booleanWithDescription", title: "with description", description: "This has a description")
}
section("defaultValue: 'true'") {
input(type: "boolean", name: "booleanWithDefaultValue", title: "", description: "", defaultValue: "true")
}
section("with image") {
input(type: "boolean", name: "booleanWithoutDescriptionWithImage", title: "without description", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", description: null)
input(type: "boolean", name: "booleanWithDescriptionWithImage", title: "with description", description: "This has a description", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
}
}
}
def inputIconPage() {
dynamicPage(name: "inputIconPage") {
section {
paragraph "`description` is not displayed for icon elements"
paragraph "`multiple` has no effect because you can only choose a single icon"
}
section("required: true") {
input(type: "icon", name: "iconRequired", title: "without description", required: true)
input(type: "icon", name: "iconRequiredWithDescription", title: "with description", description: "this is a description", required: true)
}
section("with image") {
paragraph "The image specified will be replaced after an icon is selected"
input(type: "icon", name: "iconwithImage", title: "without description", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
}
}
}
def inputImagePage() {
dynamicPage(name: "inputImagePage") {
section {
paragraph "This only exists in DeviceTypes. Someone should do something about that. (glares at MikeDave)"
paragraph "Go to the device preferences of a Mobile Presence device to see it in action"
paragraph "If you try to set the value of this, it will not behave as it would in Device Preferences"
input(type: "image", title: "This is kind of what it looks like", required: false)
}
}
}
def optionsGroup(List groups, String title) {
def group = [values:[], order: groups.size()]
group.title = title ?: ""
groups << group
return groups
}
def addValues(List groups, String key, String value) {
def lastGroup = groups[-1]
lastGroup["values"] << [
key: key,
value: value,
order: lastGroup["values"].size()
]
return groups
}
def listToMap(List original) {
original.inject([:]) { result, v ->
result[v] = v
return result
}
}
def addGroup(List groups, String title, values) {
if (values instanceof List) {
values = listToMap(values)
}
values.inject(optionsGroup(groups, title)) { result, k, v ->
return addValues(result, k, v)
}
return groups
}
def addGroup(values) {
addGroup([], null, values)
}
/* Example usage of options builder
// Creating grouped options
def newGroups = []
addGroup(newGroups, "first group", ["foo", "bar", "baz"])
addGroup(newGroups, "second group", [zero: "zero", one: "uno", two: "dos", three: "tres"])
// simple list
addGroup(["a", "b", "c"])
// simple map
addGroup(["a": "yes", "b": "no", "c": "maybe"])
*/
def inputSelectionPage() {
def englishOptions = ["One", "Two", "Three"]
def spanishOptions = ["Uno", "Dos", "Tres"]
def groupedOptions = []
addGroup(groupedOptions, "English", englishOptions)
addGroup(groupedOptions, "Spanish", spanishOptions)
dynamicPage(name: "inputSelectionPage") {
section("options variations") {
paragraph "tap these elements and look at the differences when selecting an option"
input(type: "enum", name: "selectionSimple", title: "Simple options", description: "no separators in the selectable options", groupedOptions: addGroup(englishOptions + spanishOptions))
input(type: "enum", name: "selectionGrouped", title: "Grouped options", description: "separate groups of options with headers", groupedOptions: groupedOptions)
}
section("list vs map") {
paragraph "These should be identical in UI, but are different in code and will produce different settings"
input(type: "enum", name: "selectionList", title: "Choose a device", description: "settings will be something like ['Device1 Label']", groupedOptions: addGroup(["Device1 Label", "Device2 Label"]))
input(type: "enum", name: "selectionMap", title: "Choose a device", description: "settings will be something like ['device1-id']", groupedOptions: addGroup(["device1-id": "Device1 Label", "device2-id": "Device2 Label"]))
}
section("segmented") {
paragraph "segmented should only work if there are either 2 or 3 options to choose from"
input(type: "enum", name: "selectionSegmented1", style: "segmented", title: "1 option", groupedOptions: addGroup(["One"]))
input(type: "enum", name: "selectionSegmented4", style: "segmented", title: "4 options", groupedOptions: addGroup(["One", "Two", "Three", "Four"]))
paragraph "multiple and required will have no effect on segmented selection elements. There will always be exactly 1 option selected"
input(type: "enum", name: "selectionSegmented2", style: "segmented", title: "2 options", options: ["One", "Two"])
input(type: "enum", name: "selectionSegmented3", style: "segmented", title: "3 options", options: ["One", "Two", "Three"])
paragraph "specifying defaultValue still works with segmented selection elements"
input(type: "enum", name: "selectionSegmentedWithDefault", title: "defaulted to 'two'", groupedOptions: addGroup(["One", "Two", "Three"]), defaultValue: "Two")
}
section("required: true") {
input(type: "enum", name: "selectionRequired", title: "This is required", description: "It should look different when nothing is selected", groupedOptions: addGroup(["only option"]), required: true)
}
section("multiple: true") {
input(type: "enum", name: "selectionMultiple", title: "This allows multiple selections", description: "It should look different when nothing is selected", groupedOptions: addGroup(["an option", "another option", "no way, one more?"]), multiple: true)
}
section("with image") {
input(type: "enum", name: "selectionWithImage", title: "This has an image", description: "and a description", groupedOptions: addGroup(["an option", "another option", "no way, one more?"]), image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
}
}
}
def inputTextPage() {
dynamicPage(name: "inputTextPage", title: "Every 'text' variation") {
section("style and functional differences") {
input(type: "text", name: "textRequired", title: "required: true", description: "This should look different when nothing has been entered", required: true)
input(type: "text", name: "textWithImage", title: "with image", description: "This should look different when nothing has been entered", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", required: false)
}
section("text") {
input(type: "text", name: "text", title: "This has an alpha-numeric keyboard", description: "no special formatting", required: false)
}
section("password") {
input(type: "password", name: "password", title: "This has an alpha-numeric keyboard", description: "masks value", required: false)
}
section("email") {
input(type: "email", name: "email", title: "This has an email-specific keyboard", description: "no special formatting", required: false)
}
section("phone") {
input(type: "phone", name: "phone", title: "This has a numeric keyboard", description: "formatted for phone numbers", required: false)
}
section("decimal") {
input(type: "decimal", name: "decimal", title: "This has an numeric keyboard with decimal point", description: "no special formatting", required: false)
}
section("number") {
input(type: "number", name: "number", title: "This has an numeric keyboard without decimal point", description: "no special formatting", required: false)
}
section("specified ranges") {
paragraph "You can limit number and decimal inputs to a specific range."
input(range: "50..150", type: "decimal", name: "decimalRange50..150", title: "only values between 50 and 150 will pass validation", description: "no special formatting", required: false)
paragraph "Negative limits will add a negative symbol to the keyboard."
input(range: "-50..50", type: "number", name: "numberRange-50..50", title: "only values between -50 and 50 will pass validation", description: "no special formatting", required: false)
paragraph "Specify * to not limit one side or the other."
input(range: "*..0", type: "decimal", name: "decimalRange*..0", title: "only negative values will pass validation", description: "no special formatting", required: false)
input(range: "*..*", type: "number", name: "numberRange*..*", title: "only positive values will pass validation", description: "no special formatting", required: false)
paragraph "If you don't specify a range, it defaults to 0..*"
}
}
}
def inputTimePage() {
dynamicPage(name: "inputTimePage") {
section {
input(type: "time", name: "timeWithDescription", title: "a time picker", description: "with a description", required: false)
input(type: "time", name: "timeWithoutDescription", title: "without a description", description: null, required: false)
input(type: "time", name: "timeRequired", title: "required: true", required: true)
}
}
}
/// selection subsets
def inputDevicePage() {
dynamicPage(name: "inputDevicePage") {
section("required: true") {
input(type: "device.switch", name: "deviceRequired", title: "This is required", description: "It should look different when nothing is selected")
}
section("multiple: true") {
input(type: "device.switch", name: "deviceMultiple", title: "This is required", description: "It should look different when nothing is selected", multiple: true)
}
section("with image") {
input(type: "device.switch", name: "deviceRequired", title: "This has an image", description: "and a description", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
}
}
}
def inputCapabilityPage() {
dynamicPage(name: "inputCapabilityPage") {
section("required: true") {
input(type: "capability.switch", name: "capabilityRequired", title: "This is required", description: "It should look different when nothing is selected")
}
section("multiple: true") {
input(type: "capability.switch", name: "capabilityMultiple", title: "This is required", description: "It should look different when nothing is selected", multiple: true)
}
section("with image") {
input(type: "capability.switch", name: "capabilityRequired", title: "This has an image", description: "and a description", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
}
}
}
def inputRoomPage() {
dynamicPage(name: "inputRoomPage") {
section("required: true") {
input(type: "room", name: "roomRequired", title: "This is required", description: "It should look different when nothing is selected")
}
section("multiple: true") {
input(type: "room", name: "roomMultiple", title: "This is required", description: "It should look different when nothing is selected", multiple: true)
}
section("with image") {
input(type: "room", name: "roomRequired", title: "This has an image", description: "and a description", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
}
}
}
def inputModePage() {
dynamicPage(name: "inputModePage") {
section("required: true") {
input(type: "mode", name: "modeRequired", title: "This is required", description: "It should look different when nothing is selected")
}
section("multiple: true") {
input(type: "mode", name: "modeMultiple", title: "This is required", description: "It should look different when nothing is selected", multiple: true)
}
section("with image") {
input(type: "mode", name: "modeRequired", title: "This has an image", description: "and a description", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
}
}
}
def inputHubPage() {
dynamicPage(name: "inputHubPage") {
section("required: true") {
input(type: "hub", name: "hubRequired", title: "This is required", description: "It should look different when nothing is selected")
}
section("multiple: true") {
input(type: "hub", name: "hubMultiple", title: "This is required", description: "It should look different when nothing is selected", multiple: true)
}
section("with image") {
input(type: "hub", name: "hubRequired", title: "This has an image", description: "and a description", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
}
}
}
def inputContactBookPage() {
dynamicPage(name: "inputContactBookPage") {
section("required: true") {
input(type: "contact", name: "contactRequired", title: "This is required", description: "It should look different when nothing is selected")
}
section("multiple: true") {
input(type: "contact", name: "contactMultiple", title: "This is required", description: "It should look different when nothing is selected", multiple: true)
}
section("with image") {
input(type: "contact", name: "contactRequired", title: "This has an image", description: "and a description", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
}
}
}
def appPage() {
dynamicPage(name: "appPage", title: "Every 'app' type") {
section {
paragraph "These won't work unless you create a child SmartApp to link to... Sorry."
}
section("app") {
app(
name: "app",
title: "required:false, multiple:false",
required: false,
multiple: false,
namespace: "Steve",
appName: "Child SmartApp"
)
app(name: "appRequired", title: "required:true", required: true, multiple: false, namespace: "Steve", appName: "Child SmartApp")
app(name: "appComplete", title: "state:complete", required: false, multiple: false, namespace: "Steve", appName: "Child SmartApp", state: "complete")
app(name: "appWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, multiple: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", namespace: "Steve", appName: "Child SmartApp")
}
section("multiple:true") {
app(name: "appMultiple", title: "multiple:true", required: false, multiple: true, namespace: "Steve", appName: "Child SmartApp")
}
section("multiple:true with image") {
app(name: "appMultipleWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, multiple: true, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", namespace: "Steve", appName: "Child SmartApp")
}
}
dynamicPage(name: "appPage", title: "Every 'app' type") {
section {
paragraph "These won't work unless you create a child SmartApp to link to... Sorry."
}
section("app") {
app(
name: "app",
title: "required:false, multiple:false",
required: false,
multiple: false,
namespace: "Steve",
appName: "Child SmartApp"
)
app(name: "appRequired", title: "required:true", required: true, multiple: false, namespace: "Steve", appName: "Child SmartApp")
app(name: "appComplete", title: "state:complete", required: false, multiple: false, namespace: "Steve", appName: "Child SmartApp", state: "complete")
app(name: "appWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, multiple: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", namespace: "Steve", appName: "Child SmartApp")
}
section("multiple:true") {
app(name: "appMultiple", title: "multiple:true", required: false, multiple: true, namespace: "Steve", appName: "Child SmartApp")
}
section("multiple:true with image") {
app(name: "appMultipleWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, multiple: true, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", namespace: "Steve", appName: "Child SmartApp")
}
}
}
def labelPage() {
dynamicPage(name: "labelPage", title: "Every 'Label' type") {
section("label") {
label(name: "label", title: "required:false, multiple:false", required: false, multiple: false)
label(name: "labelRequired", title: "required:true", required: true, multiple: false)
label(name: "labelWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
}
}
dynamicPage(name: "labelPage", title: "Every 'Label' type") {
section("label") {
paragraph "The difference between a label element and a text input element is that the label element will effect the SmartApp directly by setting the label. An input element will place the set value in the SmartApp's settings."
paragraph "There are 3 here as an example. Never use more than 1 label element on a page."
label(name: "label", title: "required:false, multiple:false", required: false, multiple: false)
label(name: "labelRequired", title: "required:true", required: true, multiple: false)
label(name: "labelWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
}
}
}
def modePage() {
dynamicPage(name: "modePage", title: "Every 'mode' type") { // TODO: finish this
section("mode") {
mode(name: "mode", title: "required:false, multiple:false", required: false, multiple: false)
mode(name: "modeRequired", title: "required:true", required: true, multiple: false)
mode(name: "modeMultiple", title: "multiple:true", required: false, multiple: true)
mode(name: "modeWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, multiple: true, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
}
}
dynamicPage(name: "modePage", title: "Every 'mode' type") { // TODO: finish this
section("mode") {
paragraph "The difference between a mode element and a mode input element is that the mode element will effect the SmartApp directly by setting the modes it executes in. A mode input element will place the set value in the SmartApp's settings."
paragraph "Another difference is that you can select 'All Modes' when choosing which mode the SmartApp should execute in. This is the same as selecting no modes. When a SmartApp does not have modes specified, it will execute in all modes."
paragraph "There are 4 here as an example. Never use more than 1 mode element on a page."
mode(name: "mode", title: "required:false, multiple:false", required: false, multiple: false)
mode(name: "modeRequired", title: "required:true", required: true, multiple: false)
mode(name: "modeMultiple", title: "multiple:true", required: false, multiple: true)
mode(name: "modeWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, multiple: true, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
}
}
}
def paragraphPage() {
dynamicPage(name: "paragraphPage", title: "Every 'paragraph' type") {
section("paragraph") {
paragraph "This us how you should make a paragraph element"
paragraph image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", "This is a long description, blah, blah, blah."
}
}
}
def iconPage() {
dynamicPage(name: "iconPage", title: "Every 'icon' type") { // TODO: finish this
section("icon") {
icon(name: "icon", title: "required:false, multiple:false", required: false, multiple: false)
icon(name: "iconRequired", title: "required:true", required: true, multiple: false)
icon(name: "iconWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
}
}
dynamicPage(name: "paragraphPage", title: "Every 'paragraph' type") {
section("paragraph") {
paragraph "This is how you should make a paragraph element"
paragraph image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", "This is a long description, blah, blah, blah."
}
}
}
def hrefPage() {
dynamicPage(name: "hrefPage", title: "Every 'href' type") {
section("page") {
href(name: "hrefPage", title: "required:false, multiple:false", required: false, multiple: false, page: "deadEnd")
href(name: "hrefPageRequired", title: "required:true", required: true, multiple: false, page: "deadEnd", description: "Don't make hrefs required")
href(name: "hrefPageComplete", title: "state:complete", required: false, multiple: false, page: "deadEnd", state: "complete")
href(name: "hrefPageWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", page: "deadEnd",)
}
section("external") {
href(name: "hrefExternal", title: "required:false, multiple:false", required: false, multiple: false, style: "external", url: "http://smartthings.com/")
href(name: "hrefExternalRequired", title: "required:true", required: true, multiple: false, style: "external", url: "http://smartthings.com/", description: "Don't make hrefs required")
href(name: "hrefExternalComplete", title: "state:complete", required: false, multiple: true, style: "external", url: "http://smartthings.com/", state: "complete")
href(name: "hrefExternalWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", url: "http://smartthings.com/")
}
section("embedded") {
href(name: "hrefEmbedded", title: "required:false, multiple:false", required: false, multiple: false, style: "embedded", url: "http://smartthings.com/")
href(name: "hrefEmbeddedRequired", title: "required:true", required: true, multiple: false, style: "embedded", url: "http://smartthings.com/", description: "Don't make hrefs required")
href(name: "hrefEmbeddedComplete", title: "state:complete", required: false, multiple: true, style: "embedded", url: "http://smartthings.com/", state: "complete")
href(name: "hrefEmbeddedWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", url: "http://smartthings.com/")
}
}
dynamicPage(name: "hrefPage", title: "Every 'href' variation") {
section("stylistic differences") {
href(page: "deadEnd", title: "state: 'complete'", description: "gives the appearance of an input that has been filled out", state: "complete")
href(page: "deadEnd", title: "with image", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
href(page: "deadEnd", title: "with image and description", description: "and state: 'complete'", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", state: "complete")
}
section("functional differences") {
href(page: "deadEnd", title: "to a page within the app")
href(url: "http://www.google.com", title: "to a url using all defaults")
href(url: "http://www.google.com", title: "external: true", description: "takes you outside the app", external: true)
}
}
}
def buttonsPage() {
dynamicPage(name: "buttonsPage", title: "Every 'button' type") {
section("buttons") {
buttons(name: "buttons", title: "required:false, multiple:false", required: false, multiple: false, buttons: [
[label: "foo", action: "foo"],
[label: "bar", action: "bar"]
])
buttons(name: "buttonsRequired", title: "required:true", required: true, multiple: false, buttons: [
[label: "foo", action: "foo"],
[label: "bar", action: "bar"]
])
buttons(name: "buttonsWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", buttons: [
[label: "foo", action: "foo"],
[label: "bar", action: "bar"]
])
}
section("Colored Buttons") {
buttons(name: "buttonsColoredSpecial", title: "special strings", description: "SmartThings highly recommends using these colors", buttons: [
[label: "complete", action: "bar", backgroundColor: "complete"],
[label: "required", action: "bar", backgroundColor: "required"]
])
buttons(name: "buttonsColoredHex", title: "hex values work", buttons: [
[label: "bg: #000dff", action: "foo", backgroundColor: "#000dff"],
[label: "fg: #ffac00", action: "foo", color: "#ffac00"],
[label: "both fg and bg", action: "foo", color: "#ffac00", backgroundColor: "#000dff"]
])
buttons(name: "buttonsColoredString", title: "strings work too", buttons: [
[label: "green", action: "foo", backgroundColor: "green"],
[label: "red", action: "foo", backgroundColor: "red"],
[label: "both fg and bg", action: "foo", color: "red", backgroundColor: "green"]
])
}
}
dynamicPage(name: "buttonsPage", title: "Every 'button' type") {
section("Simple Buttons") {
paragraph "If there are an odd number of buttons, the last button will span the entire view area."
buttons(name: "buttons1", title: "1 button", buttons: [
[label: "foo", action: "foo"]
])
buttons(name: "buttons2", title: "2 buttons", buttons: [
[label: "foo", action: "foo"],
[label: "bar", action: "bar"]
])
buttons(name: "buttons3", title: "3 buttons", buttons: [
[label: "foo", action: "foo"],
[label: "bar", action: "bar"],
[label: "baz", action: "baz"]
])
buttons(name: "buttonsWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", buttons: [
[label: "foo", action: "foo"],
[label: "bar", action: "bar"]
])
}
section("Colored Buttons") {
buttons(name: "buttonsColoredSpecial", title: "special strings", description: "SmartThings highly recommends using these colors", buttons: [
[label: "complete", action: "bar", backgroundColor: "complete"],
[label: "required", action: "bar", backgroundColor: "required"]
])
buttons(name: "buttonsColoredHex", title: "hex values work", buttons: [
[label: "bg: #000dff", action: "foo", backgroundColor: "#000dff"],
[label: "fg: #ffac00", action: "foo", color: "#ffac00"],
[label: "both fg and bg", action: "foo", color: "#ffac00", backgroundColor: "#000dff"]
])
buttons(name: "buttonsColoredString", title: "strings work too", buttons: [
[label: "green", action: "foo", backgroundColor: "green"],
[label: "red", action: "foo", backgroundColor: "red"],
[label: "both fg and bg", action: "foo", color: "red", backgroundColor: "green"]
])
}
}
}
def imagePage() {
dynamicPage(name: "imagePage", title: "Every 'image' type") { // TODO: finish thise
section("image") {
image "http://f.cl.ly/items/1k1S0A0m3805402o3O12/20130915-191127.jpg"
image(name: "imageWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
}
}
dynamicPage(name: "imagePage", title: "Every 'image' type") { // TODO: finish thise
section("image") {
image "http://f.cl.ly/items/1k1S0A0m3805402o3O12/20130915-191127.jpg"
image(name: "imageWithMultipleImages", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, images: ["https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", "http://f.cl.ly/items/1k1S0A0m3805402o3O12/20130915-191127.jpg"])
}
}
}
def videoPage() {
dynamicPage(name: "imagePage", title: "Every 'image' type") { // TODO: finish this
section("video") {
// TODO: update this when there is a videoElement method
element(name: "videoElement", element: "video", type: "video", title: "this is a video!", description: "I am setting long title and descriptions to test the offset", required: false, image: "http://ec2-54-161-144-215.compute-1.amazonaws.com:8081/jesse/cam1/54aafcd1c198347511c26321.jpg", video: "http://ec2-54-161-144-215.compute-1.amazonaws.com:8081/jesse/cam1/54aafcd1c198347511c2631f.mp4")
}
}
dynamicPage(name: "videoPage", title: "Every 'video' type") { // TODO: finish this
section("video") {
// TODO: update this when there is a videoElement method
element(name: "videoElement", element: "video", type: "video", title: "this is a video!", description: "I am setting long title and descriptions to test the offset", required: false, image: "http://f.cl.ly/items/0w0D1p0K2D0d190F3H3N/Image%202015-12-14%20at%207.57.27%20AM.jpg", video: "http://f.cl.ly/items/3O2L03471l2K3E3l3K1r/Zombie%20Kid%20Likes%20Turtles.mp4")
}
}
}
def flattenedPage() {
def allSections = []
firstPage().sections.each { section ->
section.body.each { hrefElement ->
if (hrefElement.page != "flattenedPage") {
allSections += "${hrefElement.page}"().sections
}
}
}
def flattenedPage = dynamicPage(name: "flattenedPage", title: "All elements in one page!") {}
flattenedPage.sections = allSections
return flattenedPage
def allSections = []
firstPage().sections[0].body.each { hrefElement ->
if (hrefElement.name != "inputPage") {
// inputPage is a bunch of hrefs
allSections += "${hrefElement.page}"().sections
}
}
// collect the input elements
inputPage().sections.each { section ->
section.body.each { hrefElement ->
allSections += "${hrefElement.page}"().sections
}
}
def flattenedPage = dynamicPage(name: "flattenedPage", title: "All elements in one page!") {}
flattenedPage.sections = allSections
return flattenedPage
}
def foo() {
dynamicPage(name: "deadEnd") {
}
dynamicPage(name: "deadEnd") {
section { }
}
}
def installed() {
log.debug "Installed with settings: ${settings}"
log.debug "Installed with settings: ${settings}"
initialize()
initialize()
}
def updated() {
log.debug "Updated with settings: ${settings}"
log.debug "Updated with settings: ${settings}"
unsubscribe()
initialize()
unsubscribe()
initialize()
}
def initialize() {
// TODO: subscribe to attributes, devices, locations, etc.
// TODO: subscribe to attributes, devices, locations, etc.
}

View File

@@ -23,7 +23,8 @@ definition(
description: "Connect and take pictures using your Foscam camera from inside the Smartthings app.",
category: "SmartThings Internal",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/foscam.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/foscam@2x.png"
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/foscam@2x.png",
singleInstance: true
)
preferences {

View File

@@ -23,7 +23,8 @@ definition(
description: "Allows you to connect your Philips Hue lights with SmartThings and control them from your Things area or Dashboard in the SmartThings Mobile app. Adjust colors by going to the Thing detail screen for your Hue lights (tap the gear on Hue tiles).\n\nPlease update your Hue Bridge first, outside of the SmartThings app, using the Philips Hue app.",
category: "SmartThings Labs",
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
)
preferences {
@@ -143,7 +144,7 @@ def bulbDiscovery() {
if (numFound == 0)
app.updateSetting("selectedBulbs", "")
if((bulbRefreshCount % 3) == 0) {
if((bulbRefreshCount % 5) == 0) {
discoverHueBulbs()
}
@@ -318,11 +319,15 @@ def addBulbs() {
def newHueBulb
if (bulbs instanceof java.util.Map) {
newHueBulb = bulbs.find { (app.id + "/" + it.value.id) == dni }
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])
}
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])
}
} else {
log.debug "$dni in not longer paired to the Hue Bridge or ID changed"
}
} else {
//backwards compatable
newHueBulb = bulbs.find { (app.id + "/" + it.id) == dni }
@@ -604,23 +609,22 @@ def parse(childDevice, description) {
}
}
def on(childDevice, transition_deprecated = 0) {
def on(childDevice) {
log.debug "Executing 'on'"
def percent = childDevice.device?.currentValue("level") as Integer
def level = Math.min(Math.round(percent * 255 / 100), 255)
put("lights/${getId(childDevice)}/state", [bri: level, on: true])
return "level: $percent"
put("lights/${getId(childDevice)}/state", [on: true])
return "Bulb is On"
}
def off(childDevice, transition_deprecated = 0) {
def off(childDevice) {
log.debug "Executing 'off'"
put("lights/${getId(childDevice)}/state", [on: false])
return "level: 0"
return "Bulb is Off"
}
def setLevel(childDevice, percent) {
log.debug "Executing 'setLevel'"
def level = Math.min(Math.round(percent * 255 / 100), 255)
def level
if (percent == 1) level = 1 else level = Math.min(Math.round(percent * 255 / 100), 255)
put("lights/${getId(childDevice)}/state", [bri: level, on: percent > 0])
}
@@ -636,7 +640,7 @@ def setHue(childDevice, percent) {
put("lights/${getId(childDevice)}/state", [hue: level])
}
def setColor(childDevice, huesettings, alert_deprecated = "", transition_deprecated = 0) {
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)
@@ -645,7 +649,7 @@ def setColor(childDevice, huesettings, alert_deprecated = "", transition_depreca
def value = [sat: sat, hue: hue, alert: alert, transitiontime: transition]
if (huesettings.level != null) {
value.bri = Math.min(Math.round(huesettings.level * 255 / 100), 255)
if (huesettings.level == 1) value.bri = 1 else value.bri = Math.min(Math.round(huesettings.level * 255 / 100), 255)
value.on = value.bri > 0
}
@@ -723,8 +727,6 @@ private getBridgeIP() {
def serialNumber = selectedHue
def bridge = getHueBridges().find { it?.value?.serialNumber?.equalsIgnoreCase(serialNumber) }?.value
if (!bridge) {
//failed because mac address sent from hub is wrong and doesn't match the hue's real mac address and serial number
//in this case we will look up the bridge by comparing the incorrect mac addresses
bridge = getHueBridges().find { it?.value?.mac?.equalsIgnoreCase(serialNumber) }?.value
}
if (bridge?.ip && bridge?.port) {

View File

@@ -98,6 +98,15 @@ def installed() {
}
def updated() {
def currentDeviceIds = settings.collect { k, devices -> devices }.flatten().collect { it.id }.unique()
def subscriptionDevicesToRemove = app.subscriptions*.device.findAll { device ->
!currentDeviceIds.contains(device.id)
}
subscriptionDevicesToRemove.each { device ->
log.debug "Removing $device.displayName subscription"
state.remove(device.id)
unsubscribe(device)
}
log.debug settings
}

View File

@@ -22,7 +22,8 @@ definition(
category: "SmartThings Labs",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/life360.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/life360@2x.png",
oauth: [displayName: "Life360", displayLink: "Life360"]
oauth: [displayName: "Life360", displayLink: "Life360"],
singleInstance: true
) {
appSetting "clientId"
appSetting "clientSecret"
@@ -591,7 +592,7 @@ def updated() {
// log.debug "External Id=${app.id}:${member.id}"
// create the device
def childDevice = addChildDevice("smartthings", "life360-user", "${app.id}.${member.id}",null,[name:member.firstName, completedSetup: true])
def childDevice = addChildDevice("smartthings", "Life360 User", "${app.id}.${member.id}",null,[name:member.firstName, completedSetup: true])
// childDevice.setMemberId(member.id)
if (childDevice)

View File

@@ -5,22 +5,23 @@
*
*/
definition(
name: "LIFX (Connect)",
namespace: "smartthings",
author: "LIFX",
description: "Allows you to use LIFX smart light bulbs with SmartThings.",
category: "Convenience",
iconUrl: "https://cloud.lifx.com/images/lifx.png",
iconX2Url: "https://cloud.lifx.com/images/lifx.png",
iconX3Url: "https://cloud.lifx.com/images/lifx.png",
oauth: true) {
appSetting "clientId"
appSetting "clientSecret"
}
name: "LIFX (Connect)",
namespace: "smartthings",
author: "LIFX",
description: "Allows you to use LIFX smart light bulbs with SmartThings.",
category: "Convenience",
iconUrl: "https://cloud.lifx.com/images/lifx.png",
iconX2Url: "https://cloud.lifx.com/images/lifx.png",
iconX3Url: "https://cloud.lifx.com/images/lifx.png",
oauth: true,
singleInstance: true) {
appSetting "clientId"
appSetting "clientSecret"
}
preferences {
page(name: "Credentials", title: "LIFX", content: "authPage", install: false)
page(name: "Credentials", title: "LIFX", content: "authPage", install: true)
}
mappings {
@@ -32,29 +33,29 @@ mappings {
path("/test") { action: [ GET: "oauthSuccess" ] }
}
def getServerUrl() { return "https://graph.api.smartthings.com" }
def apiURL(path = '/') { return "https://api.lifx.com/v1beta1${path}" }
def buildRedirectUrl(page) {
return "${serverUrl}/api/token/${state.accessToken}/smartapps/installations/${app.id}/${page}"
}
def getServerUrl() { return "https://graph.api.smartthings.com" }
def getCallbackUrl() { return "https://graph.api.smartthings.com/oauth/callback"}
def apiURL(path = '/') { return "https://api.lifx.com/v1${path}" }
def getSecretKey() { return appSettings.secretKey }
def getClientId() { return appSettings.clientId }
def authPage() {
log.debug "authPage"
log.debug "authPage test1"
if (!state.lifxAccessToken) {
log.debug "no LIFX access token"
// This is the SmartThings access token
if (!state.accessToken) {
log.debug "no access token, create access token"
createAccessToken() // predefined method
state.accessToken = createAccessToken() // predefined method
}
def description = "Tap to enter LIFX credentials"
def redirectUrl = "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${state.accessToken}" // this triggers oauthInit() below
log.debug "app id: ${app.id}"
def redirectUrl = "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${state.accessToken}&apiServerUrl=${apiServerUrl}" // this triggers oauthInit() below
// def redirectUrl = "${apiServerUrl}"
log.debug "app id: ${app.id}"
log.debug "redirect url: ${redirectUrl}"
return dynamicPage(name: "Credentials", title: "Connect to LIFX", nextPage: null, uninstall: true, install:false) {
return dynamicPage(name: "Credentials", title: "Connect to LIFX", nextPage: null, uninstall: true, install:true) {
section {
href(url:redirectUrl, required:true, title:"Connect to LIFX", description:"Tap here to connect your LIFX account")
// href(url:buildRedirectUrl("test"), title: "Message test")
}
}
} else {
@@ -62,17 +63,15 @@ def authPage() {
def options = locationOptions() ?: []
def count = options.size()
def refreshInterval = 3
return dynamicPage(name:"Credentials", title:"Select devices...", nextPage:"", refreshInterval:refreshInterval, install:true, uninstall: true) {
return dynamicPage(name:"Credentials", title:"", nextPage:"", install:true, uninstall: true) {
section("Select your location") {
input "selectedLocationId", "enum", required:true, title:"Select location (${count} found)", multiple:false, options:options
input "selectedLocationId", "enum", required:true, title:"Select location (${count} found)", multiple:false, options:options, submitOnChange: true
}
}
}
}
// OAuth
def oauthInit() {
@@ -111,7 +110,7 @@ def oauthCallback() {
}
def oauthReceiveToken(redirectUrl = null) {
// Not sure what redirectUrl is for
log.debug "receiveToken - params: ${params}"
def oauthParams = [ client_id: "${appSettings.clientId}", client_secret: "${appSettings.clientSecret}", grant_type: "authorization_code", code: params.code, scope: params.scope ] // how is params.code valid here?
def params = [
@@ -134,25 +133,25 @@ def oauthReceiveToken(redirectUrl = null) {
def oauthSuccess() {
def message = """
<p>Your LIFX Account is now connected to SmartThings!</p>
<p>Click 'Done' to finish setup.</p>
"""
<p>Your LIFX Account is now connected to SmartThings!</p>
<p>Click 'Done' to finish setup.</p>
"""
oauthConnectionStatus(message)
}
def oauthFailure() {
def message = """
<p>The connection could not be established!</p>
<p>Click 'Done' to return to the menu.</p>
"""
<p>The connection could not be established!</p>
<p>Click 'Done' to return to the menu.</p>
"""
oauthConnectionStatus(message)
}
def oauthReceivedToken() {
def message = """
<p>Your LIFX Account is already connected to SmartThings!</p>
<p>Click 'Done' to finish setup.</p>
"""
<p>Your LIFX Account is already connected to SmartThings!</p>
<p>Click 'Done' to finish setup.</p>
"""
oauthConnectionStatus(message)
}
@@ -160,74 +159,74 @@ def oauthConnectionStatus(message, redirectUrl = null) {
def redirectHtml = ""
if (redirectUrl) {
redirectHtml = """
<meta http-equiv="refresh" content="3; url=${redirectUrl}" />
"""
<meta http-equiv="refresh" content="3; url=${redirectUrl}" />
"""
}
def html = """
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width">
<title>SmartThings Connection</title>
<style type="text/css">
@font-face {
font-family: 'Swiss 721 W01 Thin';
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot');
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot?#iefix') format('embedded-opentype'),
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.woff') format('woff'),
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.ttf') format('truetype'),
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.svg#swis721_th_btthin') format('svg');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'Swiss 721 W01 Light';
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot');
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot?#iefix') format('embedded-opentype'),
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.woff') format('woff'),
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.ttf') format('truetype'),
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.svg#swis721_lt_btlight') format('svg');
font-weight: normal;
font-style: normal;
}
.container {
width: 280;
padding: 20px;
text-align: center;
}
img {
vertical-align: middle;
}
img:nth-child(2) {
margin: 0 15px;
}
p {
font-size: 1.2em;
font-family: 'Swiss 721 W01 Thin';
text-align: center;
color: #666666;
padding: 0 20px;
margin-bottom: 0;
}
span {
font-family: 'Swiss 721 W01 Light';
}
</style>
${redirectHtml}
</head>
<body>
<div class="container">
<img src='https://cloud.lifx.com/images/lifx.png' alt='LIFX icon' width='100'/>
<img src='https://s3.amazonaws.com/smartapp-icons/Partner/support/connected-device-icn%402x.png' alt='connected device icon' width="40"/>
<img src='https://s3.amazonaws.com/smartapp-icons/Partner/support/st-logo%402x.png' alt='SmartThings logo' width="100"/>
<p>
${message}
</p>
</div>
</body>
</html>
"""
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width">
<title>SmartThings Connection</title>
<style type="text/css">
@font-face {
font-family: 'Swiss 721 W01 Thin';
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot');
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot?#iefix') format('embedded-opentype'),
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.woff') format('woff'),
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.ttf') format('truetype'),
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.svg#swis721_th_btthin') format('svg');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'Swiss 721 W01 Light';
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot');
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot?#iefix') format('embedded-opentype'),
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.woff') format('woff'),
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.ttf') format('truetype'),
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.svg#swis721_lt_btlight') format('svg');
font-weight: normal;
font-style: normal;
}
.container {
width: 280;
padding: 20px;
text-align: center;
}
img {
vertical-align: middle;
}
img:nth-child(2) {
margin: 0 15px;
}
p {
font-size: 1.2em;
font-family: 'Swiss 721 W01 Thin';
text-align: center;
color: #666666;
padding: 0 20px;
margin-bottom: 0;
}
span {
font-family: 'Swiss 721 W01 Light';
}
</style>
${redirectHtml}
</head>
<body>
<div class="container">
<img src='https://cloud.lifx.com/images/lifx.png' alt='LIFX icon' width='100'/>
<img src='https://s3.amazonaws.com/smartapp-icons/Partner/support/connected-device-icn%402x.png' alt='connected device icon' width="40"/>
<img src='https://s3.amazonaws.com/smartapp-icons/Partner/support/st-logo%402x.png' alt='SmartThings logo' width="100"/>
<p>
${message}
</p>
</div>
</body>
</html>
"""
render contentType: 'text/html', data: html
}
@@ -238,7 +237,6 @@ String toQueryString(Map m) {
// App lifecycle hooks
def installed() {
enableCallback() // wtf does this do?
if (!state.accessToken) {
createAccessToken()
} else {
@@ -250,7 +248,6 @@ def installed() {
// called after settings are changed
def updated() {
enableCallback() // not sure what this does
if (!state.accessToken) {
createAccessToken()
} else {
@@ -304,27 +301,36 @@ def logErrors(options = [errorReturn: null, logObject: log], Closure c) {
state.remove("lifxAccessToken")
options.logObject.warn "Access token is not valid"
}
return options.errerReturn
return options.errorReturn
} catch (java.net.SocketTimeoutException e) {
options.logObject.warn "Connection timed out, not much we can do here"
return options.errerReturn
return options.errorReturn
}
}
def apiGET(path) {
httpGet(uri: apiURL(path), headers: apiRequestHeaders()) {response ->
logResponse(response)
return response
try {
httpGet(uri: apiURL(path), headers: apiRequestHeaders()) {response ->
logResponse(response)
return response
}
} catch (groovyx.net.http.HttpResponseException e) {
logResponse(e.response)
return e.response
}
}
def apiPUT(path, body = [:]) {
log.debug("Beginning API PUT: ${path}, ${body}")
httpPutJson(uri: apiURL(path), body: new groovy.json.JsonBuilder(body).toString(), headers: apiRequestHeaders(), ) {response ->
logResponse(response)
return response
}
}
try {
log.debug("Beginning API PUT: ${path}, ${body}")
httpPutJson(uri: apiURL(path), body: new groovy.json.JsonBuilder(body).toString(), headers: apiRequestHeaders(), ) {response ->
logResponse(response)
return response
}
} catch (groovyx.net.http.HttpResponseException e) {
logResponse(e.response)
return e.response
}}
def devicesList(selector = '') {
logErrors([]) {
@@ -339,12 +345,12 @@ def devicesList(selector = '') {
}
Map locationOptions() {
def options = [:]
def devices = devicesList()
devices.each { device ->
options[device.location.id] = device.location.name
}
log.debug("Locations: ${options}")
return options
}
@@ -358,28 +364,32 @@ def updateDevices() {
state.devices = [:]
}
def devices = devicesInLocation()
def deviceIds = devices*.id
def selectors = []
log.debug("All selectors: ${selectors}")
devices.each { device ->
def childDevice = getChildDevice(device.id)
selectors.add("${device.id}")
if (!childDevice) {
log.info("Adding device ${device.id}: ${device.capabilities}")
log.info("Adding device ${device.id}: ${device.product}")
def data = [
label: device.label,
level: sprintf("%f", (device.brightness ?: 1) * 100),
level: Math.round((device.brightness ?: 1) * 100),
switch: device.connected ? device.power : "unreachable",
colorTemperature: device.color.kelvin
]
if (device.capabilities.has_color) {
if (device.product.capabilities.has_color) {
data["color"] = colorUtil.hslToHex((device.color.hue / 3.6) as int, (device.color.saturation * 100) as int)
data["hue"] = device.color.hue / 3.6
data["saturation"] = device.color.saturation * 100
childDevice = addChildDevice("smartthings", "LIFX Color Bulb", device.id, null, data)
childDevice = addChildDevice(app.namespace, "LIFX Color Bulb", device.id, null, data)
} else {
childDevice = addChildDevice("smartthings", "LIFX White Bulb", device.id, null, data)
childDevice = addChildDevice(app.namespace, "LIFX White Bulb", device.id, null, data)
}
}
}
getChildDevices().findAll { !deviceIds.contains(it.deviceNetworkId) }.each {
getChildDevices().findAll { !selectors.contains("${it.deviceNetworkId}") }.each {
log.info("Deleting ${it.deviceNetworkId}")
deleteChildDevice(it.deviceNetworkId)
}
@@ -391,4 +401,4 @@ def refreshDevices() {
getChildDevices().each { device ->
device.refresh()
}
}
}

View File

@@ -34,7 +34,7 @@
* locks | lock | lock, unlock | locked, unlocked
* ---------------------+-------------------+-----------------------------+------------------------------------
*/
definition(
name: "Logitech Harmony (Connect)",
namespace: "smartthings",
@@ -43,11 +43,11 @@ definition(
category: "SmartThings Labs",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/harmony.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/harmony%402x.png",
oauth: [displayName: "Logitech Harmony", displayLink: "http://www.logitech.com/en-us/harmony-remotes"]
oauth: [displayName: "Logitech Harmony", displayLink: "http://www.logitech.com/en-us/harmony-remotes"],
singleInstance: true
){
appSetting "clientId"
appSetting "clientSecret"
appSetting "callbackUrl"
}
preferences(oauthPage: "deviceAuthorization") {
@@ -89,16 +89,18 @@ mappings {
}
def getServerUrl() { return "https://graph.api.smartthings.com" }
def getCallbackUrl() { "https://graph.api.smartthings.com/oauth/callback" }
def getBuildRedirectUrl() { "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${state.accessToken}&apiServerUrl=${apiServerUrl}" }
def authPage() {
def description = null
def description = null
if (!state.HarmonyAccessToken) {
if (!state.accessToken) {
log.debug "About to create access token"
createAccessToken()
}
description = "Click to enter Harmony Credentials"
def redirectUrl = "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${state.accessToken}"
def redirectUrl = buildRedirectUrl
return dynamicPage(name: "Credentials", title: "Harmony", nextPage: null, uninstall: true, install:false) {
section { href url:redirectUrl, style:"embedded", required:true, title:"Harmony", description:description }
}
@@ -110,7 +112,7 @@ def authPage() {
def huboptions = state.HarmonyHubs ?: []
def actoptions = state.HarmonyActivities ?: []
def numFoundHub = huboptions.size() ?: 0
def numFoundAct = actoptions.size() ?: 0
if((deviceRefreshCount % 5) == 0) {
@@ -120,13 +122,14 @@ def authPage() {
section("Please wait while we discover your Harmony Hubs and Activities. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") {
input "selectedhubs", "enum", required:false, title:"Select Harmony Hubs (${numFoundHub} found)", multiple:true, options:huboptions
}
if (numFoundHub > 0 && numFoundAct > 0 && false)
// Virtual activity flag
if (numFoundHub > 0 && numFoundAct > 0 && true)
section("You can also add activities as virtual switches for other convenient integrations") {
input "selectedactivities", "enum", required:false, title:"Select Harmony Activities (${numFoundAct} found)", multiple:true, options:actoptions
}
}
if (state.resethub)
section("Connection to the hub timed out. Please restart the hub and try again.") {}
}
section("Connection to the hub timed out. Please restart the hub and try again.") {}
}
}
}
@@ -138,7 +141,7 @@ def callback() {
} else {
log.warn "No authQueryString"
}
if (state.HarmonyAccessToken) {
log.debug "Access token already exists"
discovery()
@@ -163,8 +166,8 @@ def callback() {
def init() {
log.debug "Requesting Code"
def oauthParams = [client_id: "${appSettings.clientId}", scope: "remote", response_type: "code", redirect_uri: "${appSettings.callbackUrl}" ]
redirect(location: "https://home.myharmony.com/oauth2/authorize?${toQueryString(oauthParams)}")
def oauthParams = [client_id: "${appSettings.clientId}", scope: "remote", response_type: "code", redirect_uri: "${callbackUrl}" ]
redirect(location: "https://home.myharmony.com/oauth2/authorize?${toQueryString(oauthParams)}")
}
def receiveToken(redirectUrl = null) {
@@ -174,7 +177,7 @@ def receiveToken(redirectUrl = null) {
uri: "https://home.myharmony.com/oauth2/token?${toQueryString(oauthParams)}",
]
try {
httpPost(params) { response ->
httpPost(params) { response ->
state.HarmonyAccessToken = response.data.access_token
}
} catch (java.util.concurrent.TimeoutException e) {
@@ -221,7 +224,7 @@ def connectionStatus(message, redirectUrl = null) {
<meta http-equiv="refresh" content="3; url=${redirectUrl}" />
"""
}
def html = """
<!DOCTYPE html>
<html>
@@ -302,30 +305,28 @@ def buildRedirectUrl(page) {
}
def installed() {
enableCallback()
if (!state.accessToken) {
log.debug "About to create access token"
createAccessToken()
} else {
} else {
initialize()
}
}
def updated() {
unsubscribe()
unschedule()
enableCallback()
unschedule()
if (!state.accessToken) {
log.debug "About to create access token"
createAccessToken()
} else {
} else {
initialize()
}
}
}
def uninstalled() {
if (state.HarmonyAccessToken) {
try {
try {
state.HarmonyAccessToken = ""
log.debug "Success disconnecting Harmony from SmartThings"
} catch (groovyx.net.http.HttpResponseException e) {
@@ -339,7 +340,7 @@ def initialize() {
if (selectedhubs || selectedactivities) {
addDevice()
runEvery5Minutes("discovery")
}
}
}
def getHarmonydevices() {
@@ -359,20 +360,20 @@ Map discoverDevices() {
def hubname = getHubName(it.key)
def hubvalue = "${hubname}"
hubs["harmony-${hubkey}"] = hubvalue
it.value.response.data.activities.each {
it.value.response.data.activities.each {
def value = "${it.value.name}"
def key = "harmony-${hubkey}-${it.key}"
activities["${key}"] = value
}
}
}
state.HarmonyHubs = hubs
state.HarmonyActivities = activities
}
state.HarmonyActivities = activities
}
}
//CHILD DEVICE METHODS
def discovery() {
def Params = [auth: state.HarmonyAccessToken]
def Params = [auth: state.HarmonyAccessToken]
def url = "https://home.myharmony.com/cloudapi/activity/all?${toQueryString(Params)}"
try {
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
@@ -384,11 +385,11 @@ def discovery() {
poll()
} else {
log.debug "Error: $response.status"
}
}
}
} catch (groovyx.net.http.HttpResponseException e) {
if (e.statusCode == 401) { // token is expired
state.remove("HarmonyAccessToken")
state.remove("HarmonyAccessToken")
log.warn "Harmony Access token has expired"
}
} catch (java.net.SocketTimeoutException e) {
@@ -396,12 +397,12 @@ def discovery() {
state.resethub = true
} catch (e) {
log.warn "Hostname in certificate didn't match. Please try again later."
}
}
return null
}
def addDevice() {
log.trace "Adding Hubs"
log.trace "Adding Hubs"
selectedhubs.each { dni ->
def d = getChildDevice(dni)
if(!d) {
@@ -412,8 +413,8 @@ def addDevice() {
} else {
log.trace "found ${d.displayName} with id $dni already exists"
}
}
log.trace "Adding Activities"
}
log.trace "Adding Activities"
selectedactivities.each { dni ->
def d = getChildDevice(dni)
if(!d) {
@@ -424,7 +425,7 @@ def addDevice() {
} else {
log.trace "found ${d.displayName} with id $dni already exists"
}
}
}
}
def activity(dni,mode) {
@@ -432,26 +433,26 @@ def activity(dni,mode) {
def msg = "Command failed"
def url = ''
if (dni == "all") {
url = "https://home.myharmony.com/cloudapi/activity/off?${toQueryString(Params)}"
} else {
url = "https://home.myharmony.com/cloudapi/activity/off?${toQueryString(Params)}"
} else {
def aux = dni.split('-')
def hubId = aux[1]
if (mode == "hub" || (aux.size() <= 2) || (aux[2] == "off")){
url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/off?${toQueryString(Params)}"
if (mode == "hub" || (aux.size() <= 2) || (aux[2] == "off")){
url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/off?${toQueryString(Params)}"
} else {
def activityId = aux[2]
url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/${activityId}/${mode}?${toQueryString(Params)}"
}
}
}
try {
httpPostJson(uri: url) { response ->
httpPostJson(uri: url) { response ->
if (response.data.code == 200 || dni == "all") {
msg = "Command sent succesfully"
state.aux = 0
state.aux = 0
} else {
msg = "Command failed. Error: $response.data.code"
}
}
}
} catch (groovyx.net.http.HttpResponseException ex) {
log.error ex
if (state.aux == 0) {
@@ -459,7 +460,7 @@ def activity(dni,mode) {
activity(dni,mode)
} else {
msg = ex
state.aux = 0
state.aux = 0
}
} catch(Exception ex) {
msg = ex
@@ -472,10 +473,10 @@ def poll() {
// GET THE LIST OF ACTIVITIES
if (state.HarmonyAccessToken) {
getActivityList()
def Params = [auth: state.HarmonyAccessToken]
def Params = [auth: state.HarmonyAccessToken]
def url = "https://home.myharmony.com/cloudapi/state?${toQueryString(Params)}"
try {
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
def map = [:]
response.data.hubs.each {
if (it.value.message == "OK") {
@@ -488,20 +489,20 @@ def poll() {
def currentActivity = getActivityName(it.value.response.data.currentAvActivity,it.key)
hub.sendEvent(name: "currentActivity", value: currentActivity, descriptionText: "Current activity is ${currentActivity}", display: false)
}
}
}
} else {
log.trace it.value.message
}
}
}
def activities = getChildDevices()
def activities = getChildDevices()
def activitynotrunning = true
activities.each { activity ->
def act = activity.deviceNetworkId.split('-')
def act = activity.deviceNetworkId.split('-')
if (act.size() > 2) {
def aux = map.find { it.key == act[1] }
if (aux) {
def aux2 = aux.value.split(',')
def childDevice = getChildDevice(activity.deviceNetworkId)
def childDevice = getChildDevice(activity.deviceNetworkId)
if ((act[2] == aux2[0]) && (aux2[1] == "1" || aux2[1] == "2")) {
childDevice?.sendEvent(name: "switch", value: "on")
if (aux2[1] == "1")
@@ -511,30 +512,30 @@ def poll() {
if (aux2[1] == "3")
runIn(5, "poll", [overwrite: true])
}
}
}
}
}
}
return "Poll completed $map - $state.hubs"
}
} catch (groovyx.net.http.HttpResponseException e) {
if (e.statusCode == 401) { // token is expired
state.remove("HarmonyAccessToken")
state.remove("HarmonyAccessToken")
return "Harmony Access token has expired"
}
}
} catch(Exception e) {
log.trace e
}
}
}
}
def getActivityList() {
// GET ACTIVITY'S NAME
if (state.HarmonyAccessToken) {
def Params = [auth: state.HarmonyAccessToken]
def Params = [auth: state.HarmonyAccessToken]
def url = "https://home.myharmony.com/cloudapi/activity/all?${toQueryString(Params)}"
try {
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
response.data.hubs.each {
def hub = getChildDevice("harmony-${it.key}")
if (hub) {
@@ -547,10 +548,10 @@ def getActivityList() {
}
activities += [id: "off", name: "Activity OFF", type: "0"]
log.trace activities
}
hub.sendEvent(name: "activities", value: new groovy.json.JsonBuilder(activities).toString(), descriptionText: "Activities are ${activities.collect { it.name }?.join(', ')}", display: false)
}
hub.sendEvent(name: "activities", value: new groovy.json.JsonBuilder(activities).toString(), descriptionText: "Activities are ${activities.collect { it.name }?.join(', ')}", display: false)
}
}
}
}
} catch (groovyx.net.http.HttpResponseException e) {
log.trace e
@@ -559,7 +560,7 @@ def getActivityList() {
} catch(Exception e) {
log.trace e
}
}
}
return activity
}
@@ -567,16 +568,16 @@ def getActivityName(activity,hubId) {
// GET ACTIVITY'S NAME
def actname = activity
if (state.HarmonyAccessToken) {
def Params = [auth: state.HarmonyAccessToken]
def Params = [auth: state.HarmonyAccessToken]
def url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/all?${toQueryString(Params)}"
try {
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
actname = response.data.data.activities[activity].name
}
} catch(Exception e) {
log.trace e
}
}
}
return actname
}
@@ -584,19 +585,19 @@ def getActivityId(activity,hubId) {
// GET ACTIVITY'S NAME
def actid = activity
if (state.HarmonyAccessToken) {
def Params = [auth: state.HarmonyAccessToken]
def Params = [auth: state.HarmonyAccessToken]
def url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/all?${toQueryString(Params)}"
try {
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
response.data.data.activities.each {
if (it.value.name == activity)
actid = it.key
}
}
}
} catch(Exception e) {
log.trace e
}
}
}
return actid
}
@@ -604,16 +605,16 @@ def getHubName(hubId) {
// GET HUB'S NAME
def hubname = hubId
if (state.HarmonyAccessToken) {
def Params = [auth: state.HarmonyAccessToken]
def Params = [auth: state.HarmonyAccessToken]
def url = "https://home.myharmony.com/cloudapi/hub/${hubId}/discover?${toQueryString(Params)}"
try {
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
hubname = response.data.data.name
}
} catch(Exception e) {
log.trace e
}
}
}
}
return hubname
}
@@ -624,8 +625,8 @@ def sendNotification(msg) {
def hookEventHandler() {
// log.debug "In hookEventHandler method."
log.debug "request = ${request}"
def json = request.JSON
def json = request.JSON
def html = """{"code":200,"message":"OK"}"""
render contentType: 'application/json', data: html
@@ -659,12 +660,16 @@ def updateDevice() {
} else {
def device = allDevices.find { it.id == params.id }
if (device) {
if (arguments) {
device."$command"(*arguments)
} else {
device."$command"()
if (device.hasCommand("$command")) {
if (arguments) {
device."$command"(*arguments)
} else {
device."$command"()
}
render status: 204, data: "{}"
} else {
render status: 404, data: '{"msg": "Command not supported by this Device"}'
}
render status: 204, data: "{}"
} else {
render status: 404, data: '{"msg": "Device not found"}'
}

View File

@@ -49,6 +49,7 @@ preferences {
section("Via a push notification and/or an SMS message"){
input("recipients", "contact", title: "Send notifications to") {
input "phone", "phone", title: "Phone Number (for SMS, optional)", required: false
paragraph "If outside the US please make sure to enter the proper country code"
input "pushAndPhone", "enum", title: "Both Push and SMS?", required: false, options: ["Yes", "No"]
}
}

View File

@@ -22,7 +22,8 @@ definition(
description: "Allows you to control your Samsung TV from the SmartThings app. Perform basic functions like power Off, source, volume, channels and other remote control functions.",
category: "SmartThings Labs",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Samsung/samsung-remote%402x.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Samsung/samsung-remote%403x.png"
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Samsung/samsung-remote%403x.png",
singleInstance: true
)
preferences {

View File

@@ -16,8 +16,8 @@
*
*/
definition(
name: "Send HAM Bridge Command When",
namespace: "soletc.com",
name: "Send HAM Bridge Command When",
namespace: "smartthings",
author: "Scottin Pollock",
description: "Sends a command to your HAM Bridge server when SmartThings are activated.",
category: "Convenience",
@@ -25,7 +25,6 @@ definition(
iconX2Url: "http://solutionsetcetera.com/stuff/STIcons/HB@2x.png"
)
preferences {
section("Choose one or more, when..."){
input "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true

View File

@@ -10,24 +10,24 @@
* 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.
*
* Sonos Control
* Speaker Control
*
* Author: SmartThings
*
* Date: 2013-12-10
*/
definition(
name: "Sonos Control",
name: "Speaker Control",
namespace: "smartthings",
author: "SmartThings",
description: "Play or pause your Sonos when certain actions take place in your home.",
description: "Play or pause your Speaker when certain actions take place in your home.",
category: "SmartThings Labs",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos@2x.png"
)
preferences {
page(name: "mainPage", title: "Control your Sonos when something happens", install: true, uninstall: true)
page(name: "mainPage", title: "Control your Speaker when something happens", install: true, uninstall: true)
page(name: "timeIntervalInput", title: "Only during a certain time") {
section {
input "starting", "time", title: "Starting", required: false
@@ -81,7 +81,7 @@ def mainPage() {
]
}
section {
input "sonos", "capability.musicPlayer", title: "Sonos music player", required: true
input "sonos", "capability.musicPlayer", title: "Speaker music player", required: true
}
section("More options", hideable: true, hidden: true) {
input "volume", "number", title: "Set the volume volume", description: "0-100%", required: false

View File

@@ -10,7 +10,7 @@
* 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.
*
* Sonos Mood Music
* Speaker Mood Music
*
* Author: SmartThings
* Date: 2014-02-12
@@ -65,7 +65,7 @@ private saveSelectedSong() {
}
definition(
name: "Sonos Mood Music",
name: "Speaker Mood Music",
namespace: "smartthings",
author: "SmartThings",
description: "Plays a selected song or station.",
@@ -75,7 +75,7 @@ definition(
)
preferences {
page(name: "mainPage", title: "Play a selected song or station on your Sonos when something happens", nextPage: "chooseTrack", uninstall: true)
page(name: "mainPage", title: "Play a selected song or station on your Speaker when something happens", nextPage: "chooseTrack", uninstall: true)
page(name: "chooseTrack", title: "Select a song", install: true)
page(name: "timeIntervalInput", title: "Only during a certain time") {
section {
@@ -125,7 +125,7 @@ def mainPage() {
ifUnset "timeOfDay", "time", title: "At a Scheduled Time", required: false
}
section {
input "sonos", "capability.musicPlayer", title: "On this Sonos player", required: true
input "sonos", "capability.musicPlayer", title: "On this Speaker player", required: true
}
section("More options", hideable: true, hidden: true) {
input "volume", "number", title: "Set the volume", description: "0-100%", required: false

View File

@@ -10,23 +10,23 @@
* 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.
*
* Sonos Custom Message
* Speaker Custom Message
*
* Author: SmartThings
* Date: 2014-1-29
*/
definition(
name: "Sonos Notify with Sound",
name: "Speaker Notify with Sound",
namespace: "smartthings",
author: "SmartThings",
description: "Play a sound or custom message through your Sonos when the mode changes or other events occur.",
description: "Play a sound or custom message through your Speaker when the mode changes or other events occur.",
category: "SmartThings Labs",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos@2x.png"
)
preferences {
page(name: "mainPage", title: "Play a message on your Sonos when something happens", install: true, uninstall: true)
page(name: "mainPage", title: "Play a message on your Speaker when something happens", install: true, uninstall: true)
page(name: "chooseTrack", title: "Select a song or station")
page(name: "timeIntervalInput", title: "Only during a certain time") {
section {
@@ -75,8 +75,8 @@ def mainPage() {
ifUnset "timeOfDay", "time", title: "At a Scheduled Time", required: false
}
section{
input "actionType", "enum", title: "Action?", required: true, defaultValue: "Custom Message", options: [
"Custom Message",
input "actionType", "enum", title: "Action?", required: true, defaultValue: "Bell 1", options: [
//"Custom Message",
"Bell 1",
"Bell 2",
"Dogs Barking",
@@ -89,10 +89,10 @@ def mainPage() {
"Someone is arriving",
"Piano",
"Lightsaber"]
input "message","text",title:"Play this message", required:false, multiple: false
//input "message","text",title:"Play this message", required:false, multiple: false
}
section {
input "sonos", "capability.musicPlayer", title: "On this Sonos player", required: true
input "sonos", "capability.musicPlayer", title: "On this Speaker player", required: true
}
section("More options", hideable: true, hidden: true) {
input "resumePlaying", "bool", title: "Resume currently playing music after notification", required: false, defaultValue: true
@@ -409,12 +409,13 @@ private loadText() {
state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/lightsaber.mp3", duration: "10"]
break;
default:
if (message) {
/*if (message) {
state.sound = textToSpeech(message instanceof List ? message[0] : message) // not sure why this is (sometimes) needed)
}
else {
state.sound = textToSpeech("You selected the custom message option but did not enter a message in the $app.label Smart App")
}
}*/
state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/bell1.mp3", duration: "10"]
break;
}
}

View File

@@ -10,23 +10,23 @@
* 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.
*
* Sonos Weather Forecast
* Speaker Weather Forecast
*
* Author: SmartThings
* Date: 2014-1-29
*/
definition(
name: "Sonos Weather Forecast",
name: "Speaker Weather Forecast",
namespace: "smartthings",
author: "SmartThings",
description: "Play a weather report through your Sonos when the mode changes or other events occur",
description: "Play a weather report through your Speaker when the mode changes or other events occur",
category: "SmartThings Labs",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos@2x.png"
)
preferences {
page(name: "mainPage", title: "Play the weather report on your sonos", install: true, uninstall: true)
page(name: "mainPage", title: "Play the weather report on your speaker", install: true, uninstall: true)
page(name: "chooseTrack", title: "Select a song or station")
page(name: "timeIntervalInput", title: "Only during a certain time") {
section {
@@ -85,7 +85,7 @@ def mainPage() {
)
}
section {
input "sonos", "capability.musicPlayer", title: "On this Sonos player", required: true
input "sonos", "capability.musicPlayer", title: "On this Speaker player", required: true
}
section("More options", hideable: true, hidden: true) {
input "resumePlaying", "bool", title: "Resume currently playing music after weather report finishes", required: false, defaultValue: true

View File

@@ -23,7 +23,8 @@ definition(
description: "Integrate your Tesla car with SmartThings.",
category: "SmartThings Labs",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/tesla-app%402x.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/tesla-app%403x.png"
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/tesla-app%403x.png",
singleInstance: true
)
preferences {

View File

@@ -22,7 +22,8 @@ definition(
description: "Allows you to integrate your WeMo Switch and Wemo Motion sensor with SmartThings.",
category: "SmartThings Labs",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/wemo.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/wemo@2x.png"
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/wemo@2x.png",
singleInstance: true
)
preferences {
@@ -61,10 +62,7 @@ def firstPage()
log.debug "REFRESH COUNT :: ${refreshCount}"
if(!state.subscribe) {
subscribe(location, null, locationHandler, [filterEvents:false])
state.subscribe = true
}
subscribe(location, null, locationHandler, [filterEvents:false])
//ssdp request every 25 seconds
if((refreshCount % 5) == 0) {
@@ -80,7 +78,7 @@ def firstPage()
def motionsDiscovered = motionsDiscovered()
def lightSwitchesDiscovered = lightSwitchesDiscovered()
return dynamicPage(name:"firstPage", title:"Discovery Started!", nextPage:"", refreshInterval: refreshInterval, install:true, uninstall: selectedSwitches != null || selectedMotions != null || selectedLightSwitches != null) {
return dynamicPage(name:"firstPage", title:"Discovery Started!", nextPage:"", refreshInterval: refreshInterval, install:true, uninstall: true) {
section("Select a device...") {
input "selectedSwitches", "enum", required:false, title:"Select Wemo Switches \n(${switchesDiscovered.size() ?: 0} found)", multiple:true, options:switchesDiscovered
input "selectedMotions", "enum", required:false, title:"Select Wemo Motions \n(${motionsDiscovered.size() ?: 0} found)", multiple:true, options:motionsDiscovered
@@ -168,21 +166,30 @@ def getWemoLightSwitches()
def installed() {
log.debug "Installed with settings: ${settings}"
initialize()
runIn(5, "subscribeToDevices") //initial subscriptions delayed by 5 seconds
runIn(10, "refreshDevices") //refresh devices, delayed by 10 seconds
runIn(900, "doDeviceSync" , [overwrite: false]) //setup ip:port syncing every 15 minutes
// SUBSCRIBE responses come back with TIMEOUT-1801 (30 minutes), so we refresh things a bit before they expire (29 minutes)
runIn(1740, "refresh", [overwrite: false])
}
def updated() {
log.debug "Updated with settings: ${settings}"
initialize()
}
runIn(5, "subscribeToDevices") //subscribe again to new/old devices wait 5 seconds
runIn(10, "refreshDevices") //refresh devices again, delayed by 10 seconds
def initialize() {
unsubscribe()
unschedule()
subscribe(location, null, locationHandler, [filterEvents:false])
if (selectedSwitches)
addSwitches()
if (selectedMotions)
addMotions()
if (selectedLightSwitches)
addLightSwitches()
runIn(5, "subscribeToDevices") //initial subscriptions delayed by 5 seconds
runIn(10, "refreshDevices") //refresh devices, delayed by 10 seconds
runEvery5Minutes("refresh")
}
def resubscribe() {
@@ -192,8 +199,7 @@ def resubscribe() {
def refresh() {
log.debug "refresh() called"
//reschedule the refreshes
runIn(1740, "refresh", [overwrite: false])
doDeviceSync()
refreshDevices()
}
@@ -236,7 +242,8 @@ def addSwitches() {
"port": selectedSwitch.value.port
]
])
def ipvalue = convertHexToIP(selectedSwitch.value.ip)
d.sendEvent(name: "currentIP", value: ipvalue, descriptionText: "IP is ${ipvalue}")
log.debug "Created ${d.displayName} with id: ${d.id}, dni: ${d.deviceNetworkId}"
} else {
log.debug "found ${d.displayName} with id $dni already exists"
@@ -266,8 +273,9 @@ def addMotions() {
"port": selectedMotion.value.port
]
])
log.debug "Created ${d.displayName} with id: ${d.id}, dni: ${d.deviceNetworkId}"
def ipvalue = convertHexToIP(selectedMotion.value.ip)
d.sendEvent(name: "currentIP", value: ipvalue, descriptionText: "IP is ${ipvalue}")
log.debug "Created ${d.displayName} with id: ${d.id}, dni: ${d.deviceNetworkId}"
} else {
log.debug "found ${d.displayName} with id $dni already exists"
}
@@ -296,7 +304,8 @@ def addLightSwitches() {
"port": selectedLightSwitch.value.port
]
])
def ipvalue = convertHexToIP(selectedLightSwitch.value.ip)
d.sendEvent(name: "currentIP", value: ipvalue, descriptionText: "IP is ${ipvalue}")
log.debug "created ${d.displayName} with id $dni"
} else {
log.debug "found ${d.displayName} with id $dni already exists"
@@ -304,27 +313,6 @@ def addLightSwitches() {
}
}
def initialize() {
// remove location subscription afterwards
unsubscribe()
state.subscribe = false
if (selectedSwitches)
{
addSwitches()
}
if (selectedMotions)
{
addMotions()
}
if (selectedLightSwitches)
{
addLightSwitches()
}
}
def locationHandler(evt) {
def description = evt.description
def hub = evt?.hubId
@@ -333,53 +321,32 @@ def locationHandler(evt) {
log.debug parsedEvent
if (parsedEvent?.ssdpTerm?.contains("Belkin:device:controllee") || parsedEvent?.ssdpTerm?.contains("Belkin:device:insight")) {
def switches = getWemoSwitches()
if (!(switches."${parsedEvent.ssdpUSN.toString()}"))
{ //if it doesn't already exist
if (!(switches."${parsedEvent.ssdpUSN.toString()}")) {
//if it doesn't already exist
switches << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent]
}
else
{ // just update the values
} else {
log.debug "Device was already found in state..."
def d = switches."${parsedEvent.ssdpUSN.toString()}"
boolean deviceChangedValues = false
log.debug "$d.ip <==> $parsedEvent.ip"
if(d.ip != parsedEvent.ip || d.port != parsedEvent.port) {
d.ip = parsedEvent.ip
d.port = parsedEvent.port
deviceChangedValues = true
log.debug "Device's port or ip changed..."
def child = getChildDevice(parsedEvent.mac)
child.subscribe(parsedEvent.ip, parsedEvent.port)
child.poll()
}
if (deviceChangedValues) {
def children = getChildDevices()
log.debug "Found children ${children}"
children.each {
if (it.getDeviceDataByName("mac") == parsedEvent.mac) {
log.debug "updating ip and port, and resubscribing, for device ${it} with mac ${parsedEvent.mac}"
it.subscribe(parsedEvent.ip, parsedEvent.port)
}
}
}
}
}
else if (parsedEvent?.ssdpTerm?.contains("Belkin:device:sensor")) {
def motions = getWemoMotions()
if (!(motions."${parsedEvent.ssdpUSN.toString()}"))
{ //if it doesn't already exist
if (!(motions."${parsedEvent.ssdpUSN.toString()}")) {
//if it doesn't already exist
motions << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent]
}
else
{ // just update the values
} else { // just update the values
log.debug "Device was already found in state..."
def d = motions."${parsedEvent.ssdpUSN.toString()}"
@@ -412,10 +379,7 @@ def locationHandler(evt) {
if (!(lightSwitches."${parsedEvent.ssdpUSN.toString()}"))
{ //if it doesn't already exist
lightSwitches << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent]
}
else
{ // just update the values
} else {
log.debug "Device was already found in state..."
def d = lightSwitches."${parsedEvent.ssdpUSN.toString()}"
@@ -426,21 +390,11 @@ def locationHandler(evt) {
d.port = parsedEvent.port
deviceChangedValues = true
log.debug "Device's port or ip changed..."
def child = getChildDevice(parsedEvent.mac)
log.debug "updating ip and port, and resubscribing, for device with mac ${parsedEvent.mac}"
child.subscribe(parsedEvent.ip, parsedEvent.port)
}
if (deviceChangedValues) {
def children = getChildDevices()
log.debug "Found children ${children}"
children.each {
if (it.getDeviceDataByName("mac") == parsedEvent.mac) {
log.debug "updating ip and port, and resubscribing, for device ${it} with mac ${parsedEvent.mac}"
it.subscribe(parsedEvent.ip, parsedEvent.port)
}
}
}
}
}
else if (parsedEvent.headers && parsedEvent.body) {
String headerString = new String(parsedEvent.headers.decodeBase64())?.toLowerCase()
@@ -580,73 +534,30 @@ private def parseDiscoveryMessage(String description) {
}
}
}
device
}
def doDeviceSync(){
log.debug "Doing Device Sync!"
runIn(900, "doDeviceSync" , [overwrite: false]) //schedule to run again in 15 minutes
if(!state.subscribe) {
subscribe(location, null, locationHandler, [filterEvents:false])
state.subscribe = true
}
discoverAllWemoTypes()
}
def pollChildren() {
def devices = getAllChildDevices()
devices.each { d ->
//only poll switches?
d.poll()
}
private String convertHexToIP(hex) {
[convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
}
def delayPoll() {
log.debug "Executing 'delayPoll'"
runIn(5, "pollChildren")
private Integer convertHexToInt(hex) {
Integer.parseInt(hex,16)
}
/*def poll() {
log.debug "Executing 'poll'"
runIn(600, "poll", [overwrite: false]) //schedule to run again in 10 minutes
def lastPoll = getLastPollTime()
def currentTime = now()
def lastPollDiff = currentTime - lastPoll
log.debug "lastPoll: $lastPoll, currentTime: $currentTime, lastPollDiff: $lastPollDiff"
setLastPollTime(currentTime)
doDeviceSync()
}
def setLastPollTime(currentTime) {
state.lastpoll = currentTime
}
def getLastPollTime() {
state.lastpoll ?: now()
}
def now() {
new Date().getTime()
}*/
private Boolean canInstallLabs()
{
private Boolean canInstallLabs() {
return hasAllHubsOver("000.011.00603")
}
private Boolean hasAllHubsOver(String desiredFirmware)
{
private Boolean hasAllHubsOver(String desiredFirmware) {
return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware }
}
private List getRealHubFirmwareVersions()
{
private List getRealHubFirmwareVersions() {
return location.hubs*.firmwareVersionString.findAll { it }
}

View File

@@ -24,7 +24,8 @@ definition(
category: "Connections",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/withings.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/withings%402x.png",
oauth: true
oauth: true,
singleInstance: true
) {
appSetting "clientId"
appSetting "clientSecret"

View File

@@ -24,7 +24,8 @@ definition(
category: "SmartThings Internal",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience%402x.png",
oauth: true
oauth: true,
singleInstance: true
) {
appSetting "serverUrl"
}

View File

@@ -15,7 +15,7 @@
*/
definition(
name: "Sprayer Controller 2",
namespace: "",
namespace: "sprayercontroller",
author: "Cooper Lee",
description: "Control Sprayers for a period of time a number of times per hour",
category: "My Apps",

View File

@@ -58,7 +58,8 @@ definition(
description: "Connect your Quirky to SmartThings.",
category: "SmartThings Labs",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/quirky.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/quirky@2x.png"
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/quirky@2x.png",
singleInstance: true
) {
appSetting "clientId"
appSetting "clientSecret"

View File

@@ -25,7 +25,8 @@ definition(
description: "Connect your TCP bulbs to SmartThings using Cloud to Cloud integration. You must create a remote login acct on TCP Mobile App.",
category: "SmartThings Labs",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/tcp.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/tcp@2x.png"
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/tcp@2x.png",
singleInstance: true
)