Compare commits

...

219 Commits

Author SHA1 Message Date
Bruce Ravenel
9ee81263b1 MSA-692: Allows control of Aeon Micro Motor Controller, a Z-Wave device, both from the mobile app tiles, and SmartApps. The device type maps the three hardware states into ST: Up, Down, Stop, and provides commands with the same names. When used with motor shades, generally the motor has an adjustable automatic stop at the full height or extension of the shade, so the physical switch would just select Up, and that was that. The motor stopped despite the switch. The physical switch would typically also have an Off position. The micro motor controller does as well, hence the stop() command. Below is the ReadMe for this device type:
To use this device type in SmartApps, use "capability.doorControl" in Preferences to select the controller.  
Then you can reference the state of the device with device.currentValue("motor") or device.currentMotor.  
The device can be in one of three states: up, down, stop, e.g. if(device.currentMotor == "up") {...}  
There are three commands available: up(), down(), stop(), e.g. device.up()

After you include the Aeon device into ST, edit its device type in the IDE to be "Aeon Motor Controller"

Down turns on motor M1, and Up turns on motor M2.
2015-11-17 17:23:58 -06:00
Vinay Rao
01a36696d8 Merge pull request #291 from kwarodom/LiFXProd
LiFX - LiFX: update oauth/callback url using getApiServerUrl() for shard proxying and change shardUrl param to apiServerUrl
2015-11-17 15:00:30 -08:00
Yaima Valdivia
797def2935 Merge branch 'master' of github.com:SmartThingsCommunity/SmartThingsPublic 2015-11-17 10:40:04 -08:00
Yaima Valdivia
1034cd06e6 Var names reversed 2015-11-17 10:38:01 -08:00
Vinay Rao
c37729242e temporary changes to the lock DTH to account for the upcoming zigbee library changes 2015-11-17 10:38:01 -08:00
Vinay Rao
d29c3ec557 Cree DTH refresh 2015-11-17 10:38:01 -08:00
Vinay Rao
17be85b846 Moving re-certified device to generic zigbee DTH. 2015-11-17 10:38:01 -08:00
Vinay Rao
da06104563 Adding fingerprint for new bulbs and transition old color temperature osram bulbs to new one 2015-11-17 10:38:01 -08:00
Vinay Rao
5d37ac8515 new fingerprints for Yale. battery issue resolution for Yale 2015-11-17 10:38:01 -08:00
Vinay Rao
2a739fda07 adding fingerprints for osram and sengled dimmable bulbs 2015-11-17 10:38:01 -08:00
Juan Pablo Risso
f627fb4fac Reverted to previously working getBridgeIP() 2015-11-17 10:38:01 -08:00
Juan Pablo Risso
1d30a718b2 Replaced atomicState with State 2015-11-17 10:38:01 -08:00
Juan Pablo Risso
303ca7117c Added a "," 2015-11-17 10:38:01 -08:00
Juan Pablo Risso
1c96645b9f removed function ShardUrl() 2015-11-17 10:38:00 -08:00
Juan Pablo Risso
af4dc0640a added singleInstance: true 2015-11-17 10:38:00 -08:00
juano2310
80b46153dc Harmony Global Oauth 2015-11-17 10:38:00 -08:00
Mike Robinet
b92cd9c637 CREX-1094 Delete stale device subscriptions on IFTTT app update 2015-11-17 10:38:00 -08:00
Juan Pablo Risso
1039b65c81 Code Cleanup 2015-11-17 10:38:00 -08:00
juano2310
4e203e8e13 buildActionUrl("hookCallback") 2015-11-17 10:38:00 -08:00
juano2310
61398105d1 Jawbone Global Oauth 2015-11-17 10:38:00 -08:00
bflorian
505efc5463 Deleted lights on when door opens after sundown 2015-11-17 10:38:00 -08:00
bflorian
3ddc82f996 More name/namespace changes 2015-11-17 10:38:00 -08:00
Juan Pablo Risso
13a324069d Force Level = 1% to 1
This ensures that the bulb will be able to dim to it minimum
2015-11-17 10:38:00 -08:00
bflorian
2fd5859326 Misc filename and namespace changes. 2015-11-17 10:38:00 -08:00
bflorian
bbedbddf9d Filename corrections 2015-11-17 10:38:00 -08:00
bflorian
e424e7abdd Corrected filename of Z-Wave Device Multichannel 2015-11-17 10:38:00 -08:00
Tom Manley
47fbdabf6b 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-17 10:37:59 -08:00
Mike Robinet
7defe1cc61 CREX-3129 Update parent and service manager apps to be singleton 2015-11-17 10:37:59 -08:00
Mike Cousins
a78459347b update keen home smart vent device handler 2015-11-17 10:37:59 -08:00
juano2310
c473745e47 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-11-17 10:37:59 -08:00
Juan Pablo Risso
fc587ef15a 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-11-17 10:37:59 -08:00
Warodom Khamphanchai
0f3b730f26 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-11-17 10:37:59 -08:00
Warodom Khamphanchai
11df2f31b3 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-11-17 10:37:59 -08:00
Warodom Khamphanchai
5e93bca030 LiFX - change shardUrl param to apiServerUrl 2015-11-16 16:33:07 -08: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
Yaima Valdivia
4d243bf44d Rename Sonos SmartApps
https://smartthings.atlassian.net/browse/DVCSMP-607
2015-10-27 12:29:05 -07: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
Kristofer Schaller
1965f10584 Merge pull request #192 from tslagle13/master
Fix requirement for SMS
2015-10-20 13:42:08 -07:00
Warodom Khamphanchai
9c9fba0939 Merge pull request #193 from kwarodom/newDimmerSwitch
Refactor and update z-wave dimmer-switch DTH
2015-10-19 15:38:41 -07:00
Vinay Rao
725f9ebec7 Merge pull request #204 from workingmonk/white_color_temp
Generic ZigBee White color temp
2015-10-19 14:40:37 -07:00
Vinay Rao
e217805d98 generic device type for zigbee color temperature bulb 2015-10-19 14:39:44 -07:00
bflorian
ff39270ba4 Removed accidentally added device type 2015-10-16 17:21:59 -07:00
Tom Manley
d2ece83b47 Merge pull request #198 from tpmanley/feature/yale-lock
Added fingerprint for Yale YRL220 lock to ZigBee Lock DTH [DVCSMP-101]
2015-10-16 16:47:25 -05:00
Tom Manley
66dbc02274 Added fingerprint for Yale YRL220 lock to ZigBee Lock DTH [DVCSMP-101] 2015-10-16 16:45:32 -05:00
Warodom Khamphanchai
5f899a48d0 Merge pull request #135 from kwarodom/fibaroSmokeSensor
Fibaro Smoke Sensor: initial device type
2015-10-16 12:34:44 -07:00
Warodom Khamphanchai
72899ee036 Fibaro Smoke Sensor: fix request to get batteryreport after device is configured 2015-10-16 12:18:43 -07:00
Warodom Khamphanchai
9ff67e9e17 Fibaro Smoke Sensor: send commands to update tiles during joining process, add save nav operator to MSR 2015-10-16 11:27:30 -07:00
Warodom Khamphanchai
f6791d1744 Fibaro Smoke Sensor: initial device type
Fibaro Smoke Sensor: remove CO capability, set default temp report interval, check MSR
2015-10-16 11:27:30 -07:00
Matthew Nohr
8f31b48974 Merge pull request #191 from mrnohr/arrival-colors
CREX-2981 Update the background image for arrival devices
2015-10-16 09:44:21 -05:00
Vinay Rao
b6dd5168d1 Merge pull request #195 from workingmonk/improved_switch
Improved Switch DTH
2015-10-15 14:21:35 -07:00
Vinay Rao
8a37d5715a Merge pull request #194 from workingmonk/improved_dimmer
Improved dimmer
2015-10-15 14:16:54 -07:00
Vinay Rao
4ef1d12c61 updated config with updated requirements 2015-10-15 14:11:07 -07:00
Vinay Rao
045a024bca removing the simulator from the power DTH 2015-10-15 14:07:18 -07:00
Vinay Rao
4d5bf094aa Updates based on the cloud methods 2015-10-14 18:54:57 -07:00
Warodom Khamphanchai
9d016839c8 Refactor and update dimmer-swith DTH 2015-10-14 16:10:51 -07:00
Vinay Rao
e4ce916d8f Merge pull request #158 from tpmanley/feature/zigbee-lock
Generic ZigBee lock DTH with support for Yale touchscreen lever lock
2015-10-14 15:42:41 -07:00
Tom Manley
d6668a1e86 Generic ZigBee lock DTH w/ support for Kwikset models 910, 912, 914 and 916
This version does not have support for configuring door lock codes.

[DVCSMP-1030] ZigBee Kwikset Lock support
2015-10-14 17:08:01 -05:00
tslagle13
ecb975540b Fix requirement for SMS
Removed requirement to provide a SMS number is the user does not have contacts. Add logic to verify a number was provided before sending SMS.
2015-10-14 10:05:02 -07:00
Matt Nohr
cd81871f90 Update the background image for arrival devices 2015-10-14 12:00:26 -05:00
Kristofer Schaller
73415f59e1 Merge pull request #150 from tslagle13/MSA-589-9
MSA-589: Routine Director
2015-10-13 16:13:42 -07:00
SmartThings, Inc.
30fdb92141 Merge pull request #148 from tslagle13/MSA-588-8
Merged publication request 'Hello, Home Phrase Director'
2015-10-13 18:13:22 -05:00
Vinay Rao
587b3295ae Merge pull request #186 from SmartThingsCommunity/sensor_name_missing
[DVCSMP-1164] Sensor name not shown
2015-10-12 12:52:27 -07:00
Vinay Rao
9538df65e5 [DVCSMP-1164] Sensor name not shown
During acceleration events, the sensor name is not shown.
2015-10-12 10:52:53 -07:00
Juan Pablo Risso
6854665f68 Merge pull request #177 from juano2310/Zen_PR
Adds capabilities "Temperature Measurement" and "Relative Humidity Me…
2015-10-09 13:29:31 -04:00
Juan Pablo Risso
2534afbf81 Removed Humidity 2015-10-09 12:39:26 -04:00
Juan Pablo Risso
eb3d0c2874 Adds capabilities "Temperature Measurement" and "Relative Humidity Measurement" 2015-10-09 12:34:58 -04:00
SmartThings, Inc.
5f85cd2873 Merge pull request #172 from SmartThingsCommunity/MSA-603-7
Merged publication request 'Keen Vent'
2015-10-05 16:28:52 -05:00
Donald C. Kirker
7bb6f67dbc MSA-603: Submission of Keen code to new SmartThings repo. 2015-10-05 16:26:47 -05:00
Kristofer Schaller
05cf0a0cb1 Merge pull request #170 from davidsulpy/master
Fixed a potential bug if a InitialState SmartApp hasn't been updated
2015-10-05 11:39:31 -07:00
David Sulpy
f012419710 added an initialization of the atomicState.eventBuffer if the eventBuffer is null when handling an event 2015-10-04 20:19:11 -05:00
bflorian
239f771ac1 Merge pull request #163 from SmartThingsCommunity/hue-mac-check
Handle incorrect mac address reporting
2015-10-02 07:19:52 -07:00
adam
87b6715a00 Handle incorrect mac address reporting 2015-10-02 09:07:06 -05:00
Juan Pablo Risso
d6a96317bf Merge pull request #155 from juano2310/R_hue_bulbs
Hue - Fix bulb discovery
2015-10-01 11:00:46 -04:00
Vinay Rao
6d64212c93 Merge pull request #160 from workingmonk/ge_link_level_fix
AppEngine issue with state syncing handled here.
2015-09-30 15:03:11 -07:00
Vinay Rao
088e746f99 AppEngine issue with state syncing handled here. 2015-09-30 15:00:17 -07:00
Kris Schaller
c26701383e Removing weatherbug first submission because namespace issue 2015-09-30 14:35:29 -07:00
SmartThings, Inc.
0b8f1d0168 Merge pull request #159 from SmartThingsCommunity/MSA-594-2
Merged publication request 'Weather Bug Submission v2'
2015-09-30 16:34:06 -05:00
Kris Schaller
b78337c96b MSA-594: New submission to fix namespace 2015-09-30 16:33:03 -05:00
SmartThings, Inc.
9fcd327da2 Merge pull request #156 from SmartThingsCommunity/MSA-592-1
Merged publication request 'WeatherBug Home Connect'
2015-09-30 16:03:12 -05:00
Edward Dingels
c3ce69994e Modifying 'WeatherBug Home Connect' 2015-09-30 15:07:27 -05:00
Warodom Khamphanchai
5bd03d1914 Merge pull request #143 from kwarodom/zwaveWaterValve
Z-Wave Water Valve: initial device type
2015-09-30 10:51:13 -07:00
Matthew Nohr
950780d30c Merge pull request #153 from mrnohr/crex3460-educational-content-updates
CREX-3460 Update educational DGSE content with new assets
2015-09-29 19:51:45 -05:00
Warodom Khamphanchai
8040ddd6f7 Z-Wave Water Valve: remove fingerprint from fortrezz, automatic refresh when join, remove manufacturerSpecificGet from poll() 2015-09-29 16:21:08 -07:00
Edward Dingels
4863b2345e MSA-592: Allows users to share their thermostat data with WeatherBug. Provides home energy and weather analytics. 2015-09-29 16:45:21 -05:00
Juan Pablo Risso
32b4914ba0 Hue - Fix bulb discovery 2015-09-29 17:16:55 -04:00
Matt Nohr
c76a2e807b CREX-3460 Update educational DGSE content with new assets 2015-09-29 09:46:32 -05:00
Tim Slagle
39e7ddb781 MSA-589: Initial Commit for Routine Director. 2015-09-28 18:54:23 -07:00
Tim Slagle
45f08df026 Modifying 'Hello, Home Phrase Director' 2015-09-28 18:51:48 -07:00
Tim Slagle
09b91014da Modifying 'Hello, Home Phrase Director' 2015-09-28 18:50:25 -07:00
Tim Slagle
5edff0df53 MSA-588: Update to HHFD 2015-09-28 18:48:18 -07:00
Warodom Khamphanchai
617d53da43 Z-Wave Water Valve: initial device type 2015-09-28 14:54:42 -07:00
Mike Robinet
8ba5eaf74d CREX-3129 Update parent and service manager apps to be singleton 2015-09-24 15:51:51 -05:00
Kristofer Schaller
29f315ac33 Merge pull request #118 from InitialState/master
New capabilities; buffering; scheduling
2015-09-14 16:36:33 -07:00
David Sulpy
2baac34ba3 Merge pull request #9 from davidsulpy/master
added an error log when the grokerSubdomain is ever null or empty and…
2015-09-14 18:34:33 -05:00
David Sulpy
811a1af4bf added an error log when the grokerSubdomain is ever null or empty and the app is trying to ship events or create a bucket 2015-09-14 18:33:21 -05:00
David Sulpy
b765b46c50 Merge pull request #8 from davidsulpy/master
added some protection against potential error cases if grokerSubdomai…
2015-09-14 17:46:07 -05:00
David Sulpy
6eac6affcf added some protection against potential error cases if grokerSubdomain isn't properly set 2015-09-14 17:45:04 -05:00
David Sulpy
d2cecf6908 Merge pull request #7 from davidsulpy/master
added version set in the update function as well so that it will upda…
2015-09-14 16:49:08 -05:00
David Sulpy
61d2aac45a added version set in the update function as well so that it will update the version on an update 2015-09-14 16:48:27 -05:00
David Sulpy
b5d0a5e74b Merge pull request #6 from davidsulpy/master
Fixed uninstall bug; addressed update bug; creating bucket on bucket_key set; cleaned some code styling
2015-09-14 16:38:12 -05:00
David Sulpy
1c2189b63c bumped the version 2015-09-14 16:33:48 -05:00
David Sulpy
1adb4000a6 refactored the methods for creating buckets and sending events to be more idempotent friendly; called tryCreateBucket on bucketKey setting to create the bucket sooner in the workflow than an events shipment 2015-09-14 16:30:40 -05:00
David Sulpy
bad978afbd removed all simicolons as per the groovy-lang.org style guide and better consistency 2015-09-14 16:19:30 -05:00
David Sulpy
80b4d6a665 removed functions from uninstall that may have been causing errors during the uninstall process; added an initializer for the atomicState.eventBuffer on update if it's whiped away; added a check for access key in createBucket to make the function more idempotent 2015-09-14 16:16:21 -05:00
David Sulpy
f51d6542b8 Merge pull request #5 from davidsulpy/master
New capabilities; buffering; scheduling
2015-09-11 11:30:01 -05:00
David Sulpy
7baad1c35e fixed the scheduling from throwing exceptions because it was using parenthesis when passing the handler method in to the scheduling method; ensured that the scheduler doesn't send empty events 2015-09-11 11:09:03 -05:00
David Sulpy
621bcfadd2 fixed a goovy syntax issue with http posts callback closures 2015-09-11 10:39:58 -05:00
David Sulpy
83bbaebef2 merged and made some slight adjustments 2015-09-11 10:37:26 -05:00
David Sulpy
bde5abcdb5 added buffering of events to reduce the calls from ST to IS; added buffer flushing after 10 events or 15 min with a scheduler; added a version to show in logs and help troubleshoot 2015-09-11 10:31:30 -05:00
bflorian
99e48dbeed Merge branch 'production' 2015-09-11 07:09:16 -04:00
Bob Florian
2ee1b26a7f Allow Mood Cube to record states other than the current one. 2015-09-11 07:01:35 -04:00
bflorian
a7ed8f4afe Merge branch 'juano2310-PR_Hue_Final' 2015-09-10 22:16:25 -04:00
bflorian
148ee3521d Merge of juano2310-PR_Hue_Final master with conflicts in hue bridge and connect app 2015-09-10 22:16:05 -04:00
Juan Pablo Risso
d7490a086a Hue fix bulb discovery and link button 2015-09-10 21:45:08 -04:00
Juan Pablo Risso
189bec58db Merge pull request #116 from juano2310/PR_hue
atomicState to state
2015-09-10 15:11:23 -04:00
Vinay Rao
7e26fd1040 Merge pull request #115 from workingmonk/nyce_names
Giving nice names to NYCE sensors
2015-09-10 12:09:41 -07:00
Vinay Rao
a244808073 Merge pull request #113 from workingmonk/configure_errors
[DVCSMP-1005] Updating Device Config
2015-09-10 12:08:03 -07:00
Juan Pablo Risso
33f1209c80 atomicState to state (all) 2015-09-10 15:07:32 -04:00
Juan Pablo Risso
d977c4d46b atomicState to state 2015-09-10 14:59:08 -04:00
Vinay Rao
01b2f57d7a Merge pull request #112 from workingmonk/temp_humidity_sensor
Improving the stability and reliability of the temp humidity sensor
2015-09-10 10:02:01 -07:00
Vinay Rao
ab00d703bf endpoint addition and binding fixes 2015-09-10 09:59:14 -07:00
Vinay Rao
6c3a7886ed adding endpoint to make it consistent. 2015-09-10 09:54:49 -07:00
David Sulpy
5719bbcaac switched from using state to using atomicState in preparation of event buffering; using atomicState exclusively per important tip found here: http://docs.smartthings.com/en/latest/smartapp-developers-guide/state.html#atomic-state 2015-09-10 11:14:27 -05:00
Vinay Rao
324d9bf780 Giving nice names to NYCE sensors
Updating the custom names for models after I added the deviceJoinName capability to fingerprint last week.
2015-09-09 21:51:38 -07:00
Vinay Rao
eb8d861c6c Updating Device Config
Making sure that the bind commands are before the reporting
2015-09-09 19:05:17 -07:00
SmartThings, Inc.
99a4d75e4b Merge pull request #78 from SmartThingsCommunity/MSA-52-3
Merged publication request 'Fidure A1730 Series Thermostat Device Type'
2015-09-09 20:55:53 -05:00
Vinay Rao
d601484398 Improving the stability and reliability of the temp humidity sensor
1. Humidity and Temp reports back at least every hour IF no change, every 30 second if changes are continuous
2. Battery reports every 6 hours. can't set to 24hours because of int16 limit
2015-09-09 15:58:47 -07:00
bflorian
a3fbc8ebd8 Mod to OSRAM to address version control issue. 2015-09-09 08:48:29 -04:00
bflorian
bfc14ffd9e Merge branch 'master' into staging 2015-09-09 08:31:24 -04:00
bflorian
1c327d1433 Merge branch 'production'
# Conflicts:
#	smartapps/smartthings/lifx-connect.src/lifx-connect.groovy
2015-09-09 08:30:56 -04:00
Bob Florian
deee914573 Aeon home energy meter (graphing version) updates from production 2015-09-09 08:25:17 -04:00
Bob Florian
f6b541c30f LIFX updates from production 2015-09-09 08:01:24 -04:00
Bob Florian
c00fbd3652 Wattvision updates from production 2015-09-09 07:59:23 -04:00
Bob Florian
76f056180d Checked in new Withings manager from prod 2015-09-09 07:56:42 -04:00
Bob Florian
c39b63b944 Fixed error in setting light level in Mood Cube 2015-09-09 07:55:03 -04:00
Juan Pablo Risso
40bf47ec0b Merge pull request #107 from SmartThingsCommunity/master
Merge Master -> Staging
2015-09-08 15:44:34 -04:00
Juan Pablo Risso
dc2ac4bedc Merge pull request #106 from juano2310/PR_hue
Hue update to match the new UI
2015-09-08 15:43:23 -04:00
Juan Pablo Risso
0cf90064ec Removed switch capability from Bridge 2015-09-08 14:42:16 -04:00
Juan Pablo Risso
0497660ab5 Hue Removed deprecated tiles 2015-09-08 14:34:55 -04:00
Juan Pablo Risso
6005f7266e Removed deprecated tiles 2015-09-08 14:33:42 -04:00
Juan Pablo Risso
5d38cabe75 Harmony better error handling 2015-09-08 14:11:41 -04:00
Juan Pablo Risso
29f94ee6ac Hue update to match the new UI (Lux Bulb) 2015-09-08 14:00:14 -04:00
Juan Pablo Risso
8929673ff0 Hue update to match the new UI (Bulb) 2015-09-08 13:59:46 -04:00
Juan Pablo Risso
94228c258a Hue update to match the new UI (Bridge) 2015-09-08 13:59:25 -04:00
Juan Pablo Risso
1b424a8ea8 Hue update to match the new UI (Lux Bulb) 2015-09-08 13:54:31 -04:00
Juan Pablo Risso
35a7a79073 Hue update to match the new UI (Bulb) 2015-09-08 13:54:10 -04:00
Juan Pablo Risso
8dd3b2396f Hue update to match the new UI (Bridge) 2015-09-08 13:53:45 -04:00
Juan Pablo Risso
39d2def035 Hue update to match the new UI 2015-09-08 13:51:28 -04:00
Vinay Rao
e1a9f2f761 Merge pull request #105 from workingmonk/enable_garage_preference
Re-enabling garage door option in the preference after the 1.0.17 AE release
2015-09-08 08:17:38 -07:00
Vinay Rao
37eb8cc0a1 Re-enabling garage door option in the preference after the 1.0.17 AE release 2015-09-08 07:34:14 -07:00
Vinay Rao
402b0be80b Merge pull request #102 from workingmonk/lifx_bug_fixes
lifx namespace and refresh interval issue
2015-09-04 14:56:24 -07:00
Vinay Rao
a761590322 lifx namespace and refresh interval issue 2015-09-03 18:22:47 -07:00
Vinay Rao
8d4a73cd30 Merge pull request #101 from workingmonk/multi_temp_fix
temporary fix until 1.0.16 is out
2015-09-03 16:25:01 -07:00
Vinay Rao
4bf2b55c26 temporary fix until 1.0.16 is out 2015-09-03 16:11:47 -07:00
Vinay Rao
13fcbfcdc7 Merge pull request #100 from workingmonk/lifx_namespace
updating name space
2015-09-03 16:08:29 -07:00
Vinay Rao
288a4e99fa updating name space 2015-09-03 16:06:52 -07:00
Vinay Rao
55a3842b00 Merge pull request #98 from st-andersson/master
BOSE Support
2015-09-03 13:13:20 -07:00
Vinay Rao
a25fb6d84d Merge pull request #99 from workingmonk/lifx
Lifx integration
2015-09-03 13:11:53 -07:00
Henric Andersson
d9a2d8109e Fixed formatting 2015-09-03 13:09:12 -07:00
Vinay Rao
0376a5823f updating name 2015-09-03 12:27:55 -07:00
Vinay Rao
78e25c6958 Lifx integration 2015-09-03 12:21:20 -07:00
Henric Andersson
7e5d6e99d1 BOSE Support 2015-09-03 12:01:29 -07:00
Vinay Rao
f5e3a16ad1 Merge pull request #97 from workingmonk/device_join_name
[CREX-3146] Updating Device join name
2015-09-03 10:22:12 -07:00
Vinay Rao
c361387bf8 Merge pull request #95 from workingmonk/change_presence_arrival
Updating the DTH name from SmartSense Presence to Arrival Sensor
2015-09-03 09:57:02 -07:00
Vinay Rao
f0c6e085eb Additional model name for smart power outlet 2015-09-03 09:55:09 -07:00
Vinay Rao
67a1bd1256 Updating the DTH name from SmartSense Presence to Arrival Sensor 2015-09-02 22:26:05 -07:00
Vinay Rao
32182ae13b Updating the device join names for new st devices 2015-09-02 20:27:41 -07:00
Armand Raz
e56086aac3 MSA-52: Device Type for Fidure A1730 ZigBee HA 1.2 Wireless Home Automation Thermostats. Includes features such as:
- Temperature and Mode Control
- Setpoint Hold
- Viewing HVAC status
-  Remote Locking of Thermostat Keys
2015-08-26 19:52:11 -05:00
David Sulpy
6b142622db Merge pull request #4 from davidsulpy/master
New capabilities!
2015-08-25 14:27:15 -05:00
David Sulpy
8d07e81b80 Added all capabilities possible sans door controllers and buttons. 2015-08-25 12:23:05 -05:00
David Sulpy
0fcff53eba Removed door controllers from comments and further implementation to see if that fixes ST 403 error on authentication 2015-08-14 14:41:27 -05:00
84 changed files with 8035 additions and 1949 deletions

View File

@@ -0,0 +1,145 @@
/**
* Aeon Motor Controller
*
* Copyright 2015 Bruce Ravenel
*
* 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: "Aeon Motor Controller", namespace: "bravenel", author: "Bruce Ravenel") {
capability "Refresh"
capability "Actuator"
capability "doorControl"
capability "Switch"
command "up"
command "down"
command "stop"
// fingerprint inClusters: "0x26,0x32"
}
simulator {
status "up": "command: 2604, payload: FF"
status "down": "command: 2604, payload: 00"
status "stop": "command: 2605, payload: FE"
["FF", "FE", "00"].each { val ->
reply "2001$val,delay 100,2602": "command: 2603, payload: $val"
}
}
tiles {
standardTile("motor", "device.motor", width: 2, height: 2) {
state("stopUp", label:'stop', icon:"st.doors.garage.garage-open", action: 'down', backgroundColor:"#79b821")
state("stopDn", label:'stop', icon:"st.doors.garage.garage-closed", action: 'up', backgroundColor:"#79b821")
state("up", label:'up', icon:"st.doors.garage.garage-opening", action:'stop', backgroundColor:"#ffe71e")
state("down", label:'down', icon:"st.doors.garage.garage-closing", action:'stop', backgroundColor:"#ffe71e")
}
valueTile("energy", "device.energy", decoration: "flat") {
state "default", label:' '
}
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
}
standardTile("stop", "device.switch") {
state "default", label:"", action: "stop", icon:"http://cdn.device-icons.smartthings.com/sonos/stop-btn@2x.png"
}
standardTile("up", "device.switch") {
state "default", label: "Up", action: "up", icon:"http://cdn.device-icons.smartthings.com/thermostat/thermostat-up@2x.png"
}
standardTile("down", "device.switch") {
state "default", label: "Down", action: "down", icon:"http://cdn.device-icons.smartthings.com/thermostat/thermostat-down@2x.png"
}
}
main(["motor"])
details(["motor", "refresh", "energy", "up", "down", "stop",])
}
// parse events into attributes
def parse(String description) {
def result = []
def cmd = zwave.parse(description, [0x20: 1, 0x26: 3])
if (cmd) {
result = zwaveEvent(cmd)
log.debug("'$description' parsed to $result")
} else {
log.debug("Couldn't zwave.parse '$description'")
}
result
}
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) {
motorEvents(cmd)
}
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) {
motorEvents(cmd)
}
def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelReport cmd) {
motorEvents(cmd)
}
def motorEvents(physicalgraph.zwave.Command cmd) {
def result = []
def switchEvent = []
if(cmd.value == 0) {switchEvent = createEvent(name: "motor", value: "down", descriptionText: text)}
else if(cmd.value == 254) {
def stopVal = state.up ? "stopUp" : "stopDn"
switchEvent = createEvent(name: "motor", value: stopVal, descriptionText: text)
}
else if(cmd.value == 255) {switchEvent = createEvent(name: "motor", value: "up", descriptionText: text)}
result << switchEvent
}
def refresh() {
zwave.switchMultilevelV1.switchMultilevelGet().format()
}
def up() {
state.up = true
delayBetween([
zwave.basicV1.basicSet(value: 0xFF).format(),
zwave.switchMultilevelV1.switchMultilevelGet().format()
], 1000)
}
def down() {
state.up = false
delayBetween([
zwave.basicV1.basicSet(value: 0x00).format(),
zwave.switchMultilevelV1.switchMultilevelGet().format()
], 1000)
}
def stop() {
delayBetween([
zwave.switchMultilevelV1.switchMultilevelStopLevelChange().format(),
zwave.switchMultilevelV1.switchMultilevelGet().format()
], 1000)
}
def on() {
state.up = true
delayBetween([
zwave.basicV1.basicSet(value: 0xFF).format(),
zwave.switchMultilevelV1.switchMultilevelGet().format()
], 1000)
}
def off() {
state.up = false
delayBetween([
zwave.basicV1.basicSet(value: 0x00).format(),
zwave.switchMultilevelV1.switchMultilevelGet().format()
], 1000)
}

View File

@@ -0,0 +1,513 @@
// 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: "Keen Home") {
capability "Switch Level"
capability "Switch"
capability "Configuration"
capability "Refresh"
capability "Sensor"
capability "Temperature Measurement"
capability "Battery"
command "getLevel"
command "getOnOff"
command "getPressure"
command "getBattery"
command "getTemperature"
command "setZigBeeIdTile"
command "clearObstruction"
fingerprint endpoint: "1",
profileId: "0104",
inClusters: "0000,0001,0003,0004,0005,0006,0008,0020,0402,0403,0B05,FC01,FC02",
outClusters: "0019"
}
// simulator metadata
simulator {
// status messages
status "on": "on/off: 1"
status "off": "on/off: 0"
// reply messages
reply "zcl on-off on": "on/off: 1"
reply "zcl on-off off": "on/off: 0"
}
// 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: "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"
}
standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat") {
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
}
valueTile("temperature", "device.temperature", inactiveLabel: false) {
state "temperature", label:'${currentValue}°',
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"]
]
}
valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat") {
state "battery", label: 'Battery \n${currentValue}%', backgroundColor:"#ffffff"
}
valueTile("zigbeeId", "device.zigbeeId", inactiveLabel: true, decoration: "flat") {
state "serial", label:'${currentValue}', backgroundColor:"#ffffff"
}
main "switch"
details(["switch","refresh","temperature","levelSliderControl","battery"])
}
}
/**** PARSE METHODS ****/
def parse(String description) {
log.debug "description: $description"
Map map = [:]
if (description?.startsWith('catchall:')) {
map = parseCatchAllMessage(description)
}
else if (description?.startsWith('read attr -')) {
map = parseReportAttributeMessage(description)
}
else if (description?.startsWith('temperature: ') || description?.startsWith('humidity: ')) {
map = parseCustomMessage(description)
}
else if (description?.startsWith('on/off: ')) {
map = parseOnOffMessage(description)
}
log.debug "Parse returned $map"
return map ? createEvent(map) : null
}
private Map parseCatchAllMessage(String description) {
log.debug "parseCatchAllMessage"
def cluster = zigbee.parse(description)
log.debug "cluster: ${cluster}"
if (shouldProcessMessage(cluster)) {
log.debug "processing message"
switch(cluster.clusterId) {
case 0x0001:
return makeBatteryResult(cluster.data.last())
break
case 0x0402:
// temp is last 2 data values. reverse to swap endian
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
def value = convertTemperatureHex(temp)
return makeTemperatureResult(value)
break
case 0x0006:
return makeOnOffResult(cluster.data[-1])
break
}
}
return [:]
}
private boolean shouldProcessMessage(cluster) {
// 0x0B is default response indicating message got through
// 0x07 is bind message
if (cluster.profileId != 0x0104 ||
cluster.command == 0x0B ||
cluster.command == 0x07 ||
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)) {
return false
}
return true
}
private Map parseReportAttributeMessage(String description) {
log.debug "parseReportAttributeMessage"
Map descMap = (description - "read attr - ").split(",").inject([:]) { map, param ->
def nameAndValue = param.split(":")
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
}
log.debug "Desc Map: $descMap"
if (descMap.cluster == "0006" && descMap.attrId == "0000") {
return makeOnOffResult(Int.parseInt(descMap.value));
}
else if (descMap.cluster == "0008" && descMap.attrId == "0000") {
return makeLevelResult(descMap.value)
}
else if (descMap.cluster == "0402" && descMap.attrId == "0000") {
def value = convertTemperatureHex(descMap.value)
return makeTemperatureResult(value)
}
else if (descMap.cluster == "0001" && descMap.attrId == "0021") {
return makeBatteryResult(Integer.parseInt(descMap.value, 16))
}
else if (descMap.cluster == "0403" && descMap.attrId == "0020") {
return makePressureResult(Integer.parseInt(descMap.value, 16))
}
else if (descMap.cluster == "0000" && descMap.attrId == "0006") {
return makeSerialResult(new String(descMap.value.decodeHex()))
}
// shouldn't get here
return [:]
}
private Map parseCustomMessage(String description) {
Map resultMap = [:]
if (description?.startsWith('temperature: ')) {
// log.debug "${description}"
// def value = zigbee.parseHATemperatureValue(description, "temperature: ", getTemperatureScale())
// log.debug "split: " + description.split(": ")
def value = Double.parseDouble(description.split(": ")[1])
// log.debug "${value}"
resultMap = makeTemperatureResult(convertTemperature(value))
}
return resultMap
}
private Map parseOnOffMessage(String description) {
Map resultMap = [:]
if (description?.startsWith('on/off: ')) {
def value = Integer.parseInt(description - "on/off: ")
resultMap = makeOnOffResult(value)
}
return resultMap
}
private Map makeOnOffResult(rawValue) {
log.debug "makeOnOffResult: ${rawValue}"
def linkText = getLinkText(device)
def value = rawValue == 1 ? "on" : "off"
return [
name: "switch",
value: value,
descriptionText: "${linkText} is ${value}"
]
}
private Map makeLevelResult(rawValue) {
def linkText = getLinkText(device)
def value = Integer.parseInt(rawValue, 16)
def rangeMax = 254
// catch obstruction level
if (value == 255) {
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.
return [
name: "switch",
value: "obstructed",
descriptionText: "${linkText} is obstructed. Please power cycle."
]
}
value = Math.floor(value / rangeMax * 100)
return [
name: "level",
value: value,
descriptionText: "${linkText} level is ${value}%"
]
}
private Map makePressureResult(rawValue) {
log.debug 'makePressureResut'
def linkText = getLinkText(device)
def pascals = rawValue / 10
def result = [
name: 'pressure',
descriptionText: "${linkText} pressure is ${pascals}Pa",
value: pascals
]
return result
}
private Map makeBatteryResult(rawValue) {
// log.debug 'makeBatteryResult'
def linkText = getLinkText(device)
// log.debug
[
name: 'battery',
value: rawValue,
descriptionText: "${linkText} battery is at ${rawValue}%"
]
}
private Map makeTemperatureResult(value) {
// log.debug 'makeTemperatureResult'
def linkText = getLinkText(device)
// log.debug "tempOffset: ${tempOffset}"
if (tempOffset) {
def offset = tempOffset as int
// log.debug "offset: ${offset}"
def v = value as int
// log.debug "v: ${v}"
value = v + offset
// log.debug "value: ${value}"
}
return [
name: 'temperature',
value: "" + value,
descriptionText: "${linkText} is ${value}°${temperatureScale}",
]
}
/**** HELPER METHODS ****/
private def convertTemperatureHex(value) {
// log.debug "convertTemperatureHex(${value})"
def celsius = Integer.parseInt(value, 16).shortValue() / 100
// log.debug "celsius: ${celsius}"
return convertTemperature(celsius)
}
private def convertTemperature(celsius) {
// log.debug "convertTemperature()"
if(getTemperatureScale() == "C"){
return celsius
} else {
def fahrenheit = Math.round(celsiusToFahrenheit(celsius) * 100) /100
// log.debug "converted to F: ${fahrenheit}"
return fahrenheit
}
}
private def makeSerialResult(serial) {
log.debug "makeSerialResult: " + serial
def linkText = getLinkText(device)
sendEvent([
name: "serial",
value: serial,
descriptionText: "${linkText} has serial ${serial}" ])
return [
name: "serial",
value: serial,
descriptionText: "${linkText} has serial ${serial}" ]
}
// 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() {
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() {
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 {}"
}
def clearObstruction() {
def linkText = getLinkText(device)
log.debug "attempting to clear ${linkText} obstruction"
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")
}
else {
sendEvent(name: "switch", value: "off", descriptionText: "${linkText} is off by setting level to 0")
}
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",
"send 0x${device.deviceNetworkId} 1 1", "delay 200"
]
}
def getLevel() {
log.debug "getLevel()"
// 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"]
}
def getTemperature() {
log.debug "getTemperature()"
["st rattr 0x${device.deviceNetworkId} 1 0x0402 0"]
}
def getBattery() {
log.debug "getBattery()"
["st rattr 0x${device.deviceNetworkId} 1 0x0001 0x0021"]
}
def setZigBeeIdTile() {
log.debug "setZigBeeIdTile() - ${device.zigbeeId}"
def linkText = getLinkText(device)
sendEvent([
name: "zigbeeId",
value: device.zigbeeId,
descriptionText: "${linkText} has zigbeeId ${device.zigbeeId}" ])
return [
name: "zigbeeId",
value: device.zigbeeId,
descriptionText: "${linkText} has zigbeeId ${device.zigbeeId}" ]
}
def refresh() {
getOnOff() +
getLevel() +
getTemperature() +
getPressure() +
getBattery()
}
def configure() {
log.debug "CONFIGURE"
// get ZigBee ID by hidden tile because that's the only way we can do it
setZigBeeIdTile()
def configCmds = [
// 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
// 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",
// 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",
// 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 60 60 {0A00}", "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",
// 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",
]
return configCmds + refresh()
}

View File

@@ -56,7 +56,7 @@ metadata {
state "configure", label: '', action: "configuration.configure", icon: "st.secondary.configure"
}
PLATFORM_graphTile(name: "powerGraph", attribute: "device.power")
graphTile(name: "powerGraph", attribute: "device.power")
main(["power", "energy"])
details(["powerGraph", "power", "energy", "reset", "refresh", "configure"])
@@ -68,16 +68,8 @@ metadata {
// ========================================================
preferences {
input name: "graphPrecision", type: "enum", title: "Graph Precision", description: "Daily", required: true, options: PLATFORM_graphPrecisionOptions(), defaultValue: "Daily"
input name: "graphType", type: "enum", title: "Graph Type", description: selectedGraphType(), required: false, options: PLATFORM_graphTypeOptions()
}
def selectedGraphPrecision() {
graphPrecision ?: "Daily"
}
def selectedGraphType() {
graphType ?: "line"
input name: "graphPrecision", type: "enum", title: "Graph Precision", description: "Daily", required: true, options: graphPrecisionOptions(), defaultValue: "Daily"
input name: "graphType", type: "enum", title: "Graph Type", description: "line", required: false, options: graphTypeOptions()
}
// ========================================================
@@ -91,22 +83,6 @@ mappings {
GET: "renderGraph"
]
}
path("/graphDataSizes") { // for testing. remove before publishing
action:
[
GET: "graphDataSizes"
]
}
}
def graphDataSizes() { // for testing. remove before publishing
state.findAll { k, v -> k.startsWith("measure.") }.inject([:]) { attributes, attributeData ->
attributes[attributeData.key] = attributeData.value.inject([:]) { dateTypes, dateTypeData ->
dateTypes[dateTypeData.key] = dateTypeData.value.size()
dateTypes
}
attributes
}
}
// ========================================================
@@ -121,8 +97,7 @@ def parse(String description) {
}
log.debug "Parse returned ${result?.descriptionText}"
PLATFORM_migrateGraphDataIfNeeded()
PLATFORM_storeData(result.name, result.value)
storeGraphData(result.name, result.value)
return result
}
@@ -176,535 +151,15 @@ def configure() {
def renderGraph() {
def data = PLATFORM_fetchGraphData(params.attribute)
def data = fetchGraphData(params.attribute)
def totalData = data*.runningSum
def xValues = data*.unixTime
def yValues = [
Total: [color: "#49a201", data: totalData, type: selectedGraphType()]
Total: [color: "#49a201", data: totalData]
]
PLATFORM_renderGraph(attribute: params.attribute, xValues: xValues, yValues: yValues, focus: "Total", label: "Watts")
}
// TODO: // ========================================================
// TODO: // PLATFORM CODE !!! DO NOT ALTER !!!
// TODO: // ========================================================
// ========================================================
// PLATFORM TILES
// ========================================================
def PLATFORM_graphTile(Map tileParams) {
def cleanAttribute = tileParams.attribute - "device." - "capability."
htmlTile([name: tileParams.name, attribute: tileParams.attribute, action: "graph/${cleanAttribute}", width: 3, height: 2] + tileParams)
}
// ========================================================
// PLATFORM GRAPH RENDERING
// ========================================================
private PLATFORM_graphTypeOptions() {
[
"line", // DEFAULT
"spline",
"step",
"area",
"area-spline",
"area-step",
"bar",
"scatter",
"pie",
"donut",
"gauge",
]
}
private PLATFORM_renderGraph(graphParams) {
String attribute = graphParams.attribute
List xValues = graphParams.xValues
Map yValues = graphParams.yValues
String focus = graphParams.focus ?: ""
String label = graphParams.label ?: ""
/*
def xValues = [1, 2]
def yValues = [
High: [type: "spline", data: [5, 6], color: "#bc2323"],
Low: [type: "spline", data: [0, 1], color: "#153591"]
]
Available type values:
line // DEFAULT
spline
step
area
area-spline
area-step
bar
scatter
pie
donut
gauge
*/
def graphData = PLATFORM_buildGraphData(xValues, yValues, label)
def legendData = yValues*.key
def focusJS = focus ? "chart.focus('${focus}')" : "// focus not specified"
def flowColumn = focus ?: yValues ? yValues.keySet().first() : null
def htmlTitle = "${(device.label ?: device.name)} ${attribute.capitalize()} Graph"
renderHTML(htmlTitle) { html ->
html.head {
"""
<!-- Load c3.css -->
<link href="https://www.dropbox.com/s/m6ptp72cw4nx0sp/c3.css?dl=1" rel="stylesheet" type="text/css">
<!-- Load d3.js and c3.js -->
<script src="https://www.dropbox.com/s/9x22jyfu5qyacpp/d3.v3.min.js?dl=1" charset="utf-8"></script>
<script src="https://www.dropbox.com/s/to7dtcn403l7mza/c3.js?dl=1"></script>
<script>
function getDocumentHeight() {
var body = document.body;
var html = document.documentElement;
return html.clientHeight;
}
function getDocumentWidth() {
var body = document.body;
var html = document.documentElement;
return html.clientWidth;
}
</script>
<style>
.legend {
position: absolute;
width: 80%;
padding-left: 15%;
z-index: 999;
padding-top: 5px;
}
.legend span {
width: ${100 / yValues.size()}%;
display: inline-block;
text-align: center;
cursor: pointer;
color: white;
}
</style>
"""
}
html.body {
"""
<div class="legend"></div>
<div id="chart" style="max-height: 120px; position: relative;"></div>
<script>
// Generate the chart
var chart = c3.generate(${graphData as grails.converters.JSON});
// Resize the chart to the size of the device tile
chart.resize({height:getDocumentHeight(), width:getDocumentWidth()});
// Focus data if specified
${focusJS}
// Update the chart when ${attribute} events are received
function ${attribute}(evt) {
var newValue = ['${flowColumn}'];
newValue.push(evt.value);
var newX = ['x'];
newX.push(evt.unixTime);
chart.flow({
columns: [
newX,
newValue
]
});
}
// Build the custom legend
d3.select('.legend').selectAll('span')
.data(${legendData as grails.converters.JSON})
.enter().append('span')
.attr('data-id', function (id) { return id; })
.html(function (id) { return id; })
.each(function (id) {
d3.select(this).style('background-color', chart.color(id));
})
.on('mouseover', function (id) {
chart.focus(id);
})
.on('mouseout', function (id) {
chart.revert();
})
.on('click', function (id) {
chart.toggle(id);
});
</script>
"""
}
}
}
private PLATFORM_buildGraphData(List xValues, Map yValues, String label = "") {
/*
def xValues = [1, 2]
def yValues = [
High: [type: "spline", data: [5, 6], color: "#bc2323"],
Low: [type: "spline", data: [0, 1], color: "#153591"]
]
*/
[
interaction: [
enabled: false
],
bindto : '#chart',
padding : [
left : 30,
right : 30,
bottom: 0,
top : 0
],
legend : [
show: false,
// hide : false,//(yValues.keySet().size() < 2),
// position: 'inset',
// inset: [
// anchor: "top-right"
// ],
// item: [
// onclick: "do nothing" // (yValues.keySet().size() > 1) ? null : "do nothing"
// ]
],
data : [
x : "x",
columns: [(["x"] + xValues)] + yValues.collect { k, v -> [k] + v.data },
types : yValues.inject([:]) { total, current -> total[current.key] = current.value.type; return total },
colors : yValues.inject([:]) { total, current -> total[current.key] = current.value.color; return total }
],
axis : [
x: [
type: 'timeseries',
tick: [
centered: true,
culling : [max: 7],
fit : true,
format : PLATFORM_getGraphDateFormat()
// format: PLATFORM_getGraphDateFormatFunction() // throws securityException when trying to escape javascript
]
],
y: [
label : label,
padding: [
top: 50
]
]
]
]
}
private PLATFORM_getGraphDateFormat(dateType = selectedGraphPrecision()) {
// https://github.com/mbostock/d3/wiki/Time-Formatting
def graphDateFormat
switch (dateType) {
case "Live":
graphDateFormat = "%I:%M" // hour (12-hour clock) as a decimal number [00,12] // AM or PM
break
case "Hourly":
graphDateFormat = "%I %p" // hour (12-hour clock) as a decimal number [00,12] // AM or PM
break
case "Daily":
graphDateFormat = "%a" // abbreviated weekday name
break
case "Monthly":
graphDateFormat = "%b" // abbreviated month name
break
case "Annually":
graphDateFormat = "%y" // year without century as a decimal number [00,99]
break
}
graphDateFormat
}
private String PLATFORM_getGraphDateFormatFunction(dateType = selectedGraphPrecision()) {
def graphDateFunction = "function(date) { return date; }"
switch (dateType) {
case "Live":
graphDateFunction = """
function(date) {
return.getMinutes();
}
"""
break;
case "Hourly":
graphDateFunction = """ function(date) {
var hour = date.getHours();
if (hour == 0) {
return String(/12 am/).substring(1).slice(0,-1);
} else if (hour > 12) {
return hour -12 + String(/ pm/).substring(1).slice(0,-1);
} else {
return hour + String(/ am/).substring(1).slice(0,-1);
}
}"""
break
case "Daily":
graphDateFunction = """ function(date) {
var day = date.getDay();
switch(day) {
case 0: return String(/Sun/).substring(1).slice(0,-1);
case 1: return String(/Mon/).substring(1).slice(0,-1);
case 2: return String(/Tue/).substring(1).slice(0,-1);
case 3: return String(/Wed/).substring(1).slice(0,-1);
case 4: return String(/Thu/).substring(1).slice(0,-1);
case 5: return String(/Fri/).substring(1).slice(0,-1);
case 6: return String(/Sat/).substring(1).slice(0,-1);
}
}"""
break
case "Monthly":
graphDateFunction = """ function(date) {
var month = date.getMonth();
switch(month) {
case 0: return String(/Jan/).substring(1).slice(0,-1);
case 1: return String(/Feb/).substring(1).slice(0,-1);
case 2: return String(/Mar/).substring(1).slice(0,-1);
case 3: return String(/Apr/).substring(1).slice(0,-1);
case 4: return String(/May/).substring(1).slice(0,-1);
case 5: return String(/Jun/).substring(1).slice(0,-1);
case 6: return String(/Jul/).substring(1).slice(0,-1);
case 7: return String(/Aug/).substring(1).slice(0,-1);
case 8: return String(/Sep/).substring(1).slice(0,-1);
case 9: return String(/Oct/).substring(1).slice(0,-1);
case 10: return String(/Nov/).substring(1).slice(0,-1);
case 11: return String(/Dec/).substring(1).slice(0,-1);
}
}"""
break
case "Annually":
graphDateFunction = """
function(date) {
return.getFullYear();
}
"""
break
}
groovy.json.StringEscapeUtils.escapeJavaScript(graphDateFunction)
}
private jsEscapeString(str = "") {
"String(/${str}/).substring(1).slice(0,-1);"
}
private PLATFORM_fetchGraphData(attribute) {
log.debug "PLATFORM_fetchGraphData(${attribute})"
/*
[
[
dateString: "2014-12-1",
unixTime: 1421931600000,
min: 0,
max: 10,
average: 5
],
...
]
*/
def attributeBucket = state["measure.${attribute}"] ?: [:]
def dateType = selectedGraphPrecision()
attributeBucket[dateType]
}
// ========================================================
// PLATFORM DATA STORAGE
// ========================================================
private PLATFORM_graphPrecisionOptions() { ["Live", "Hourly", "Daily", "Monthly", "Annually"] }
private PLATFORM_storeData(attribute, value) {
PLATFORM_graphPrecisionOptions().each { dateType ->
PLATFORM_addDataToBucket(attribute, value, dateType)
}
}
/*
[
Hourly: [
[
dateString: "2014-12-1",
unixTime: 1421931600000,
min: 0,
max: 10,
average: 5
],
...
],
...
]
*/
private PLATFORM_addDataToBucket(attribute, value, dateType) {
def numberValue = value.toBigDecimal()
def attributeKey = "measure.${attribute}"
def attributeBucket = state[attributeKey] ?: [:]
def dateTypeBucket = attributeBucket[dateType] ?: []
def now = new Date()
def itemDateString = now.format("PLATFORM_get${dateType}Format"())
def item = dateTypeBucket.find { it.dateString == itemDateString }
if (!item) {
// no entry for this data point yet, fill with initial values
item = [:]
item.average = numberValue
item.runningSum = numberValue
item.runningCount = 1
item.min = numberValue
item.max = numberValue
item.unixTime = now.getTime()
item.dateString = itemDateString
// add the new data point
dateTypeBucket << item
// clear out old data points
def old = PLATFORM_getOldDateString(dateType)
if (old) { // annual data never gets cleared
dateTypeBucket.findAll { it.unixTime < old }.each { dateTypeBucket.remove(it) }
}
// limit the size of the bucket. Live data can stack up fast
def sizeLimit = 25
if (dateTypeBucket.size() > sizeLimit) {
dateTypeBucket = dateTypeBucket[-sizeLimit..-1]
}
} else {
//re-calculate average/min/max for this bucket
item.runningSum = (item.runningSum.toBigDecimal()) + numberValue
item.runningCount = item.runningCount.toInteger() + 1
item.average = item.runningSum.toBigDecimal() / item.runningCount.toInteger()
if (item.min == null) {
item.min = numberValue
} else if (numberValue < item.min.toBigDecimal()) {
item.min = numberValue
}
if (item.max == null) {
item.max = numberValue
} else if (numberValue > item.max.toBigDecimal()) {
item.max = numberValue
}
}
attributeBucket[dateType] = dateTypeBucket
state[attributeKey] = attributeBucket
}
private PLATFORM_getOldDateString(dateType) {
def now = new Date()
def date
switch (dateType) {
case "Live":
date = now.getTime() - 60 * 60 * 1000 // 1h * 60m * 60s * 1000ms // 1 hour
break
case "Hourly":
date = (now - 1).getTime()
break
case "Daily":
date = (now - 10).getTime()
break
case "Monthly":
date = (now - 30).getTime()
break
case "Annually":
break
}
date
}
private PLATFORM_getLiveFormat() { "HH:mm:ss" }
private PLATFORM_getHourlyFormat() { "yyyy-MM-dd'T'HH" }
private PLATFORM_getDailyFormat() { "yyyy-MM-dd" }
private PLATFORM_getMonthlyFormat() { "yyyy-MM" }
private PLATFORM_getAnnuallyFormat() { "yyyy" }
// ========================================================
// PLATFORM GRAPH DATA MIGRATION
// ========================================================
private PLATFORM_migrateGraphDataIfNeeded() {
if (!state.hasMigratedOldGraphData) {
def acceptableKeys = PLATFORM_graphPrecisionOptions()
def needsMigration = state.findAll { k, v -> v.keySet().findAll { !acceptableKeys.contains(it) } }.keySet()
needsMigration.each { PLATFORM_migrateGraphData(it) }
state.hasMigratedOldGraphData = true
}
}
private PLATFORM_migrateGraphData(attribute) {
log.trace "about to migrate ${attribute}"
def attributeBucket = state[attribute] ?: [:]
def migratedAttributeBucket = [:]
attributeBucket.findAll { k, v -> !PLATFORM_graphPrecisionOptions().contains(k) }.each { oldDateString, oldItem ->
def dateType = oldDateString.contains('T') ? "Hourly" : PLATFORM_graphPrecisionOptions().find {
"PLATFORM_get${it}Format"().size() == oldDateString.size()
}
def dateTypeFormat = "PLATFORM_get${dateType}Format"()
def newBucket = attributeBucket[dateType] ?: []
/*
def existingNewItem = newBucket.find { it.dateString == oldDateString }
if (existingNewItem) {
newBucket.remove(existingNewItem)
}
*/
def newItem = [
min : oldItem.min,
max : oldItem.max,
average : oldItem.average,
runningSum : oldItem.runningSum,
runningCount: oldItem.runningCount,
dateString : oldDateString,
unixTime : new Date().parse(dateTypeFormat, oldDateString).getTime()
]
newBucket << newItem
migratedAttributeBucket[dateType] = newBucket
}
state[attribute] = migratedAttributeBucket
renderGraph(attribute: params.attribute, xValues: xValues, yValues: yValues, focus: "Total", label: "Watts")
}

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

@@ -12,7 +12,7 @@
*
*/
metadata {
definition (name: "SmartSense Presence", namespace: "smartthings", author: "SmartThings") {
definition (name: "Arrival Sensor", namespace: "smartthings", author: "SmartThings") {
capability "Tone"
capability "Actuator"
capability "Signal Strength"
@@ -34,8 +34,8 @@ metadata {
preferences {
section {
image(name: 'educationalcontent', multiple: true, images: [
"http://cdn.device-gse.smartthings.com/Arrival/Arrival1.png",
"http://cdn.device-gse.smartthings.com/Arrival/Arrival2.png"
"http://cdn.device-gse.smartthings.com/Arrival/Arrival1.jpg",
"http://cdn.device-gse.smartthings.com/Arrival/Arrival2.jpg"
])
}
}
@@ -43,7 +43,7 @@ metadata {
tiles {
standardTile("presence", "device.presence", width: 2, height: 2, canChangeBackground: true) {
state "present", labelIcon:"st.presence.tile.present", backgroundColor:"#53a7c0"
state "not present", labelIcon:"st.presence.tile.not-present", backgroundColor:"#ffffff"
state "not present", labelIcon:"st.presence.tile.not-present", backgroundColor:"#ebeef2"
}
standardTile("beep", "device.beep", decoration: "flat") {
state "beep", label:'', action:"tone.beep", icon:"st.secondary.beep", backgroundColor:"#ffffff"

View File

@@ -0,0 +1,989 @@
/**
* Bose SoundTouch
*
* 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.
*
*/
// Needed to be able to serialize the XmlSlurper data back to XML
import groovy.xml.XmlUtil
// for the UI
metadata {
definition (name: "Bose SoundTouch", namespace: "smartthings", author: "SmartThings") {
/**
* List our capabilties. Doing so adds predefined command(s) which
* belong to the capability.
*/
capability "Switch"
capability "Refresh"
capability "Music Player"
capability "Polling"
/**
* Define all commands, ie, if you have a custom action not
* covered by a capability, you NEED to define it here or
* the call will not be made.
*
* To call a capability function, just prefix it with the name
* of the capability, for example, refresh would be "refresh.refresh"
*/
command "preset1"
command "preset2"
command "preset3"
command "preset4"
command "preset5"
command "preset6"
command "aux"
command "everywhereJoin"
command "everywhereLeave"
}
/**
* Define the various tiles and the states that they can be in.
* The 2nd parameter defines an event which the tile listens to,
* if received, it tries to map it to a state.
*
* You can also use ${currentValue} for the value of the event
* or ${name} for the name of the event. Just make SURE to use
* single quotes, otherwise it will only be interpreted at time of
* launch, instead of every time the event triggers.
*/
valueTile("nowplaying", "device.nowplaying", width: 2, height: 1, decoration:"flat") {
state "nowplaying", label:'${currentValue}', action:"refresh.refresh"
}
standardTile("switch", "device.switch", width: 1, height: 1, canChangeIcon: true) {
state "off", label: '${name}', action: "switch.on", icon: "st.Electronics.electronics16", backgroundColor: "#ffffff"
state "on", label: '${name}', action: "switch.off", icon: "st.Electronics.electronics16", backgroundColor: "#79b821"
}
valueTile("1", "device.station1", decoration: "flat", canChangeIcon: false) {
state "station1", label:'${currentValue}', action:"preset1"
}
valueTile("2", "device.station2", decoration: "flat", canChangeIcon: false) {
state "station2", label:'${currentValue}', action:"preset2"
}
valueTile("3", "device.station3", decoration: "flat", canChangeIcon: false) {
state "station3", label:'${currentValue}', action:"preset3"
}
valueTile("4", "device.station4", decoration: "flat", canChangeIcon: false) {
state "station4", label:'${currentValue}', action:"preset4"
}
valueTile("5", "device.station5", decoration: "flat", canChangeIcon: false) {
state "station5", label:'${currentValue}', action:"preset5"
}
valueTile("6", "device.station6", decoration: "flat", canChangeIcon: false) {
state "station6", label:'${currentValue}', action:"preset6"
}
valueTile("aux", "device.switch", decoration: "flat", canChangeIcon: false) {
state "default", label:'Auxillary\nInput', action:"aux"
}
standardTile("refresh", "device.nowplaying", decoration: "flat", canChangeIcon: false) {
state "default", label:'', action:"refresh", icon:"st.secondary.refresh"
}
controlTile("volume", "device.volume", "slider", height:1, width:3, range:"(0..100)") {
state "volume", action:"music Player.setLevel"
}
standardTile("playpause", "device.playpause", decoration: "flat") {
state "pause", label:'', icon:'st.sonos.play-btn', action:'music Player.play'
state "play", label:'', icon:'st.sonos.pause-btn', action:'music Player.pause'
}
standardTile("prev", "device.switch", decoration: "flat", canChangeIcon: false) {
state "default", label:'', action:"music Player.previousTrack", icon:"st.sonos.previous-btn"
}
standardTile("next", "device.switch", decoration: "flat", canChangeIcon: false) {
state "default", label:'', action:"music Player.nextTrack", icon:"st.sonos.next-btn"
}
valueTile("everywhere", "device.everywhere", width:2, height:1, decoration:"flat") {
state "join", label:"Join\nEverywhere", action:"everywhereJoin"
state "leave", label:"Leave\nEverywhere", action:"everywhereLeave"
// Final state is used if the device is in a state where joining is not possible
state "unavailable", label:"Not Available"
}
// Defines which tile to show in the overview
main "switch"
// Defines which tile(s) to show when user opens the detailed view
details ([
"nowplaying", "refresh", // Row 1 (112)
"prev", "playpause", "next", // Row 2 (123)
"volume", // Row 3 (111)
"1", "2", "3", // Row 4 (123)
"4", "5", "6", // Row 5 (123)
"aux", "everywhere"]) // Row 6 (122)
}
/**************************************************************************
* The following section simply maps the actions as defined in
* the metadata into onAction() calls.
*
* This is preferred since some actions can be dealt with more
* efficiently this way. Also keeps all user interaction code in
* one place.
*
*/
def off() { onAction("off") }
def on() { onAction("on") }
def volup() { onAction("volup") }
def voldown() { onAction("voldown") }
def preset1() { onAction("1") }
def preset2() { onAction("2") }
def preset3() { onAction("3") }
def preset4() { onAction("4") }
def preset5() { onAction("5") }
def preset6() { onAction("6") }
def aux() { onAction("aux") }
def refresh() { onAction("refresh") }
def setLevel(level) { onAction("volume", level) }
def play() { onAction("play") }
def pause() { onAction("pause") }
def mute() { onAction("mute") }
def unmute() { onAction("unmute") }
def previousTrack() { onAction("previous") }
def nextTrack() { onAction("next") }
def everywhereJoin() { onAction("ejoin") }
def everywhereLeave() { onAction("eleave") }
/**************************************************************************/
/**
* Main point of interaction with things.
* This function is called by SmartThings Cloud with the resulting data from
* any action (see HubAction()).
*
* Conversely, to execute any actions, you need to return them as a single
* item or a list (flattened).
*
* @param data Data provided by the cloud
* @return an action or a list() of actions. Can also return null if no further
* action is desired at this point.
*/
def parse(String event) {
def data = parseLanMessage(event)
def actions = []
// List of permanent root node handlers
def handlers = [
"nowPlaying" : "boseParseNowPlaying",
"volume" : "boseParseVolume",
"presets" : "boseParsePresets",
"zone" : "boseParseEverywhere",
]
// No need to deal with non-XML data
if (!data.headers || !data.headers?."content-type".contains("xml"))
return null
// Move any pending callbacks into ready state
prepareCallbacks()
def xml = new XmlSlurper().parseText(data.body)
// Let each parser take a stab at it
handlers.each { node,func ->
if (xml.name() == node)
actions << "$func"(xml)
}
// If we have callbacks waiting for this...
actions << processCallbacks(xml)
// Be nice and helpful
if (actions.size() == 0) {
log.warn "parse(): Unhandled data = " + lan
return null
}
// Issue new actions
return actions.flatten()
}
/**
* Called when the devicetype is first installed.
*
* @return action(s) to take or null
*/
def installed() {
onAction("refresh")
}
/**
* Responsible for dealing with user input and taking the
* appropiate action.
*
* @param user The user interaction
* @param data Additional data (optional)
* @return action(s) to take (or null if none)
*/
def onAction(String user, data=null) {
log.info "onAction(${user})"
// Keep IP address current (since device may have changed)
state.address = parent.resolveDNI2Address(device.deviceNetworkId)
// Process action
def actions = null
switch (user) {
case "on":
actions = boseSetPowerState(true)
break
case "off":
boseSetNowPlaying(null, "STANDBY")
actions = boseSetPowerState(false)
break
case "volume":
actions = boseSetVolume(data)
break
case "aux":
boseSetNowPlaying(null, "AUX")
boseZoneReset()
sendEvent(name:"everywhere", value:"unavailable")
case "1":
case "2":
case "3":
case "4":
case "5":
case "6":
actions = boseSetInput(user)
break
case "refresh":
boseSetNowPlaying(null, "REFRESH")
actions = [boseRefreshNowPlaying(), boseGetPresets(), boseGetVolume(), boseGetEverywhereState()]
break
case "play":
actions = [boseSetPlayMode(true), boseRefreshNowPlaying()]
break
case "pause":
actions = [boseSetPlayMode(false), boseRefreshNowPlaying()]
break
case "previous":
actions = [boseChangeTrack(-1), boseRefreshNowPlaying()]
break
case "next":
actions = [boseChangeTrack(1), boseRefreshNowPlaying()]
break
case "mute":
actions = boseSetMute(true)
break
case "unmute":
actions = boseSetMute(false)
break
case "ejoin":
actions = boseZoneJoin()
break
case "eleave":
actions = boseZoneLeave()
break
default:
log.error "Unhandled action: " + user
}
// Make sure we don't have nested lists
if (actions instanceof List)
return actions.flatten()
return actions
}
/**
* Called every so often (every 5 minutes actually) to refresh the
* tiles so the user gets the correct information.
*/
def poll() {
return boseRefreshNowPlaying()
}
/**
* Joins this speaker into the everywhere zone
*/
def boseZoneJoin() {
def results = []
def posts = parent.boseZoneJoin(this)
for (post in posts) {
if (post['endpoint'])
results << bosePOST(post['endpoint'], post['body'], post['host'])
}
sendEvent(name:"everywhere", value:"leave")
results << boseRefreshNowPlaying()
return results
}
/**
* Removes this speaker from the everywhere zone
*/
def boseZoneLeave() {
def results = []
def posts = parent.boseZoneLeave(this)
for (post in posts) {
if (post['endpoint'])
results << bosePOST(post['endpoint'], post['body'], post['host'])
}
sendEvent(name:"everywhere", value:"join")
results << boseRefreshNowPlaying()
return results
}
/**
* Removes this speaker and any children WITHOUT
* signaling the speakers themselves. This is needed
* in certain cases where we know the user action will
* cause the zone to collapse (for example, AUX)
*/
def boseZoneReset() {
parent.boseZoneReset()
}
/**
* Handles <nowPlaying></nowPlaying> information and can also
* perform addtional actions if there is a pending command
* stored in the state variable. For example, the power is
* handled this way.
*
* @param xmlData Data to parse
* @return command
*/
def boseParseNowPlaying(xmlData) {
def result = []
// Perform display update, allow it to add additional commands
if (boseSetNowPlaying(xmlData)) {
result << boseRefreshNowPlaying()
}
return result
}
/**
* Parses volume data
*
* @param xmlData Data to parse
* @return command
*/
def boseParseVolume(xmlData) {
def result = []
sendEvent(name:"volume", value:xmlData.actualvolume.text())
sendEvent(name:"mute", value:(Boolean.toBoolean(xmlData.muteenabled.text()) ? "unmuted" : "muted"))
return result
}
/**
* Parses the result of the boseGetEverywhereState() call
*
* @param xmlData
*/
def boseParseEverywhere(xmlData) {
// No good way of detecting the correct state right now
}
/**
* Parses presets and updates the buttons
*
* @param xmlData Data to parse
* @return command
*/
def boseParsePresets(xmlData) {
def result = []
state.preset = [:]
def missing = ["1", "2", "3", "4", "5", "6"]
for (preset in xmlData.preset) {
def id = preset.attributes()['id']
def name = preset.ContentItem.itemName[0].text().replaceAll(~/ +/, "\n")
if (name == "##TRANS_SONGS##")
name = "Local\nPlaylist"
sendEvent(name:"station${id}", value:name)
missing = missing.findAll { it -> it != id }
// Store the presets into the state for recall later
state.preset["$id"] = XmlUtil.serialize(preset.ContentItem)
}
for (id in missing) {
state.preset["$id"] = null
sendEvent(name:"station${id}", value:"Preset $id\n\nNot set")
}
return result
}
/**
* Based on <nowPlaying></nowPlaying>, updates the visual
* representation of the speaker
*
* @param xmlData The nowPlaying info
* @param override Provide the source type manually (optional)
*
* @return true if it would prefer a refresh soon
*/
def boseSetNowPlaying(xmlData, override=null) {
def needrefresh = false
def nowplaying = null
if (xmlData && xmlData.playStatus) {
switch(xmlData.playStatus) {
case "BUFFERING_STATE":
nowplaying = "Please wait\nBuffering..."
needrefresh = true
break
case "PLAY_STATE":
sendEvent(name:"playpause", value:"play")
break
case "PAUSE_STATE":
case "STOP_STATE":
sendEvent(name:"playpause", value:"pause")
break
}
}
// If the previous section didn't handle this, take another stab at it
if (!nowplaying) {
nowplaying = ""
switch (override ? override : xmlData.attributes()['source']) {
case "AUX":
nowplaying = "Auxiliary Input"
break
case "AIRPLAY":
nowplaying = "Air Play"
break
case "STANDBY":
nowplaying = "Standby"
break
case "INTERNET_RADIO":
nowplaying = "${xmlData.stationName.text()}\n\n${xmlData.description.text()}"
break
case "REFRESH":
nowplaying = "Please wait"
break
case "SPOTIFY":
case "DEEZER":
case "PANDORA":
case "IHEART":
if (xmlData.ContentItem.itemName[0])
nowplaying += "[${xmlData.ContentItem.itemName[0].text()}]\n\n"
case "STORED_MUSIC":
nowplaying += "${xmlData.track.text()}"
if (xmlData.artist)
nowplaying += "\nby\n${xmlData.artist.text()}"
if (xmlData.album)
nowplaying += "\n\n(${xmlData.album.text()})"
break
default:
if (xmlData != null)
nowplaying = "${xmlData.ContentItem.itemName[0].text()}"
else
nowplaying = "Unknown"
}
}
// Some last parsing which only deals with actual data from device
if (xmlData) {
if (xmlData.attributes()['source'] == "STANDBY") {
log.trace "nowPlaying reports standby: " + XmlUtil.serialize(xmlData)
sendEvent(name:"switch", value:"off")
} else {
sendEvent(name:"switch", value:"on")
}
boseSetPlayerAttributes(xmlData)
}
// Do not allow a standby device or AUX to be master
if (!parent.boseZoneHasMaster() && (override ? override : xmlData.attributes()['source']) == "STANDBY")
sendEvent(name:"everywhere", value:"unavailable")
else if ((override ? override : xmlData.attributes()['source']) == "AUX")
sendEvent(name:"everywhere", value:"unavailable")
else if (boseGetZone()) {
log.info "We're in the zone: " + boseGetZone()
sendEvent(name:"everywhere", value:"leave")
} else
sendEvent(name:"everywhere", value:"join")
sendEvent(name:"nowplaying", value:nowplaying)
return needrefresh
}
/**
* Updates the attributes exposed by the music Player capability
*
* @param xmlData The NowPlaying XML data
*/
def boseSetPlayerAttributes(xmlData) {
// Refresh attributes
def trackText = ""
def trackDesc = ""
def trackData = [:]
switch (xmlData.attributes()['source']) {
case "STANDBY":
trackData["station"] = trackText = trackDesc = "Standby"
break
case "AUX":
trackData["station"] = trackText = trackDesc = "Auxiliary Input"
break
case "AIRPLAY":
trackData["station"] = trackText = trackDesc = "Air Play"
break
case "SPOTIFY":
case "DEEZER":
case "PANDORA":
case "IHEART":
case "STORED_MUSIC":
trackText = trackDesc = "${xmlData.track.text()}"
trackData["name"] = xmlData.track.text()
if (xmlData.artist) {
trackText += " by ${xmlData.artist.text()}"
trackDesc += " - ${xmlData.artist.text()}"
trackData["artist"] = xmlData.artist.text()
}
if (xmlData.album) {
trackText += " (${xmlData.album.text()})"
trackData["album"] = xmlData.album.text()
}
break
case "INTERNET_RADIO":
trackDesc = xmlData.stationName.text()
trackText = xmlData.stationName.text() + ": " + xmlData.description.text()
trackData["station"] = xmlData.stationName.text()
break
default:
trackText = trackDesc = xmlData.ContentItem.itemName[0].text()
}
sendEvent(name:"trackDescription", value:trackDesc, descriptionText:trackText)
}
/**
* Queries the state of the "play everywhere" mode
*
* @return command
*/
def boseGetEverywhereState() {
return boseGET("/getZone")
}
/**
* Generates a remote key event
*
* @param key The name of the key
*
* @return command
*
* @note It's VITAL that it's done as two requests, or it will ignore the
* the second key info.
*/
def boseKeypress(key) {
def press = "<key state=\"press\" sender=\"Gabbo\">${key}</key>"
def release = "<key state=\"release\" sender=\"Gabbo\">${key}</key>"
return [bosePOST("/key", press), bosePOST("/key", release)]
}
/**
* Pauses or plays current preset
*
* @param play If true, plays, else it pauses (depending on preset, may stop)
*
* @return command
*/
def boseSetPlayMode(boolean play) {
log.trace "Sending " + (play ? "PLAY" : "PAUSE")
return boseKeypress(play ? "PLAY" : "PAUSE")
}
/**
* Sets the volume in a deterministic way.
*
* @param New volume level, ranging from 0 to 100
*
* @return command
*/
def boseSetVolume(int level) {
def result = []
int vol = Math.min(100, Math.max(level, 0))
sendEvent(name:"volume", value:"${vol}")
return [bosePOST("/volume", "<volume>${vol}</volume>"), boseGetVolume()]
}
/**
* Sets the mute state, unfortunately, for now, we need to query current
* state before taking action (no discrete mute/unmute)
*
* @param mute If true, mutes the system
* @return command
*/
def boseSetMute(boolean mute) {
queueCallback('volume', 'cb_boseSetMute', mute ? 'MUTE' : 'UNMUTE')
return boseGetVolume()
}
/**
* Callback for boseSetMute(), checks current state and changes it
* if it doesn't match the requested state.
*
* @param xml The volume XML data
* @param mute The new state of mute
*
* @return command
*/
def cb_boseSetMute(xml, mute) {
def result = []
if ((xml.muteenabled.text() == 'false' && mute == 'MUTE') ||
(xml.muteenabled.text() == 'true' && mute == 'UNMUTE'))
{
result << boseKeypress("MUTE")
}
log.trace("muteunmute: " + ((mute == "MUTE") ? "unmute" : "mute"))
sendEvent(name:"muteunmute", value:((mute == "MUTE") ? "unmute" : "mute"))
return result
}
/**
* Refreshes the state of the volume
*
* @return command
*/
def boseGetVolume() {
return boseGET("/volume")
}
/**
* Changes the track to either the previous or next
*
* @param direction > 0 = next track, < 0 = previous track, 0 = no action
* @return command
*/
def boseChangeTrack(int direction) {
if (direction < 0) {
return boseKeypress("PREV_TRACK")
} else if (direction > 0) {
return boseKeypress("NEXT_TRACK")
}
return []
}
/**
* Sets the input to preset 1-6 or AUX
*
* @param input The input (one of 1,2,3,4,5,6,aux)
*
* @return command
*
* @note If no presets have been loaded, it will first refresh the presets.
*/
def boseSetInput(input) {
log.info "boseSetInput(${input})"
def result = []
if (!state.preset) {
result << boseGetPresets()
queueCallback('presets', 'cb_boseSetInput', input)
} else {
result << cb_boseSetInput(null, input)
}
return result
}
/**
* Callback used by boseSetInput(), either called directly by
* boseSetInput() if we already have presets, or called after
* retreiving the presets for the first time.
*
* @param xml The presets XML data
* @param input Desired input
*
* @return command
*
* @note Uses KEY commands for AUX, otherwise /select endpoint.
* Reason for this is latency. Since keypresses are done
* in pairs (press + release), you could accidentally change
* the preset if there is a long delay between the two.
*/
def cb_boseSetInput(xml, input) {
def result = []
if (input >= "1" && input <= "6" && state.preset["$input"])
result << bosePOST("/select", state.preset["$input"])
else if (input.toLowerCase() == "aux") {
result << boseKeypress("AUX_INPUT")
}
// Horrible workaround... but we need to delay
// the update by at least a few seconds...
result << boseRefreshNowPlaying(3000)
return result
}
/**
* Sets the power state of the bose unit
*
* @param device The device in-question
* @param enable True to power on, false to power off
*
* @return command
*
* @note Will first query state before acting since there
* is no discreete call.
*/
def boseSetPowerState(boolean enable) {
log.info "boseSetPowerState(${enable})"
queueCallback('nowPlaying', "cb_boseSetPowerState", enable ? "POWERON" : "POWEROFF")
return boseRefreshNowPlaying()
}
/**
* Callback function used by boseSetPowerState(), is used
* to handle the fact that we only have a toggle for power.
*
* @param xml The XML data from nowPlaying
* @param state The requested state
*
* @return command
*/
def cb_boseSetPowerState(xml, state) {
def result = []
if ( (xml.attributes()['source'] == "STANDBY" && state == "POWERON") ||
(xml.attributes()['source'] != "STANDBY" && state == "POWEROFF") )
{
result << boseKeypress("POWER")
if (state == "POWERON") {
result << boseRefreshNowPlaying()
queueCallback('nowPlaying', "cb_boseConfirmPowerOn", 5)
}
}
return result.flatten()
}
/**
* We're sometimes too quick on the draw and get a refreshed nowPlaying
* which shows standby (essentially, the device has yet to completely
* transition to awake state), so we need to poll a few times extra
* to make sure we get it right.
*
* @param xml The XML data from nowPlaying
* @param tries A counter which will decrease, once it reaches zero,
* we give up and assume that whatever we got was correct.
* @return command
*/
def cb_boseConfirmPowerOn(xml, tries) {
def result = []
log.warn "boseConfirmPowerOn() attempt #" + tries
if (xml.attributes()['source'] == "STANDBY" && tries > 0) {
result << boseRefreshNowPlaying()
queueCallback('nowPlaying', "cb_boseConfirmPowerOn", tries-1)
}
return result
}
/**
* Requests an update on currently playing item(s)
*
* @param delay If set to non-zero, delays x ms before issuing
*
* @return command
*/
def boseRefreshNowPlaying(delay=0) {
if (delay > 0) {
return ["delay ${delay}", boseGET("/now_playing")]
}
return boseGET("/now_playing")
}
/**
* Requests the list of presets
*
* @return command
*/
def boseGetPresets() {
return boseGET("/presets")
}
/**
* Utility function, makes GET requests to BOSE device
*
* @param path What endpoint
*
* @return command
*/
def boseGET(String path) {
new physicalgraph.device.HubAction([
method: "GET",
path: path,
headers: [
HOST: state.address + ":8090",
]])
}
/**
* Utility function, makes a POST request to the BOSE device with
* the provided data.
*
* @param path What endpoint
* @param data What data
* @param address Specific ip and port (optional)
*
* @return command
*/
def bosePOST(String path, String data, String address=null) {
new physicalgraph.device.HubAction([
method: "POST",
path: path,
body: data,
headers: [
HOST: address ?: (state.address + ":8090"),
]])
}
/**
* Queues a callback function for when a specific XML root is received
* Will execute on subsequent parse() call(s), never on the current
* parse() call.
*
* @param root The root node that this callback should react to
* @param func Name of the function
* @param param Parameters for function (optional)
*/
def queueCallback(String root, String func, param=null) {
if (!state.pending)
state.pending = [:]
if (!state.pending[root])
state.pending[root] = []
state.pending[root] << ["$func":"$param"]
}
/**
* Transfers the pending callbacks into readiness state
* so they can be executed by processCallbacks()
*
* This is needed to avoid reacting to queueCallbacks() within
* the same loop.
*/
def prepareCallbacks() {
if (!state.pending)
return
if (!state.ready)
state.ready = [:]
state.ready << state.pending
state.pending = [:]
}
/**
* Executes any ready callback for a specific root node
* with associated parameter and then clears that entry.
*
* If a callback returns data, it's added to a list of
* commands which is returned to the caller of this function
*
* Once a callback has been used, it's removed from the list
* of queued callbacks (ie, it executes only once!)
*
* @param xml The XML data to be examined and delegated
* @return list of commands
*/
def processCallbacks(xml) {
def result = []
if (!state.ready)
return result
if (state.ready[xml.name()]) {
state.ready[xml.name()].each { callback ->
callback.each { func, param ->
if (func != "func") {
if (param)
result << "$func"(xml, param)
else
result << "$func"(xml)
}
}
}
state.ready.remove(xml.name())
}
return result.flatten()
}
/**
* State managament for the Play Everywhere zone.
* This is typically called from the parent.
*
* A device is either:
*
* null = Not participating
* server = running the show
* client = under the control of the server
*
* @param newstate (see above for types)
*/
def boseSetZone(String newstate) {
log.debug "boseSetZone($newstate)"
state.zone = newstate
// Refresh our state
if (newstate) {
sendEvent(name:"everywhere", value:"leave")
} else {
sendEvent(name:"everywhere", value:"join")
}
}
/**
* Used by the Everywhere zone, returns the current state
* of zone membership (null, server, client)
* This is typically called from the parent.
*
* @return state
*/
def boseGetZone() {
return state.zone
}
/**
* Sets the DeviceID of this particular device.
*
* Needs to be done this way since DNI is not always
* the same as DeviceID which is used internally by
* BOSE.
*
* @param devID The DeviceID
*/
def boseSetDeviceID(String devID) {
state.deviceID = devID
}
/**
* Retrieves the DeviceID for this device
*
* @return deviceID
*/
def boseGetDeviceID() {
return state.deviceID
}
/**
* Returns the IP of this device
*
* @return IP address
*/
def getDeviceIP() {
return parent.resolveDNI2Address(device.deviceNetworkId)
}

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

@@ -55,141 +55,136 @@ metadata {
}
}
standardTile("indicator", "device.indicatorStatus", height: 2, width: 2, inactiveLabel: false, decoration: "flat") {
standardTile("indicator", "device.indicatorStatus", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
state "when off", action:"indicator.indicatorWhenOn", icon:"st.indicators.lit-when-off"
state "when on", action:"indicator.indicatorNever", icon:"st.indicators.lit-when-on"
state "never", action:"indicator.indicatorWhenOff", icon:"st.indicators.never-lit"
}
standardTile("refresh", "device.switch", height: 2, width: 2, inactiveLabel: false, decoration: "flat") {
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
standardTile("refresh", "device.switch", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
}
valueTile("level", "device.level", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "level", label:'${currentValue} %', unit:"%", backgroundColor:"#ffffff"
}
main(["switch"])
details(["switch", "refresh", "indicator"])
details(["switch", "level", "indicator", "refresh"])
}
}
def parse(String description) {
def item1 = [
canBeCurrentState: false,
linkText: getLinkText(device),
isStateChange: false,
displayed: false,
descriptionText: description,
value: description
]
def result
def cmd = zwave.parse(description, [0x20: 1, 0x26: 1, 0x70: 1])
if (cmd) {
result = createEvent(cmd, item1)
def result = null
if (description != "updated") {
log.debug "parse() >> zwave.parse($description)"
def cmd = zwave.parse(description, [0x20: 1, 0x26: 1, 0x70: 1])
if (cmd) {
result = zwaveEvent(cmd)
}
}
else {
item1.displayed = displayed(description, item1.isStateChange)
result = [item1]
if (result?.name == 'hail' && hubFirmwareLessThan("000.011.00602")) {
result = [result, response(zwave.basicV1.basicGet())]
log.debug "Was hailed: requesting state update"
} else {
log.debug "Parse returned ${result?.descriptionText}"
}
log.debug "Parse returned ${result?.descriptionText}"
result
return result
}
def createEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd, Map item1) {
def result = doCreateEvent(cmd, item1)
for (int i = 0; i < result.size(); i++) {
result[i].type = "physical"
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) {
dimmerEvents(cmd)
}
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) {
dimmerEvents(cmd)
}
def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelReport cmd) {
dimmerEvents(cmd)
}
def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelSet cmd) {
dimmerEvents(cmd)
}
private dimmerEvents(physicalgraph.zwave.Command cmd) {
def value = (cmd.value ? "on" : "off")
def result = [createEvent(name: "switch", value: value)]
if (cmd.value && cmd.value <= 100) {
result << createEvent(name: "level", value: cmd.value, unit: "%")
}
result
return result
}
def createEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd, Map item1) {
def result = doCreateEvent(cmd, item1)
for (int i = 0; i < result.size(); i++) {
result[i].type = "physical"
}
result
}
def createEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelStartLevelChange cmd, Map item1) {
[]
}
def createEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelStopLevelChange cmd, Map item1) {
[response(zwave.basicV1.basicGet())]
}
def createEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelSet cmd, Map item1) {
def result = doCreateEvent(cmd, item1)
for (int i = 0; i < result.size(); i++) {
result[i].type = "physical"
}
result
}
def createEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelReport cmd, Map item1) {
def result = doCreateEvent(cmd, item1)
result[0].descriptionText = "${item1.linkText} is ${item1.value}"
result[0].handlerName = cmd.value ? "statusOn" : "statusOff"
for (int i = 0; i < result.size(); i++) {
result[i].type = "digital"
}
result
}
def doCreateEvent(physicalgraph.zwave.Command cmd, Map item1) {
def result = [item1]
item1.name = "switch"
item1.value = cmd.value ? "on" : "off"
item1.handlerName = item1.value
item1.descriptionText = "${item1.linkText} was turned ${item1.value}"
item1.canBeCurrentState = true
item1.isStateChange = isStateChange(device, item1.name, item1.value)
item1.displayed = item1.isStateChange
if (cmd.value >= 5) {
def item2 = new LinkedHashMap(item1)
item2.name = "level"
item2.value = cmd.value as String
item2.unit = "%"
item2.descriptionText = "${item1.linkText} dimmed ${item2.value} %"
item2.canBeCurrentState = true
item2.isStateChange = isStateChange(device, item2.name, item2.value)
item2.displayed = false
result << item2
}
result
}
def zwaveEvent(physicalgraph.zwave.commands.configurationv1.ConfigurationReport cmd) {
log.debug "ConfigurationReport $cmd"
def value = "when off"
if (cmd.configurationValue[0] == 1) {value = "when on"}
if (cmd.configurationValue[0] == 2) {value = "never"}
[name: "indicatorStatus", value: value, display: false]
createEvent([name: "indicatorStatus", value: value])
}
def createEvent(physicalgraph.zwave.Command cmd, Map map) {
// Handles any Z-Wave commands we aren't interested in
log.debug "UNHANDLED COMMAND $cmd"
def zwaveEvent(physicalgraph.zwave.commands.hailv1.Hail cmd) {
createEvent([name: "hail", value: "hail", descriptionText: "Switch button was pressed", displayed: false])
}
def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport 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)
createEvent([descriptionText: "$device.displayName MSR: $msr", isStateChange: false])
}
def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelStopLevelChange cmd) {
[createEvent(name:"switch", value:"on"), response(zwave.switchMultilevelV1.switchMultilevelGet().format())]
}
def zwaveEvent(physicalgraph.zwave.Command cmd) {
// Handles all Z-Wave commands we aren't interested in
[:]
}
def on() {
log.info "on"
delayBetween([zwave.basicV1.basicSet(value: 0xFF).format(), zwave.switchMultilevelV1.switchMultilevelGet().format()], 5000)
delayBetween([
zwave.basicV1.basicSet(value: 0xFF).format(),
zwave.switchMultilevelV1.switchMultilevelGet().format()
],5000)
}
def off() {
delayBetween ([zwave.basicV1.basicSet(value: 0x00).format(), zwave.switchMultilevelV1.switchMultilevelGet().format()], 5000)
delayBetween([
zwave.basicV1.basicSet(value: 0x00).format(),
zwave.switchMultilevelV1.switchMultilevelGet().format()
],5000)
}
def setLevel(value) {
log.debug "setLevel >> value: $value"
def valueaux = value as Integer
def level = Math.min(valueaux, 99)
def level = Math.max(Math.min(valueaux, 99), 0)
if (level > 0) {
sendEvent(name: "switch", value: "on")
} else {
sendEvent(name: "switch", value: "off")
}
sendEvent(name: "level", value: level, unit: "%")
delayBetween ([zwave.basicV1.basicSet(value: level).format(), zwave.switchMultilevelV1.switchMultilevelGet().format()], 5000)
}
def setLevel(value, duration) {
log.debug "setLevel >> value: $value, duration: $duration"
def valueaux = value as Integer
def level = Math.min(valueaux, 99)
def level = Math.max(Math.min(valueaux, 99), 0)
def dimmingDuration = duration < 128 ? duration : 128 + Math.round(duration / 60)
zwave.switchMultilevelV2.switchMultilevelSet(value: level, dimmingDuration: dimmingDuration).format()
def getStatusDelay = duration < 128 ? (duration*1000)+2000 : (Math.round(duration / 60)*60*1000)+2000
delayBetween ([zwave.switchMultilevelV2.switchMultilevelSet(value: level, dimmingDuration: dimmingDuration).format(),
zwave.switchMultilevelV1.switchMultilevelGet().format()], getStatusDelay)
}
def poll() {
@@ -197,21 +192,27 @@ def poll() {
}
def refresh() {
zwave.switchMultilevelV1.switchMultilevelGet().format()
log.debug "refresh() is called"
def commands = []
commands << zwave.switchMultilevelV1.switchMultilevelGet().format()
if (getDataValue("MSR") == null) {
commands << zwave.manufacturerSpecificV1.manufacturerSpecificGet().format()
}
delayBetween(commands,100)
}
def indicatorWhenOn() {
sendEvent(name: "indicatorStatus", value: "when on", display: false)
sendEvent(name: "indicatorStatus", value: "when on")
zwave.configurationV1.configurationSet(configurationValue: [1], parameterNumber: 3, size: 1).format()
}
def indicatorWhenOff() {
sendEvent(name: "indicatorStatus", value: "when off", display: false)
sendEvent(name: "indicatorStatus", value: "when off")
zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 3, size: 1).format()
}
def indicatorNever() {
sendEvent(name: "indicatorStatus", value: "never", display: false)
sendEvent(name: "indicatorStatus", value: "never")
zwave.configurationV1.configurationSet(configurationValue: [2], parameterNumber: 3, size: 1).format()
}
@@ -222,4 +223,4 @@ def invertSwitch(invert=true) {
else {
zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 4, size: 1).format()
}
}
}

View File

@@ -0,0 +1,455 @@
/**
* 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.
*
*/
metadata {
definition (name: "Fibaro Smoke Sensor", namespace: "smartthings", author: "SmartThings") {
capability "Battery" //attributes: battery
capability "Configuration" //commands: configure()
capability "Sensor"
capability "Smoke Detector" //attributes: smoke ("detected","clear","tested")
capability "Temperature Measurement" //attributes: temperature
attribute "tamper", "enum", ["detected", "clear"]
attribute "heatAlarm", "enum", ["overheat detected", "clear", "rapid temperature rise", "underheat detected"]
fingerprint deviceId: "0x0701", inClusters: "0x5E, 0x86, 0x72, 0x5A, 0x59, 0x85, 0x73, 0x84, 0x80, 0x71, 0x56, 0x70, 0x31, 0x8E, 0x22, 0x9C, 0x98, 0x7A", outClusters: "0x20, 0x8B"
}
simulator {
//battery
for (int i in [0, 5, 10, 15, 50, 99, 100]) {
status "battery ${i}%": new physicalgraph.zwave.Zwave().securityV1.securityMessageEncapsulation().encapsulate(
new physicalgraph.zwave.Zwave().batteryV1.batteryReport(batteryLevel: i)
).incomingMessage()
}
status "battery 100%": "command: 8003, payload: 64"
status "battery 5%": "command: 8003, payload: 05"
//smoke
status "smoke detected": "command: 7105, payload: 01 01"
status "smoke clear": "command: 7105, payload: 01 00"
status "smoke tested": "command: 7105, payload: 01 03"
//temperature
for (int i = 0; i <= 100; i += 20) {
status "temperature ${i}F": new physicalgraph.zwave.Zwave().securityV1.securityMessageEncapsulation().encapsulate(
new physicalgraph.zwave.Zwave().sensorMultilevelV5.sensorMultilevelReport(scaledSensorValue: i, precision: 1, sensorType: 1, scale: 1)
).incomingMessage()
}
}
preferences {
input description: "After successful installation, please click B-button at the Fibaro Smoke Sensor to update device status and configuration",
title: "Instructions", displayDuringSetup: true, type: "paragraph", element: "paragraph"
input description: "Enter the menu by press and hold B-button for 3 seconds. Once indicator glows WHITE, release the B-button. Visual indicator will start changing colours in sequence. Press B-button briefly when visual indicator glows GREEN",
title: "To check smoke detection state", displayDuringSetup: true, type: "paragraph", element: "paragraph"
input description: "Please consult Fibaro Smoke Sensor 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 "smokeSensorSensitivity", "enum", title: "Smoke Sensor Sensitivity", options: ["High","Medium","Low"], defaultValue: "${smokeSensorSensitivity}", displayDuringSetup: true
input "zwaveNotificationStatus", "enum", title: "Notifications Status", options: ["disabled","casing opened","exceeding temperature threshold", "lack of Z-Wave range", "all notifications"],
defaultValue: "${zwaveNotificationStatus}", displayDuringSetup: true
input "visualIndicatorNotificationStatus", "enum", title: "Visual Indicator Notifications Status",
options: ["disabled","casing opened","exceeding temperature threshold", "lack of Z-Wave range", "all notifications"],
defaultValue: "${visualIndicatorNotificationStatus}", displayDuringSetup: true
input "soundNotificationStatus", "enum", title: "Sound Notifications Status",
options: ["disabled","casing opened","exceeding temperature threshold", "lack of Z-Wave range", "all notifications"],
defaultValue: "${soundNotificationStatus}", displayDuringSetup: true
input "temperatureReportInterval", "enum", title: "Temperature Report Interval",
options: ["reports inactive", "5 minutes", "15 minutes", "30 minutes", "1 hour", "6 hours", "12 hours", "18 hours", "24 hours"], defaultValue: "${temperatureReportInterval}", displayDuringSetup: true
input "temperatureReportHysteresis", "number", title: "Temperature Report Hysteresis", description: "Available settings: 1-100 C", range: "1..100", displayDuringSetup: true
input "temperatureThreshold", "number", title: "Overheat Temperature Threshold", description: "Available settings: 0 or 2-100 C", range: "0..100", displayDuringSetup: true
input "excessTemperatureSignalingInterval", "enum", title: "Excess Temperature Signaling Interval",
options: ["5 minutes", "15 minutes", "30 minutes", "1 hour", "6 hours", "12 hours", "18 hours", "24 hours"], defaultValue: "${excessTemperatureSignalingInterval}", displayDuringSetup: true
input "lackOfZwaveRangeIndicationInterval", "enum", title: "Lack of Z-Wave Range Indication Interval",
options: ["5 minutes", "15 minutes", "30 minutes", "1 hour", "6 hours", "12 hours", "18 hours", "24 hours"], defaultValue: "${lackOfZwaveRangeIndicationInterval}", displayDuringSetup: true
}
tiles (scale: 2){
multiAttributeTile(name:"smoke", type: "lighting", width: 6, height: 4){
tileAttribute ("device.smoke", key: "PRIMARY_CONTROL") {
attributeState("clear", label:"CLEAR", icon:"st.alarm.smoke.clear", backgroundColor:"#ffffff")
attributeState("detected", label:"SMOKE", icon:"st.alarm.smoke.smoke", backgroundColor:"#e86d13")
attributeState("tested", label:"TEST", icon:"st.alarm.smoke.test", backgroundColor:"#e86d13")
attributeState("replacement required", label:"REPLACE", icon:"st.alarm.smoke.test", backgroundColor:"#FFFF66")
attributeState("unknown", label:"UNKNOWN", icon:"st.alarm.smoke.test", backgroundColor:"#ffffff")
}
tileAttribute ("device.battery", key: "SECONDARY_CONTROL") {
attributeState "battery", label:'Battery: ${currentValue}%', unit:"%"
}
}
valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "battery", label:'${currentValue}% battery', unit:"%"
}
valueTile("temperature", "device.temperature", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "temperature", label:'${currentValue}°', unit:"C"
}
valueTile("heatAlarm", "device.heatAlarm", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "clear", label:'TEMPERATURE OK', backgroundColor:"#ffffff"
state "overheat detected", label:'OVERHEAT DETECTED', backgroundColor:"#ffffff"
state "rapid temperature rise", label:'RAPID TEMP RISE', backgroundColor:"#ffffff"
state "underheat detected", label:'UNDERHEAT DETECTED', backgroundColor:"#ffffff"
}
valueTile("tamper", "device.tamper", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "clear", label:'NO TAMPER', backgroundColor:"#ffffff"
state "detected", label:'TAMPER DETECTED', backgroundColor:"#ffffff"
}
main "smoke"
details(["smoke","temperature"])
}
}
def updated() {
log.debug "Updated with settings: ${settings}"
setConfigured("false") //wait until the next time device wakeup to send configure command
}
def parse(String description) {
log.debug "parse() >> description: $description"
def result = null
if (description.startsWith("Err 106")) {
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.")
} else if (description != "updated") {
log.debug "parse() >> zwave.parse(description)"
def cmd = zwave.parse(description, [0x31: 5, 0x71: 3, 0x84: 1])
if (cmd) {
result = zwaveEvent(cmd)
}
}
log.debug "After zwaveEvent(cmd) >> Parsed '${description}' to ${result.inspect()}"
return result
}
def zwaveEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd) {
log.info "Executing zwaveEvent 86 (VersionV1): 12 (VersionReport) with cmd: $cmd"
def fw = "${cmd.applicationVersion}.${cmd.applicationSubVersion}"
updateDataValue("fw", fw)
def text = "$device.displayName: firmware version: $fw, Z-Wave version: ${cmd.zWaveProtocolVersion}.${cmd.zWaveProtocolSubVersion}"
createEvent(descriptionText: text, isStateChange: false)
}
def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) {
def map = [ name: "battery", unit: "%" ]
if (cmd.batteryLevel == 0xFF) {
map.value = 1
map.descriptionText = "${device.displayName} battery is low"
map.isStateChange = true
} else {
map.value = cmd.batteryLevel
}
setConfigured("true") //when battery is reported back meaning configuration is done
//Store time of last battery update so we don't ask every wakeup, see WakeUpNotification handler
state.lastbatt = now()
createEvent(map)
}
def zwaveEvent(physicalgraph.zwave.commands.applicationstatusv1.ApplicationBusy cmd) {
def msg = cmd.status == 0 ? "try again later" :
cmd.status == 1 ? "try again in $cmd.waitTime seconds" :
cmd.status == 2 ? "request queued" : "sorry"
createEvent(displayed: true, descriptionText: "$device.displayName is busy, $msg")
}
def zwaveEvent(physicalgraph.zwave.commands.applicationstatusv1.ApplicationRejectedRequest cmd) {
createEvent(displayed: true, descriptionText: "$device.displayName rejected the last request")
}
def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) {
setSecured()
def encapsulatedCommand = cmd.encapsulatedCommand([0x31: 5, 0x71: 3, 0x84: 1])
if (encapsulatedCommand) {
log.debug "command: 98 (Security) 81(SecurityMessageEncapsulation) encapsulatedCommand: $encapsulatedCommand"
zwaveEvent(encapsulatedCommand)
} else {
log.warn "Unable to extract encapsulated cmd from $cmd"
createEvent(descriptionText: cmd.toString())
}
}
def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityCommandsSupportedReport cmd) {
log.info "Executing zwaveEvent 98 (SecurityV1): 03 (SecurityCommandsSupportedReport) with cmd: $cmd"
setSecured()
log.info "checking this MSR : ${getDataValue("MSR")} before sending configuration to device"
if (getDataValue("MSR")?.startsWith("010F-0C02")){
response(configure()) //configure device using SmartThings default settings
}
}
def zwaveEvent(physicalgraph.zwave.commands.securityv1.NetworkKeyVerify cmd) {
log.info "Executing zwaveEvent 98 (SecurityV1): 07 (NetworkKeyVerify) with cmd: $cmd (node is securely included)"
createEvent(name:"secureInclusion", value:"success", descriptionText:"Secure inclusion was successful", isStateChange: true, displayed: true)
//after device securely joined the network, call configure() to config device
setSecured()
log.info "checking this MSR : ${getDataValue("MSR")} before sending configuration to device"
if (getDataValue("MSR")?.startsWith("010F-0C02")){
response(configure()) //configure device using SmartThings default settings
}
}
def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) {
log.info "Executing zwaveEvent 71 (NotificationV3): 05 (NotificationReport) with cmd: $cmd"
def result = []
if (cmd.notificationType == 7) {
switch (cmd.event) {
case 0:
result << createEvent(name: "tamper", value: "clear", displayed: false)
break
case 3:
result << createEvent(name: "tamper", value: "detected", descriptionText: "$device.displayName casing was opened")
break
}
} else if (cmd.notificationType == 1) { //Smoke Alarm (V2)
log.debug "notificationv3.NotificationReport: for Smoke Alarm (V2)"
result << smokeAlarmEvent(cmd.event)
} else if (cmd.notificationType == 4) { // Heat Alarm (V2)
log.debug "notificationv3.NotificationReport: for Heat Alarm (V2)"
result << heatAlarmEvent(cmd.event)
} else {
log.warn "Need to handle this cmd.notificationType: ${cmd.notificationType}"
result << createEvent(descriptionText: cmd.toString(), isStateChange: false)
}
result
}
def smokeAlarmEvent(value) {
log.debug "smokeAlarmEvent(value): $value"
def map = [name: "smoke"]
if (value == 1 || value == 2) {
map.value = "detected"
map.descriptionText = "$device.displayName detected smoke"
} else if (value == 0) {
map.value = "clear"
map.descriptionText = "$device.displayName is clear (no smoke)"
} else if (value == 3) {
map.value = "tested"
map.descriptionText = "$device.displayName smoke alarm test"
} else if (value == 4) {
map.value = "replacement required"
map.descriptionText = "$device.displayName replacement required"
} else {
map.value = "unknown"
map.descriptionText = "$device.displayName unknown event"
}
createEvent(map)
}
def heatAlarmEvent(value) {
log.debug "heatAlarmEvent(value): $value"
def map = [name: "heatAlarm"]
if (value == 1 || value == 2) {
map.value = "overheat detected"
map.descriptionText = "$device.displayName overheat detected"
} else if (value == 0) {
map.value = "clear"
map.descriptionText = "$device.displayName heat alarm cleared (no overheat)"
} else if (value == 3 || value == 4) {
map.value = "rapid temperature rise"
map.descriptionText = "$device.displayName rapid temperature rise"
} else if (value == 5 || value == 6) {
map.value = "underheat detected"
map.descriptionText = "$device.displayName underheat detected"
} else {
map.value = "unknown"
map.descriptionText = "$device.displayName unknown event"
}
createEvent(map)
}
def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) {
log.info "Executing zwaveEvent 84 (WakeUpV1): 07 (WakeUpNotification) with cmd: $cmd"
log.info "checking this MSR : ${getDataValue("MSR")} before sending configuration to device"
def result = [createEvent(descriptionText: "${device.displayName} woke up", isStateChange: false)]
def cmds = []
/* check MSR = "manufacturerId-productTypeId" to make sure configuration commands are sent to the right model */
if (!isConfigured() && getDataValue("MSR")?.startsWith("010F-0C02")) {
result << response(configure()) // configure a newly joined device or joined device with preference update
} else {
//Only ask for battery if we haven't had a BatteryReport in a while
if (!state.lastbatt || (new Date().time) - state.lastbatt > 24*60*60*1000) {
log.debug("Device has been configured sending >> batteryGet()")
cmds << zwave.securityV1.securityMessageEncapsulation().encapsulate(zwave.batteryV1.batteryGet()).format()
cmds << "delay 1200"
}
log.debug("Device has been configured sending >> wakeUpNoMoreInformation()")
cmds << zwave.wakeUpV1.wakeUpNoMoreInformation().format()
result << response(cmds) //tell device back to sleep
}
result
}
def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) {
log.info "Executing zwaveEvent 31 (SensorMultilevelV5): 05 (SensorMultilevelReport) with cmd: $cmd"
def map = [:]
switch (cmd.sensorType) {
case 1:
map.name = "temperature"
def cmdScale = cmd.scale == 1 ? "F" : "C"
map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, cmd.precision)
map.unit = getTemperatureScale()
break
default:
map.descriptionText = cmd.toString()
}
createEvent(map)
}
def zwaveEvent(physicalgraph.zwave.commands.deviceresetlocallyv1.DeviceResetLocallyNotification cmd) {
log.info "Executing zwaveEvent 5A (DeviceResetLocallyV1) : 01 (DeviceResetLocallyNotification) with cmd: $cmd"
createEvent(descriptionText: cmd.toString(), isStateChange: true, displayed: true)
}
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 result = []
def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId)
updateDataValue("MSR", msr)
log.debug "After device is securely joined, send commands to update tiles"
result << zwave.batteryV1.batteryGet()
result << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x01)
result << zwave.wakeUpV1.wakeUpNoMoreInformation()
[[descriptionText:"${device.displayName} MSR report"], response(commands(result, 5000))]
}
def zwaveEvent(physicalgraph.zwave.commands.associationv2.AssociationReport cmd) {
def result = []
if (cmd.nodeId.any { it == zwaveHubNodeId }) {
result << createEvent(descriptionText: "$device.displayName is associated in group ${cmd.groupingIdentifier}")
} else if (cmd.groupingIdentifier == 1) {
result << createEvent(descriptionText: "Associating $device.displayName in group ${cmd.groupingIdentifier}")
result << response(zwave.associationV1.associationSet(groupingIdentifier:cmd.groupingIdentifier, nodeId:zwaveHubNodeId))
}
result
}
def zwaveEvent(physicalgraph.zwave.Command cmd) {
log.warn "General zwaveEvent cmd: ${cmd}"
createEvent(descriptionText: cmd.toString(), isStateChange: false)
}
def configure() {
// This sensor joins as a secure device if you tripple-click the button to include it
log.debug "configure() >> isSecured() : ${isSecured()}"
if (!isSecured()) {
log.debug "Fibaro smoke sensor not sending configure until secure"
return []
} else {
log.info "${device.displayName} is configuring its settings"
def request = []
//1. configure wakeup interval : available: 0, 4200s-65535s, device default 21600s(6hr)
request += zwave.wakeUpV1.wakeUpIntervalSet(seconds:6*3600, nodeid:zwaveHubNodeId)
//2. Smoke Sensitivity 3 levels: 1-HIGH , 2-MEDIUM (default), 3-LOW
if (smokeSensorSensitivity && smokeSensorSensitivity != "null") {
request += zwave.configurationV1.configurationSet(parameterNumber: 1, size: 1,
scaledConfigurationValue:
smokeSensorSensitivity == "High" ? 1 :
smokeSensorSensitivity == "Medium" ? 2 :
smokeSensorSensitivity == "Low" ? 3 : 2)
}
//3. Z-Wave notification status: 0-all disabled (default), 1-casing open enabled, 2-exceeding temp enable
if (zwaveNotificationStatus && zwaveNotificationStatus != "null"){
request += zwave.configurationV1.configurationSet(parameterNumber: 2, size: 1, scaledConfigurationValue: notificationOptionValueMap[zwaveNotificationStatus] ?: 0)
}
//4. Visual indicator notification status: 0-all disabled (default), 1-casing open enabled, 2-exceeding temp enable, 4-lack of range notification
if (visualIndicatorNotificationStatus && visualIndicatorNotificationStatus != "null") {
request += zwave.configurationV1.configurationSet(parameterNumber: 3, size: 1, scaledConfigurationValue: notificationOptionValueMap[visualIndicatorNotificationStatus] ?: 0)
}
//5. Sound notification status: 0-all disabled (default), 1-casing open enabled, 2-exceeding temp enable, 4-lack of range notification
if (soundNotificationStatus && soundNotificationStatus != "null") {
request += zwave.configurationV1.configurationSet(parameterNumber: 4, size: 1, scaledConfigurationValue: notificationOptionValueMap[soundNotificationStatus] ?: 0)
}
//6. Temperature report interval: 0-report inactive, 1-8640 (multiply by 10 secs) [10s-24hr], default 180 (30 minutes)
if (temperatureReportInterval && temperatureReportInterval != "null") {
request += zwave.configurationV1.configurationSet(parameterNumber: 20, size: 2, scaledConfigurationValue: timeOptionValueMap[temperatureReportInterval] ?: 180)
} else { //send SmartThings default configuration
request += zwave.configurationV1.configurationSet(parameterNumber: 20, size: 2, scaledConfigurationValue: 180)
}
//7. Temperature report hysteresis: 1-100 (in 0.1C step) [0.1C - 10C], default 10 (1 C)
if (temperatureReportHysteresis && temperatureReportHysteresis != null) {
request += zwave.configurationV1.configurationSet(parameterNumber: 21, size: 1, scaledConfigurationValue: temperatureReportHysteresis < 1 ? 1 : temperatureReportHysteresis > 100 ? 100 : temperatureReportHysteresis)
}
//8. Temperature threshold: 1-100 (C), default 55 (C)
if (temperatureThreshold && temperatureThreshold != null) {
request += zwave.configurationV1.configurationSet(parameterNumber: 30, size: 1, scaledConfigurationValue: temperatureThreshold < 1 ? 1 : temperatureThreshold > 100 ? 100 : temperatureThreshold)
}
//9. Excess temperature signaling interval: 1-8640 (multiply by 10 secs) [10s-24hr], default 180 (30 minutes)
if (excessTemperatureSignalingInterval && excessTemperatureSignalingInterval != "null") {
request += zwave.configurationV1.configurationSet(parameterNumber: 31, size: 2, scaledConfigurationValue: timeOptionValueMap[excessTemperatureSignalingInterval] ?: 180)
} else { //send SmartThings default configuration
request += zwave.configurationV1.configurationSet(parameterNumber: 31, size: 2, scaledConfigurationValue: 180)
}
//10. Lack of Z-Wave range indication interval: 1-8640 (multiply by 10 secs) [10s-24hr], default 2160 (6 hours)
if (lackOfZwaveRangeIndicationInterval && lackOfZwaveRangeIndicationInterval != "null") {
request += zwave.configurationV1.configurationSet(parameterNumber: 32, size: 2, scaledConfigurationValue: timeOptionValueMap[lackOfZwaveRangeIndicationInterval] ?: 2160)
} else {
request += zwave.configurationV1.configurationSet(parameterNumber: 32, size: 2, scaledConfigurationValue: 2160)
}
//11. get battery level when device is paired
request += zwave.batteryV1.batteryGet()
//12. get temperature reading from device
request += zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x01)
commands(request) + ["delay 10000", zwave.wakeUpV1.wakeUpNoMoreInformation().format()]
}
}
private def getTimeOptionValueMap() { [
"5 minutes" : 30,
"15 minutes" : 90,
"30 minutes" : 180,
"1 hour" : 360,
"6 hours" : 2160,
"12 hours" : 4320,
"18 hours" : 6480,
"24 hours" : 8640,
"reports inactive" : 0,
]}
private def getNotificationOptionValueMap() { [
"disabled" : 0,
"casing opened" : 1,
"exceeding temperature threshold" : 2,
"lack of Z-Wave range" : 4,
"all notifications" : 7,
]}
private command(physicalgraph.zwave.Command cmd) {
if (isSecured()) {
log.info "Sending secured command: ${cmd}"
zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format()
} else {
log.info "Sending unsecured command: ${cmd}"
cmd.format()
}
}
private commands(commands, delay=200) {
log.info "inside commands: ${commands}"
delayBetween(commands.collect{ command(it) }, delay)
}
private setConfigured(configure) {
updateDataValue("configured", configure)
}
private isConfigured() {
getDataValue("configured") == "true"
}
private setSecured() {
updateDataValue("secured", "true")
}
private isSecured() {
getDataValue("secured") == "true"
}

View File

@@ -0,0 +1,887 @@
/**
* Fidure Thermostat, Based on ZigBee thermostat (SmartThings)
*
* Author: Fidure
* Date: 2014-12-13
* Updated: 2015-08-26
*/
metadata {
// Automatically generated. Make future change here.
definition (name: "Fidure Thermostat", namespace: "smartthings", author: "SmartThings") {
capability "Actuator"
capability "Temperature Measurement"
capability "Thermostat"
capability "Configuration"
capability "Refresh"
capability "Sensor"
capability "Polling"
attribute "displayTemperature","number"
attribute "displaySetpoint", "string"
command "raiseSetpoint"
command "lowerSetpoint"
attribute "upButtonState", "string"
attribute "downButtonState", "string"
attribute "runningMode", "string"
attribute "lockLevel", "string"
command "setThermostatTime"
command "lock"
attribute "prorgammingOperation", "number"
attribute "prorgammingOperationDisplay", "string"
command "Program"
attribute "setpointHold", "string"
attribute "setpointHoldDisplay", "string"
command "Hold"
attribute "holdExpiary", "string"
attribute "lastTimeSync", "string"
attribute "thermostatOperatingState", "string"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0201,0204,0B05", outClusters: "000A, 0019"
}
// simulator metadata
simulator { }
// pref
preferences {
input ("hold_time", "enum", title: "Default Hold Time in Hours",
description: "Default Hold Duration in hours",
range: "1..24", options: ["No Hold", "2 Hours", "4 Hours", "8 Hours", "12 Hours", "1 Day"],
displayDuringSetup: false)
input ("sync_clock", "boolean", title: "Synchronize Thermostat Clock Automatically?", options: ["Yes","No"])
input ("lock_level", "enum", title: "Thermostat Screen Lock Level", options: ["Full","Mode Only", "Setpoint"])
}
tiles {
valueTile("temperature", "displayTemperature", width: 2, height: 2) {
state("temperature", label:'${currentValue}°', unit:"F",
backgroundColors:[
[value: 0, color: "#153591"],
[value: 7, color: "#1e9cbb"],
[value: 15, color: "#90d2a7"],
[value: 23, color: "#44b621"],
[value: 29, color: "#f1d801"],
[value: 35, color: "#d04e00"],
[value: 36, color: "#bc2323"],
// fahrenheit range
[value: 37, 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") {
state "off", action:"thermostat.setThermostatMode", icon:"st.thermostat.heating-cooling-off"
state "cool", action:"thermostat.setThermostatMode", icon:"st.thermostat.cool"
state "heat", action:"thermostat.setThermostatMode", icon:"st.thermostat.heat"
state "auto", action:"thermostat.setThermostatMode", icon:"st.thermostat.auto"
}
standardTile("fanMode", "device.thermostatFanMode", inactiveLabel: false, decoration: "flat") {
state "fanAuto", label:'${name}', action:"thermostat.setThermostatFanMode"
state "fanOn", label:'${name}', action:"thermostat.setThermostatFanMode"
}
standardTile("hvacStatus", "thermostatOperatingState", inactiveLabel: false, decoration: "flat") {
state "Resting", label: 'Resting'
state "Heating", icon:"st.thermostat.heating"
state "Cooling", icon:"st.thermostat.cooling"
}
standardTile("lock", "lockLevel", inactiveLabel: false, decoration: "flat") {
state "Unlocked", action:"lock", label:'${name}'
state "Mode Only", action:"lock", label:'${name}'
state "Setpoint", action:"lock", label:'${name}'
state "Full", action:"lock", label:'${name}'
}
controlTile("heatSliderControl", "device.heatingSetpoint", "slider", height: 1, width: 2, inactiveLabel: false, range: "$min..$max") {
state "setHeatingSetpoint", action:"thermostat.setHeatingSetpoint", backgroundColor:"#d04e00"
}
valueTile("heatingSetpoint", "device.heatingSetpoint", inactiveLabel: false, decoration: "flat") {
state "heat", label:'${currentValue}° heat', unit:"F", backgroundColor:"#ffffff"
}
controlTile("coolSliderControl", "device.coolingSetpoint", "slider", height: 1, width: 2, inactiveLabel: false, range: "$min..$max") {
state "setCoolingSetpoint", action:"thermostat.setCoolingSetpoint", backgroundColor: "#1e9cbb"
}
valueTile("coolingSetpoint", "device.coolingSetpoint", inactiveLabel: false, decoration: "flat") {
state "cool", label:'${currentValue}° cool', unit:"F", backgroundColor:"#ffffff"
}
standardTile("refresh", "device.temperature", inactiveLabel: false, decoration: "flat") {
state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
}
standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat") {
state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure"
}
valueTile("scheduleText", "prorgammingOperation", inactiveLabel: false, decoration: "flat", width: 2) {
state "default", label: 'Schedule'
}
valueTile("schedule", "prorgammingOperationDisplay", inactiveLabel: false, decoration: "flat") {
state "default", action:"Program", label: '${currentValue}'
}
valueTile("hold", "setpointHoldDisplay", inactiveLabel: false, decoration: "flat", width: 3) {
state "setpointHold", action:"Hold", label: '${currentValue}'
}
valueTile("setpoint", "displaySetpoint", width: 2, height: 2)
{
state("displaySetpoint", label: '${currentValue}°',
backgroundColor: "#919191")
}
standardTile("upButton", "upButtonState", decoration: "flat", inactiveLabel: false) {
state "normal", action:"raiseSetpoint", backgroundColor:"#919191", icon:"st.thermostat.thermostat-up"
state "pressed", action:"raiseSetpoint", backgroundColor:"#ff0000", icon:"st.thermostat.thermostat-up"
}
standardTile("downButton", "downButtonState", decoration: "flat", inactiveLabel: false) {
state "normal", action:"lowerSetpoint", backgroundColor:"#919191", icon:"st.thermostat.thermostat-down"
state "pressed", action:"lowerSetpoint", backgroundColor:"#ff9191", icon:"st.thermostat.thermostat-down"
}
main "temperature"
details([ "temperature", "mode", "hvacStatus","setpoint","upButton","downButton","scheduleText", "schedule", "hold",
"heatSliderControl", "heatingSetpoint","coolSliderControl", "coolingSetpoint", "lock", "refresh", "configure"])
}
}
def getMin() {
try
{
if (getTemperatureScale() == "C")
return 10
else
return 50
} catch (all)
{
return 10
}
}
def getMax() {
try {
if (getTemperatureScale() == "C")
return 30
else
return 86
} catch (all)
{
return 86
}
}
// parse events into attributes
def parse(String description) {
log.debug "Parse description $description"
def result = []
if (description?.startsWith("read attr -")) {
//TODO: Parse RAW strings for multiple attributes
def descMap = parseDescriptionAsMap(description)
log.debug "Desc Map: $descMap"
for ( atMap in descMap.attrs)
{
def map = [:]
if (descMap.cluster == "0201")
{
//log.trace "attribute: ${atMap.attrId} "
switch(atMap.attrId.toLowerCase())
{
case "0000":
map.name = "temperature"
map.value = getTemperature(atMap.value)
result += createEvent("name":"displayTemperature", "value": getDisplayTemperature(atMap.value))
break;
case "0005":
//log.debug "hex time: ${descMap.value}"
if (atMap.encoding == "23")
{
map.name = "holdExpiary"
map.value = "${convertToTime(atMap.value).getTime()}"
//log.trace "HOLD EXPIRY: ${atMap.value} is ${map.value}"
updateHoldLabel("HoldExp", "${map.value}")
}
break;
case "0011":
map.name = "coolingSetpoint"
map.value = getDisplayTemperature(atMap.value)
updateSetpoint(map.name,map.value)
break;
case "0012":
map.name = "heatingSetpoint"
map.value = getDisplayTemperature(atMap.value)
updateSetpoint(map.name,map.value)
break;
case "001c":
map.name = "thermostatMode"
map.value = getModeMap()[atMap.value]
updateSetpoint(map.name,map.value)
break;
case "001e": //running mode enum8
map.name = "runningMode"
map.value = getModeMap()[atMap.value]
updateSetpoint(map.name,map.value)
break;
case "0023": // setpoint hold enum8
map.name = "setpointHold"
map.value = getHoldMap()[atMap.value]
updateHoldLabel("Hold", map.value)
break;
case "0024": // hold duration int16u
map.name = "setpointHoldDuration"
map.value = Integer.parseInt("${atMap.value}", 16)
break;
case "0025": // thermostat programming operation bitmap8
map.name = "prorgammingOperation"
def val = getProgrammingMap()[Integer.parseInt("${atMap.value}", 16) & 0x01]
result += createEvent("name":"prorgammingOperationDisplay", "value": val)
map.value = atMap.value
break;
case "0029":
// relay state
map.name = "thermostatOperatingState"
map.value = getThermostatOperatingState(atMap.value)
break;
}
} else if (descMap.cluster == "0204")
{
if (atMap.attrId == "0001")
{
map.name = "lockLevel"
map.value = getLockMap()[atMap.value]
}
}
if (map) {
result += createEvent(map)
}
}
}
log.debug "Parse returned $result"
return result
}
def parseDescriptionAsMap(description) {
def map = (description - "read attr - ").split(",").inject([:]) { map, param ->
def nameAndValue = param.split(":")
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
}
def attrId = map.get('attrId')
def encoding = map.get('encoding')
def value = map.get('value')
def result = map.get('result')
def list = [];
if (getDataLengthByType(map.get('encoding')) < map.get('value').length()) {
def raw = map.get('raw')
def size = Long.parseLong(''+ map.get('size'), 16)
def index = 12;
def len
//log.trace "processing multi attributes"
while((index-12) < size) {
attrId = flipHexStringEndianness(raw[index..(index+3)])
index+= 4;
if (result == "success")
index+=2;
encoding = raw[index..(index+1)]
index+= 2;
len =getDataLengthByType(encoding)
value = flipHexStringEndianness(raw[index..(index+len-1)])
index+=len;
list += ['attrId': "$attrId", 'encoding':"$encoding", 'value': "$value"]
}
}
else
list += ['attrId': "$attrId", 'encoding': "$encoding", 'value': "$value"]
map.remove('value')
map.remove('encoding')
map.remove('attrId')
map += ['attrs' : list ]
}
def flipHexStringEndianness(s)
{
s = s.reverse()
def sb = new StringBuilder()
for (int i=0; i < s.length() -1; i+=2)
sb.append(s.charAt(i+1)).append(s.charAt(i))
sb
}
def getDataLengthByType(t)
{
// number of bytes in each static data type
def map = ["08":1, "09":2, "0a":3, "0b":4, "0c":5, "0d":6, "0e":7, "0f":8, "10":1, "18":1, "19":2, "1a":3, "1b":4,
"1c":5,"1d":6, "1e":7, "1f":8, "20":1, "21":2, "22":3, "23":4, "24":5, "25":6, "26":7, "27":8, "28":1, "29":2,
"2a":3, "2b":4, "2c":5, "2d":6, "2e":7, "2f":8, "30":1, "31":2, "38":2, "39":4, "40":8, "e0":4, "e1":4, "e2":4,
"e8":2, "e9":2, "ea":4, "f0":8, "f1":16]
// return number of hex chars
return map.get(t) * 2
}
def getProgrammingMap() { [
0:"Off",
1:"On"
]}
def getModeMap() { [
"00":"off",
"01":"auto",
"03":"cool",
"04":"heat"
]}
def getFanModeMap() { [
"04":"fanOn",
"05":"fanAuto"
]}
def getHoldMap()
{[
"00":"Off",
"01":"On"
]}
def updateSetpoint(attrib, val)
{
def cool = device.currentState("coolingSetpoint")?.value
def heat = device.currentState("heatingSetpoint")?.value
def runningMode = device.currentState("runningMode")?.value
def mode = device.currentState("thermostatMode")?.value
def value = '--';
if ("heat" == mode && heat != null)
value = heat;
else if ("cool" == mode && cool != null)
value = cool;
else if ("auto" == mode && runningMode == "cool" && cool != null)
value = cool;
else if ("auto" == mode && runningMode == "heat" && heat != null)
value = heat;
sendEvent("name":"displaySetpoint", "value": value)
}
def raiseSetpoint()
{
sendEvent("name":"upButtonState", "value": "pressed")
sendEvent("name":"upButtonState", "value": "normal")
adjustSetpoint(5)
}
def lowerSetpoint()
{
sendEvent("name":"downButtonState", "value": "pressed")
sendEvent("name":"downButtonState", "value": "normal")
adjustSetpoint(-5)
}
def adjustSetpoint(value)
{
def runningMode = device.currentState("runningMode")?.value
def mode = device.currentState("thermostatMode")?.value
//default to both heat and cool
def modeData = 0x02
if ("heat" == mode || "heat" == runningMode)
modeData = "00"
else if ("cool" == mode || "cool" == runningMode)
modeData = "01"
def amountData = String.format("%02X", value)[-2..-1]
"st cmd 0x${device.deviceNetworkId} 1 0x201 0 {" + modeData + " " + amountData + "}"
}
def getDisplayTemperature(value)
{
def t = Integer.parseInt("$value", 16);
if (getTemperatureScale() == "C") {
t = (((t + 4) / 10) as Integer) / 10;
} else {
t = ((10 *celsiusToFahrenheit(t/100)) as Integer)/ 10;
}
return t;
}
def updateHoldLabel(attr, value)
{
def currentHold = (device?.currentState("setpointHold")?.value)?: "..."
def holdExp = device?.currentState("holdExpiary")?.value
holdExp = holdExp?: "${(new Date()).getTime()}"
if ("Hold" == attr)
{
currentHold = value
}
if ("HoldExp" == attr)
{
holdExp = value
}
boolean past = ( (new Date(holdExp.toLong()).getTime()) < (new Date().getTime()))
if ("HoldExp" == attr)
{
if (!past)
currentHold = "On"
else
currentHold = "Off"
}
def holdString = (currentHold == "On")?
( (past)? "Is On" : "Ends ${compareWithNow(holdExp.toLong())}") :
((currentHold == "Off")? " is Off" : " ...")
sendEvent("name":"setpointHoldDisplay", "value": "Hold ${holdString}")
}
def getSetPointHoldDuration()
{
def holdTime = 0
if (settings.hold_time?.contains("Hours"))
{
holdTime = Integer.parseInt(settings.hold_time[0..1].trim())
}
else if (settings.hold_time?.contains("Day"))
{
holdTime = Integer.parseInt(settings.hold_time[0..1].trim()) * 24
}
def currentHoldDuration = device.currentState("setpointHoldDuration")?.value
if (Short.parseShort('0'+ (currentHoldDuration?: 0)) != (holdTime * 60))
{
[
"st wattr 0x${device.deviceNetworkId} 1 0x201 0x24 0x21 {" +
String.format("%04X", ((holdTime * 60) as Short)) // switch to zigbee endian
+ "}", "delay 100",
"st rattr 0x${device.deviceNetworkId} 1 0x201 0x24", "delay 200",
]
} else
{
[]
}
}
def Hold()
{
def currentHold = device.currentState("setpointHold")?.value
def next = (currentHold == "On") ? "00" : "01"
def nextHold = getHoldMap()[next]
sendEvent("name":"setpointHold", "value":nextHold)
// set the duration first if it's changed
[
"st wattr 0x${device.deviceNetworkId} 1 0x201 0x23 0x30 {$next}", "delay 100" ,
"raw 0x201 {04 21 11 00 00 05 00 }","delay 200", // hold expiry time
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
] + getSetPointHoldDuration()
}
def compareWithNow(d)
{
long mins = (new Date(d)).getTime() - (new Date()).getTime()
mins /= 1000 * 60;
log.trace "mins: ${mins}"
boolean past = (mins < 0)
def ret = (past)? "" : "in "
if (past)
mins *= -1;
float t = 0;
// minutes
if (mins < 60)
{
ret += (mins as Integer) + " min" + ((mins > 1)? 's' : '')
}else if (mins < 1440)
{
t = ( Math.round((14 + mins)/30) as Integer) / 2
ret += t + " hr" + ((t > 1)? 's' : '')
} else
{
t = (Math.round((359 + mins)/720) as Integer) / 2
ret += t + " day" + ((t > 1)? 's' : '')
}
ret += (past)? " ago": ""
log.trace "ret: ${ret}"
ret
}
def convertToTime(data)
{
def time = Integer.parseInt("$data", 16) as long;
time *= 1000;
time += 946684800000; // 481418694
time -= location.timeZone.getRawOffset() + location.timeZone.getDSTSavings();
def d = new Date(time);
//log.trace "converted $data to Time $d"
return d;
}
def Program()
{
def currentSched = device.currentState("prorgammingOperation")?.value
def next = Integer.parseInt(currentSched?: "00", 16);
if ( (next & 0x01) == 0x01)
next = next & 0xfe;
else
next = next | 0x01;
def nextSched = getProgrammingMap()[next & 0x01]
"st wattr 0x${device.deviceNetworkId} 1 0x201 0x25 0x18 {$next}"
}
def getThermostatOperatingState(value)
{
String[] m = [ "heating", "cooling", "fan", "Heat2", "Cool2", "Fan2", "Fan3"]
String desc = 'idle'
value = Integer.parseInt(''+value, 16)
// only check for 1-stage for A1730
for ( i in 0..2 ) {
if (value & 1 << i)
desc = m[i]
}
desc
}
def checkLastTimeSync(delay)
{
def lastSync = device.currentState("lastTimeSync")?.value
if (!lastSync)
lastSync = "${new Date(0)}"
if (settings.sync_clock ?: false && lastSync != new Date(0))
{
sendEvent("name":"lastTimeSync", "value":"${new Date(0)}")
}
long duration = (new Date()).getTime() - (new Date(lastSync)).getTime()
// log.debug "check Time: $lastSync duration: ${duration} settings.sync_clock: ${settings.sync_clock}"
if (duration > 86400000)
{
sendEvent("name":"lastTimeSync", "value":"${new Date()}")
return setThermostatTime()
}
return []
}
def readAttributesCommand(cluster, attribList)
{
def attrString = ''
for ( val in attribList ) {
attrString += ' ' + String.format("%02X %02X", val & 0xff , (val >> 8) & 0xff)
}
//log.trace "list: " + attrString
["raw "+ cluster + " {00 00 00 $attrString}","delay 100",
"send 0x${device.deviceNetworkId} 1 1", "delay 100",
]
}
def refresh()
{
log.debug "refresh called"
// log.trace "list: " + readAttributesCommand(0x201, [0x1C,0x1E,0x23])
readAttributesCommand(0x201, [0x00,0x11,0x12]) +
readAttributesCommand(0x201, [0x1C,0x1E,0x23]) +
readAttributesCommand(0x201, [0x24,0x25,0x29]) +
[
"st rattr 0x${device.deviceNetworkId} 1 0x204 0x01", "delay 200", // lock status
"raw 0x201 {04 21 11 00 00 05 00 }" , "delay 500", // hold expiary
"send 0x${device.deviceNetworkId} 1 1" , "delay 1500",
] + checkLastTimeSync(2000)
}
def poll() {
log.trace "poll called"
refresh()
}
def getTemperature(value) {
def celsius = Integer.parseInt("$value", 16) / 100
if(getTemperatureScale() == "C"){
return celsius as Integer
} else {
return celsiusToFahrenheit(celsius) as Integer
}
}
def setHeatingSetpoint(degrees) {
def temperatureScale = getTemperatureScale()
def degreesInteger = degrees as Integer
sendEvent("name":"heatingSetpoint", "value":degreesInteger)
def celsius = (getTemperatureScale() == "C") ? degreesInteger : (fahrenheitToCelsius(degreesInteger) as Double).round(2)
"st wattr 0x${device.deviceNetworkId} 1 0x201 0x12 0x29 {" + hex(celsius*100) + "}"
}
def setCoolingSetpoint(degrees) {
def degreesInteger = degrees as Integer
sendEvent("name":"coolingSetpoint", "value":degreesInteger)
def celsius = (getTemperatureScale() == "C") ? degreesInteger : (fahrenheitToCelsius(degreesInteger) as Double).round(2)
"st wattr 0x${device.deviceNetworkId} 1 0x201 0x11 0x29 {" + hex(celsius*100) + "}"
}
def modes() {
["off", "heat", "cool"]
}
def setThermostatFanMode() {
def currentFanMode = device.currentState("thermostatFanMode")?.value
//log.debug "switching fan from current mode: $currentFanMode"
def returnCommand
switch (currentFanMode) {
case "fanAuto":
returnCommand = fanOn()
break
case "fanOn":
returnCommand = fanAuto()
break
}
if(!currentFanMode) { returnCommand = fanAuto() }
returnCommand
}
def setThermostatMode() {
def currentMode = device.currentState("thermostatMode")?.value
def modeOrder = modes()
def index = modeOrder.indexOf(currentMode)
def next = index >= 0 && index < modeOrder.size() - 1 ? modeOrder[index + 1] : modeOrder[0]
setThermostatMode(next)
}
def setThermostatMode(String next) {
def val = (getModeMap().find { it.value == next }?.key)?: "00"
// log.trace "mode changing to $next sending value: $val"
sendEvent("name":"thermostatMode", "value":"$next")
["st wattr 0x${device.deviceNetworkId} 1 0x201 0x1C 0x30 {$val}"] +
refresh()
}
def setThermostatFanMode(String value) {
log.debug "setThermostatFanMode({$value})"
"$value"()
}
def off() {
setThermostatMode("off")
}
def cool() {
setThermostatMode("cool")}
def heat() {
setThermostatMode("heat")
}
def auto() {
setThermostatMode("auto")
}
def on() {
fanOn()
}
def fanOn() {
sendEvent("name":"thermostatFanMode", "value":"fanOn")
"st wattr 0x${device.deviceNetworkId} 1 0x202 0 0x30 {04}"
}
def fanAuto() {
sendEvent("name":"thermostatFanMode", "value":"fanAuto")
"st wattr 0x${device.deviceNetworkId} 1 0x202 0 0x30 {05}"
}
def updated()
{
def lastSync = device.currentState("lastTimeSync")?.value
if ((settings.sync_clock ?: false) == false)
{
log.debug "resetting last sync time. Used to be: $lastSync"
sendEvent("name":"lastTimeSync", "value":"${new Date(0)}")
}
}
def getLockMap()
{[
"00":"Unlocked",
"01":"Mode Only",
"02":"Setpoint",
"03":"Full",
"04":"Full",
"05":"Full",
]}
def lock()
{
def currentLock = device.currentState("lockLevel")?.value
def val = getLockMap().find { it.value == currentLock }?.key
//log.debug "current lock is: ${val}"
if (val == "00")
val = getLockMap().find { it.value == (settings.lock_level ?: "Full") }?.key
else
val = "00"
"st rattr 0x${device.deviceNetworkId} 1 0x204 0x01"
}
def setThermostatTime()
{
if ((settings.sync_clock ?: false))
{
log.debug "sync time is disabled, leaving"
return []
}
Date date = new Date();
String zone = location.timeZone.getRawOffset() + " DST " + location.timeZone.getDSTSavings();
long millis = date.getTime(); // Millis since Unix epoch
millis -= 946684800000; // adjust for ZigBee EPOCH
// adjust for time zone and DST offset
millis += location.timeZone.getRawOffset() + location.timeZone.getDSTSavings();
//convert to seconds
millis /= 1000;
// print to a string for hex capture
String s = String.format("%08X", millis);
// hex capture for message format
String data = " " + s.substring(6, 8) + " " + s.substring(4, 6) + " " + s.substring(2, 4)+ " " + s.substring(0, 2);
[
"raw 0x201 {04 21 11 00 02 0f 00 23 ${data} }",
"send 0x${device.deviceNetworkId} 1 ${endpointId}"
]
}
def configure() {
[
"zdo bind 0x${device.deviceNetworkId} 1 1 0x201 {${device.zigbeeId}} {}", "delay 500",
"zcl global send-me-a-report 0x201 0x0000 0x29 20 300 {19 00}", // report temperature changes over 0.2C
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
"zcl global send-me-a-report 0x201 0x001C 0x30 10 305 { }", // mode
"send 0x${device.deviceNetworkId} 1 ${endpointId}","delay 500",
"zcl global send-me-a-report 0x201 0x0025 0x18 10 310 { 00 }", // schedule on/off
"send 0x${device.deviceNetworkId} 1 ${endpointId}","delay 500",
"zcl global send-me-a-report 0x201 0x001E 0x30 10 315 { 00 }", // running mode
"send 0x${device.deviceNetworkId} 1 ${endpointId}","delay 500",
"zcl global send-me-a-report 0x201 0x0011 0x29 10 320 {32 00}", // cooling setpoint delta: 0.5C (0x3200 in little endian)
"send 0x${device.deviceNetworkId} 1 ${endpointId}","delay 500",
"zcl global send-me-a-report 0x201 0x0012 0x29 10 320 {32 00}", // cooling setpoint delta: 0.5C (0x3200 in little endian)
"send 0x${device.deviceNetworkId} 1 ${endpointId}","delay 500",
"zcl global send-me-a-report 0x201 0x0029 0x19 10 325 { 00 }", "delay 200", // relay status
"send 0x${device.deviceNetworkId} 1 ${endpointId}","delay 500",
"zcl global send-me-a-report 0x201 0x0023 0x30 10 330 { 00 }", // hold
"send 0x${device.deviceNetworkId} 1 ${endpointId}","delay 1500",
] + refresh()
}
private hex(value) {
new BigInteger(Math.round(value).toString()).toString(16)
}
private getEndpointId()
{
new BigInteger(device.endpointId, 16).toString()
}

View File

@@ -19,7 +19,6 @@ metadata {
capability "Sensor"
fingerprint deviceId: "0x1000", inClusters: "0x25,0x72,0x86,0x71,0x22,0x70"
fingerprint deviceId: "0x1006", inClusters: "0x25"
}
// simulator metadata

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
@@ -317,7 +317,7 @@ def setLevel(value) {
state.trigger = "setLevel"
state.lvl = "${level}"
if (dimRate) {
if (dimRate && (state?.rate != null)) {
cmds << "st cmd 0x${device.deviceNetworkId} 1 8 4 {${level} ${state.rate}}"
}
else {

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: "Logitech 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

@@ -15,19 +15,27 @@ metadata {
// TODO: define status and reply messages here
}
tiles {
tiles(scale: 2) {
multiAttributeTile(name:"rich-control"){
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
attributeState "default", label: "Hue Bridge", action: "", icon: "st.Lighting.light99-hue", backgroundColor: "#F3C200"
}
tileAttribute ("serialNumber", key: "SECONDARY_CONTROL") {
attributeState "default", label:'SN: ${currentValue}'
}
}
standardTile("icon", "icon", width: 1, height: 1, canChangeIcon: false, inactiveLabel: true, canChangeBackground: false) {
state "default", label: "Hue Bridge", action: "", icon: "st.Lighting.light99-hue", backgroundColor: "#FFFFFF"
}
valueTile("serialNumber", "device.serialNumber", decoration: "flat", height: 1, width: 2, inactiveLabel: false) {
state "default", label:'SN: ${currentValue}'
}
valueTile("networkAddress", "device.networkAddress", decoration: "flat", height: 1, width: 2, inactiveLabel: false) {
valueTile("networkAddress", "device.networkAddress", decoration: "flat", height: 2, width: 4, inactiveLabel: false) {
state "default", label:'${currentValue}', height: 1, width: 2, inactiveLabel: false
}
main (["icon"])
details(["networkAddress","serialNumber"])
details(["rich-control", "networkAddress"])
}
}
@@ -36,7 +44,6 @@ def parse(description) {
log.debug "Parsing '${description}'"
def results = []
def result = parent.parse(this, description)
if (result instanceof physicalgraph.device.HubAction){
log.trace "HUE BRIDGE HubAction received -- DOES THIS EVER HAPPEN?"
results << result
@@ -44,32 +51,30 @@ def parse(description) {
//do nothing
log.trace "HUE BRIDGE was updated"
} else {
log.trace "HUE BRIDGE, OTHER"
def map = description
if (description instanceof String) {
map = stringToMap(description)
}
if (map?.name && map?.value) {
log.trace "HUE BRIDGE, GENERATING EVENT: $map.name: $map.value"
results << createEvent(name: "${map?.name}", value: "${map?.value}")
}
else {
log.trace "HUE BRIDGE, OTHER"
results << createEvent(name: "${map.name}", value: "${map.value}")
} else {
log.trace "Parsing description"
def msg = parseLanMessage(description)
if (msg.body) {
def contentType = msg.headers["Content-Type"]
if (contentType?.contains("json")) {
def bulbs = new groovy.json.JsonSlurper().parseText(msg.body)
if (bulbs.state) {
log.warn "NOT PROCESSED: $msg.body"
}
else {
log.debug "HUE BRIDGE, GENERATING BULB LIST EVENT: $bulbs"
sendEvent(name: "bulbList", value: device.hub.id, isStateChange: true, data: bulbs, displayed: false)
log.info "Bridge response: $msg.body"
} else {
// Sending Bulbs List to parent"
if (parent.state.inBulbDiscovery)
log.info parent.bulbListHandler(device.hub.id, msg.body)
}
}
else if (contentType?.contains("xml")) {
log.debug "HUE BRIDGE, SWALLOWING BRIDGE DESCRIPTION RESPONSE -- BRIDGE ALREADY PRESENT"
log.debug "HUE BRIDGE ALREADY PRESENT"
}
}
}

View File

@@ -1,3 +1,4 @@
/**
* Hue Bulb
*
@@ -15,8 +16,8 @@ metadata {
capability "Sensor"
command "setAdjustedColor"
command "reset"
command "refresh"
command "reset"
command "refresh"
}
simulator {
@@ -49,7 +50,6 @@ metadata {
main(["switch"])
details(["switch", "levelSliderControl", "rgbSelector", "refresh", "reset"])
}
// parse events into attributes
@@ -68,13 +68,13 @@ def parse(description) {
}
// handle commands
def on(transition = "4") {
log.trace parent.on(this,transition)
def on() {
log.trace parent.on(this)
sendEvent(name: "switch", value: "on")
}
def off(transition = "4") {
log.trace parent.off(this,transition)
def off() {
log.trace parent.off(this)
sendEvent(name: "switch", value: "off")
}
@@ -107,9 +107,9 @@ def setHue(percent) {
sendEvent(name: "hue", value: percent)
}
def setColor(value,alert = "none",transition = 4) {
def setColor(value) {
log.debug "setColor: ${value}, $this"
parent.setColor(this, value, alert, transition)
parent.setColor(this, value)
if (value.hue) { sendEvent(name: "hue", value: value.hue)}
if (value.saturation) { sendEvent(name: "saturation", value: value.saturation)}
if (value.hex) { sendEvent(name: "color", value: value.hex)}

View File

@@ -19,24 +19,41 @@ metadata {
simulator {
// TODO: define status and reply messages here
}
tiles(scale: 2) {
multiAttributeTile(name:"rich-control", type: "lighting", canChangeIcon: true){
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
attributeState "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
attributeState "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
}
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
attributeState "level", action:"switch level.setLevel", range:"(0..100)"
}
tileAttribute ("device.level", key: "SECONDARY_CONTROL") {
attributeState "level", label: 'Level ${currentValue}%'
}
}
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
state "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
state "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
state "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
state "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
}
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false, range:"(0..100)") {
state "level", action:"switch level.setLevel"
}
standardTile("refresh", "device.switch", inactiveLabel: false, height: 2, width: 2, decoration: "flat") {
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
}
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
state "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821"
state "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff"
}
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: 2, inactiveLabel: false, range:"(0..100)") {
state "level", action:"switch level.setLevel"
}
valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") {
state "level", label: 'Level ${currentValue}%'
}
main(["switch"])
details(["switch", "levelSliderControl", "refresh"])
main(["switch"])
details(["rich-control", "refresh"])
}
}
// parse events into attributes

View File

@@ -0,0 +1,225 @@
/**
* LIFX Color Bulb
*
* Copyright 2015 LIFX
*
*/
metadata {
definition (name: "LIFX Color Bulb", namespace: "smartthings", author: "LIFX") {
capability "Actuator"
capability "Color Control"
capability "Color Temperature"
capability "Switch"
capability "Switch Level" // brightness
capability "Polling"
capability "Refresh"
capability "Sensor"
}
simulator {
}
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", 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("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", height: 2, width: 2) {
state "colorTemp", label: '${currentValue}K'
}
main "switch"
details(["switch", "colorTempSliderControl", "colorTemp", "refresh"])
}
}
// parse events into attributes
def parse(String description) {
if (description == 'updated') {
return // don't poll when config settings is being updated as it may time out
}
poll()
}
// handle commands
def setHue(percentage) {
log.debug "setHue ${percentage}"
parent.logErrors(logObject: log) {
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")
} else {
log.error("Bad setHue result: [${resp.status}] ${resp.data}")
}
}
}
def setSaturation(percentage) {
log.debug "setSaturation ${percentage}"
parent.logErrors(logObject: log) {
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")
} else {
log.error("Bad setSaturation result: [${resp.status}] ${resp.data}")
}
}
}
def setColor(Map color) {
log.debug "setColor ${color}"
def attrs = []
def events = []
color.each { key, value ->
switch (key) {
case "hue":
attrs << "hue:${value * 3.6}"
events << createEvent(name: "hue", value: value)
break
case "saturation":
attrs << "saturation:${value / 100}"
events << createEvent(name: "saturation", value: value)
break
case "colorTemperature":
attrs << "kelvin:${value}"
events << createEvent(name: "colorTemperature", value: value)
break
}
}
parent.logErrors(logObject:log) {
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")
events.each { sendEvent(it) }
} else {
log.error("Bad setColor result: [${resp.status}] ${resp.data}")
}
}
}
def setLevel(percentage) {
log.debug "setLevel ${percentage}"
if (percentage < 1 && percentage > 0) {
percentage = 1 // clamp to 1%
}
if (percentage == 0) {
sendEvent(name: "level", value: 0) // Otherwise the level value tile does not update
return off() // if the brightness is set to 0, just turn it off
}
parent.logErrors(logObject:log) {
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}")
}
}
}
def setColorTemperature(kelvin) {
log.debug "Executing 'setColorTemperature' to ${kelvin}"
parent.logErrors() {
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")
sendEvent(name: "saturation", value: 0)
} else {
log.error("Bad setLevel result: [${resp.status}] ${resp.data}")
}
}
}
def on() {
log.debug "Device setOn"
parent.logErrors() {
if (parent.apiPUT("/lights/${selector()}/state", [power: "on"]) != null) {
sendEvent(name: "switch", value: "on")
}
}
}
def off() {
log.debug "Device setOff"
parent.logErrors() {
if (parent.apiPUT("/lights/${selector()}/state", [power: "off"]) != null) {
sendEvent(name: "switch", value: "off")
}
}
}
def poll() {
log.debug "Executing 'poll' for ${device} ${this} ${device.deviceNetworkId}"
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[0]
log.debug("Data: ${data}")
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: "saturation", value: data.color.saturation * 100)
sendEvent(name: "colorTemperature", value: data.color.kelvin)
sendEvent(name: "model", value: "${data.product.company} ${data.product.name}")
return []
}
def refresh() {
log.debug "Executing 'refresh'"
poll()
}
def selector() {
if (device.deviceNetworkId.contains(":")) {
return device.deviceNetworkId
} else {
return "id:${device.deviceNetworkId}"
}
}

View File

@@ -0,0 +1,155 @@
/**
* LIFX White Bulb
*
* Copyright 2015 LIFX
*
*/
metadata {
definition (name: "LIFX White Bulb", namespace: "smartthings", author: "LIFX") {
capability "Actuator"
capability "Color Temperature"
capability "Switch"
capability "Switch Level" // brightness
capability "Polling"
capability "Refresh"
capability "Sensor"
}
simulator {
}
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", 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("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", height: 2, width: 2) {
state "colorTemp", label: '${currentValue}K'
}
main "switch"
details(["switch", "colorTempSliderControl", "colorTemp", "refresh"])
}
}
// parse events into attributes
def parse(String description) {
if (description == 'updated') {
return // don't poll when config settings is being updated as it may time out
}
poll()
}
// handle commands
def setLevel(percentage) {
log.debug "setLevel ${percentage}"
if (percentage < 1 && percentage > 0) {
percentage = 1 // clamp to 1%
}
if (percentage == 0) {
sendEvent(name: "level", value: 0) // Otherwise the level value tile does not update
return off() // if the brightness is set to 0, just turn it off
}
parent.logErrors(logObject:log) {
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}")
}
}
}
def setColorTemperature(kelvin) {
log.debug "Executing 'setColorTemperature' to ${kelvin}"
parent.logErrors() {
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")
sendEvent(name: "saturation", value: 0)
sendEvent(name: "switch", value: "on")
} else {
log.error("Bad setColorTemperature result: [${resp.status}] ${resp.data}")
}
}
}
def on() {
log.debug "Device setOn"
parent.logErrors() {
if (parent.apiPUT("/lights/${selector()}/state", [power: "on"]) != null) {
sendEvent(name: "switch", value: "on")
}
}
}
def off() {
log.debug "Device setOff"
parent.logErrors() {
if (parent.apiPUT("/lights/${selector()}/state", [power: "off"]) != null) {
sendEvent(name: "switch", value: "off")
}
}
}
def poll() {
log.debug "Executing 'poll' for ${device} ${this} ${device.deviceNetworkId}"
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[0]
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 []
}
def refresh() {
log.debug "Executing 'refresh'"
poll()
}
def selector() {
if (device.deviceNetworkId.contains(":")) {
return device.deviceNetworkId
} else {
return "id:${device.deviceNetworkId}"
}
}

View File

@@ -25,7 +25,7 @@ metadata {
tiles {
standardTile("presence", "device.presence", width: 2, height: 2, canChangeBackground: true) {
state("present", labelIcon:"st.presence.tile.mobile-present", backgroundColor:"#53a7c0")
state("not present", labelIcon:"st.presence.tile.mobile-not-present", backgroundColor:"#ffffff")
state("not present", labelIcon:"st.presence.tile.mobile-not-present", backgroundColor:"#ebeef2")
}
main "presence"
details "presence"

View File

@@ -24,8 +24,8 @@ metadata {
command "enrollResponse"
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3041"
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3043"
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3045"
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3043", deviceJoinName: "NYCE Ceiling Motion Sensor"
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3045", deviceJoinName: "NYCE Curtain Motion Sensor"
}
tiles {

View File

@@ -24,10 +24,10 @@ metadata {
command "enrollResponse"
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3011"
fingerprint inClusters: "0000,0001,0003,0500,0020", manufacturer: "NYCE", model: "3011"
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3014"
fingerprint inClusters: "0000,0001,0003,0500,0020", manufacturer: "NYCE", model: "3014"
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3011", deviceJoinName: "NYCE Door/Window Sensor"
fingerprint inClusters: "0000,0001,0003,0500,0020", manufacturer: "NYCE", model: "3011", deviceJoinName: "NYCE Door/Window Sensor"
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3014", deviceJoinName: "NYCE Tilt Sensor"
fingerprint inClusters: "0000,0001,0003,0500,0020", manufacturer: "NYCE", model: "3014", deviceJoinName: "NYCE Tilt Sensor"
}
simulator {

View File

@@ -22,10 +22,8 @@ metadata {
command "setAdjustedColor"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Gardenspot RGB"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY Gardenspot RGB"
}
// simulator metadata

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

@@ -28,6 +28,9 @@ metadata {
// indicates that device keeps track of heartbeat (in state.heartbeat)
attribute "heartbeat", "string"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B04,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3200", deviceJoinName: "Outlet"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B04,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3200-Sgb", deviceJoinName: "Outlet"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B04,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "4257050-RZHAC", deviceJoinName: "Outlet"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B04,0B05", outClusters: "0019"
}
@@ -45,8 +48,8 @@ metadata {
preferences {
section {
image(name: 'educationalcontent', multiple: true, images: [
"http://cdn.device-gse.smartthings.com/Outlet/US/OutletUS1.png",
"http://cdn.device-gse.smartthings.com/Outlet/US/OutletUS2.png"
"http://cdn.device-gse.smartthings.com/Outlet/US/OutletUS1.jpg",
"http://cdn.device-gse.smartthings.com/Outlet/US/OutletUS2.jpg"
])
}
}

View File

@@ -24,9 +24,9 @@ metadata {
command "enrollResponse"
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3315-S"
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"
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3315-Seu", deviceJoinName: "Water Leak Sensor"
}
@@ -280,16 +280,14 @@ def configure() {
def configCmds = [
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
"zcl global send-me-a-report 1 0x20 0x20 300 0600 {01}", "delay 200",
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 500",
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
"zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}", "delay 200",
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
"zdo bind 0x${device.deviceNetworkId} 1 1 0x402 {${device.zigbeeId}} {}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} 1 1 0x001 {${device.zigbeeId}} {}", "delay 500"
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 500",
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
"send 0x${device.deviceNetworkId} 1 1", "delay 500"
]
return configCmds + refresh() // send refresh cmds as part of config
}
@@ -299,7 +297,7 @@ def enrollResponse() {
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
[
//Resending the CIE in case the enroll request is sent before CIE is written
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
//Enroll Response
"raw 0x500 {01 23 00 00 00}",

View File

@@ -25,7 +25,7 @@ metadata {
command "enrollResponse"
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3305-S"
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3325-S"
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3325-S", deviceJoinName: "Motion Sensor"
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"
@@ -39,9 +39,9 @@ metadata {
preferences {
section {
image(name: 'educationalcontent', multiple: true, images: [
"http://cdn.device-gse.smartthings.com/Motion/Motion1.png",
"http://cdn.device-gse.smartthings.com/Motion/Motion2.png",
"http://cdn.device-gse.smartthings.com/Motion/Motion3.png"
"http://cdn.device-gse.smartthings.com/Motion/Motion1.jpg",
"http://cdn.device-gse.smartthings.com/Motion/Motion2.jpg",
"http://cdn.device-gse.smartthings.com/Motion/Motion3.jpg"
])
}
section {
@@ -301,20 +301,18 @@ def configure() {
log.debug "Configuring Reporting, IAS CIE, and Bindings."
def configCmds = [
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x20 {${device.zigbeeId}} {}", "delay 200",
"zcl global send-me-a-report 1 0x20 0x20 300 3600 {01}",
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 200",
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x402 {${device.zigbeeId}} {}", "delay 200",
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 200",
"zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 1 {${device.zigbeeId}} {}", "delay 200"
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500"
]
return configCmds + refresh() // send refresh cmds as part of config
return configCmds + refresh() // send refresh cmds as part of config
}
def enrollResponse() {
@@ -322,12 +320,12 @@ def enrollResponse() {
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
[
//Resending the CIE in case the enroll request is sent before CIE is written
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
//Enroll Response
"raw 0x500 {01 23 00 00 00}",
"send 0x${device.deviceNetworkId} 1 1", "delay 200"
]
]
}
private getEndpointId() {

View File

@@ -292,18 +292,16 @@ def configure() {
log.debug "Configuring Reporting, IAS CIE, and Bindings."
def configCmds = [
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x20 {${device.zigbeeId}} {}", "delay 200",
"zcl global send-me-a-report 1 0x20 0x20 300 3600 {01}",
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 200",
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x402 {${device.zigbeeId}} {}", "delay 200",
"zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 1 {${device.zigbeeId}} {}", "delay 200"
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 200",
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500"
]
return configCmds + refresh() // send refresh cmds as part of config
}
@@ -313,7 +311,7 @@ def enrollResponse() {
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
[
//Resending the CIE in case the enroll request is sent before CIE is written
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
//Enroll Response
"raw 0x500 {01 23 00 00 00}",

View File

@@ -29,7 +29,7 @@
command "enrollResponse"
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"
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3321-S", deviceJoinName: "Multipurpose Sensor"
attribute "status", "string"
}
@@ -54,10 +54,10 @@
preferences {
section {
image(name: 'educationalcontent', multiple: true, images: [
"http://cdn.device-gse.smartthings.com/Multi/Multi1.png",
"http://cdn.device-gse.smartthings.com/Multi/Multi2.png",
"http://cdn.device-gse.smartthings.com/Multi/Multi3.png",
"http://cdn.device-gse.smartthings.com/Multi/Multi4.png"
"http://cdn.device-gse.smartthings.com/Multi/Multi1.jpg",
"http://cdn.device-gse.smartthings.com/Multi/Multi2.jpg",
"http://cdn.device-gse.smartthings.com/Multi/Multi3.jpg",
"http://cdn.device-gse.smartthings.com/Multi/Multi4.jpg"
])
}
section {
@@ -346,8 +346,8 @@ def getTemperature(value) {
log.debug "Acceleration"
def name = "acceleration"
def value = numValue.endsWith("1") ? "active" : "inactive"
//def linkText = getLinkText(device)
def descriptionText = "was $value"
def linkText = getLinkText(device)
def descriptionText = "$linkText was $value"
def isStateChange = isStateChange(device, name, value)
[
name: name,
@@ -395,35 +395,34 @@ def getTemperature(value) {
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
log.debug "Configuring Reporting"
def configCmds = [
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 1 {${device.zigbeeId}} {}", "delay 200",
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
def configCmds = [
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x20 {${device.zigbeeId}} {}", "delay 200",
"zcl global send-me-a-report 1 0x20 0x20 600 3600 {01}",
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 200",
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x402 {${device.zigbeeId}} {}", "delay 200",
"zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}",
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 200",
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0xFC02 {${device.zigbeeId}} {}", "delay 200",
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0xFC02 {${device.zigbeeId}} {}", "delay 200",
"zcl mfg-code 0x104E",
"zcl global send-me-a-report 0xFC02 0x0010 0x18 300 3600 {01}",
"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 global send-me-a-report 0xFC02 0x0012 0x29 300 3600 {01}",
"zcl mfg-code 0x104E",
"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 global send-me-a-report 0xFC02 0x0013 0x29 300 3600 {01}",
"zcl mfg-code 0x104E",
"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 global send-me-a-report 0xFC02 0x0014 0x29 300 3600 {01}",
"zcl mfg-code 0x104E",
"zcl global send-me-a-report 0xFC02 0x0014 0x29 1 3600 {01}",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500"
]
@@ -440,7 +439,7 @@ def enrollResponse() {
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
[
//Resending the CIE in case the enroll request is sent before CIE is written
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
//Enroll Response
"raw 0x500 {01 23 00 00 00}",

View File

@@ -297,29 +297,27 @@ def getTemperature(value) {
return refreshCmds + enrollResponse()
}
def configure() {
def configure() {
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
log.debug "Configuring Reporting, IAS CIE, and Bindings."
def configCmds = [
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
log.debug "Configuring Reporting, IAS CIE, and Bindings."
def configCmds = [
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x20 {${device.zigbeeId}} {}", "delay 200",
"zcl global send-me-a-report 1 0x20 0x20 600 3600 {01}",
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 200",
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} 1 1 0x402 {${device.zigbeeId}} {}", "delay 500",
"zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}",
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 500",
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} 1 1 0xFC02 {${device.zigbeeId}} {}", "delay 500",
"zcl global send-me-a-report 0xFC02 2 0x18 300 3600 {01}",
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
"zdo bind 0x${device.deviceNetworkId} 1 1 1 {${device.zigbeeId}} {}", "delay 500"
]
return configCmds + refresh() // send refresh cmds as part of config
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0xFC02 {${device.zigbeeId}} {}", "delay 500",
"zcl global send-me-a-report 0xFC02 2 0x18 30 3600 {01}",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500"
]
return configCmds + refresh() // send refresh cmds as part of config
}
def enrollResponse() {
@@ -327,7 +325,7 @@ def enrollResponse() {
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
[
//Resending the CIE in case the enroll request is sent before CIE is written
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
//Enroll Response
"raw 0x500 {01 23 00 00 00}",

View File

@@ -275,22 +275,16 @@ def configure() {
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
log.debug "Configuring Reporting, IAS CIE, and Bindings."
def configCmds = [
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
"zcl global send-me-a-report 1 0x20 0x20 600 3600 {01}",
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
"zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}",
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
//"raw 0x500 {01 23 00 00 00}", "delay 200",
//"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
"zdo bind 0x${device.deviceNetworkId} 1 1 0x402 {${device.zigbeeId}} {}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} 1 1 1 {${device.zigbeeId}} {}", "delay 500"
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 500",
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 500",
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
"send 0x${device.deviceNetworkId} 1 1", "delay 500"
]
return configCmds + refresh() // send refresh cmds as part of config
}
@@ -300,7 +294,7 @@ def enrollResponse() {
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
[
//Resending the CIE in case the enroll request is sent before CIE is written
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
//Enroll Response
"raw 0x500 {01 23 00 00 00}",

View File

@@ -253,22 +253,19 @@ def configure() {
log.debug "Configuring Reporting and Bindings."
def configCmds = [
"zdo bind 0x${device.deviceNetworkId} 1 1 1 {${device.zigbeeId}} {}", "delay 500",
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
"zcl global send-me-a-report 1 0x20 0x20 600 3600 {0100}", "delay 500",
"send 0x${device.deviceNetworkId} 1 1", "delay 1000",
"zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}", "delay 200",
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
"zcl global send-me-a-report 0xFC45 0 0x29 300 3600 {6400}", "delay 200",
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
"zdo bind 0x${device.deviceNetworkId} 1 1 0xFC45 {${device.zigbeeId}} {}", "delay 1000",
"zdo bind 0x${device.deviceNetworkId} 1 1 0x402 {${device.zigbeeId}} {}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} 1 1 1 {${device.zigbeeId}} {}"
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
"zdo bind 0x${device.deviceNetworkId} 1 1 0xFC45 {${device.zigbeeId}} {}", "delay 500",
"zcl global send-me-a-report 0xFC45 0 0x29 30 3600 {6400}",
"send 0x${device.deviceNetworkId} 1 1", "delay 500"
]
return configCmds + refresh() // send refresh cmds as part of config
return configCmds + refresh() // send refresh cmds as part of config
}
private hex(value) {

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

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

@@ -0,0 +1,101 @@
/**
* 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.
*
*/
metadata {
definition (name: "ZigBee Dimmer Power", namespace: "smartthings", author: "SmartThings") {
capability "Actuator"
capability "Configuration"
capability "Refresh"
capability "Power Meter"
capability "Sensor"
capability "Switch"
capability "Switch Level"
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,0B05,0702", outClusters: "000A,0019", manufacturer: "Jasco Products", model: "45852", deviceJoinName: "GE Zigbee Plug-In Dimmer"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0B05,0702", outClusters: "000A,0019", manufacturer: "Jasco Products", model: "45857", deviceJoinName: "GE Zigbee In-Wall Dimmer"
}
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.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
attributeState "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
}
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
attributeState "level", action:"switch level.setLevel"
}
tileAttribute ("power", key: "SECONDARY_CONTROL") {
attributeState "power", label:'${currentValue} W'
}
}
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.debug "description is $description"
def resultMap = zigbee.getKnownDescription(description)
if (resultMap) {
log.info resultMap
if (resultMap.type == "update") {
log.info "$device updates: ${resultMap.value}"
}
else if (resultMap.type == "power") {
def powerValue
if (device.getDataValue("manufacturer") != "OSRAM") { //OSRAM devices do not reliably update power
powerValue = (resultMap.value as Integer)/10 //TODO: The divisor value needs to be set as part of configuration
sendEvent(name: "power", value: powerValue)
}
}
else {
sendEvent(name: resultMap.type, value: resultMap.value)
}
}
else {
log.warn "DID NOT PARSE MESSAGE for description : $description"
log.debug zigbee.parseDescriptionAsMap(description)
}
}
def off() {
zigbee.off()
}
def on() {
zigbee.on()
}
def setLevel(value) {
zigbee.setLevel(value)
}
def refresh() {
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.simpleMeteringPowerRefresh() + zigbee.electricMeasurementPowerRefresh() + zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.simpleMeteringPowerConfig() + zigbee.electricMeasurementPowerConfig()
}
def configure() {
log.debug "Configuring Reporting and Bindings."
zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.simpleMeteringPowerConfig() + zigbee.electricMeasurementPowerConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.simpleMeteringPowerRefresh() + zigbee.electricMeasurementPowerRefresh()
}

View File

@@ -11,133 +11,80 @@
* for the specific language governing permissions and limitations under the License.
*
*/
metadata {
definition (name: "ZigBee Dimmer", namespace: "smartthings", author: "SmartThings") {
capability "Switch Level"
capability "Actuator"
capability "Switch"
capability "Configuration"
capability "Sensor"
capability "Refresh"
definition (name: "ZigBee Dimmer", namespace: "smartthings", author: "SmartThings") {
capability "Actuator"
capability "Configuration"
capability "Refresh"
capability "Sensor"
capability "Switch"
capability "Switch Level"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0B05", outClusters: "0019"
}
// simulator metadata
simulator {
// status messages
status "on": "on/off: 1"
status "off": "on/off: 0"
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"
}
// reply messages
reply "zcl on-off on": "on/off: 1"
reply "zcl on-off off": "on/off: 0"
}
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.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
attributeState "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.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"
}
valueTile("level", "device.level", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "level", label:'${currentValue} %', unit:"%", backgroundColor:"#ffffff"
}
main "switch"
details(["switch", "refresh", "level", "levelSliderControl"])
}
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.info description
if (description?.startsWith("catchall:")) {
def msg = zigbee.parse(description)
log.trace msg
log.trace "data: $msg.data"
}
else {
def name = description?.startsWith("on/off: ") ? "switch" : null
def value = name == "switch" ? (description?.endsWith(" 1") ? "on" : "off") : null
def result = createEvent(name: name, value: value)
log.debug "Parse returned ${result?.descriptionText}"
return result
}
}
log.debug "description is $description"
// Commands to device
def on() {
log.debug "on()"
sendEvent(name: "switch", value: "on")
"st cmd 0x${device.deviceNetworkId} ${endpointId} 6 1 {}"
def resultMap = zigbee.getKnownDescription(description)
if (resultMap) {
log.info resultMap
if (resultMap.type == "update") {
log.info "$device updates: ${resultMap.value}"
}
else {
sendEvent(name: resultMap.type, value: resultMap.value)
}
}
else {
log.warn "DID NOT PARSE MESSAGE for description : $description"
log.debug zigbee.parseDescriptionAsMap(description)
}
}
def off() {
log.debug "off()"
sendEvent(name: "switch", value: "off")
"st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}"
zigbee.off()
}
def on() {
zigbee.on()
}
def setLevel(value) {
log.trace "setLevel($value)"
def cmds = []
if (value == 0) {
sendEvent(name: "switch", value: "off")
cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}"
}
else if (device.latestValue("switch") == "off") {
sendEvent(name: "switch", value: "on")
cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 1 {}"
}
sendEvent(name: "level", value: value)
def level = hexString(Math.round(value * 255/100))
cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 8 4 {${level} 0000}"
//log.debug cmds
cmds
zigbee.setLevel(value)
}
def refresh() {
[
"st wattr 0x${device.deviceNetworkId} 1 6 0", "delay 200",
"st wattr 0x${device.deviceNetworkId} 1 8 0"
]
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.onOffConfig() + zigbee.levelConfig()
}
def configure() {
/*log.debug "binding to switch and level control cluster"
[
"zdo bind 0x${device.deviceNetworkId} 1 1 6 {${device.zigbeeId}} {}", "delay 200",
"zdo bind 0x${device.deviceNetworkId} 1 1 8 {${device.zigbeeId}} {}"
]
*/
//set transition time to 2 seconds. Not currently working.
"st wattr 0x${device.deviceNetworkId} 1 8 0x10 0x21 {1400}"
}
private hex(value, width=2) {
def s = new BigInteger(Math.round(value).toString()).toString(16)
while (s.size() < width) {
s = "0" + s
}
s
}
private getEndpointId() {
new BigInteger(device.endpointId, 16).toString()
log.debug "Configuring Reporting and Bindings."
zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh()
}

View File

@@ -0,0 +1,184 @@
/**
* ZigBee Lock
*
* 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.
*
*/
metadata {
definition (name: "ZigBee Lock", namespace: "smartthings", author: "SmartThings")
{
capability "Actuator"
capability "Lock"
capability "Refresh"
capability "Sensor"
capability "Battery"
capability "Configuration"
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0004,0005,0009,0020,0101,0402,0B05,FDBD", outClusters: "000A,0019",
manufacturer: "Kwikset", model: "SMARTCODE_DEADBOLT_5", deviceJoinName: "Kwikset 5-Button Deadbolt"
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0004,0005,0009,0020,0101,0402,0B05,FDBD", outClusters: "000A,0019",
manufacturer: "Kwikset", model: "SMARTCODE_LEVER_5", deviceJoinName: "Kwikset 5-Button Lever"
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0004,0005,0009,0020,0101,0402,0B05,FDBD", outClusters: "000A,0019",
manufacturer: "Kwikset", model: "SMARTCODE_DEADBOLT_10", deviceJoinName: "Kwikset 10-Button Deadbolt"
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 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) {
multiAttributeTile(name:"toggle", type:"generic", width:6, height:4){
tileAttribute ("device.lock", key:"PRIMARY_CONTROL") {
attributeState "locked", label:'locked', action:"lock.unlock", icon:"st.locks.lock.locked", backgroundColor:"#79b821", nextState:"unlocking"
attributeState "unlocked", label:'unlocked', action:"lock.lock", icon:"st.locks.lock.unlocked", backgroundColor:"#ffffff", nextState:"locking"
attributeState "unknown", label:"unknown", action:"lock.lock", icon:"st.locks.lock.unknown", backgroundColor:"#ffffff", nextState:"locking"
attributeState "locking", label:'locking', icon:"st.locks.lock.locked", backgroundColor:"#79b821"
attributeState "unlocking", label:'unlocking', icon:"st.locks.lock.unlocked", backgroundColor:"#ffffff"
}
}
standardTile("lock", "device.lock", inactiveLabel:false, decoration:"flat", width:2, height:2) {
state "default", label:'lock', action:"lock.lock", icon:"st.locks.lock.locked", nextState:"locking"
}
standardTile("unlock", "device.lock", inactiveLabel:false, decoration:"flat", width:2, height:2) {
state "default", label:'unlock', action:"lock.unlock", icon:"st.locks.lock.unlocked", nextState:"unlocking"
}
valueTile("battery", "device.battery", inactiveLabel:false, decoration:"flat", width:2, height:2) {
state "battery", label:'${currentValue}% battery', unit:""
}
standardTile("refresh", "device.lock", inactiveLabel:false, decoration:"flat", width:2, height:2) {
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
}
main "toggle"
details(["toggle", "lock", "unlock", "battery", "refresh"])
}
}
// Globals
private getCLUSTER_POWER() { 0x0001 }
private getCLUSTER_DOORLOCK() { 0x0101 }
private getDOORLOCK_CMD_LOCK_DOOR() { 0x00 }
private getDOORLOCK_CMD_UNLOCK_DOOR() { 0x01 }
private getDOORLOCK_ATTR_LOCKSTATE() { 0x0000 }
private getPOWER_ATTR_BATTERY_PERCENTAGE_REMAINING() { 0x0021 }
private getTYPE_U8() { 0x20 }
private getTYPE_ENUM8() { 0x30 }
// Public methods
def installed() {
log.trace "installed()"
}
def uninstalled() {
log.trace "uninstalled()"
}
def configure() {
/*
def cmds =
zigbee.configSetup("${CLUSTER_DOORLOCK}", "${DOORLOCK_ATTR_LOCKSTATE}",
"${TYPE_ENUM8}", 0, 3600, "{01}") +
zigbee.configSetup("${CLUSTER_POWER}", "${POWER_ATTR_BATTERY_PERCENTAGE_REMAINING}",
"${TYPE_U8}", 600, 21600, "{01}")
*/
def zigbeeId = device.zigbeeId
def cmds =
[
"zdo bind 0x${device.deviceNetworkId} 0x${device.endpointId} 1 ${CLUSTER_DOORLOCK} {$zigbeeId} {}", "delay 200",
"zcl global send-me-a-report ${CLUSTER_DOORLOCK} ${DOORLOCK_ATTR_LOCKSTATE} ${TYPE_ENUM8} 0 3600 {01}", "delay 200",
"send 0x${device.deviceNetworkId} 1 0x${device.endpointId}", "delay 200",
"zdo bind 0x${device.deviceNetworkId} 0x${device.endpointId} 1 ${CLUSTER_POWER} {$zigbeeId} {}", "delay 200",
"zcl global send-me-a-report ${CLUSTER_POWER} ${POWER_ATTR_BATTERY_PERCENTAGE_REMAINING} ${TYPE_U8} 600 21600 {01}", "delay 200",
"send 0x${device.deviceNetworkId} 1 0x${device.endpointId}", "delay 200",
]
log.info "configure() --- cmds: $cmds"
return cmds + refresh() // send refresh cmds as part of config
}
def refresh() {
def cmds =
zigbee.refreshData("${CLUSTER_DOORLOCK}", "${DOORLOCK_ATTR_LOCKSTATE}") +
zigbee.refreshData("${CLUSTER_POWER}", "${POWER_ATTR_BATTERY_PERCENTAGE_REMAINING}")
log.info "refresh() --- cmds: $cmds"
return cmds
}
def parse(String description) {
log.trace "parse() --- description: $description"
Map map = [:]
if (description?.startsWith('read attr -')) {
map = parseReportAttributeMessage(description)
}
log.debug "parse() --- Parse returned $map"
def result = map ? createEvent(map) : null
return result
}
// Lock capability commands
def lock() {
def cmds = zigbee.zigbeeCommand("${CLUSTER_DOORLOCK}", "${DOORLOCK_CMD_LOCK_DOOR}", "{}")
log.info "lock() -- cmds: $cmds"
//return cmds
"st cmd 0x${device.deviceNetworkId} 0x${device.endpointId} ${CLUSTER_DOORLOCK} ${DOORLOCK_CMD_LOCK_DOOR} {}"
}
def unlock() {
def cmds = zigbee.zigbeeCommand("${CLUSTER_DOORLOCK}", "${DOORLOCK_CMD_UNLOCK_DOOR}", "{}")
log.info "unlock() -- cmds: $cmds"
//return cmds
"st cmd 0x${device.deviceNetworkId} 0x${device.endpointId} ${CLUSTER_DOORLOCK} ${DOORLOCK_CMD_UNLOCK_DOOR} {}"
}
// Private methods
private Map parseReportAttributeMessage(String description) {
log.trace "parseReportAttributeMessage() --- description: $description"
Map descMap = zigbee.parseDescriptionAsMap(description)
log.debug "parseReportAttributeMessage() --- descMap: $descMap"
Map resultMap = [:]
if (descMap.clusterInt == CLUSTER_POWER && descMap.attrInt == POWER_ATTR_BATTERY_PERCENTAGE_REMAINING) {
resultMap.name = "battery"
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) {
def value = Integer.parseInt(descMap.value, 16)
resultMap.name = "lock"
resultMap.putAll([0:["value":"unknown",
"descriptionText":"Not fully locked"],
1:["value":"locked"],
2:["value":"unlocked"]].get(value,
["value":"unknown",
"descriptionText":"Unknown lock state"]))
log.info "parseReportAttributeMessage() --- lock: ${resultMap.value}"
}
else {
log.debug "parseReportAttributeMessage() --- ignoring attribute"
}
return resultMap
}

View File

@@ -0,0 +1,92 @@
/**
* 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.
*
*/
metadata {
definition (name: "ZigBee Switch Power", namespace: "smartthings", author: "SmartThings") {
capability "Actuator"
capability "Configuration"
capability "Refresh"
capability "Power Meter"
capability "Sensor"
capability "Switch"
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,0B05,0702", outClusters: "0003, 000A,0019", manufacturer: "Jasco Products", model: "45853", deviceJoinName: "GE ZigBee Plug-In Switch"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B05,0702", outClusters: "000A,0019", manufacturer: "Jasco Products", model: "45856", deviceJoinName: "GE ZigBee In-Wall Switch"
}
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.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
attributeState "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
}
tileAttribute ("power", key: "SECONDARY_CONTROL") {
attributeState "power", label:'${currentValue} W'
}
}
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.debug "description is $description"
def resultMap = zigbee.getKnownDescription(description)
if (resultMap) {
log.info resultMap
if (resultMap.type == "update") {
log.info "$device updates: ${resultMap.value}"
}
else if (resultMap.type == "power") {
def powerValue
if (device.getDataValue("manufacturer") != "OSRAM") { //OSRAM devices do not reliably update power
powerValue = (resultMap.value as Integer)/10 //TODO: The divisor value needs to be set as part of configuration
sendEvent(name: "power", value: powerValue)
}
}
else {
sendEvent(name: resultMap.type, value: resultMap.value)
}
}
else {
log.warn "DID NOT PARSE MESSAGE for description : $description"
log.debug zigbee.parseDescriptionAsMap(description)
}
}
def off() {
zigbee.off()
}
def on() {
zigbee.on()
}
def refresh() {
zigbee.onOffRefresh() + zigbee.simpleMeteringPowerRefresh() + zigbee.electricMeasurementPowerRefresh() + zigbee.onOffConfig() + zigbee.simpleMeteringPowerConfig() + zigbee.electricMeasurementPowerConfig()
}
def configure() {
log.debug "Configuring Reporting and Bindings."
zigbee.onOffConfig() + zigbee.simpleMeteringPowerConfig() + zigbee.electricMeasurementPowerConfig() + zigbee.onOffRefresh() + zigbee.simpleMeteringPowerRefresh() + zigbee.electricMeasurementPowerRefresh()
}

View File

@@ -0,0 +1,89 @@
/**
* 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.
*
*/
metadata {
definition (name: "ZigBee Switch", namespace: "smartthings", author: "SmartThings") {
capability "Actuator"
capability "Configuration"
capability "Refresh"
capability "Sensor"
capability "Switch"
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006"
}
// simulator metadata
simulator {
// status messages
status "on": "on/off: 1"
status "off": "on/off: 0"
// reply messages
reply "zcl on-off on": "on/off: 1"
reply "zcl on-off off": "on/off: 0"
}
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"
}
}
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.debug "description is $description"
def resultMap = zigbee.getKnownDescription(description)
if (resultMap) {
log.info resultMap
if (resultMap.type == "update") {
log.info "$device updates: ${resultMap.value}"
}
else {
sendEvent(name: resultMap.type, value: resultMap.value)
}
}
else {
log.warn "DID NOT PARSE MESSAGE for description : $description"
log.debug zigbee.parseDescriptionAsMap(description)
}
}
def off() {
zigbee.off()
}
def on() {
zigbee.on()
}
def refresh() {
zigbee.onOffRefresh() + zigbee.onOffConfig()
}
def configure() {
log.debug "Configuring Reporting and Bindings."
zigbee.onOffConfig() + zigbee.onOffRefresh()
}

View File

@@ -0,0 +1,134 @@
/**
* 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.
*
* ZigBee White Color Temperature Bulb
*
* Author: SmartThings
* Date: 2015-09-22
*/
metadata {
definition (name: "ZigBee White Color Temperature Bulb", namespace: "smartthings", author: "SmartThings") {
capability "Actuator"
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,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
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"
}
tileAttribute ("colorName", key: "SECONDARY_CONTROL") {
attributeState "colorName", label:'${currentValue}'
}
}
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
}
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", width: 4, height: 2, inactiveLabel: false, range:"(2700..6500)") {
state "colorTemperature", action:"color temperature.setColorTemperature"
}
valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "colorTemperature", label: '${currentValue} K'
}
main(["switch"])
details(["switch", "colorTempSliderControl", "colorTemp", "refresh"])
}
}
// Parse incoming device messages to generate events
def parse(String description) {
log.debug "description is $description"
def finalResult = zigbee.getKnownDescription(description)
if (finalResult) {
log.info finalResult
if (finalResult.type == "update") {
log.info "$device updates: ${finalResult.value}"
}
else {
sendEvent(name: finalResult.type, value: finalResult.value)
}
}
else {
log.warn "DID NOT PARSE MESSAGE for description : $description"
log.debug zigbee.parseDescriptionAsMap(description)
}
}
def off() {
zigbee.off()
}
def on() {
zigbee.on()
}
def setLevel(value) {
zigbee.setLevel(value)
}
def refresh() {
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh() + zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.colorTemperatureConfig()
}
def configure() {
log.debug "Configuring Reporting and Bindings."
zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.colorTemperatureConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh()
}
def setColorTemperature(value) {
setGenericName(value)
zigbee.setColorTemperature(value)
}
//Naming based on the wiki article here: http://en.wikipedia.org/wiki/Color_temperature
def setGenericName(value){
if (value != null) {
def genericName = "White"
if (value < 3300) {
genericName = "Soft White"
} else if (value < 4150) {
genericName = "Moonlight"
} else if (value <= 5000) {
genericName = "Cool White"
} else if (value >= 5000) {
genericName = "Daylight"
}
sendEvent(name: "colorName", value: genericName)
}
}

View File

@@ -0,0 +1,124 @@
/**
* 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.
*
*/
metadata {
definition (name: "Z-Wave Water Valve", namespace: "smartthings", author: "SmartThings") {
capability "Actuator"
capability "Valve"
capability "Polling"
capability "Refresh"
capability "Sensor"
fingerprint deviceId: "0x1006", inClusters: "0x25"
}
// simulator metadata
simulator {
status "open": "command: 2503, payload: FF"
status "close": "command: 2503, payload: 00"
// reply messages
reply "2001FF,delay 100,2502": "command: 2503, payload: FF"
reply "200100,delay 100,2502": "command: 2503, payload: 00"
}
// tile definitions
tiles(scale: 2) {
multiAttributeTile(name:"valve", type: "generic", width: 6, height: 4, canChangeIcon: true){
tileAttribute ("device.contact", key: "PRIMARY_CONTROL") {
attributeState "open", label: '${name}', action: "valve.close", icon: "st.valves.water.open", backgroundColor: "#53a7c0", nextState:"closing"
attributeState "closed", label: '${name}', action: "valve.open", icon: "st.valves.water.closed", backgroundColor: "#e86d13", nextState:"opening"
attributeState "opening", label: '${name}', action: "valve.close", icon: "st.valves.water.open", backgroundColor: "#ffe71e"
attributeState "closing", label: '${name}', action: "valve.open", icon: "st.valves.water.closed", backgroundColor: "#ffe71e"
}
}
standardTile("refresh", "device.contact", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
}
main "valve"
details(["valve","refresh"])
}
}
def updated() {
response(refresh())
}
def parse(String description) {
log.trace "parse description : $description"
def result = null
def cmd = zwave.parse(description, [0x20: 1])
if (cmd) {
result = createEvent(zwaveEvent(cmd))
}
log.debug "Parse returned ${result?.descriptionText}"
return result
}
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) {
def value = cmd.value == 0xFF ? "open" : cmd.value == 0x00 ? "closed" : "unknown"
[name: "contact", value: value, descriptionText: "$device.displayName valve is $value"]
}
def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { //TODO should show MSR when device is discovered
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)
[descriptionText: "$device.displayName MSR: $msr", isStateChange: false]
}
def zwaveEvent(physicalgraph.zwave.commands.deviceresetlocallyv1.DeviceResetLocallyNotification cmd) {
[descriptionText: cmd.toString(), isStateChange: true, displayed: true]
}
def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) {
def value = cmd.value == 0xFF ? "open" : cmd.value == 0x00 ? "closed" : "unknown"
[name: "contact", value: value, descriptionText: "$device.displayName valve is $value"]
}
def zwaveEvent(physicalgraph.zwave.Command cmd) {
[:] // Handles all Z-Wave commands we aren't interested in
}
def open() {
delayBetween([
zwave.basicV1.basicSet(value: 0xFF).format(),
zwave.switchBinaryV1.switchBinaryGet().format()
],10000) //wait for a water valve to be completely opened
}
def close() {
delayBetween([
zwave.basicV1.basicSet(value: 0x00).format(),
zwave.switchBinaryV1.switchBinaryGet().format()
],10000) //wait for a water valve to be completely closed
}
def poll() {
zwave.switchBinaryV1.switchBinaryGet().format()
}
def refresh() {
log.debug "refresh() is called"
def commands = [zwave.switchBinaryV1.switchBinaryGet().format()]
if (getDataValue("MSR") == null) {
commands << zwave.manufacturerSpecificV1.manufacturerSpecificGet().format()
}
delayBetween(commands,100)
}

View File

@@ -1,48 +0,0 @@
/**
* Switch Too
*
* Copyright 2015 Bob Florian
*
* 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: "Switch Too", author: "Bob Florian") {
capability "Switch"
}
simulator {
// TODO: define status and reply messages here
}
tiles {
// TODO: define your main and details tiles here
}
}
// parse events into attributes
def parse(String description) {
log.debug "Parsing '${description}'"
// TODO: handle 'switch' attribute
}
// handle commands
def on() {
log.debug "Executing 'on'"
// TODO: handle 'on' command
}
def off() {
log.debug "Executing 'off'"
// TODO: handle 'off' command
}

View File

@@ -8,6 +8,7 @@ metadata {
definition (name: "Zen Thermostat", namespace: "zenwithin", author: "ZenWithin") {
capability "Actuator"
capability "Thermostat"
capability "Temperature Measurement"
capability "Configuration"
capability "Refresh"
capability "Sensor"

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

@@ -11,8 +11,13 @@
* 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
* 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
*/
definition(
name: "Initial State Event Streamer",
namespace: "initialstate.events",
@@ -28,32 +33,31 @@ import groovy.json.JsonSlurper
preferences {
section("Choose which devices to monitor...") {
//input "accelerometers", "capability.accelerationSensor", title: "Accelerometers", multiple: true, required: false
input "accelerometers", "capability.accelerationSensor", title: "Accelerometers", multiple: true, required: false
input "alarms", "capability.alarm", title: "Alarms", multiple: true, required: false
//input "batteries", "capability.battery", title: "Batteries", multiple: true, required: false
//input "beacons", "capability.beacon", title: "Beacons", multiple: true, required: false
//input "buttons", "capability.button", title: "Buttons", multiple: true, required: false
//input "cos", "capability.carbonMonoxideDetector", title: "Carbon Monoxide Detectors", multiple: true, required: false
//input "colors", "capability.colorControl", title: "Color Controllers", multiple: true, required: false
input "batteries", "capability.battery", title: "Batteries", multiple: true, required: false
input "beacons", "capability.beacon", title: "Beacons", multiple: true, required: false
input "cos", "capability.carbonMonoxideDetector", title: "Carbon Monoxide Detectors", multiple: true, required: false
input "colors", "capability.colorControl", title: "Color Controllers", multiple: true, required: false
input "contacts", "capability.contactSensor", title: "Contact Sensors", multiple: true, required: false
//input "doorsControllers", "capability.doorControl", title: "Door Controllers", multiple: true, required: false
//input "energyMeters", "capability.energyMeter", title: "Energy Meters", multiple: true, required: false
//input "illuminances", "capability.illuminanceMeasurement", title: "Illuminance Meters", multiple: true, required: false
input "doorsControllers", "capability.doorControl", title: "Door Controllers", multiple: true, required: false
input "energyMeters", "capability.energyMeter", title: "Energy Meters", multiple: true, required: false
input "illuminances", "capability.illuminanceMeasurement", title: "Illuminance Meters", multiple: true, required: false
input "locks", "capability.lock", title: "Locks", multiple: true, required: false
input "motions", "capability.motionSensor", title: "Motion Sensors", multiple: true, required: false
//input "musicPlayers", "capability.musicPlayer", title: "Music Players", multiple: true, required: false
//input "powerMeters", "capability.powerMeter", title: "Power Meters", multiple: true, required: false
input "musicPlayers", "capability.musicPlayer", title: "Music Players", multiple: true, required: false
input "powerMeters", "capability.powerMeter", title: "Power Meters", multiple: true, required: false
input "presences", "capability.presenceSensor", title: "Presence Sensors", multiple: true, required: false
input "humidities", "capability.relativeHumidityMeasurement", title: "Humidity Meters", multiple: true, required: false
//input "relaySwitches", "capability.relaySwitch", title: "Relay Switches", multiple: true, required: false
//input "sleepSensors", "capability.sleepSensor", title: "Sleep Sensors", multiple: true, required: false
//input "smokeDetectors", "capability.smokeDetector", title: "Smoke Detectors", multiple: true, required: false
//input "peds", "capability.stepSensor", title: "Pedometers", multiple: true, required: false
input "relaySwitches", "capability.relaySwitch", title: "Relay Switches", multiple: true, required: false
input "sleepSensors", "capability.sleepSensor", title: "Sleep Sensors", multiple: true, required: false
input "smokeDetectors", "capability.smokeDetector", title: "Smoke Detectors", multiple: true, required: false
input "peds", "capability.stepSensor", title: "Pedometers", multiple: true, required: false
input "switches", "capability.switch", title: "Switches", multiple: true, required: false
input "switchLevels", "capability.switchLevel", title: "Switch Levels", multiple: true, required: false
input "temperatures", "capability.temperatureMeasurement", title: "Temperature Sensors", multiple: true, required: false
input "thermostats", "capability.thermostat", title: "Thermostats", multiple: true, required: false
//input "valves", "capability.valve", title: "Valves", multiple: true, required: false
input "valves", "capability.valve", title: "Valves", multiple: true, required: false
input "waterSensors", "capability.waterSensor", title: "Water Sensors", multiple: true, required: false
}
}
@@ -74,77 +78,71 @@ mappings {
}
def subscribeToEvents() {
/*if (accelerometers != null) {
if (accelerometers != null) {
subscribe(accelerometers, "acceleration", genericHandler)
}*/
}
if (alarms != null) {
subscribe(alarms, "alarm", genericHandler)
}
/*if (batteries != null) {
if (batteries != null) {
subscribe(batteries, "battery", genericHandler)
}*/
/*if (beacons != null) {
}
if (beacons != null) {
subscribe(beacons, "presence", genericHandler)
}*/
/*
if (buttons != null) {
subscribe(buttons, "button", genericHandler)
}*/
/*if (cos != null) {
}
if (cos != null) {
subscribe(cos, "carbonMonoxide", genericHandler)
}*/
/*if (colors != null) {
}
if (colors != null) {
subscribe(colors, "hue", genericHandler)
subscribe(colors, "saturation", genericHandler)
subscribe(colors, "color", genericHandler)
}*/
}
if (contacts != null) {
subscribe(contacts, "contact", genericHandler)
}
/*if (doorsControllers != null) {
subscribe(doorsControllers, "door", genericHandler)
}*/
/*if (energyMeters != null) {
if (energyMeters != null) {
subscribe(energyMeters, "energy", genericHandler)
}*/
/*if (illuminances != null) {
}
if (illuminances != null) {
subscribe(illuminances, "illuminance", genericHandler)
}*/
}
if (locks != null) {
subscribe(locks, "lock", genericHandler)
}
if (motions != null) {
subscribe(motions, "motion", genericHandler)
}
/*if (musicPlayers != null) {
if (musicPlayers != null) {
subscribe(musicPlayers, "status", genericHandler)
subscribe(musicPlayers, "level", genericHandler)
subscribe(musicPlayers, "trackDescription", genericHandler)
subscribe(musicPlayers, "trackData", genericHandler)
subscribe(musicPlayers, "mute", genericHandler)
}*/
/*if (powerMeters != null) {
}
if (powerMeters != null) {
subscribe(powerMeters, "power", genericHandler)
}*/
}
if (presences != null) {
subscribe(presences, "presence", genericHandler)
}
if (humidities != null) {
subscribe(humidities, "humidity", genericHandler)
}
/*if (relaySwitches != null) {
if (relaySwitches != null) {
subscribe(relaySwitches, "switch", genericHandler)
}*/
/*if (sleepSensors != null) {
}
if (sleepSensors != null) {
subscribe(sleepSensors, "sleeping", genericHandler)
}*/
/*if (smokeDetectors != null) {
}
if (smokeDetectors != null) {
subscribe(smokeDetectors, "smoke", genericHandler)
}*/
/*if (peds != null) {
}
if (peds != null) {
subscribe(peds, "steps", genericHandler)
subscribe(peds, "goal", genericHandler)
}*/
}
if (switches != null) {
subscribe(switches, "switch", genericHandler)
}
@@ -163,9 +161,9 @@ def subscribeToEvents() {
subscribe(thermostats, "thermostatFanMode", genericHandler)
subscribe(thermostats, "thermostatOperatingState", genericHandler)
}
/*if (valves != null) {
if (valves != null) {
subscribe(valves, "contact", genericHandler)
}*/
}
if (waterSensors != null) {
subscribe(waterSensors, "water", genericHandler)
}
@@ -173,23 +171,23 @@ def subscribeToEvents() {
def getAccessKey() {
log.trace "get access key"
if (state.accessKey == null) {
if (atomicState.accessKey == null) {
httpError(404, "Access Key Not Found")
} else {
[
accessKey: state.accessKey
accessKey: atomicState.accessKey
]
}
}
def getBucketKey() {
log.trace "get bucket key"
if (state.bucketKey == null) {
if (atomicState.bucketKey == null) {
httpError(404, "Bucket key Not Found")
} else {
[
bucketKey: state.bucketKey,
bucketName: state.bucketName
bucketKey: atomicState.bucketKey,
bucketName: atomicState.bucketName
]
}
}
@@ -202,53 +200,94 @@ def setBucketKey() {
log.debug "bucket name: $newBucketName"
log.debug "bucket key: $newBucketKey"
if (newBucketKey && (newBucketKey != state.bucketKey || newBucketName != state.bucketName)) {
state.bucketKey = "$newBucketKey"
state.bucketName = "$newBucketName"
state.isBucketCreated = false
if (newBucketKey && (newBucketKey != atomicState.bucketKey || newBucketName != atomicState.bucketName)) {
atomicState.bucketKey = "$newBucketKey"
atomicState.bucketName = "$newBucketName"
atomicState.isBucketCreated = false
}
tryCreateBucket()
}
def setAccessKey() {
log.trace "set access key"
def newAccessKey = request.JSON?.accessKey
def newGrokerSubdomain = request.JSON?.grokerSubdomain
if (newAccessKey && newAccessKey != state.accessKey) {
state.accessKey = "$newAccessKey"
state.isBucketCreated = false
if (newGrokerSubdomain && newGrokerSubdomain != "" && newGrokerSubdomain != atomicState.grokerSubdomain) {
atomicState.grokerSubdomain = "$newGrokerSubdomain"
atomicState.isBucketCreated = false
}
if (newAccessKey && newAccessKey != atomicState.accessKey) {
atomicState.accessKey = "$newAccessKey"
atomicState.isBucketCreated = false
}
}
def installed() {
atomicState.version = "1.0.18"
subscribeToEvents()
state.isBucketCreated = false
atomicState.isBucketCreated = false
atomicState.grokerSubdomain = "groker"
atomicState.eventBuffer = []
runEvery15Minutes(flushBuffer)
log.debug "installed (version $atomicState.version)"
}
def updated() {
atomicState.version = "1.0.18"
unsubscribe()
if (state.bucketKey != null && state.accessKey != null) {
state.isBucketCreated = false
if (atomicState.bucketKey != null && atomicState.accessKey != null) {
atomicState.isBucketCreated = false
}
if (atomicState.eventBuffer == null) {
atomicState.eventBuffer = []
}
if (atomicState.grokerSubdomain == null || atomicState.grokerSubdomain == "") {
atomicState.grokerSubdomain = "groker"
}
subscribeToEvents()
log.debug "updated (version $atomicState.version)"
}
def createBucket() {
def uninstalled() {
log.debug "uninstalled (version $atomicState.version)"
}
if (!state.bucketName) {
state.bucketName = state.bucketKey
def tryCreateBucket() {
// can't ship events if there is no grokerSubdomain
if (atomicState.grokerSubdomain == null || atomicState.grokerSubdomain == "") {
log.error "streaming url is currently null"
return
}
// if the bucket has already been created, no need to continue
if (atomicState.isBucketCreated) {
return
}
if (!atomicState.bucketName) {
atomicState.bucketName = atomicState.bucketKey
}
def bucketName = "${state.bucketName}"
def bucketKey = "${state.bucketKey}"
def accessKey = "${state.accessKey}"
if (!atomicState.accessKey) {
return
}
def bucketName = "${atomicState.bucketName}"
def bucketKey = "${atomicState.bucketKey}"
def accessKey = "${atomicState.accessKey}"
def bucketCreateBody = new JsonSlurper().parseText("{\"bucketKey\": \"$bucketKey\", \"bucketName\": \"$bucketName\"}")
def bucketCreatePost = [
uri: 'https://groker.initialstate.com/api/buckets',
uri: "https://${atomicState.grokerSubdomain}.initialstate.com/api/buckets",
headers: [
"Content-Type": "application/json",
"X-IS-AccessKey": accessKey
@@ -258,10 +297,20 @@ def createBucket() {
log.debug bucketCreatePost
httpPostJson(bucketCreatePost) {
log.debug "bucket posted"
state.isBucketCreated = true
try {
// Create a bucket on Initial State so the data has a logical grouping
httpPostJson(bucketCreatePost) { resp ->
log.debug "bucket posted"
if (resp.status >= 400) {
log.error "bucket not created successfully"
} else {
atomicState.isBucketCreated = true
}
}
} catch (e) {
log.error "bucket creation error: $e"
}
}
def genericHandler(evt) {
@@ -273,33 +322,80 @@ def genericHandler(evt) {
}
def value = "$evt.value"
tryCreateBucket()
eventHandler(key, value)
}
def eventHandler(name, value) {
// 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() {
log.trace "About to flush the buffer on schedule"
if (atomicState.eventBuffer != null && atomicState.eventBuffer.size() > 0) {
tryShipEvents()
}
}
if (state.accessKey == null || state.bucketKey == null) {
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 = []
}
eventBuffer << [key: "$name", value: "$value", epoch: "$epoch"]
log.debug eventBuffer
atomicState.eventBuffer = eventBuffer
if (eventBuffer.size() >= 10) {
tryShipEvents()
}
}
// a helper function for shipping the atomicState.eventBuffer to Initial State
def tryShipEvents() {
// can't ship events if there is no grokerSubdomain
if (atomicState.grokerSubdomain == null || atomicState.grokerSubdomain == "") {
log.error "streaming url is currently null"
return
}
// can't ship if access key and bucket key are null, so finish trying
if (atomicState.accessKey == null || atomicState.bucketKey == null) {
return
}
if (!state.isBucketCreated) {
createBucket()
}
def eventBody = new JsonSlurper().parseText("[{\"key\": \"$name\", \"value\": \"$value\"}]")
def eventPost = [
uri: 'https://groker.initialstate.com/api/events',
uri: "https://${atomicState.grokerSubdomain}.initialstate.com/api/events",
headers: [
"Content-Type": "application/json",
"X-IS-BucketKey": "${state.bucketKey}",
"X-IS-AccessKey": "${state.accessKey}"
"X-IS-BucketKey": "${atomicState.bucketKey}",
"X-IS-AccessKey": "${atomicState.accessKey}",
"Accept-Version": "0.0.2"
],
body: eventBody
body: atomicState.eventBuffer
]
log.debug eventPost
httpPostJson(eventPost) {
log.debug "event data posted"
try {
// post the events to initial state
httpPostJson(eventPost) { resp ->
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

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

@@ -0,0 +1,607 @@
/**
* Bose SoundTouch (Connect)
*
* Copyright 2015 SmartThings
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
* for the specific language governing permissions and limitations under the License.
*
*/
definition(
name: "Bose SoundTouch (Connect)",
namespace: "smartthings",
author: "SmartThings",
description: "Control your Bose SoundTouch speakers",
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",
singleInstance: true
)
preferences {
page(name:"deviceDiscovery", title:"Device Setup", content:"deviceDiscovery", refreshTimeout:5)
}
/**
* Get the urn that we're looking for
*
* @return URN which we are looking for
*
* @todo This + getUSNQualifier should be one and should use regular expressions
*/
def getDeviceType() {
return "urn:schemas-upnp-org:device:MediaRenderer:1" // Bose
}
/**
* If not null, returns an additional qualifier for ssdUSN
* to avoid spamming the network
*
* @return Additional qualifier OR null if not needed
*/
def getUSNQualifier() {
return "uuid:BO5EBO5E-F00D-F00D-FEED-"
}
/**
* Get the name of the new device to instantiate in the user's smartapps
* This must be an app owned by the namespace (see #getNameSpace).
*
* @return name
*/
def getDeviceName() {
return "Bose SoundTouch"
}
/**
* Returns the namespace this app and siblings use
*
* @return namespace
*/
def getNameSpace() {
return "smartthings"
}
/**
* The deviceDiscovery page used by preferences. Will automatically
* make calls to the underlying discovery mechanisms as well as update
* whenever new devices are discovered AND verified.
*
* @return a dynamicPage() object
*/
def deviceDiscovery()
{
if(canInstallLabs())
{
def refreshInterval = 3 // Number of seconds between refresh
int deviceRefreshCount = !state.deviceRefreshCount ? 0 : state.deviceRefreshCount as int
state.deviceRefreshCount = deviceRefreshCount + refreshInterval
def devices = getSelectableDevice()
def numFound = devices.size() ?: 0
// Make sure we get location updates (contains LAN data such as SSDP results, etc)
subscribeNetworkEvents()
//device discovery request every 15s
if((deviceRefreshCount % 15) == 0) {
discoverDevices()
}
// Verify request every 3 seconds except on discoveries
if(((deviceRefreshCount % 3) == 0) && ((deviceRefreshCount % 15) != 0)) {
verifyDevices()
}
log.trace "Discovered devices: ${devices}"
return dynamicPage(name:"deviceDiscovery", title:"Discovery Started!", nextPage:"", refreshInterval:refreshInterval, install:true, uninstall: true) {
section("Please wait while we discover your ${getDeviceName()}. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") {
input "selecteddevice", "enum", required:false, title:"Select ${getDeviceName()} (${numFound} found)", multiple:true, options:devices
}
}
}
else
{
def upgradeNeeded = """To use SmartThings Labs, your Hub should be completely up to date.
To update your Hub, access Location Settings in the Main Menu (tap the gear next to your location name), select your Hub, and choose "Update Hub"."""
return dynamicPage(name:"deviceDiscovery", title:"Upgrade needed!", nextPage:"", install:true, uninstall: true) {
section("Upgrade") {
paragraph "$upgradeNeeded"
}
}
}
}
/**
* Called by SmartThings Cloud when user has selected device(s) and
* pressed "Install".
*/
def installed() {
log.trace "Installed with settings: ${settings}"
initialize()
}
/**
* Called by SmartThings Cloud when app has been updated
*/
def updated() {
log.trace "Updated with settings: ${settings}"
unsubscribe()
initialize()
}
/**
* Called by SmartThings Cloud when user uninstalls the app
*
* We don't need to manually do anything here because any children
* are automatically removed upon the removal of the parent.
*
* Only time to do anything here is when you need to notify
* the remote end. And even then you're discouraged from removing
* the children manually.
*/
def uninstalled() {
}
/**
* If user has selected devices, will start monitoring devices
* for changes (new address, port, etc...)
*/
def initialize() {
log.trace "initialize()"
state.subscribe = false
if (selecteddevice) {
addDevice()
refreshDevices()
subscribeNetworkEvents(true)
}
}
/**
* Adds the child devices based on the user's selection
*
* Uses selecteddevice defined in the deviceDiscovery() page
*/
def addDevice(){
def devices = getVerifiedDevices()
def devlist
log.trace "Adding childs"
// If only one device is selected, we don't get a list (when using simulator)
if (!(selecteddevice instanceof List)) {
devlist = [selecteddevice]
} else {
devlist = selecteddevice
}
log.trace "These are being installed: ${devlist}"
devlist.each { dni ->
def d = getChildDevice(dni)
if(!d) {
def newDevice = devices.find { (it.value.mac) == dni }
def deviceName = newDevice?.value.name
if (!deviceName)
deviceName = getDeviceName() + "[${newDevice?.value.name}]"
d = addChildDevice(getNameSpace(), getDeviceName(), dni, newDevice?.value.hub, [label:"${deviceName}"])
d.boseSetDeviceID(newDevice.value.deviceID)
log.trace "Created ${d.displayName} with id $dni"
} else {
log.trace "${d.displayName} with id $dni already exists"
}
}
}
/**
* Resolves a DeviceNetworkId to an address. Primarily used by children
*
* @param dni Device Network id
* @return address or null
*/
def resolveDNI2Address(dni) {
def device = getVerifiedDevices().find { (it.value.mac) == dni }
if (device) {
return convertHexToIP(device.value.networkAddress)
}
return null
}
/**
* Joins a child to the "Play Everywhere" zone
*
* @param child The speaker joining the zone
* @return A list of maps with POST data
*/
def boseZoneJoin(child) {
log = child.log // So we can debug this function
def results = []
def result = [:]
// Find the master (if any)
def server = getChildDevices().find{ it.boseGetZone() == "server" }
if (server) {
log.debug "boseJoinZone() We have a server already, so lets add the new speaker"
child.boseSetZone("client")
result['endpoint'] = "/setZone"
result['host'] = server.getDeviceIP() + ":8090"
result['body'] = "<zone master=\"${server.boseGetDeviceID()}\" senderIPAddress=\"${server.getDeviceIP()}\">"
getChildDevices().each{ it ->
log.trace "child: " + child
log.trace "zone : " + it.boseGetZone()
if (it.boseGetZone() || it.boseGetDeviceID() == child.boseGetDeviceID())
result['body'] = result['body'] + "<member ipaddress=\"${it.getDeviceIP()}\">${it.boseGetDeviceID()}</member>"
}
result['body'] = result['body'] + '</zone>'
} else {
log.debug "boseJoinZone() No server, add it!"
result['endpoint'] = "/setZone"
result['host'] = child.getDeviceIP() + ":8090"
result['body'] = "<zone master=\"${child.boseGetDeviceID()}\" senderIPAddress=\"${child.getDeviceIP()}\">"
result['body'] = result['body'] + "<member ipaddress=\"${child.getDeviceIP()}\">${child.boseGetDeviceID()}</member>"
result['body'] = result['body'] + '</zone>'
child.boseSetZone("server")
}
results << result
return results
}
def boseZoneReset() {
getChildDevices().each{ it.boseSetZone(null) }
}
def boseZoneHasMaster() {
return getChildDevices().find{ it.boseGetZone() == "server" } != null
}
/**
* Removes a speaker from the play everywhere zone.
*
* @param child Which speaker is leaving
* @return a list of maps with POST data
*/
def boseZoneLeave(child) {
log = child.log // So we can debug this function
def results = []
def result = [:]
// First, tag us as a non-member
child.boseSetZone(null)
// Find the master (if any)
def server = getChildDevices().find{ it.boseGetZone() == "server" }
if (server && server.boseGetDeviceID() != child.boseGetDeviceID()) {
log.debug "boseLeaveZone() We have a server, so tell him we're leaving"
result['endpoint'] = "/removeZoneSlave"
result['host'] = server.getDeviceIP() + ":8090"
result['body'] = "<zone master=\"${server.boseGetDeviceID()}\" senderIPAddress=\"${server.getDeviceIP()}\">"
result['body'] = result['body'] + "<member ipaddress=\"${child.getDeviceIP()}\">${child.boseGetDeviceID()}</member>"
result['body'] = result['body'] + '</zone>'
results << result
} else {
log.debug "boseLeaveZone() No server, then...uhm, we probably were it!"
// Dismantle the entire thing, first send this to master
result['endpoint'] = "/removeZoneSlave"
result['host'] = child.getDeviceIP() + ":8090"
result['body'] = "<zone master=\"${child.boseGetDeviceID()}\" senderIPAddress=\"${child.getDeviceIP()}\">"
getChildDevices().each{ dev ->
if (dev.boseGetZone() || dev.boseGetDeviceID() == child.boseGetDeviceID())
result['body'] = result['body'] + "<member ipaddress=\"${dev.getDeviceIP()}\">${dev.boseGetDeviceID()}</member>"
}
result['body'] = result['body'] + '</zone>'
results << result
// Also issue this to each individual client
getChildDevices().each{ dev ->
if (dev.boseGetZone() && dev.boseGetDeviceID() != child.boseGetDeviceID()) {
log.trace "Additional device: " + dev
result['host'] = dev.getDeviceIP() + ":8090"
results << result
}
}
}
return results
}
/**
* Define our XML parsers
*
* @return mapping of root-node <-> parser function
*/
def getParsers() {
[
"root" : "parseDESC",
"info" : "parseINFO"
]
}
/**
* Called when location has changed, contains information from
* network transactions. See deviceDiscovery() for where it is
* registered.
*
* @param evt Holds event information
*/
def onLocation(evt) {
// Convert the event into something we can use
def lanEvent = parseLanMessage(evt.description, true)
lanEvent << ["hub":evt?.hubId]
// Determine what we need to do...
if (lanEvent?.ssdpTerm?.contains(getDeviceType()) &&
(getUSNQualifier() == null ||
lanEvent?.ssdpUSN?.contains(getUSNQualifier())
)
)
{
parseSSDP(lanEvent)
}
else if (
lanEvent.headers && lanEvent.body &&
lanEvent.headers."content-type".contains("xml")
)
{
def parsers = getParsers()
def xmlData = new XmlSlurper().parseText(lanEvent.body)
// Let each parser take a stab at it
parsers.each { node,func ->
if (xmlData.name() == node)
"$func"(xmlData)
}
}
}
/**
* Handles SSDP description file.
*
* @param xmlData
*/
private def parseDESC(xmlData) {
log.info "parseDESC()"
def devicetype = getDeviceType().toLowerCase()
def devicetxml = body.device.deviceType.text().toLowerCase()
// Make sure it's the type we want
if (devicetxml == devicetype) {
def devices = getDevices()
def device = devices.find {it?.key?.contains(xmlData?.device?.UDN?.text())}
if (device && !device.value?.verified) {
// Unlike regular DESC, we cannot trust this just yet, parseINFO() decides all
device.value << [name:xmlData?.device?.friendlyName?.text(),model:xmlData?.device?.modelName?.text(), serialNumber:xmlData?.device?.serialNum?.text()]
} else {
log.error "parseDESC(): The xml file returned a device that didn't exist"
}
}
}
/**
* Handle BOSE <info></info> result. This is an alternative to
* using the SSDP description standard. Some of the speakers do
* not support SSDP description, so we need this as well.
*
* @param xmlData
*/
private def parseINFO(xmlData) {
log.info "parseINFO()"
def devicetype = getDeviceType().toLowerCase()
def deviceID = xmlData.attributes()['deviceID']
def device = getDevices().find {it?.key?.contains(deviceID)}
if (device && !device.value?.verified) {
device.value << [name:xmlData?.name?.text(),model:xmlData?.type?.text(), serialNumber:xmlData?.serialNumber?.text(), "deviceID":deviceID, verified: true]
}
}
/**
* Handles SSDP discovery messages and adds them to the list
* of discovered devices. If it already exists, it will update
* the port and location (in case it was moved).
*
* @param lanEvent
*/
def parseSSDP(lanEvent) {
//SSDP DISCOVERY EVENTS
def USN = lanEvent.ssdpUSN.toString()
def devices = getDevices()
if (!(devices."${USN}")) {
//device does not exist
log.trace "parseSDDP() Adding Device \"${USN}\" to known list"
devices << ["${USN}":lanEvent]
} else {
// update the values
def d = devices."${USN}"
if (d.networkAddress != lanEvent.networkAddress || d.deviceAddress != lanEvent.deviceAddress) {
log.trace "parseSSDP() Updating device location (ip & port)"
d.networkAddress = lanEvent.networkAddress
d.deviceAddress = lanEvent.deviceAddress
}
}
}
/**
* Generates a Map object which can be used with a preference page
* to represent a list of devices detected and verified.
*
* @return Map with zero or more devices
*/
Map getSelectableDevice() {
def devices = getVerifiedDevices()
def map = [:]
devices.each {
def value = "${it.value.name}"
def key = it.value.mac
map["${key}"] = value
}
map
}
/**
* Starts the refresh loop, making sure to keep us up-to-date with changes
*
*/
private refreshDevices() {
discoverDevices()
verifyDevices()
runIn(300, "refreshDevices")
}
/**
* Starts a subscription for network events
*
* @param force If true, will unsubscribe and subscribe if necessary (Optional, default false)
*/
private subscribeNetworkEvents(force=false) {
if (force) {
unsubscribe()
state.subscribe = false
}
if(!state.subscribe) {
subscribe(location, null, onLocation, [filterEvents:false])
state.subscribe = true
}
}
/**
* Issues a SSDP M-SEARCH over the LAN for a specific type (see getDeviceType())
*/
private discoverDevices() {
log.trace "discoverDevice() Issuing SSDP request"
sendHubCommand(new physicalgraph.device.HubAction("lan discovery ${getDeviceType()}", physicalgraph.device.Protocol.LAN))
}
/**
* Walks through the list of unverified devices and issues a verification
* request for each of them (basically calling verifyDevice() per unverified)
*/
private verifyDevices() {
def devices = getDevices().findAll { it?.value?.verified != true }
devices.each {
verifyDevice(
it?.value?.mac,
convertHexToIP(it?.value?.networkAddress),
convertHexToInt(it?.value?.deviceAddress),
it?.value?.ssdpPath
)
}
}
/**
* Verify the device, in this case, we need to obtain the info block which
* holds information such as the actual mac to use in certain scenarios.
*
* Without this mac (henceforth referred to as deviceID), we can't do multi-speaker
* functions.
*
* @param deviceNetworkId The DNI of the device
* @param ip The address of the device on the network (not the same as DNI)
* @param port The port to use (0 will be treated as invalid and will use 80)
* @param devicessdpPath The URL path (for example, /desc)
*
* @note Result is captured in locationHandler()
*/
private verifyDevice(String deviceNetworkId, String ip, int port, String devicessdpPath) {
if(ip) {
def address = ip + ":8090"
sendHubCommand(new physicalgraph.device.HubAction([
method: "GET",
path: "/info",
headers: [
HOST: address,
]]))
} else {
log.warn("verifyDevice() IP address was empty")
}
}
/**
* Returns an array of devices which have been verified
*
* @return array of verified devices
*/
def getVerifiedDevices() {
getDevices().findAll{ it?.value?.verified == true }
}
/**
* Returns all discovered devices or an empty array if none
*
* @return array of devices
*/
def getDevices() {
state.devices = state.devices ?: [:]
}
/**
* Converts a hexadecimal string to an integer
*
* @param hex The string with a hexadecimal value
* @return An integer
*/
private Integer convertHexToInt(hex) {
Integer.parseInt(hex,16)
}
/**
* Converts an IP address represented as 0xAABBCCDD to AAA.BBB.CCC.DDD
*
* @param hex Address represented in hex
* @return String containing normal IPv4 dot notation
*/
private String convertHexToIP(hex) {
if (hex)
[convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
else
hex
}
/**
* Tests if this setup can support SmarthThing Labs items
*
* @return true if it supports it.
*/
private Boolean canInstallLabs()
{
return hasAllHubsOver("000.011.00603")
}
/**
* Tests if the firmwares on all hubs owned by user match or exceed the
* provided version number.
*
* @param desiredFirmware The version that must match or exceed
* @return true if hub has same or newer
*/
private Boolean hasAllHubsOver(String desiredFirmware)
{
return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware }
}
/**
* Creates a list of firmware version for every hub the user has
*
* @return List of firmwares
*/
private List getRealHubFirmwareVersions()
{
return location.hubs*.firmwareVersionString.findAll { it }
}

View File

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

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

@@ -15,7 +15,7 @@
* for the specific language governing permissions and limitations under the License.
*
*/
definition(
name: "Hue (Connect)",
namespace: "smartthings",
@@ -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 {
@@ -64,10 +65,14 @@ def bridgeDiscovery(params=[:])
def options = bridges ?: []
def numFound = options.size() ?: 0
if(!state.subscribe) {
subscribe(location, null, locationHandler, [filterEvents:false])
state.subscribe = true
}
if (numFound == 0 && state.bridgeRefreshCount > 25) {
log.trace "Cleaning old bridges memory"
state.bridges = [:]
state.bridgeRefreshCount = 0
app.updateSetting("selectedHue", "")
}
subscribe(location, null, locationHandler, [filterEvents:false])
//bridge discovery request every 15 //25 seconds
if((bridgeRefreshCount % 5) == 0) {
@@ -94,11 +99,20 @@ def bridgeLinking()
def nextPage = ""
def title = "Linking with your Hue"
def paragraphText = "Press the button on your Hue Bridge to setup a link."
def paragraphText
def hueimage = null
if (selectedHue) {
paragraphText = "Press the button on your Hue Bridge to setup a link. "
hueimage = "http://huedisco.mediavibe.nl/wp-content/uploads/2013/09/pair-bridge.png"
} else {
paragraphText = "You haven't selected a Hue Bridge, please Press \"Done\" and select one before clicking next."
hueimage = null
}
if (state.username) { //if discovery worked
nextPage = "bulbDiscovery"
title = "Success! - click 'Next'"
title = "Success!"
paragraphText = "Linking to your hub was a success! Please click 'Next'!"
hueimage = null
}
if((linkRefreshcount % 2) == 0 && !state.username) {
@@ -106,30 +120,39 @@ def bridgeLinking()
}
return dynamicPage(name:"bridgeBtnPush", title:title, nextPage:nextPage, refreshInterval:refreshInterval) {
section("Button Press") {
section("") {
paragraph """${paragraphText}"""
if (hueimage != null)
image "${hueimage}"
}
}
}
def bulbDiscovery()
{
def bulbDiscovery() {
int bulbRefreshCount = !state.bulbRefreshCount ? 0 : state.bulbRefreshCount as int
state.bulbRefreshCount = bulbRefreshCount + 1
def refreshInterval = 3
def options = bulbsDiscovered() ?: []
def numFound = options.size() ?: 0
if((bulbRefreshCount % 3) == 0) {
state.inBulbDiscovery = true
def bridge = null
if (selectedHue) {
bridge = getChildDevice(selectedHue)
subscribe(bridge, "bulbList", bulbListData)
}
state.bridgeRefreshCount = 0
def bulboptions = bulbsDiscovered() ?: [:]
def numFound = bulboptions.size() ?: 0
if (numFound == 0)
app.updateSetting("selectedBulbs", "")
if((bulbRefreshCount % 5) == 0) {
discoverHueBulbs()
}
return dynamicPage(name:"bulbDiscovery", title:"Bulb Discovery Started!", nextPage:"", refreshInterval:refreshInterval, install:true, uninstall: true) {
section("Please wait while we discover your Hue Bulbs. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") {
input "selectedBulbs", "enum", required:false, title:"Select Hue Bulbs (${numFound} found)", multiple:true, options:options
input "selectedBulbs", "enum", required:false, title:"Select Hue Bulbs (${numFound} found)", multiple:true, options:bulboptions
}
section {
section {
def title = getBridgeIP() ? "Hue bridge (${getBridgeIP()})" : "Find bridges"
href "bridgeDiscovery", title: title, description: "", state: selectedHue ? "complete" : "incomplete", params: [override: true]
@@ -194,24 +217,29 @@ Map bridgesDiscovered() {
Map bulbsDiscovered() {
def bulbs = getHueBulbs()
def map = [:]
def bulbmap = [:]
if (bulbs instanceof java.util.Map) {
bulbs.each {
def value = "${it?.value?.name}"
def key = app.id +"/"+ it?.value?.id
map["${key}"] = value
def value = "${it.value.name}"
def key = app.id +"/"+ it.value.id
bulbmap["${key}"] = value
}
} else { //backwards compatable
bulbs.each {
def value = "${it?.name}"
def key = app.id +"/"+ it?.id
map["${key}"] = value
def value = "${it.name}"
def key = app.id +"/"+ it.id
logg += "$value - $key, "
bulbmap["${key}"] = value
}
}
map
return bulbmap
}
def getHueBulbs() {
def bulbListData(evt) {
state.bulbs = evt.jsonData
}
Map getHueBulbs() {
state.bulbs = state.bulbs ?: [:]
}
@@ -231,24 +259,19 @@ def installed() {
def updated() {
log.trace "Updated with settings: ${settings}"
unschedule()
unsubscribe()
unsubscribe()
initialize()
}
def initialize() {
log.debug "Initializing"
state.subscribe = false
state.bridgeSelectedOverride = false
def bridge = null
log.debug "Initializing"
unsubscribe(bridge)
state.inBulbDiscovery = false
state.bridgeRefreshCount = 0
state.bulbRefreshCount = 0
if (selectedHue) {
addBridge()
bridge = getChildDevice(selectedHue)
subscribe(bridge, "bulbList", bulbListHandler)
}
if (selectedBulbs) {
addBulbs()
addBridge()
addBulbs()
doDeviceSync()
runEvery5Minutes("doDeviceSync")
}
@@ -263,38 +286,49 @@ def manualRefresh() {
def uninstalled(){
state.bridges = [:]
state.subscribe = false
state.username = null
}
// Handles events to add new bulbs
def bulbListHandler(evt) {
def bulbs = [:]
log.trace "Adding bulbs to state..."
state.bridgeProcessedLightList = true
evt.jsonData.each { k,v ->
log.trace "$k: $v"
if (v instanceof Map) {
bulbs[k] = [id: k, name: v.name, type: v.type, hub:evt.value]
}
}
state.bulbs = bulbs
log.info "${bulbs.size()} bulbs found"
def bulbListHandler(hub, data = "") {
def msg = "Bulbs list not processed. Only while in settings menu."
def bulbs = [:]
if (state.inBulbDiscovery) {
def logg = ""
log.trace "Adding bulbs to state..."
state.bridgeProcessedLightList = true
def object = new groovy.json.JsonSlurper().parseText(data)
object.each { k,v ->
if (v instanceof Map)
bulbs[k] = [id: k, name: v.name, type: v.type, hub:hub]
}
}
def bridge = null
if (selectedHue)
bridge = getChildDevice(selectedHue)
bridge.sendEvent(name: "bulbList", value: hub, data: bulbs, isStateChange: true, displayed: false)
msg = "${bulbs.size()} bulbs found. ${bulbs}"
return msg
}
def addBulbs() {
def bulbs = getHueBulbs()
selectedBulbs.each { dni ->
selectedBulbs?.each { dni ->
def d = getChildDevice(dni)
if(!d) {
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])
}
} else {
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 }
d = addChildDevice("smartthings", "Hue Bulb", dni, newHueBulb?.hub, ["label":newHueBulb?.name])
@@ -322,7 +356,7 @@ def addBridge() {
def d = getChildDevice(selectedHue)
if(!d) {
// compatibility with old devices
def newbridge = true
def newbridge = true
childDevices.each {
if (it.getDeviceDataByName("mac")) {
def newDNI = "${it.getDeviceDataByName("mac")}"
@@ -332,22 +366,27 @@ def addBridge() {
it.setDeviceNetworkId("${newDNI}")
if (oldDNI == selectedHue)
app.updateSetting("selectedHue", newDNI)
newbridge = false
newbridge = false
}
}
}
}
}
if (newbridge) {
d = addChildDevice("smartthings", "Hue Bridge", selectedHue, vbridge.value.hub)
log.debug "created ${d.displayName} with id ${d.deviceNetworkId}"
def childDevice = getChildDevice(d.deviceNetworkId)
childDevice.sendEvent(name: "serialNumber", value: vbridge.value.serialNumber)
if (vbridge.value.ip && vbridge.value.port) {
if (vbridge.value.ip.contains("."))
if (vbridge.value.ip.contains(".")) {
childDevice.sendEvent(name: "networkAddress", value: vbridge.value.ip + ":" + vbridge.value.port)
else
childDevice.sendEvent(name: "networkAddress", value: convertHexToIP(vbridge.value.ip) + ":" + convertHexToInt(vbridge.value.port))
} else
childDevice.updateDataValue("networkAddress", vbridge.value.ip + ":" + vbridge.value.port)
} else {
childDevice.sendEvent(name: "networkAddress", value: convertHexToIP(vbridge.value.ip) + ":" + convertHexToInt(vbridge.value.port))
childDevice.updateDataValue("networkAddress", convertHexToIP(vbridge.value.ip) + ":" + convertHexToInt(vbridge.value.port))
}
} else {
childDevice.sendEvent(name: "networkAddress", value: convertHexToIP(vbridge.value.networkAddress) + ":" + convertHexToInt(vbridge.value.deviceAddress))
childDevice.updateDataValue("networkAddress", convertHexToIP(vbridge.value.networkAddress) + ":" + convertHexToInt(vbridge.value.deviceAddress))
}
}
} else {
log.debug "found ${d.displayName} with id $selectedHue already exists"
@@ -355,7 +394,6 @@ def addBridge() {
}
}
def locationHandler(evt) {
def description = evt.description
log.trace "Location: $description"
@@ -397,8 +435,11 @@ def locationHandler(evt) {
}
}
}
} else {
networkAddress = d.latestState('networkAddress').stringValue
} else {
if (d.getDeviceDataByName("networkAddress"))
networkAddress = d.getDeviceDataByName("networkAddress")
else
networkAddress = d.latestState('networkAddress').stringValue
log.trace "Host: $host - $networkAddress"
if(host != networkAddress) {
log.debug "Device's port or ip changed for device $d..."
@@ -406,7 +447,8 @@ def locationHandler(evt) {
dstate.port = port
dstate.name = "Philips hue ($ip)"
d.sendEvent(name:"networkAddress", value: host)
}
d.updateDataValue("networkAddress", host)
}
}
}
}
@@ -454,17 +496,13 @@ def locationHandler(evt) {
def doDeviceSync(){
log.trace "Doing Hue Device Sync!"
//shrink the large bulb lists
convertBulbListToMap()
poll()
if(!state.subscribe) {
try {
subscribe(location, null, locationHandler, [filterEvents:false])
state.subscribe = true
}
} catch (all) {
log.trace "Subscription already exist"
}
discoverBridges()
}
@@ -473,44 +511,49 @@ def doDeviceSync(){
/////////////////////////////////////
def parse(childDevice, description) {
def parsedEvent = parseLanMessage(description)
def parsedEvent = parseLanMessage(description)
if (parsedEvent.headers && parsedEvent.body) {
def headerString = parsedEvent.headers.toString()
if (headerString?.contains("json")) {
def body = new groovy.json.JsonSlurper().parseText(parsedEvent.body)
if (body instanceof java.util.HashMap)
{ //poll response
def bodyString = parsedEvent.body.toString()
if (headerString?.contains("json")) {
def body
try {
body = new groovy.json.JsonSlurper().parseText(bodyString)
} catch (all) {
log.warn "Parsing Body failed - trying again..."
poll()
}
if (body instanceof java.util.HashMap) {
//poll response
def bulbs = getChildDevices()
//for each bulb
for (bulb in body) {
def d = bulbs.find{it.deviceNetworkId == "${app.id}/${bulb.key}"}
def d = bulbs.find{it.deviceNetworkId == "${app.id}/${bulb.key}"}
if (d) {
if (bulb.value.state?.reachable) {
sendEvent(d.deviceNetworkId, [name: "switch", value: bulb.value?.state?.on ? "on" : "off"])
sendEvent(d.deviceNetworkId, [name: "level", value: Math.round(bulb.value.state.bri * 100 / 255)])
if (bulb.value.state.sat) {
def hue = Math.min(Math.round(bulb.value.state.hue * 100 / 65535), 65535) as int
def sat = Math.round(bulb.value.state.sat * 100 / 255) as int
def hex = colorUtil.hslToHex(hue, sat)
sendEvent(d.deviceNetworkId, [name: "color", value: hex])
sendEvent(d.deviceNetworkId, [name: "hue", value: hue])
sendEvent(d.deviceNetworkId, [name: "saturation", value: sat])
}
} else {
sendEvent(d.deviceNetworkId, [name: "switch", value: "off"])
sendEvent(d.deviceNetworkId, [name: "level", value: 100])
if (bulb.value.state.sat) {
def hue = 23
def sat = 56
def hex = colorUtil.hslToHex(23, 56)
sendEvent(d.deviceNetworkId, [name: "color", value: hex])
sendEvent(d.deviceNetworkId, [name: "hue", value: hue])
sendEvent(d.deviceNetworkId, [name: "saturation", value: sat])
}
}
sendEvent(d.deviceNetworkId, [name: "switch", value: bulb.value?.state?.on ? "on" : "off"])
sendEvent(d.deviceNetworkId, [name: "level", value: Math.round(bulb.value.state.bri * 100 / 255)])
if (bulb.value.state.sat) {
def hue = Math.min(Math.round(bulb.value.state.hue * 100 / 65535), 65535) as int
def sat = Math.round(bulb.value.state.sat * 100 / 255) as int
def hex = colorUtil.hslToHex(hue, sat)
sendEvent(d.deviceNetworkId, [name: "color", value: hex])
sendEvent(d.deviceNetworkId, [name: "hue", value: hue])
sendEvent(d.deviceNetworkId, [name: "saturation", value: sat])
}
} else {
sendEvent(d.deviceNetworkId, [name: "switch", value: "off"])
sendEvent(d.deviceNetworkId, [name: "level", value: 100])
if (bulb.value.state.sat) {
def hue = 23
def sat = 56
def hex = colorUtil.hslToHex(23, 56)
sendEvent(d.deviceNetworkId, [name: "color", value: hex])
sendEvent(d.deviceNetworkId, [name: "hue", value: hue])
sendEvent(d.deviceNetworkId, [name: "saturation", value: sat])
}
}
}
}
}
}
else
{ //put response
@@ -559,30 +602,29 @@ def parse(childDevice, description) {
}
}
}
}
} else {
log.debug "parse - got something other than headers,body..."
return []
}
}
def on(childDevice, transition = 4) {
def on(childDevice) {
log.debug "Executing 'on'"
// Assume bulb is off if no current state is found for level to avoid bulbs getting stuck in off after initial discovery
def percent = childDevice.device?.currentValue("level") as Integer ?: 0
def level = Math.min(Math.round(percent * 255 / 100), 255)
put("lights/${getId(childDevice)}/state", [bri: level, on: true, transitiontime: transition])
return "level: $percent"
put("lights/${getId(childDevice)}/state", [on: true])
return "Bulb is On"
}
def off(childDevice, transition = 4) {
def off(childDevice) {
log.debug "Executing 'off'"
put("lights/${getId(childDevice)}/state", [on: false, transitiontime: transition])
put("lights/${getId(childDevice)}/state", [on: false])
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])
}
@@ -598,19 +640,21 @@ def setHue(childDevice, percent) {
put("lights/${getId(childDevice)}/state", [hue: level])
}
def setColor(childDevice, color, alert = "none", transition = 4) {
log.debug "Executing 'setColor($color)'"
def hue = Math.min(Math.round(color.hue * 65535 / 100), 65535)
def sat = Math.min(Math.round(color.saturation * 255 / 100), 255)
def setColor(childDevice, huesettings) {
log.debug "Executing 'setColor($huesettings)'"
def hue = Math.min(Math.round(huesettings.hue * 65535 / 100), 65535)
def sat = Math.min(Math.round(huesettings.saturation * 255 / 100), 255)
def alert = huesettings.alert ? huesettings.alert : "none"
def transition = huesettings.transition ? huesettings.transition : 4
def value = [sat: sat, hue: hue, alert: alert, transitiontime: transition]
if (color.level != null) {
value.bri = Math.min(Math.round(color.level * 255 / 100), 255)
if (huesettings.level != null) {
if (huesettings.level == 1) value.bri = 1 else value.bri = Math.min(Math.round(huesettings.level * 255 / 100), 255)
value.on = value.bri > 0
}
if (color.switch) {
value.on = color.switch == "on"
if (huesettings.switch) {
value.on = huesettings.switch == "on"
}
log.debug "sending command $value"
@@ -640,15 +684,19 @@ private getId(childDevice) {
private poll() {
def host = getBridgeIP()
def uri = "/api/${state.username}/lights/"
log.debug "GET: $host$uri"
sendHubCommand(new physicalgraph.device.HubAction("""GET ${uri} HTTP/1.1
try {
sendHubCommand(new physicalgraph.device.HubAction("""GET ${uri} HTTP/1.1
HOST: ${host}
""", physicalgraph.device.Protocol.LAN, selectedHue))
} catch (all) {
log.warn "Parsing Body failed - trying again..."
doDeviceSync()
}
}
private put(path, body) {
def host = getBridgeIP()
def host = getBridgeIP()
def uri = "/api/${state.username}/$path"
def bodyJSON = new groovy.json.JsonBuilder(body).toString()
def length = bodyJSON.getBytes().size().toString()
@@ -668,12 +716,19 @@ ${bodyJSON}
private getBridgeIP() {
def host = null
if (selectedHue) {
def d = getChildDevice(dni)
if (d)
host = d.latestState('networkAddress').stringValue
def d = getChildDevice(selectedHue)
if (d) {
if (d.getDeviceDataByName("networkAddress"))
host = d.getDeviceDataByName("networkAddress")
else
host = d.latestState('networkAddress').stringValue
}
if (host == null || host == "") {
def serialNumber = selectedHue
def bridge = getHueBridges().find { it?.value?.serialNumber?.equalsIgnoreCase(serialNumber) }?.value
if (!bridge) {
bridge = getHueBridges().find { it?.value?.mac?.equalsIgnoreCase(serialNumber) }?.value
}
if (bridge?.ip && bridge?.port) {
if (bridge?.ip.contains("."))
host = "${bridge?.ip}:${bridge?.port}"
@@ -681,9 +736,9 @@ private getBridgeIP() {
host = "${convertHexToIP(bridge?.ip)}:${convertHexToInt(bridge?.port)}"
} else if (bridge?.networkAddress && bridge?.deviceAddress)
host = "${convertHexToIP(bridge?.networkAddress)}:${convertHexToInt(bridge?.deviceAddress)}"
}
}
log.trace "Bridge: $selectedHue - Host: $host"
}
}
return host
}

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"

View File

@@ -0,0 +1,404 @@
/**
* LIFX
*
* Copyright 2015 LIFX
*
*/
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,
singleInstance: true) {
appSetting "clientId"
appSetting "clientSecret"
}
preferences {
page(name: "Credentials", title: "LIFX", content: "authPage", install: true)
}
mappings {
path("/receivedToken") { action: [ POST: "oauthReceivedToken", GET: "oauthReceivedToken"] }
path("/receiveToken") { action: [ POST: "oauthReceiveToken", GET: "oauthReceiveToken"] }
path("/hookCallback") { action: [ POST: "hookEventHandler", GET: "hookEventHandler"] }
path("/oauth/callback") { action: [ GET: "oauthCallback" ] }
path("/oauth/initialize") { action: [ GET: "oauthInit"] }
path("/test") { action: [ GET: "oauthSuccess" ] }
}
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 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"
state.accessToken = createAccessToken() // predefined method
}
def description = "Tap to enter LIFX credentials"
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:true) {
section {
href(url:redirectUrl, required:true, title:"Connect to LIFX", description:"Tap here to connect your LIFX account")
}
}
} else {
log.debug "have LIFX access token"
def options = locationOptions() ?: []
def count = options.size()
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, submitOnChange: true
}
}
}
}
// OAuth
def oauthInit() {
def oauthParams = [client_id: "${appSettings.clientId}", scope: "remote_control:all", response_type: "code" ]
log.info("Redirecting user to OAuth setup")
redirect(location: "https://cloud.lifx.com/oauth/authorize?${toQueryString(oauthParams)}")
}
def oauthCallback() {
def redirectUrl = null
if (params.authQueryString) {
redirectUrl = URLDecoder.decode(params.authQueryString.replaceAll(".+&redirect_url=", ""))
} else {
log.warn "No authQueryString"
}
if (state.lifxAccessToken) {
log.debug "Access token already exists"
success()
} else {
def code = params.code
if (code) {
if (code.size() > 6) {
// LIFX code
log.debug "Exchanging code for access token"
oauthReceiveToken(redirectUrl)
} else {
// Initiate the LIFX OAuth flow.
oauthInit()
}
} else {
log.debug "This code should be unreachable"
success()
}
}
}
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 = [
uri: "https://cloud.lifx.com/oauth/token",
body: oauthParams,
headers: [
"User-Agent": "SmartThings Integration"
]
]
httpPost(params) { response ->
state.lifxAccessToken = response.data.access_token
}
if (state.lifxAccessToken) {
oauthSuccess()
} else {
oauthFailure()
}
}
def oauthSuccess() {
def message = """
<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>
"""
oauthConnectionStatus(message)
}
def oauthReceivedToken() {
def message = """
<p>Your LIFX Account is already connected to SmartThings!</p>
<p>Click 'Done' to finish setup.</p>
"""
oauthConnectionStatus(message)
}
def oauthConnectionStatus(message, redirectUrl = null) {
def redirectHtml = ""
if (redirectUrl) {
redirectHtml = """
<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>
"""
render contentType: 'text/html', data: html
}
String toQueryString(Map m) {
return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&")
}
// App lifecycle hooks
def installed() {
if (!state.accessToken) {
createAccessToken()
} else {
initialize()
}
// Check for new devices and remove old ones every 3 hours
runEvery3Hours('updateDevices')
}
// called after settings are changed
def updated() {
if (!state.accessToken) {
createAccessToken()
} else {
initialize()
}
}
def uninstalled() {
log.info("Uninstalling, removing child devices...")
unschedule('updateDevices')
removeChildDevices(getChildDevices())
}
private removeChildDevices(devices) {
devices.each {
deleteChildDevice(it.deviceNetworkId) // 'it' is default
}
}
// called after Done is hit after selecting a Location
def initialize() {
log.debug "initialize"
updateDevices()
}
// Misc
Map apiRequestHeaders() {
return ["Authorization": "Bearer ${state.lifxAccessToken}",
"Accept": "application/json",
"Content-Type": "application/json",
"User-Agent": "SmartThings Integration"
]
}
// Requests
def logResponse(response) {
log.info("Status: ${response.status}")
log.info("Body: ${response.data}")
}
// API Requests
// logObject is because log doesn't work if this method is being called from a Device
def logErrors(options = [errorReturn: null, logObject: log], Closure c) {
try {
return c()
} catch (groovyx.net.http.HttpResponseException e) {
options.logObject.error("got error: ${e}, body: ${e.getResponse().getData()}")
if (e.statusCode == 401) { // token is expired
state.remove("lifxAccessToken")
options.logObject.warn "Access token is not valid"
}
return options.errorReturn
} catch (java.net.SocketTimeoutException e) {
options.logObject.warn "Connection timed out, not much we can do here"
return options.errorReturn
}
}
def apiGET(path) {
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 = [:]) {
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([]) {
def resp = apiGET("/lights/${selector}")
if (resp.status == 200) {
return resp.data
} else {
log.error("Non-200 from device list call. ${resp.status} ${resp.data}")
return []
}
}
}
Map locationOptions() {
def options = [:]
def devices = devicesList()
devices.each { device ->
options[device.location.id] = device.location.name
}
log.debug("Locations: ${options}")
return options
}
def devicesInLocation() {
return devicesList("location_id:${settings.selectedLocationId}")
}
// ensures the devices list is up to date
def updateDevices() {
if (!state.devices) {
state.devices = [:]
}
def devices = devicesInLocation()
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.product}")
def data = [
label: device.label,
level: Math.round((device.brightness ?: 1) * 100),
switch: device.connected ? device.power : "unreachable",
colorTemperature: device.color.kelvin
]
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(app.namespace, "LIFX Color Bulb", device.id, null, data)
} else {
childDevice = addChildDevice(app.namespace, "LIFX White Bulb", device.id, null, data)
}
}
}
getChildDevices().findAll { !selectors.contains("${it.deviceNetworkId}") }.each {
log.info("Deleting ${it.deviceNetworkId}")
deleteChildDevice(it.deviceNetworkId)
}
runIn(1, 'refreshDevices') // Asynchronously refresh devices so we don't block
}
def refreshDevices() {
log.info("Refreshing all devices...")
getChildDevices().each { device ->
device.refresh()
}
}

View File

@@ -34,20 +34,20 @@
* locks | lock | lock, unlock | locked, unlocked
* ---------------------+-------------------+-----------------------------+------------------------------------
*/
definition(
name: "Logitech Harmony (Connect)",
namespace: "smartthings",
author: "Juan Pablo Risso",
author: "SmartThings",
description: "Allows you to integrate your Logitech Harmony account with SmartThings.",
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,22 +385,24 @@ 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) {
log.warn "Connection to the hub timed out. Please restart the hub and try again."
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) {
@@ -410,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) {
@@ -422,7 +425,7 @@ def addDevice() {
} else {
log.trace "found ${d.displayName} with id $dni already exists"
}
}
}
}
def activity(dni,mode) {
@@ -430,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) {
@@ -457,9 +460,11 @@ def activity(dni,mode) {
activity(dni,mode)
} else {
msg = ex
state.aux = 0
state.aux = 0
}
}
} catch(Exception ex) {
msg = ex
}
runIn(10, "poll", [overwrite: true])
return msg
}
@@ -468,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") {
@@ -484,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")
@@ -507,28 +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) {
@@ -541,17 +548,19 @@ 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
} catch (java.net.SocketTimeoutException e) {
log.trace e
}
}
} catch(Exception e) {
log.trace e
}
}
return activity
}
@@ -559,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 (groovyx.net.http.HttpResponseException e) {
} catch(Exception e) {
log.trace e
}
}
}
}
return actname
}
@@ -576,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 (groovyx.net.http.HttpResponseException e) {
} catch(Exception e) {
log.trace e
}
}
}
}
return actid
}
@@ -596,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 (groovyx.net.http.HttpResponseException e) {
} catch(Exception e) {
log.trace e
}
}
}
}
return hubname
}
@@ -616,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
@@ -651,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

@@ -79,12 +79,9 @@ def scenePage(params=[:]) {
href "devicePage", title: "Show Device States", params: [sceneId:sceneId], description: "", state: sceneIsDefined(sceneId) ? "complete" : "incomplete"
}
if (sceneId == currentSceneId) {
section {
href "saveStatesPage", title: "Record Current Device States", params: [sceneId:sceneId], description: ""
}
}
section {
href "saveStatesPage", title: "Record Current Device States", params: [sceneId:sceneId], description: ""
}
}
}
@@ -225,7 +222,7 @@ private restoreStates(sceneId) {
if (type == "level") {
log.debug "${light.displayName} level is '$level'"
if (level != null) {
light.setLevel(value)
light.setLevel(level)
}
}
else if (type == "color") {

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 {
@@ -92,7 +92,7 @@ def mainPage() {
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

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

@@ -346,18 +346,20 @@ private getSensorJSON(id, key) {
def sensorUrl = "${wattvisionBaseURL()}/partners/smartthings/sensor_list?api_id=${id}&api_key=${key}"
httpGet(uri: sensorUrl) { response ->
httpGet(uri: sensorUrl) { response ->
def json = new org.json.JSONObject(response.data)
def sensors = [:]
state.sensors = json
json.each { sensorId, sensorName ->
response.data.each { sensorId, sensorName ->
sensors[sensorId] = sensorName
createChild(sensorId, sensorName)
}
}
state.sensors = sensors
return "success"
}
}
def createChild(sensorId, sensorName) {

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) {
@@ -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

@@ -0,0 +1,775 @@
/**
* Title: Withings Service Manager
* Description: Connect Your Withings Devices
*
* Author: steve
* Date: 1/9/15
*
*
* Copyright 2015 steve
*
* 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: "Withings Manager",
namespace: "smartthings",
author: "SmartThings",
description: "Connect With Withings",
category: "",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/withings.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/withings%402x.png",
iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Partner/withings%402x.png",
oauth: true
) {
appSetting "consumerKey"
appSetting "consumerSecret"
}
// ========================================================
// PAGES
// ========================================================
preferences {
page(name: "authPage")
}
def authPage() {
def installOptions = false
def description = "Required (tap to set)"
def authState
if (oauth_token()) {
// TODO: Check if it's valid
if (true) {
description = "Saved (tap to change)"
installOptions = true
authState = "complete"
} else {
// Worth differentiating here? (no longer valid vs. non-existent state.externalAuthToken?)
description = "Required (tap to set)"
}
}
dynamicPage(name: "authPage", install: installOptions, uninstall: true) {
section {
if (installOptions) {
input(name: "withingsLabel", type: "text", title: "Add a name", description: null, required: true)
}
href url: shortUrl("authenticate"), style: "embedded", required: false, title: "Authenticate with Withings", description: description, state: authState
}
}
}
// ========================================================
// MAPPINGS
// ========================================================
mappings {
path("/authenticate") {
action:
[
GET: "authenticate"
]
}
path("/x") {
action:
[
GET: "exchangeTokenFromWithings"
]
}
path("/n") {
action:
[POST: "notificationReceived"]
}
path("/test/:action") {
action:
[GET: "test"]
}
}
def test() {
"${params.action}"()
}
def authenticate() {
// do not hit userAuthorizationUrl when the page is executed. It will replace oauth_tokens
// instead, redirect through here so we know for sure that the user wants to authenticate
// plus, the short-lived tokens that are used during authentication are only valid for 2 minutes
// so make sure we give the user as much of that 2 minutes as possible to enter their credentials and deal with network latency
log.trace "starting Withings authentication flow"
redirect location: userAuthorizationUrl()
}
def exchangeTokenFromWithings() {
// Withings hits us here during the oAuth flow
// log.trace "exchangeTokenFromWithings ${params}"
atomicState.userid = params.userid // TODO: restructure this for multi-user access
exchangeToken()
}
def notificationReceived() {
// log.trace "notificationReceived params: ${params}"
def notificationParams = [
startdate: params.startdate,
userid : params.userid,
enddate : params.enddate,
]
def measures = wGetMeasures(notificationParams)
sendMeasureEvents(measures)
return [status: 0]
}
// ========================================================
// HANDLERS
// ========================================================
def installed() {
log.debug "Installed with settings: ${settings}"
initialize()
}
def updated() {
log.debug "Updated with settings: ${settings}"
// wRevokeAllNotifications()
unsubscribe()
initialize()
}
def initialize() {
if (!getChild()) { createChild() }
app.updateLabel(withingsLabel)
wCreateNotification()
backfillMeasures()
}
// ========================================================
// CHILD DEVICE
// ========================================================
private getChild() {
def children = childDevices
children.size() ? children.first() : null
}
private void createChild() {
def child = addChildDevice("smartthings", "Withings User", userid(), null, [name: app.label, label: withingsLabel])
atomicState.child = [dni: child.deviceNetworkId]
}
// ========================================================
// URL HELPERS
// ========================================================
def stBaseUrl() {
if (!atomicState.serverUrl) {
stToken()
atomicState.serverUrl = buildActionUrl("").split(/api\//).first()
}
return atomicState.serverUrl
}
def stToken() {
atomicState.accessToken ?: createAccessToken()
}
def shortUrl(path = "", urlParams = [:]) {
attachParams("${stBaseUrl()}api/t/${stToken()}/s/${app.id}/${path}", urlParams)
}
def noTokenUrl(path = "", urlParams = [:]) {
attachParams("${stBaseUrl()}api/smartapps/installations/${app.id}/${path}", urlParams)
}
def attachParams(url, urlParams = [:]) {
[url, toQueryString(urlParams)].findAll().join("?")
}
String toQueryString(Map m = [:]) {
// log.trace "toQueryString. URLEncoder will be used on ${m}"
return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&")
}
// ========================================================
// WITHINGS MEASURES
// ========================================================
def unixTime(date = new Date()) {
def unixTime = date.time / 1000 as int
// log.debug "converting ${date.time} to ${unixTime}"
unixTime
}
def backfillMeasures() {
// log.trace "backfillMeasures"
def measureParams = [startdate: unixTime(new Date() - 10)]
def measures = wGetMeasures(measureParams)
sendMeasureEvents(measures)
}
// this is body measures. // TODO: get activity and others too
def wGetMeasures(measureParams = [:]) {
def baseUrl = "https://wbsapi.withings.net/measure"
def urlParams = [
action : "getmeas",
userid : userid(),
startdate : unixTime(new Date() - 5),
enddate : unixTime(),
oauth_token: oauth_token()
] + measureParams
def measureData = fetchDataFromWithings(baseUrl, urlParams)
// log.debug "measureData: ${measureData}"
measureData.body.measuregrps.collect { parseMeasureGroup(it) }.flatten()
}
/*
[
body:[
measuregrps:[
[
category:1, // 1 for real measurements, 2 for user objectives.
grpid:310040317,
measures:[
[
unit:0, // Power of ten the "value" parameter should be multiplied to to get the real value. Eg : value = 20 and unit=-1 means the value really is 2.0
value:60, // Value for the measure in S.I units (kilogram, meters, etc.). Value should be multiplied by 10 to the power of "unit" (see below) to get the real value.
type:11 // 1 : Weight (kg), 4 : Height (meter), 5 : Fat Free Mass (kg), 6 : Fat Ratio (%), 8 : Fat Mass Weight (kg), 9 : Diastolic Blood Pressure (mmHg), 10 : Systolic Blood Pressure (mmHg), 11 : Heart Pulse (bpm), 54 : SP02(%)
],
[
unit:-3,
value:-1000,
type:18
]
],
date:1422750210,
attrib:2
]
],
updatetime:1422750227
],
status:0
]
*/
def sendMeasureEvents(measures) {
// log.debug "measures: ${measures}"
measures.each {
if (it.name && it.value) {
sendEvent(userid(), it)
}
}
}
def parseMeasureGroup(measureGroup) {
long time = measureGroup.date // must be long. INT_MAX is too small
time *= 1000
measureGroup.measures.collect { parseMeasure(it) + [date: new Date(time)] }
}
def parseMeasure(measure) {
// log.debug "parseMeasure($measure)"
[
name : measureAttribute(measure),
value: measureValue(measure)
]
}
def measureValue(measure) {
def value = measure.value * 10.power(measure.unit)
if (measure.type == 1) { // Weight (kg)
value *= 2.20462262 // kg to lbs
}
value
}
String measureAttribute(measure) {
def attribute = ""
switch (measure.type) {
case 1: attribute = "weight"; break;
case 4: attribute = "height"; break;
case 5: attribute = "leanMass"; break;
case 6: attribute = "fatRatio"; break;
case 8: attribute = "fatMass"; break;
case 9: attribute = "diastolicPressure"; break;
case 10: attribute = "systolicPressure"; break;
case 11: attribute = "heartPulse"; break;
case 54: attribute = "SP02"; break;
}
return attribute
}
String measureDescription(measure) {
def description = ""
switch (measure.type) {
case 1: description = "Weight (kg)"; break;
case 4: description = "Height (meter)"; break;
case 5: description = "Fat Free Mass (kg)"; break;
case 6: description = "Fat Ratio (%)"; break;
case 8: description = "Fat Mass Weight (kg)"; break;
case 9: description = "Diastolic Blood Pressure (mmHg)"; break;
case 10: description = "Systolic Blood Pressure (mmHg)"; break;
case 11: description = "Heart Pulse (bpm)"; break;
case 54: description = "SP02(%)"; break;
}
return description
}
// ========================================================
// WITHINGS NOTIFICATIONS
// ========================================================
def wNotificationBaseUrl() { "https://wbsapi.withings.net/notify" }
def wNotificationCallbackUrl() { shortUrl("n") }
def wGetNotification() {
def userId = userid()
def url = wNotificationBaseUrl()
def params = [
action: "subscribe"
]
}
// TODO: keep track of notification expiration
def wCreateNotification() {
def baseUrl = wNotificationBaseUrl()
def urlParams = [
action : "subscribe",
userid : userid(),
callbackurl: wNotificationCallbackUrl(),
oauth_token: oauth_token(),
comment : "hmm" // TODO: figure out what to do here. spaces seem to break the request
]
fetchDataFromWithings(baseUrl, urlParams)
}
def wRevokeAllNotifications() {
def notifications = wListNotifications()
notifications.each {
wRevokeNotification([callbackurl: it.callbackurl]) // use the callbackurl Withings has on file
}
}
def wRevokeNotification(notificationParams = [:]) {
def baseUrl = wNotificationBaseUrl()
def urlParams = [
action : "revoke",
userid : userid(),
callbackurl: wNotificationCallbackUrl(),
oauth_token: oauth_token()
] + notificationParams
fetchDataFromWithings(baseUrl, urlParams)
}
def wListNotifications() {
/*
{
body: {
profiles: [
{
appli: 1,
expires: 2147483647,
callbackurl: "https://graph.api.smartthings.com/api/t/72ab3e57-5839-4cca-9562-dcc818f83bc9/s/537757a0-c4c8-40ea-8cea-aa283915bbd9/n",
comment: "hmm"
}
]
},
status: 0
}*/
def baseUrl = wNotificationBaseUrl()
def urlParams = [
action : "list",
userid : userid(),
callbackurl: wNotificationCallbackUrl(),
oauth_token: oauth_token()
]
def notificationData = fetchDataFromWithings(baseUrl, urlParams)
notificationData.body.profiles
}
def defaultOauthParams() {
defaultParameterKeys().inject([:]) { keyMap, currentKey ->
keyMap[currentKey] = "${currentKey}"()
keyMap
}
}
// ========================================================
// WITHINGS DATA FETCHING
// ========================================================
def fetchDataFromWithings(baseUrl, urlParams) {
// log.debug "fetchDataFromWithings(${baseUrl}, ${urlParams})"
def defaultParams = defaultOauthParams()
def paramStrings = buildOauthParams(urlParams + defaultParams)
// log.debug "paramStrings: $paramStrings"
def url = buildOauthUrl(baseUrl, paramStrings, oauth_token_secret())
def json
// log.debug "about to make request to ${url}"
httpGet(uri: url, headers: ["Content-Type": "application/json"]) { response ->
json = new groovy.json.JsonSlurper().parse(response.data)
}
return json
}
// ========================================================
// WITHINGS OAUTH LOGGING
// ========================================================
def wLogEnabled() { false } // For troubleshooting Oauth flow
void wLog(message = "") {
if (!wLogEnabled()) { return }
def wLogMessage = atomicState.wLogMessage
if (wLogMessage.length()) {
wLogMessage += "\n|"
}
wLogMessage += message
atomicState.wLogMessage = wLogMessage
}
void wLogNew(seedMessage = "") {
if (!wLogEnabled()) { return }
def olMessage = atomicState.wLogMessage
if (oldMessage) {
log.debug "purging old wLogMessage: ${olMessage}"
}
atomicState.wLogMessage = seedMessage
}
String wLogMessage() {
if (!wLogEnabled()) { return }
def wLogMessage = atomicState.wLogMessage
atomicState.wLogMessage = ""
wLogMessage
}
// ========================================================
// WITHINGS OAUTH DESCRIPTION
// >>>>>> The user opens the authPage for this SmartApp
// STEP 1 get a token to be used in the url the user taps
// STEP 2 generate the url to be tapped by the user
// >>>>>> The user taps the url and logs in to Withings
// STEP 3 generate a token to be used for accessing user data
// STEP 4 access user data
// ========================================================
// ========================================================
// WITHINGS OAUTH STEP 1: get an oAuth "request token"
// ========================================================
def requestTokenUrl() {
wLogNew "WITHINGS OAUTH STEP 1: get an oAuth 'request token'"
def keys = defaultParameterKeys() + "oauth_callback"
def paramStrings = buildOauthParams(keys.sort())
buildOauthUrl("https://oauth.withings.com/account/request_token", paramStrings, "")
}
// ========================================================
// WITHINGS OAUTH STEP 2: End-user authorization
// ========================================================
def userAuthorizationUrl() {
// get url from Step 1
def tokenUrl = requestTokenUrl()
// collect token from Withings
collectTokenFromWithings(tokenUrl)
wLogNew "WITHINGS OAUTH STEP 2: End-user authorization"
def keys = defaultParameterKeys() + "oauth_token"
def paramStrings = buildOauthParams(keys.sort())
buildOauthUrl("https://oauth.withings.com/account/authorize", paramStrings, oauth_token_secret())
}
// ========================================================
// WITHINGS OAUTH STEP 3: Generating access token
// ========================================================
def exchangeTokenUrl() {
wLogNew "WITHINGS OAUTH STEP 3: Generating access token"
def keys = defaultParameterKeys() + ["oauth_token", "userid"]
def paramStrings = buildOauthParams(keys.sort())
buildOauthUrl("https://oauth.withings.com/account/access_token", paramStrings, oauth_token_secret())
}
def exchangeToken() {
def tokenUrl = exchangeTokenUrl()
// log.debug "about to hit ${tokenUrl}"
try {
// replace old token with a long-lived token
def token = collectTokenFromWithings(tokenUrl)
// log.debug "collected token from Withings: ${token}"
renderAction("authorized", "Withings Connection")
}
catch (Exception e) {
log.error e
renderAction("notAuthorized", "Withings Connection Failed")
}
}
// ========================================================
// OAUTH 1.0
// ========================================================
def defaultParameterKeys() {
[
"oauth_consumer_key",
"oauth_nonce",
"oauth_signature_method",
"oauth_timestamp",
"oauth_version"
]
}
def oauth_consumer_key() { consumerKey }
def oauth_nonce() { nonce() }
def nonce() { UUID.randomUUID().toString().replaceAll("-", "") }
def oauth_signature_method() { "HMAC-SHA1" }
def oauth_timestamp() { (int) (new Date().time / 1000) }
def oauth_version() { 1.0 }
def oauth_callback() { shortUrl("x") }
def oauth_token() { atomicState.wToken?.oauth_token }
def oauth_token_secret() { atomicState.wToken?.oauth_token_secret }
def userid() { atomicState.userid }
String hmac(String oAuthSignatureBaseString, String oAuthSecret) throws java.security.SignatureException {
if (!oAuthSecret.contains("&")) { log.warn "Withings requires \"&\" to be included no matter what" }
// get an hmac_sha1 key from the raw key bytes
def signingKey = new javax.crypto.spec.SecretKeySpec(oAuthSecret.getBytes(), "HmacSHA1")
// get an hmac_sha1 Mac instance and initialize with the signing key
def mac = javax.crypto.Mac.getInstance("HmacSHA1")
mac.init(signingKey)
// compute the hmac on input data bytes
byte[] rawHmac = mac.doFinal(oAuthSignatureBaseString.getBytes())
return org.apache.commons.codec.binary.Base64.encodeBase64String(rawHmac)
}
Map parseResponseString(String responseString) {
// log.debug "parseResponseString: ${responseString}"
responseString.split("&").inject([:]) { c, it ->
def parts = it.split('=')
def k = parts[0]
def v = parts[1]
c[k] = v
return c
}
}
String applyParams(endpoint, oauthParams) { endpoint + "?" + oauthParams.sort().join("&") }
String buildSignature(endpoint, oAuthParams, oAuthSecret) {
def oAuthSignatureBaseParts = ["GET", endpoint, oAuthParams.join("&")]
def oAuthSignatureBaseString = oAuthSignatureBaseParts.collect { URLEncoder.encode(it) }.join("&")
wLog " ==> oAuth signature base string : \n${oAuthSignatureBaseString}"
wLog " .. applying hmac-sha1 to base string, with secret : ${oAuthSecret} (notice the \"&\")"
wLog " .. base64 encode then url-encode the hmac-sha1 hash"
String hmacResult = hmac(oAuthSignatureBaseString, oAuthSecret)
def signature = URLEncoder.encode(hmacResult)
wLog " ==> oauth_signature = ${signature}"
return signature
}
List buildOauthParams(List parameterKeys) {
wLog " .. adding oAuth parameters : "
def oauthParams = []
parameterKeys.each { key ->
def value = "${key}"()
wLog " ${key} = ${value}"
oauthParams << "${key}=${URLEncoder.encode(value.toString())}"
}
wLog " .. sorting all request parameters alphabetically "
oauthParams.sort()
}
List buildOauthParams(Map parameters) {
wLog " .. adding oAuth parameters : "
def oauthParams = []
parameters.each { k, v ->
wLog " ${k} = ${v}"
oauthParams << "${k}=${URLEncoder.encode(v.toString())}"
}
wLog " .. sorting all request parameters alphabetically "
oauthParams.sort()
}
String buildOauthUrl(String endpoint, List parameterStrings, String oAuthTokenSecret) {
wLog "Api endpoint : ${endpoint}"
wLog "Signing request :"
def oAuthSecret = "${consumerSecret}&${oAuthTokenSecret}"
def signature = buildSignature(endpoint, parameterStrings, oAuthSecret)
parameterStrings << "oauth_signature=${signature}"
def finalUrl = applyParams(endpoint, parameterStrings)
wLog "Result: ${finalUrl}"
if (wLogEnabled()) {
log.debug wLogMessage()
}
return finalUrl
}
def collectTokenFromWithings(tokenUrl) {
// get token from Withings using the url generated in Step 1
def tokenString
httpGet(uri: tokenUrl) { resp -> // oauth_token=<token_key>&oauth_token_secret=<token_secret>
tokenString = resp.data.toString()
// log.debug "collectTokenFromWithings: ${tokenString}"
}
def token = parseResponseString(tokenString)
atomicState.wToken = token
return token
}
// ========================================================
// APP SETTINGS
// ========================================================
def getConsumerKey() { appSettings.consumerKey }
def getConsumerSecret() { appSettings.consumerSecret }
// figure out how to put this in settings
def getUserId() { atomicState.wToken?.userid }
// ========================================================
// HTML rendering
// ========================================================
def renderAction(action, title = "") {
log.debug "renderAction: $action"
renderHTML(title) {
head { "${action}HtmlHead"() }
body { "${action}HtmlBody"() }
}
}
def authorizedHtmlHead() {
log.trace "authorizedHtmlHead"
"""
<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: 560px;
padding: 40px;*/
/*background: #eee;*/
text-align: center;
}
img {
vertical-align: middle;
max-width:20%;
}
img:nth-child(2) {
margin: 0 30px;
}
p {
/*font-size: 1.2em;*/
font-family: 'Swiss 721 W01 Thin';
text-align: center;
color: #666666;
padding: 0 10px;
margin-bottom: 0;
}
/*
p:last-child {
margin-top: 0px;
}
*/
span {
font-family: 'Swiss 721 W01 Light';
}
</style>
"""
}
def authorizedHtmlBody() {
"""
<div class="container">
<img src="https://s3.amazonaws.com/smartapp-icons/Partner/withings@2x.png" alt="withings icon" />
<img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/connected-device-icn%402x.png" alt="connected device icon" />
<img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/st-logo%402x.png" alt="SmartThings logo" />
<p>Your Withings scale is now connected to SmartThings!</p>
<p>Click 'Done' to finish setup.</p>
</div>
"""
}
def notAuthorizedHtmlHead() {
log.trace "notAuthorizedHtmlHead"
authorizedHtmlHead()
}
def notAuthorizedHtmlBody() {
"""
<div class="container">
<p>There was an error connecting to SmartThings!</p>
<p>Click 'Done' to try again.</p>
</div>
"""
}

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

@@ -1,4 +1,4 @@
/**
/**
* Magic Home
*
* Copyright 2014 Tim Slagle
@@ -198,7 +198,7 @@
//set home mode when house is occupied
def setHome() {
sendOutOfDateNotification()
log.info("Setting Home Mode!!")
if(anyoneIsHome()) {
if(state.sunMode == "sunset"){
@@ -319,3 +319,14 @@
private hideOptionsSection() {
(starting || ending || days || modes) ? false : true
}
def sendOutOfDateNotification(){
if(!state.lastTime){
state.lastTime = (new Date() + 31).getTime()
sendNotification("Your version of Hello, Home Phrase Director is currently out of date. Please look for the new version of Hello, Home Phrase Director now called 'Routine Director' in the marketplace.")
}
else if (((new Date()).getTime()) >= state.lastTime){
sendNotification("Your version of Hello, Home Phrase Director is currently out of date. Please look for the new version of Hello, Home Phrase Director now called 'Routine Director' in the marketplace.")
state.lastTime = (new Date() + 31).getTime()
}
}

View File

@@ -0,0 +1,346 @@
/**
* Rotuine Director
*
*
* Changelog
*
* 2015-09-01
* --Added Contact Book
* --Removed references to phrases and replaced with routines
* --Added bool logic to inputs instead of enum for "yes" "no" options
* --Fixed halting error with code installation
*
* Copyright 2015 Tim Slagle
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at:
*
* 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: "Routine Director",
namespace: "tslagle13",
author: "Tim Slagle",
description: "Monitor a set of presence sensors and activate routines based on whether your home is empty or occupied. Each presence status change will check against the current 'sun state' to run routines based on occupancy and whether the sun is up or down.",
category: "Convenience",
iconUrl: "http://icons.iconarchive.com/icons/icons8/ios7/512/Very-Basic-Home-Filled-icon.png",
iconX2Url: "http://icons.iconarchive.com/icons/icons8/ios7/512/Very-Basic-Home-Filled-icon.png"
)
preferences {
page(name: "selectRoutines")
page(name: "Settings", title: "Settings", uninstall: true, install: true) {
section("False alarm threshold (defaults to 10 min)") {
input "falseAlarmThreshold", "decimal", title: "Number of minutes", required: false
}
section("Zip code (for sunrise/sunset)") {
input "zip", "text", required: true
}
section("Notifications") {
input "sendPushMessage", "bool", title: "Send notifications when house is empty?"
input "sendPushMessageHome", "bool", title: "Send notifications when home is occupied?"
}
section("Send Notifications?") {
input("recipients", "contact", title: "Send notifications to") {
input "phone", "phone", title: "Send an SMS to this number?", required:false
}
}
section(title: "More options", hidden: hideOptionsSection(), hideable: true) {
label title: "Assign a name", required: false
input "days", "enum", title: "Only on certain days of the week", multiple: true, required: false,
options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
input "modes", "mode", title: "Only when mode is", multiple: true, required: false
}
}
}
def selectRoutines() {
def configured = (settings.awayDay && settings.awayNight && settings.homeDay && settings.homeNight)
dynamicPage(name: "selectRoutines", title: "Configure", nextPage: "Settings", uninstall: true) {
section("Who?") {
input "people", "capability.presenceSensor", title: "Monitor These Presences", required: true, multiple: true, submitOnChange: true
}
def routines = location.helloHome?.getPhrases()*.label
if (routines) {
routines.sort()
section("Run This Routine When...") {
log.trace routines
input "awayDay", "enum", title: "Everyone Is Away And It's Day", required: true, options: routines, submitOnChange: true
input "awayNight", "enum", title: "Everyone Is Away And It's Night", required: true, options: routines, submitOnChange: true
input "homeDay", "enum", title: "At Least One Person Is Home And It's Day", required: true, options: routines, submitOnChange: true
input "homeNight", "enum", title: "At Least One Person Is Home And It's Night", required: true, options: routines, submitOnChange: true
}
/* section("Select modes used for each condition.") { This allows the director to know which rotuine has already been ran so it does not run again if someone else comes home.
input "homeModeDay", "mode", title: "Select Mode Used for 'Home Day'", required: true
input "homeModeNight", "mode", title: "Select Mode Used for 'Home Night'", required: true
}*/
}
}
}
def installed() {
log.debug "Updated with settings: ${settings}"
initialize()
}
def updated() {
log.debug "Updated with settings: ${settings}"
unsubscribe()
initialize()
}
def initialize() {
subscribe(people, "presence", presence)
checkSun()
subscribe(location, "sunrise", setSunrise)
subscribe(location, "sunset", setSunset)
state.homestate = null
}
//check current sun state when installed.
def checkSun() {
def zip = settings.zip as String
def sunInfo = getSunriseAndSunset(zipCode: zip)
def current = now()
if (sunInfo.sunrise.time < current && sunInfo.sunset.time > current) {
state.sunMode = "sunrise"
runIn(60,"setSunrise")
}
else {
state.sunMode = "sunset"
runIn(60,"setSunset")
}
}
//change to sunrise mode on sunrise event
def setSunrise(evt) {
state.sunMode = "sunrise";
changeSunMode(newMode);
log.debug "Current sun mode is ${state.sunMode}"
}
//change to sunset mode on sunset event
def setSunset(evt) {
state.sunMode = "sunset";
changeSunMode(newMode)
log.debug "Current sun mode is ${state.sunMode}"
}
//change mode on sun event
def changeSunMode(newMode) {
if (allOk) {
if (everyoneIsAway()) /*&& (state.sunMode == "sunrise")*/ {
log.info("Home is Empty Setting New Away Mode")
def delay = (falseAlarmThreshold != null && falseAlarmThreshold != "") ? falseAlarmThreshold * 60 : 10 * 60
setAway()
}
/*
else if (everyoneIsAway() && (state.sunMode == "sunset")) {
log.info("Home is Empty Setting New Away Mode")
def delay = (falseAlarmThreshold != null && falseAlarmThreshold != "") ? falseAlarmThreshold * 60 : 10 * 60
setAway()
}*/
else if (anyoneIsHome()) {
log.info("Home is Occupied Setting New Home Mode")
setHome()
}
}
}
//presence change run logic based on presence state of home
def presence(evt) {
if (allOk) {
if (evt.value == "not present") {
log.debug("Checking if everyone is away")
if (everyoneIsAway()) {
log.info("Nobody is home, running away sequence")
def delay = (falseAlarmThreshold != null && falseAlarmThreshold != "") ? falseAlarmThreshold * 60 : 10 * 60
runIn(delay, "setAway")
}
}
else {
def lastTime = state[evt.deviceId]
if (lastTime == null || now() - lastTime >= 1 * 60000) {
log.info("Someone is home, running home sequence")
setHome()
}
state[evt.deviceId] = now()
}
}
}
//if empty set home to one of the away modes
def setAway() {
if (everyoneIsAway()) {
if (state.sunMode == "sunset") {
def message = "Performing \"${awayNight}\" for you as requested."
log.info(message)
sendAway(message)
location.helloHome.execute(settings.awayNight)
state.homestate = "away"
}
else if (state.sunMode == "sunrise") {
def message = "Performing \"${awayDay}\" for you as requested."
log.info(message)
sendAway(message)
location.helloHome.execute(settings.awayDay)
state.homestate = "away"
}
else {
log.debug("Mode is the same, not evaluating")
}
}
}
//set home mode when house is occupied
def setHome() {
log.info("Setting Home Mode!!")
if (anyoneIsHome()) {
if (state.sunMode == "sunset") {
if (state.homestate != "homeNight") {
def message = "Performing \"${homeNight}\" for you as requested."
log.info(message)
sendHome(message)
location.helloHome.execute(settings.homeNight)
state.homestate = "homeNight"
}
}
if (state.sunMode == "sunrise") {
if (state.homestate != "homeDay") {
def message = "Performing \"${homeDay}\" for you as requested."
log.info(message)
sendHome(message)
location.helloHome.execute(settings.homeDay)
state.homestate = "homeDay"
}
}
}
}
private everyoneIsAway() {
def result = true
if(people.findAll { it?.currentPresence == "present" }) {
result = false
}
log.debug("everyoneIsAway: ${result}")
return result
}
private anyoneIsHome() {
def result = false
if(people.findAll { it?.currentPresence == "present" }) {
result = true
}
log.debug("anyoneIsHome: ${result}")
return result
}
def sendAway(msg) {
if (sendPushMessage) {
if (recipients) {
sendNotificationToContacts(msg, recipients)
}
else {
sendPush(msg)
if(phone){
sendSms(phone, msg)
}
}
}
log.debug(msg)
}
def sendHome(msg) {
if (sendPushMessageHome) {
if (recipients) {
sendNotificationToContacts(msg, recipients)
}
else {
sendPush(msg)
if(phone){
sendSms(phone, msg)
}
}
}
log.debug(msg)
}
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 getTimeIntervalLabel() {
(starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z"): ""
}
private hideOptionsSection() {
(starting || ending || days || modes) ? false: true
}

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
)

View File

@@ -0,0 +1,307 @@
/**
* WeatherBug Home
*
* Copyright 2015 WeatherBug
*
*/
definition(
name: "WeatherBug Home",
namespace: "WeatherBug",
author: "WeatherBug Home",
description: "WeatherBug Home",
category: "My Apps",
iconUrl: "http://stg.static.myenergy.enqa.co/apps/wbhc/v2/images/weatherbughomemedium.png",
iconX2Url: "http://stg.static.myenergy.enqa.co/apps/wbhc/v2/images/weatherbughomemedium.png",
iconX3Url: "http://stg.static.myenergy.enqa.co/apps/wbhc/v2/images/weatherbughome.png",
oauth: [displayName: "WeatherBug Home", displayLink: "http://weatherbughome.com/"])
preferences {
section("Select thermostats") {
input "thermostatDevice", "capability.thermostat", multiple: true
}
}
mappings {
path("/appInfo") { action: [ GET: "getAppInfo" ] }
path("/getLocation") { action: [ GET: "getLoc" ] }
path("/currentReport/:id") { action: [ GET: "getCurrentReport" ] }
path("/setTemp/:temp/:id") { action: [ POST: "setTemperature", GET: "setTemperature" ] }
}
/**
* This API call will be leveraged by a WeatherBug Home Service to retrieve
* data from the installed SmartApp, including the location data, and
* a list of the devices that were authorized to be accessed. The WeatherBug
* Home Service will leverage this data to represent the connected devices as well as their
* location and associated the data with a WeatherBug user account.
* Privacy Policy: http://weatherbughome.com/privacy/
* @return Location, including id, latitude, longitude, zip code, and name, and the list of devices
*/
def getAppInfo() {
def devices = thermostatDevice
def lat = location.latitude
def lon = location.longitude
if(!(devices instanceof Collection))
{
devices = [devices]
}
return [
Id: UUID.randomUUID().toString(),
Code: 200,
ErrorMessage: null,
Result: [ "Devices": devices,
"Location":[
"Id": location.id,
"Latitude":lat,
"Longitude":lon,
"ZipCode":location.zipCode,
"Name":location.name
]
]
]
}
/**
* This API call will be leveraged by a WeatherBug Home Service to retrieve
* location data from the installed SmartApp. The WeatherBug
* Home Service will leverage this data to associate the location to a WeatherBug Home account
* Privacy Policy: http://weatherbughome.com/privacy/
*
* @return Location, including id, latitude, longitude, zip code, and name
*/
def getLoc() {
return [
Id: UUID.randomUUID().toString(),
Code: 200,
ErrorMessage: null,
Result: [
"Id": location.id,
"Latitude":location.latitude,
"Longitude":location.longitude,
"ZipCode":location.zipCode,
"Name":location.name]
]
}
/**
* This API call will be leveraged by a WeatherBug Home Service to retrieve
* thermostat data and store it for display to a WeatherBug user.
* Privacy Policy: http://weatherbughome.com/privacy/
*
* @param id The id of the device to get data for
* @return Thermostat data including temperature, set points, running modes, and operating states
*/
def getCurrentReport() {
log.debug "device id parameter=" + params.id
def unixTime = (int)((new Date().getTime() / 1000))
def device = thermostatDevice.find{ it.id == params.id}
if(device == null)
{
return [
Id: UUID.randomUUID().toString(),
Code: 404,
ErrorMessage: "Device not found. id=" + params.id,
Result: null
]
}
return [
Id: UUID.randomUUID().toString(),
Code: 200,
ErrorMessage: null,
Result: [
DeviceId: device.id,
LocationId: location.id,
ReportType: 2,
ReportList: [
[Key: "Temperature", Value: GetOrDefault(device, "temperature")],
[Key: "ThermostatSetpoint", Value: GetOrDefault(device, "thermostatSetpoint")],
[Key: "CoolingSetpoint", Value: GetOrDefault(device, "coolingSetpoint")],
[Key: "HeatingSetpoint", Value: GetOrDefault(device, "heatingSetpoint")],
[Key: "ThermostatMode", Value: GetOrDefault(device, "thermostatMode")],
[Key: "ThermostatFanMode", Value: GetOrDefault(device, "thermostatFanMode")],
[Key: "ThermostatOperatingState", Value: GetOrDefault(device, "thermostatOperatingState")]
],
UnixTime: unixTime
]
]
}
/**
* This API call will be leveraged by a WeatherBug Home Service to set
* the thermostat setpoint.
* Privacy Policy: http://weatherbughome.com/privacy/
*
* @param id The id of the device to set
* @return Indication of whether the operation succeeded or failed
def setTemperature() {
log.debug "device id parameter=" + params.id
def device = thermostatDevice.find{ it.id == params.id}
if(device != null)
{
def mode = device.latestState('thermostatMode').stringValue
def value = params.temp as Integer
log.trace "Suggested temperature: $value, $mode"
if ( mode == "cool")
device.setCoolingSetpoint(value)
else if ( mode == "heat")
device.setHeatingSetpoint(value)
return [
Id: UUID.randomUUID().toString(),
Code: 200,
ErrorMessage: null,
Result: null
]
}
return [
Id: UUID.randomUUID().toString(),
Code : 404,
ErrorMessage: "Device not found. id=" + params.id,
Result: null
]
}
*/
def installed() {
log.debug "Installed with settings: ${settings}"
initialize()
}
/**
* The updated event will be pushed to a WeatherBug Home Service to notify the system to take appropriate action.
* Data that will be sent includes the list of devices, and location data
* Privacy Policy: http://weatherbughome.com/privacy/
*/
def updated() {
log.debug "Updated with settings: ${settings}"
log.debug "Updated with state: ${state}"
log.debug "Updated with location ${location} ${location.id} ${location.name}"
unsubscribe()
initialize()
def postParams = [
uri: 'https://smartthingsrec.api.earthnetworks.com/api/v1/receive/smartapp/update',
body: [
"Devices": devices,
"Location":[
"Id": location.id,
"Latitude":location.latitude,
"Longitude":location.longitude,
"ZipCode":location.zipCode,
"Name":location.name
]
]
]
sendToWeatherBug(postParams)
}
/*
* Subscribe to changes on the thermostat attributes
*/
def initialize() {
log.trace "initialize enter"
subscribe(thermostatDevice, "heatingSetpoint", pushLatest)
subscribe(thermostatDevice, "coolingSetpoint", pushLatest)
subscribe(thermostatDevice, "thermostatSetpoint", pushLatest)
subscribe(thermostatDevice, "thermostatMode", pushLatest)
subscribe(thermostatDevice, "thermostatFanMode", pushLatest)
subscribe(thermostatDevice, "thermostatOperatingState", pushLatest)
subscribe(thermostatDevice, "temperature", pushLatest)
}
/**
* The uninstall event will be pushed to a WeatherBug Home Service to notify the system to take appropriate action.
* Data that will be sent includes the list of devices, and location data
* Privacy Policy: http://weatherbughome.com/privacy/
*/
def uninstalled() {
log.trace "uninstall entered"
def postParams = [
uri: 'https://smartthingsrec.api.earthnetworks.com/api/v1/receive/smartapp/delete',
body: [
"Devices": devices,
"Location":[
"Id": location.id,
"Latitude":location.latitude,
"Longitude":location.longitude,
"ZipCode":location.zipCode,
"Name":location.name
]
]
]
sendToWeatherBug(postParams)
}
/**
* This method will push the latest thermostat data to the WeatherBug Home Service so it can store
* and display the data to the WeatherBug user. Data pushed includes the thermostat data as well
* as location id.
* Privacy Policy: http://weatherbughome.com/privacy/
*/
def pushLatest(evt) {
def unixTime = (int)((new Date().getTime() / 1000))
def device = thermostatDevice.find{ it.id == evt.deviceId}
def postParams = [
uri: 'https://smartthingsrec.api.earthnetworks.com/api/v1/receive',
body: [
DeviceId: evt.deviceId,
LocationId: location.id,
ReportType: 2,
ReportList: [
[Key: "Temperature", Value: GetOrDefault(device, "temperature")],
[Key: "ThermostatSetpoint", Value: GetOrDefault(device, "thermostatSetpoint")],
[Key: "CoolingSetpoint", Value: GetOrDefault(device, "coolingSetpoint")],
[Key: "HeatingSetpoint", Value: GetOrDefault(device, "heatingSetpoint")],
[Key: "ThermostatMode", Value: GetOrDefault(device, "thermostatMode")],
[Key: "ThermostatFanMode", Value: GetOrDefault(device, "thermostatFanMode")],
[Key: "ThermostatOperatingState", Value: GetOrDefault(device, "thermostatOperatingState")]
],
UnixTime: unixTime
]
]
log.debug postParams
sendToWeatherBug(postParams)
}
/*
* This method attempts to get the value of a device attribute, but if an error occurs null is returned
* @return The device attribute value, or null
*/
def GetOrDefault(device, attrib)
{
def val
try{
val = device.latestValue(attrib)
}catch(ex)
{
log.debug "Failed to get attribute " + attrib + " from device " + device
val = null
}
return val
}
/*
* Convenience method that sends data to WeatherBug, logging any exceptions that may occur
* Privacy Policy: http://weatherbughome.com/privacy/
*/
def sendToWeatherBug(postParams)
{
try{
log.debug postParams
httpPostJson(postParams) { resp ->
resp.headers.each {
log.debug "${it.name} : ${it.value}"
}
log.debug "response contentType: ${resp.contentType}"
log.debug "response data: ${resp.data}"
}
log.debug "Communication with WeatherBug succeeded";
}catch(ex)
{
log.debug "Communication with WeatherBug failed.\n${ex}";
}
}