Compare commits

..

137 Commits

Author SHA1 Message Date
Donald C. Kirker
0d0a3f5ebb Protect ternary operations in configure
Protect ternary operations against order of operations, resulting in incorrect math and math against null values.
2016-07-26 06:32:23 -07:00
Donald C. Kirker
be7fad76fc Add updated Aeon Multisensor 6 fingerprint 2016-07-26 05:30:31 -07:00
Steve Vlaminck
f0e87fa5e9 Merge pull request #1070 from vlaminck/DVCSMP-1904
Fix: Gentle Wake Up requires contact input for Contact Book users [DVCSMP-1904]
2016-07-22 08:23:51 -05:00
vlaminck
1b385afa5b Fix: Gentle Wake Up requires contact input for Contact Book users 2016-07-22 08:16:42 -05:00
Vinay Rao
863c49ffd4 Merge pull request #1066 from SmartThingsCommunity/staging
Rolling down staging hotfix to production
2016-07-19 15:55:53 -07:00
Vinay Rao
2f0d8d814b Merge pull request #1064 from SmartThingsCommunity/production
Rolling down production hotfix to staging
2016-07-19 15:06:43 -07:00
Vinay Rao
a86eba494f Merge pull request #1063 from tslagle13/add-capability-SS-open/closed
Add capability sensor to SS Open/Closed
2016-07-18 15:46:21 -07:00
tslagle13
2549372bb7 Add capability sensor to SS Open/Closed
There are some integrations out there using the "Actuator" and "Sensor" Capabilities and this doesn't show up for them.
2016-07-18 15:20:06 -07:00
Vinay Rao
38cdde7479 Merge pull request #1057 from SmartThingsCommunity/DVCSMP-1892
DVCSMP-1892 Fix error in Z-Wave Door/Window Sensor wake up handler
2016-07-14 10:54:36 -07:00
Duncan McKee
853f616cc8 DVCSMP-1892 Fix error in Z-Wave Door/Window Sensor wake up handler 2016-07-14 13:51:36 -04:00
Vinay Rao
2b3a4e1278 Merge pull request #1055 from SmartThingsCommunity/staging
Rolling down staging hotfix to master
2016-07-13 19:45:29 -07:00
Vinay Rao
825e693efd Merge pull request #1054 from SmartThingsCommunity/production
Rolling down prod hotfix to staging
2016-07-13 19:44:57 -07:00
Vinay Rao
686c8f7337 Merge pull request #1053 from rappleg/FixFalseBatteryNotificationOnNewerGroovyVersion
PRP-192 Fix false low battery notifications on Groovy 2.4.4
2016-07-13 19:40:55 -07:00
rappleg
11f4e42fe9 PRP-192 Fix false low battery notifications on Groovy 2.4.4 2016-07-13 21:22:57 -05:00
Steve Vlaminck
bc459ae178 Merge pull request #1051 from vlaminck/GWU-exception-fix
fix divide by zero exception [DVCSMP-1891]
2016-07-13 10:01:10 -05:00
vlaminck
1392b6e1a5 fix divide by zero exception 2016-07-13 09:27:23 -05:00
Vinay Rao
3db96faa00 Merge pull request #1049 from SmartThingsCommunity/master
Rolling up master to staging
2016-07-12 15:04:26 -07:00
Vinay Rao
399fbcb676 Merge pull request #1048 from SmartThingsCommunity/staging
Rolling down staging hotfix to master
2016-07-12 15:03:28 -07:00
Vinay Rao
eed1ced71b Merge pull request #1047 from SmartThingsCommunity/staging
Rolling up staging for production deploy
2016-07-12 14:33:19 -07:00
Vinay Rao
d080833d5c Merge pull request #1046 from SmartThingsCommunity/production
Rolling down production hotfix to staging
2016-07-12 14:26:10 -07:00
Vinay Rao
e998528e8e Merge pull request #1041 from rappleg/ProductionFixJsonSlurperHashMap
PRP-172 Fix Hue Connect parse errors on newer versions of Groovy
2016-07-11 23:49:10 -07:00
Jack Chi
3472ee329d Merge pull request #1036 from jackchi/healthcheck-zwave-aeon
[CHF-138] Adds HealthCheck to Z-Wave Sensors
2016-07-11 11:21:30 -07:00
Juan Pablo Risso
577b127287 PRP-151 Harmony cloud fix + PENG-161 - Allow setLevel (#1035)
* PRP-151 - Harmony Cloud

Added is IP

Try around Integer.parseInt

declare int p outside try

* Remove unsubscribe & unschedule

* PENG-161 - Allow setLevel for switch capability

Last version
2016-07-11 10:04:32 -04:00
rappleg
d85566bb98 PRP-172 Fix Hue Connect parse errors on newer versions of Groovy 2016-07-10 15:35:12 -05:00
jackchi
5f41af35e2 [CHF-138] Adds HealthCheck to Z-Wave Sensors 2016-07-08 15:36:58 -07:00
Duncan McKee
e1a5b4dd27 Merge pull request #1034 from SmartThingsCommunity/DVCSMP-1879
DVCSMP-1879 Fix error in Z-Wave Door/Window Sensor throwing exceptions on update
2016-07-06 17:43:42 -04:00
Duncan McKee
a2baa37901 DVCSMP-1879 Fix error in Z-Wave Door/Window Sensor throwing exceptions on update 2016-07-06 17:08:15 -04:00
Vinay Rao
922ab45343 Merge pull request #1033 from SmartThingsCommunity/master
Rolling up master to staging for deploy
2016-07-06 13:59:03 -07:00
Vinay Rao
962774996e Merge pull request #1032 from SmartThingsCommunity/staging
Rolling down staging hotfix to master
2016-07-06 13:57:35 -07:00
Vinay Rao
d79594cbcb Merge pull request #1031 from SmartThingsCommunity/production
Rolling down production hotfix to staging
2016-07-06 13:56:39 -07:00
Vinay Rao
bf8fe4cad7 Merge pull request #1030 from rappleg/RevertLazyMapNewerGroovyVersion
Revert "PRP-172 Fix Hue Connect parse errors on newer versions of Groovy"
2016-07-06 13:53:32 -07:00
rappleg
65752ce378 Revert "PRP-172 Fix Hue Connect parse errors on newer versions of Groovy"
This reverts commit be7f6a76a9.
2016-07-06 14:44:58 -05:00
spurohit1
95f08aeb3d Update logitech-harmony-connect.groovy (#1009)
If port is present in the callbackUrl then send command to hub else post the request through http.
Removed isIP function since it is no longer required.
2016-07-06 09:40:29 -04:00
Vinay Rao
cd7bc1b262 Merge pull request #1023 from rappleg/FixHueConnectParseErrorsGrailsUpgradeProduction
PRP-172 Fix Hue Connect parse errors on newer versions of Groovy
2016-06-30 00:46:24 -07:00
rappleg
be7f6a76a9 PRP-172 Fix Hue Connect parse errors on newer versions of Groovy 2016-06-30 02:42:24 -05:00
Vinay Rao
10e5b7e9d7 Merge pull request #1021 from SmartThingsCommunity/master
Rolling up master to staging
2016-06-28 22:57:32 -07:00
Vinay Rao
fc38c534f9 Merge pull request #1020 from SmartThingsCommunity/staging
Rolling down staging hotfix to master
2016-06-28 21:04:33 -07:00
Vinay Rao
90e6dc91eb Merge pull request #1018 from SmartThingsCommunity/staging
Rolling up staging to production
2016-06-28 14:16:10 -07:00
Donald C. Kirker
2e502024a6 Merge pull request #1015 from SmartThingsCommunity/DVCSMP-1826
New handlers for Vision ZW+ Motion and Door/Window DVCSMP-1826 DVCSMP-1831
2016-06-28 10:13:59 -07:00
Steve Vlaminck
fd47bcb8a8 Merge pull request #1011 from vlaminck/gwu-exception-handling
Gwu exception handling
2016-06-23 12:26:42 -05:00
vlaminck
972599b1b5 Provide a way to remove unsupported devices from settings 2016-06-23 12:19:54 -05:00
vlaminck
f5d3cca6a0 Guard against devices that don't support necessary commands 2016-06-23 09:27:54 -05:00
Jack Chi
6d3ae11e44 Merge pull request #1008 from jackchi/healthcheck-open-close
[CHF-131] Adding Health Check to V1 SmartSense Open/Close Sensor
2016-06-22 11:03:45 -07:00
jackchi
dd63e76dfb [CHF-131] Adding Health Check to V1 SmartSense Open/Close Sensor 2016-06-22 10:19:40 -07:00
Duncan McKee
7d6f37d98f Z-Wave Plus Secure Sensors: reduce configure() delay for join v2 2016-06-22 11:12:10 -04:00
Vinay Rao
a015742d65 Merge pull request #1005 from SmartThingsCommunity/master
Rolling up master to staging
2016-06-21 13:42:00 -07:00
Vinay Rao
3ee8f86aa3 Merge pull request #1004 from SmartThingsCommunity/staging
Rolling up staging to prod
2016-06-21 13:19:59 -07:00
Juan Pablo Risso
9b285ec93b PRP-151 - Harmony Cloud (#1003)
- Added isIP() to check if a local or cloud callback URL
- Added try around Integer.parseInt
2016-06-21 13:19:53 -04:00
Duncan McKee
23f66e3caa Z-Wave Plus Door/Window Sensor DVCSMP-1831 2016-06-20 17:35:31 -04:00
Duncan McKee
7a44c59581 Z-Wave Plus Motion/Temp Sensor device type DVCSMP-1826 2016-06-18 15:48:56 -04:00
Duncan McKee
4d343d9bcf Z-Wave D/W: send requests secure encapsulated when necessary DVCSMP-1826 2016-06-17 21:49:08 -04:00
Juan Pablo Risso
777f8f7e20 PENG-161 - Logitech Harmony don't allow undefined commands (#965)
https://smartthings.atlassian.net/browse/PENG-161

extra )

New getCapabilityName()

Small fixes

Extra colon

capName

Added .id
2016-06-16 15:26:39 -04:00
Vinay Rao
de6d84acd2 Merge pull request #994 from SmartThingsCommunity/master
Rolling up master to staging
2016-06-14 15:59:24 -07:00
Vinay Rao
eac48382e8 Merge pull request #993 from SmartThingsCommunity/staging
Rolling down hotfix to master
2016-06-14 15:55:53 -07:00
Vinay Rao
d1a910f11f Merge pull request #992 from SmartThingsCommunity/staging
Rolling up staging to prod
2016-06-14 13:11:06 -07:00
Vinay Rao
9f09a4b0b2 Merge pull request #991 from SmartThingsCommunity/production
Rolling down hotfix
2016-06-14 13:10:03 -07:00
Donald C. Kirker
3c47fe7b60 Merge pull request #990 from dkirker/astra-devices
Add re-type for Vision Motion Sensor Plus.
2016-06-14 11:41:04 -07:00
Donald Kirker
45a0822e9b Add re-type for Vision Motion Sensor Plus. 2016-06-14 11:07:31 -07:00
Rohan Desai
8cc87f3858 Merge pull request #966 from rohandesai/PENG-160
PENG-160 Alfred workflow should not allow undefined commands
2016-06-13 14:17:10 -07:00
Vinay Rao
e818695947 Merge pull request #987 from SmartThingsCommunity/staging
Roll down hotfix
2016-06-13 10:50:29 -07:00
Vinay Rao
bbba20288e Merge pull request #986 from larsfinander/DVCSMP-1819_Philips_Hue_Incorrect_folder_name_for_Ambiance_bulb_DTH_staging
DVCSMP-1819 Philips Hue: Incorrect folder name for Ambiance bulb DTH
2016-06-13 10:49:34 -07:00
Lars Finander
ff9dd3f6e2 DVCSMP-1819 Philips Hue: Incorrect folder name for Ambiance bulb DTH
-Renamed folder to .src
2016-06-13 10:46:25 -07:00
Vinay Rao
a94a62d34c Merge pull request #982 from rohandesai/netatmo-hotfix
NPE fix for NetAtmo
2016-06-09 11:30:51 -07:00
Rohan Desai
e861d3c256 added fix for NPE 2016-06-09 11:04:55 -07:00
Vinay Rao
7adff88d0f Merge pull request #978 from SmartThingsCommunity/master
Rolling up master to staging
2016-06-07 16:47:52 -07:00
Vinay Rao
ad1f1b2dc9 Merge pull request #977 from SmartThingsCommunity/staging
Rolling down staging hotfix to master
2016-06-07 16:44:35 -07:00
Vinay Rao
8d8b039dda Merge pull request #975 from SmartThingsCommunity/staging
Rolling up staging to production
2016-06-07 12:23:55 -07:00
Duncan McKee
c875547942 Merge pull request #947 from SmartThingsCommunity/DPROT-110
DPROT-110 Add manufacturer fingerprints to Z-Wave Motion Sensor
2016-06-07 12:11:17 -04:00
Zach Varberg
1676a9c381 Merge pull request #942 from varzac/update-smartsense-multi
DVCSMP-1801: mfgCode in the ZigBee library for smartsense-multi-sensor
2016-06-07 09:50:39 -05:00
Rohan Desai
49d293e749 PENG-160 UBI should not allow undefined commands
- added some more changes

added changes to alfred workflow
2016-06-03 15:25:50 -07:00
Lars Finander
5d1b033486 Merge pull request #962 from larsfinander/DVCSMP-1676_Philips_Hue_timeout_message_search
DVCSMP-1676 Philips Hue: Need to provide timeout message if search ta…
2016-06-03 10:26:29 -07:00
Lars Finander
31f77513da DVCSMP-1676 Philips Hue: Need to provide timeout message if search takes too long.
DVCSMP-1675 Changed to show bridge serial number instead of IP
-Shows timeout screen after 5 minutes
-Removed old image code that didnt work for one of the pages
-Handled null pointer when adding unsupported devices
2016-06-02 21:13:00 -07:00
rohandesai
98d7829d1a Merge pull request #957 from rohandesai/peng-158-staging
PENG-158 UBI should not allow undefined commands
2016-06-02 14:33:49 -07:00
Vinay Rao
c6f706e47a Merge pull request #960 from SmartThingsCommunity/staging
Rolling down Staging to master
2016-06-02 14:30:49 -07:00
Vinay Rao
532afd7336 Merge pull request #959 from rohandesai/delete-prempoint
removed Prempoint from staging
2016-06-02 14:30:02 -07:00
Rohan Desai
ac7f1a0c66 removed Prempoint from staging 2016-06-02 14:24:29 -07:00
Lars Finander
cb26f055d7 Merge pull request #950 from larsfinander/DVCSMP-400_Philips_Hue_Not_Yet_Configured
DVCSMP-400 Philips Hue: Hue bridge & Bulbs displaying as Not Yet Conf…
2016-06-02 12:14:44 -07:00
Rohan Desai
34107f935e PENG-158 UBI should not allow undefined commands
- now validating commands per capability of the device in the smartapp

removed commented out code
2016-06-02 11:18:54 -07:00
Lars Finander
cc2d19e951 DVCSMP-400 Philips Hue: Hue bridge & Bulbs displaying as Not Yet Configured 2016-06-01 18:06:46 -07:00
rohandesai
031a15ec86 Merge pull request #897 from rohandesai/PENG-158
PENG-158 UBI should not allow undefined commands
2016-06-01 15:22:45 -07:00
Vinay Rao
fc2db2575d Merge pull request #949 from SmartThingsCommunity/staging
Rolling down hotfix to master
2016-06-01 12:18:54 -07:00
Vinay Rao
fd549631e6 Merge pull request #948 from SmartThingsCommunity/revert-828-stage2_v1_multi_garage
Revert "DVCSMP-1107 Stage 2 of adding preferences to support garage s…
2016-06-01 12:11:49 -07:00
Vinay Rao
417c246d61 Revert "DVCSMP-1107 Stage 2 of adding preferences to support garage sensor in v1 safari mutli sensor" 2016-06-01 12:09:38 -07:00
Zach Varberg
038d770691 Use mfgCode in the ZigBee library for smartsense-multi-sensor
With the change to the ZigBee library in appengine to add
the optional manufacturers code, the custom writing of
ZigBee commands can be replaced with calls to the library

This resolves https://smartthings.atlassian.net/browse/DVCSMP-1801
2016-06-01 10:23:30 -05:00
Duncan McKee
ce28ec2039 DPROT-110 Add manufacturer fingerprints to Z-Wave Motion Sensor 2016-05-31 18:29:40 -04:00
Vinay Rao
0f1781c02e Merge pull request #946 from SmartThingsCommunity/master
Rolling up master to staging
2016-05-31 14:22:14 -07:00
Vinay Rao
4ce6ee0890 Merge pull request #945 from SmartThingsCommunity/staging
Rolling down staging hotfixes to master
2016-05-31 14:14:13 -07:00
Vinay Rao
f8050a5cd5 Merge pull request #944 from SmartThingsCommunity/production
Rolling down production hotfix to staging
2016-05-31 14:07:40 -07:00
Jack Chi
d44dac448b Merge pull request #920 from jackchi/smartsense-motion-power
[CHF-74] Add Smartsense device-health related documentation
2016-05-31 11:24:42 -07:00
Vinay Rao
67c20abcba Merge pull request #828 from workingmonk/stage2_v1_multi_garage
DVCSMP-1107 Stage 2 of adding preferences to support garage sensor in v1 safari mutli sensor
2016-05-31 11:13:58 -07:00
Duncan McKee
d56e132896 Merge pull request #941 from SmartThingsCommunity/INTL-626-prod
INTL-626 Add mfr fingerprint for Fibaro Smoke Sensor
2016-05-31 14:08:48 -04:00
Duncan McKee
009ec2539d INTL-626 Add mfr fingerprint for Fibaro Smoke Sensor 2016-05-31 14:00:59 -04:00
Lars Finander
ff0860cbe1 Merge pull request #934 from larsfinander/DVCSMP-1795_Philips_Hue_Hue_Ambience_bulb
DVCSMP-1795 Philips Hue: Add support for Hue Ambience bulb
2016-05-27 09:57:24 -07:00
Lars Finander
ecfb99974b DVCSMP-1795 Philips Hue: Add support for Hue Ambience bulb
-Add support for Hue White Ambiance Bulb (white,dimmable+color temp)
2016-05-27 09:45:26 -07:00
Jason Botello
aa3a18f421 Merge pull request #865 from SmartThingsCommunity/MSA-1255-1
MSA-1255: Gidjit - Smart Launcher with SmartThings support
2016-05-25 15:13:55 -05:00
Vinay Rao
c2f18a91be Merge pull request #906 from SmartThingsCommunity/master
Rolling up master to staging for next week's deploy
2016-05-24 16:28:51 -07:00
Vinay Rao
fc6b14b85e Merge pull request #921 from SmartThingsCommunity/staging
Rolling down staging hotfix to master
2016-05-24 16:27:08 -07:00
jackchi
0c1208928f [CHF-74] Add Smartsense device-health related documentation for motion sensor 2016-05-24 15:17:40 -07:00
Vinay Rao
02d9963fab Merge pull request #919 from SmartThingsCommunity/staging
Rolling up staging to production for release May 24
2016-05-24 13:59:11 -07:00
Vinay Rao
f131fb71cf Merge pull request #918 from SmartThingsCommunity/production
Rolling down production hotfix to staging
2016-05-24 13:51:26 -07:00
Vinay Rao
9c27ed6cb7 Merge pull request #913 from SmartThingsCommunity/revert-904-revert-901-DVCSMP-1739-revert-untested-code
Revert "DVCSMP-1739 Revert "Undo DVCSMP-1739 Revert "make sure reformat the storeGraphData"""
2016-05-23 16:05:09 -07:00
Dwight Liu
35edaa19c7 Revert "DVCSMP-1739 Revert "Undo DVCSMP-1739 Revert "make sure reformat the storeGraphData""" 2016-05-23 14:38:40 -07:00
Vinay Rao
dc09201866 Merge pull request #912 from larsfinander/DVCSMP-1770_Add_MSR_to_zwave-switch_staging
DVCSMP-1770 Add MSR to zwave-switch and configure at pairing
2016-05-23 14:22:40 -07:00
Lars Finander
6afcbf8f70 DVCSMP-1770 Add MSR to zwave-switch and configure at pairing
-Make ZWave switch and Dimmer switch behave the same and save MSR and
manufacturer name in data
2016-05-23 14:13:10 -07:00
rohandesai
2894d52efa Merge pull request #909 from SmartThingsCommunity/MSA-1285-3
MSA-1285: May 2016 Submission
2016-05-20 13:39:33 -07:00
Will Price
a21f9f177c MSA-1285: Updated submission, already reviewed, just cleaning up the pull request. 2016-05-20 14:22:19 -05:00
rohandesai
02f968b8cb Merge pull request #889 from SmartThingsCommunity/MSA-1278-2
MSA-1278: Updates to dynamic object execution for security compliance
2016-05-20 10:30:48 -07:00
Rohan Desai
32f0385e30 PENG-158 UBI should not allow undefined commands
- now validating commands per capability of the device in the smartapp

removed commented out code
2016-05-19 11:59:13 -07:00
Juan Pablo Risso
b105d9d80e DVCSMP-1775 - Call poll() every 5 minutes to reduce duplicated calls (#902)
* DVCSMP-1775 - Call poll() every 5 minutes to reduce duplicated calls

Call poll() every 5 minutes instead of discovery() to reduce duplicated
getActivityList() call.

* Better error logging
2016-05-19 13:58:44 -04:00
Chelsea Okey
9bfad5d6a4 Merge pull request #908 from ch3lsea/INTL-527
INTL-527 Temperature Notifications Should Respect Location Temp Scale
2016-05-19 10:41:01 -05:00
chelseaokey
85a335d365 INTL-527 Temperature Notifications Should Respect Location Temp Scale 2016-05-19 09:59:45 -05:00
Jim Anderson
40e6778e31 Merge pull request #898 from jimmyjames/add-tiles-readme
DOCS-284 adding README for example/test tiles
2016-05-18 13:58:45 -05:00
Jim Anderson
feba6643a6 Merge pull request #893 from jimmyjames/tile-examples-default-state
DOCS-284 - update tiles for best practices, add setColor to colorWheel
2016-05-17 14:07:55 -05:00
Vinay Rao
49c893771d Merge pull request #905 from SmartThingsCommunity/staging
Rolling down staging hotfix to master
2016-05-17 10:06:48 -07:00
Vinay Rao
d73f4c2ded Merge pull request #904 from SmartThingsCommunity/revert-901-DVCSMP-1739-revert-untested-code
DVCSMP-1739 Revert "Undo DVCSMP-1739 Revert "make sure reformat the storeGraphData""
2016-05-17 10:01:27 -07:00
Vinay Rao
13a056ec8d Revert "Undo DVCSMP-1739 Revert "make sure reformat the storeGraphData"" 2016-05-17 09:59:03 -07:00
Lars Finander
f55452d1c6 Merge pull request #887 from larsfinander/Add_MSR_to_zwave-switch
DVCSMP-1770 Add MSR to zwave-switch and configure at pairing
2016-05-16 13:37:18 -07:00
rohandesai
14b5fd41b8 Merge pull request #895 from rohandesai/DVCSMP-1709
DVCSMP-1709 Android>SmartApps>PlatLinkConnector>Unable To Add
2016-05-16 13:18:22 -07:00
rohandesai
0f4d7bd520 Merge pull request #868 from rohandesai/DVCSMP-1699-life360
DVCSMP-1699 Na02>Missing/Failed Oauth Tokens - Life360
2016-05-16 13:18:13 -07:00
rohandesai
a95fbf2612 Merge pull request #867 from rohandesai/DVCSMP-1699
DVCSMP-1699 Na02>Missing/Failed Oauth Tokens - Netatmo
2016-05-16 13:17:57 -07:00
Jack Chi
0f60ca61cb Merge pull request #899 from jackchi/add-category-smartsense
[CHF-74] Add category to SmartSense DTH
2016-05-16 11:50:00 -07:00
jackchi
293a73136e [CHF-74] Add category to SmartSense DTH 2016-05-16 11:43:21 -07:00
Jim Anderson
26a0f6f939 DOCS-284 adding README for example/test tiles 2016-05-13 21:10:54 -05:00
Rohan Desai
8de3276ce6 DVCSMP-1709 Android>SmartApps>PlatLinkConnector>Unable To Add
- now getting the right shard url
2016-05-13 11:13:58 -07:00
Jim Anderson
f77c5810c6 DOCS-284 - update tiles for best practices, add setColor to colorWheel tile 2016-05-13 10:26:45 -05:00
Luke Bredeson
9fc5f14dd7 Merge pull request #235 from lbredeso/button-controller-dst-fix
SHARD-211: DST fix for Button Controller
2016-05-13 10:20:59 -05:00
Lars Finander
55d7a4a263 Merge pull request #886 from larsfinander/DVCSMP-1761_Remove_heartbeat
DVCSMP-1716 Remove heartbeat from device types
2016-05-12 21:01:17 -07:00
Matthew Hartley
56eef9cf22 MSA-1278: The Prempoint SmartApp is a service bridge to allow Prempoint users to import, share, and manage devices that are controlled by their SmartThings hub. 2016-05-12 14:28:40 -05:00
Rohan Desai
b33d621696 DVCSMP-1699 Na02>Missing/Failed Oauth Tokens
- now calling the method to get the right shard url for Life360
- removed serverUrl from the app settings
2016-05-12 11:25:36 -07:00
Rohan Desai
45b78eff8d DVCSMP-1699 Na02>Missing/Failed Oauth Tokens - Netatmo
- refactored the oauth component for the app to work in different shards, if supported
- fixed some white spacing issues and indents
- addressed comments
- edited spacing
2016-05-12 11:18:09 -07:00
Lars Finander
a133406b6e DVCSMP-1770 Add MSR to zwave-switch and configure at pairing
-Make ZWave switch and Dimmer switch behave the same and save MSR and
manufacturer name in data
2016-05-11 23:41:19 -07:00
Lars Finander
bd62962ee1 DVCSMP-1716 Remove heartbeat from device types
-Remove heartbeat from OSRAM white bulb and Smartpower Outlet
-Heartbeat was a hack previously used for Amazon Echo
2016-05-11 23:06:09 -07:00
Lars Finander
a5da182bf4 Merge pull request #861 from larsfinander/DVCSMP-1738_Phillips_HUE_brightness_level
DVCSMP-1738 Philips HUE: Detail page showing the Brightness level twice
2016-05-11 15:25:36 -07:00
Matthew Page
ab2ba8104d MSA-1255: The following app submitted will be an endpoint for our IOS app Gidjit.
Gidjit is like having a home screen based on your environment. It scans your surroundings looking for home devices, personal electronic devices, times and locations of interest, and more. It will present a list of corresponding launcher panels that allow you to quickly access actions like open an app, control/monitor a device, execute a SmartThings Routine, or link to the control/launch screen of a device. For example based on the users location they will quickly be able to access a preset routine. We are really looking forward to having SmartThings in integration with the app. 

Link: https://itunes.apple.com/us/app/gidjit-smart-launcher/id961388659?mt=8

The app currently will not have the SmartThing's support until the next version that will be released when this is hopefully approved, as we will also have to go through the Apple approval process allowing them to test the features.

Thank you for your time!

Matt
2016-05-07 16:51:30 -05:00
Lars Finander
27c05f4e5b DVCSMP-1738 Philips HUE: Detail page showing the Brightness level twice
-Removed secondary control from all Hue DTH
2016-05-06 09:29:41 -07:00
Vinay Rao
d17cadc4c7 stage 2 of adding preferences to support garage sensor in mutli sensor 2016-04-22 17:41:57 -07:00
Luke Bredeson
91c01dc643 DST fix for Button Controller 2015-10-29 12:35:28 -05:00
48 changed files with 1975 additions and 650 deletions

View File

@@ -22,12 +22,14 @@ metadata {
capability "Configuration"
capability "Sensor"
capability "Battery"
capability "Health Check"
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"
fingerprint deviceId: "0x2101", inClusters: "0x5E,0x86,0x72,0x59,0x85,0x73,0x71,0x84,0x80,0x30,0x31,0x70,0x7A,0x5A"
}
simulator {
@@ -326,6 +328,9 @@ def zwaveEvent(physicalgraph.zwave.Command cmd) {
}
def configure() {
// allow device user configured or default 16 min to check in; double the periodic reporting interval
sendEvent(name: "checkInterval", value: 2* (timeOptionValueMap[reportInterval] ?: (2*8*60)), displayed: false)
// This sensor joins as a secure device if you double-click the button to include it
log.debug "${device.displayName} is configuring its settings"
def request = []
@@ -352,7 +357,7 @@ def configure() {
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: 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

View File

@@ -20,6 +20,9 @@ metadata {
capability "Configuration"
capability "Sensor"
capability "Battery"
capability "Health Check"
command "configureAfterSecure"
fingerprint deviceId: "0x0701", inClusters: "0x5E,0x86,0x72,0x59,0x85,0x73,0x71,0x84,0x80,0x30,0x31,0x70,0x98,0x7A", outClusters:"0x5A"
}
@@ -245,6 +248,8 @@ def configureAfterSecure() {
def configure() {
// log.debug "configure()"
//["delay 30000"] + secure(zwave.securityV1.securityCommandsSupportedGet())
// allow device 16 min to check in; double the periodic reporting interval
sendEvent(name: "checkInterval", value: 2*8*60, displayed: false)
}
private setConfigured() {

View File

@@ -20,6 +20,7 @@ metadata {
capability "Illuminance Measurement"
capability "Sensor"
capability "Battery"
capability "Health Check"
fingerprint deviceId: "0x2001", inClusters: "0x30,0x31,0x80,0x84,0x70,0x85,0x72,0x86"
}
@@ -180,6 +181,9 @@ def zwaveEvent(physicalgraph.zwave.Command cmd) {
}
def configure() {
// allow device 10 min to check in; double the periodic reporting interval
sendEvent(name: "checkInterval", value: 2*5*60, displayed: false)
delayBetween([
// send binary sensor report instead of basic set for motion
zwave.configurationV1.configurationSet(parameterNumber: 5, size: 1, scaledConfigurationValue: 2).format(),

View File

@@ -138,6 +138,7 @@ def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerS
log.debug "productTypeId: ${cmd.productTypeId}"
def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId)
updateDataValue("MSR", msr)
updateDataValue("manufacturer", cmd.manufacturerName)
createEvent([descriptionText: "$device.displayName MSR: $msr", isStateChange: false])
}

View File

@@ -21,6 +21,7 @@ metadata {
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"
fingerprint mfr:"010F", prod:"0C02", model:"1002"
}
simulator {
//battery

View File

@@ -37,9 +37,6 @@ metadata {
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
attributeState "level", action:"switch level.setLevel", range:"(0..100)"
}
tileAttribute ("device.level", key: "SECONDARY_CONTROL") {
attributeState "level", label: 'Level ${currentValue}%'
}
tileAttribute ("device.color", key: "COLOR_CONTROL") {
attributeState "color", action:"setAdjustedColor"
}

View File

@@ -38,9 +38,6 @@ metadata {
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
attributeState "level", action:"switch level.setLevel", range:"(0..100)"
}
tileAttribute ("device.level", key: "SECONDARY_CONTROL") {
attributeState "level", label: 'Level ${currentValue}%'
}
tileAttribute ("device.color", key: "COLOR_CONTROL") {
attributeState "color", action:"setAdjustedColor"
}

View File

@@ -33,9 +33,6 @@ metadata {
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}%'
}
}
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false, range:"(0..100)") {

View File

@@ -0,0 +1,110 @@
/**
* Hue White Ambiance Bulb
*
* Philips Hue Type "Color Temperature Light"
*
* Author: SmartThings
*/
// for the UI
metadata {
// Automatically generated. Make future change here.
definition (name: "Hue White Ambiance Bulb", namespace: "smartthings", author: "SmartThings") {
capability "Switch Level"
capability "Actuator"
capability "Color Temperature"
capability "Switch"
capability "Refresh"
command "refresh"
}
simulator {
// TODO: define status and reply messages here
}
tiles (scale: 2){
multiAttributeTile(name:"rich-control", type: "lighting", width: 6, height: 4, canChangeIcon: true){
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
attributeState "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#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)"
}
}
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", width: 4, height: 2, inactiveLabel: false, range:"(2000..6500)") {
state "colorTemperature", action:"color temperature.setColorTemperature"
}
valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "colorTemperature", label: '${currentValue} K'
}
standardTile("refresh", "device.refresh", height: 2, width: 2, inactiveLabel: false, decoration: "flat") {
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
}
main(["rich-control"])
details(["rich-control", "colorTempSliderControl", "colorTemp", "refresh"])
}
}
// parse events into attributes
def parse(description) {
log.debug "parse() - $description"
def results = []
def map = description
if (description instanceof String) {
log.debug "Hue Ambience Bulb stringToMap - ${map}"
map = stringToMap(description)
}
if (map?.name && map?.value) {
results << createEvent(name: "${map?.name}", value: "${map?.value}")
}
results
}
// handle commands
void on() {
log.trace parent.on(this)
sendEvent(name: "switch", value: "on")
}
void off() {
log.trace parent.off(this)
sendEvent(name: "switch", value: "off")
}
void setLevel(percent) {
log.debug "Executing 'setLevel'"
if (percent != null && percent >= 0 && percent <= 100) {
parent.setLevel(this, percent)
sendEvent(name: "level", value: percent)
sendEvent(name: "switch", value: "on")
} else {
log.warn "$percent is not 0-100"
}
}
void setColorTemperature(value) {
if (value) {
log.trace "setColorTemperature: ${value}k"
parent.setColorTemperature(this, value)
sendEvent(name: "colorTemperature", value: value)
sendEvent(name: "switch", value: "on")
} else {
log.warn "Invalid color temperature"
}
}
void refresh() {
log.debug "Executing 'refresh'"
parent.manualRefresh()
}

View File

@@ -19,11 +19,6 @@ metadata {
capability "Sensor"
attribute "colorName", "string"
// indicates that device keeps track of heartbeat (in state.heartbeat)
attribute "heartbeat", "string"
}
// simulator metadata
@@ -75,9 +70,6 @@ metadata {
def parse(String description) {
//log.trace description
// save heartbeat (i.e. last time we got a message from device)
state.heartbeat = Calendar.getInstance().getTimeInMillis()
if (description?.startsWith("catchall:")) {
if(description?.endsWith("0100") ||description?.endsWith("1001") || description?.matches("on/off\\s*:\\s*1"))
{
@@ -132,7 +124,6 @@ def off() {
}
def refresh() {
sendEvent(name: "heartbeat", value: "alive", displayed:false)
[
"st rattr 0x${device.deviceNetworkId} ${endpointId} 6 0", "delay 500",
"st rattr 0x${device.deviceNetworkId} ${endpointId} 8 0", "delay 500",

View File

@@ -16,7 +16,7 @@
metadata {
// Automatically generated. Make future change here.
definition (name: "SmartPower Outlet", namespace: "smartthings", author: "SmartThings") {
definition (name: "SmartPower Outlet", namespace: "smartthings", author: "SmartThings", category: "C1") {
capability "Actuator"
capability "Switch"
capability "Power Meter"
@@ -25,9 +25,6 @@ metadata {
capability "Sensor"
capability "Health Check"
// 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"
@@ -81,9 +78,6 @@ metadata {
def parse(String description) {
log.debug "description is $description"
// save heartbeat (i.e. last time we got a message from device)
state.heartbeat = Calendar.getInstance().getTimeInMillis()
def finalResult = zigbee.getKnownDescription(description)
//TODO: Remove this after getKnownDescription can parse it automatically
@@ -124,7 +118,6 @@ def on() {
}
def refresh() {
sendEvent(name: "heartbeat", value: "alive", displayed:false)
zigbee.onOffRefresh() + zigbee.refreshData("0x0B04", "0x050B")
}

View File

@@ -15,7 +15,7 @@
*/
metadata {
definition (name: "SmartSense Moisture Sensor",namespace: "smartthings", author: "SmartThings") {
definition (name: "SmartSense Moisture Sensor",namespace: "smartthings", author: "SmartThings", category: "C2") {
capability "Configuration"
capability "Battery"
capability "Refresh"
@@ -255,7 +255,8 @@ private Map getBatteryResult(rawValue) {
def minVolts = 2.1
def maxVolts = 3.0
def pct = (volts - minVolts) / (maxVolts - minVolts)
result.value = Math.min(100, (int) pct * 100)
def roundedPct = Math.round(pct * 100)
result.value = Math.min(100, roundedPct)
result.descriptionText = "{{ device.displayName }} battery was {{ value }}%"
}
}

View File

@@ -0,0 +1,2 @@
.st-ignore
README.md

View File

@@ -0,0 +1,30 @@
# Smartsense Motion Sensor
Works with:
* [Samsung SmartThings Motion Sensor](https://shop.smartthings.com/#!/products/samsung-smartthings-motion-sensor)
## Table of contents
* [Capabilities](#capabilities)
* [Health]($health)
## Capabilities
* **Configuration** - _configure()_ command called when device is installed or device preferences updated
* **Motion Sensor** - can detect motion
* **Battery** - defines device uses a battery
* **Refresh** - _refresh()_ command for status updates
* **Health Check** - indicates ability to get device health notifications
## Device Health
A Category C2 motion sensor that has 120min check-in interval

View File

@@ -15,7 +15,7 @@
*/
metadata {
definition (name: "SmartSense Motion Sensor", namespace: "smartthings", author: "SmartThings") {
definition (name: "SmartSense Motion Sensor", namespace: "smartthings", author: "SmartThings", category: "C2") {
capability "Motion Sensor"
capability "Configuration"
capability "Battery"
@@ -271,7 +271,8 @@ private Map getBatteryResult(rawValue) {
def minVolts = 2.1
def maxVolts = 3.0
def pct = (volts - minVolts) / (maxVolts - minVolts)
result.value = Math.min(100, (int) pct * 100)
def roundedPct = Math.round(pct * 100)
result.value = Math.min(100, roundedPct)
result.descriptionText = "{{ device.displayName }} battery was {{ value }}%"
}
}

View File

@@ -24,7 +24,7 @@ metadata {
capability "Temperature Measurement"
capability "Refresh"
capability "Sensor"
command "enrollResponse"
}
@@ -73,7 +73,7 @@ metadata {
def parse(String description) {
log.debug "description: $description"
Map map = [:]
if (description?.startsWith('catchall:')) {
map = parseCatchAllMessage(description)
@@ -87,10 +87,10 @@ def parse(String description) {
else if (description?.startsWith('zone status')) {
map = parseIasMessage(description)
}
log.debug "Parse returned $map"
def result = map ? createEvent(map) : null
if (description?.startsWith('enroll request')) {
List cmds = enrollResponse()
log.debug "enroll response: ${cmds}"
@@ -128,7 +128,7 @@ private Map parseCatchAllMessage(String description) {
private boolean shouldProcessMessage(cluster) {
// 0x0B is default response indicating message got through
// 0x07 is bind message
boolean ignoredMessage = cluster.profileId != 0x0104 ||
boolean ignoredMessage = cluster.profileId != 0x0104 ||
cluster.command == 0x0B ||
cluster.command == 0x07 ||
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)
@@ -141,7 +141,7 @@ private Map parseReportAttributeMessage(String description) {
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
}
log.debug "Desc Map: $descMap"
Map resultMap = [:]
if (descMap.cluster == "0402" && descMap.attrId == "0000") {
def value = getTemperature(descMap.value)
@@ -153,11 +153,11 @@ private Map parseReportAttributeMessage(String description) {
else if (descMap.cluster == "0406" && descMap.attrId == "0000") {
def value = descMap.value.endsWith("01") ? "active" : "inactive"
resultMap = getMotionResult(value)
}
}
return resultMap
}
private Map parseCustomMessage(String description) {
Map resultMap = [:]
if (description?.startsWith('temperature: ')) {
@@ -170,7 +170,7 @@ private Map parseCustomMessage(String description) {
private Map parseIasMessage(String description) {
List parsedMsg = description.split(' ')
String msgCode = parsedMsg[2]
Map resultMap = [:]
switch(msgCode) {
case '0x0020': // Closed/No Motion/Dry
@@ -240,7 +240,8 @@ private Map getBatteryResult(rawValue) {
def minVolts = 2.1
def maxVolts = 3.0
def pct = (volts - minVolts) / (maxVolts - minVolts)
result.value = Math.min(100, (int) pct * 100)
def roundedPct = Math.round(pct * 100)
result.value = Math.min(100, roundedPct)
result.descriptionText = "${linkText} battery was ${result.value}%"
}
}

View File

@@ -15,7 +15,7 @@
*/
metadata {
definition (name: "SmartSense Multi Sensor", namespace: "smartthings", author: "SmartThings") {
definition (name: "SmartSense Multi Sensor", namespace: "smartthings", author: "SmartThings", category: "C2") {
capability "Three Axis"
capability "Battery"
@@ -338,7 +338,8 @@ private Map getBatteryResult(rawValue) {
def minVolts = 2.1
def maxVolts = 3.0
def pct = (volts - minVolts) / (maxVolts - minVolts)
result.value = Math.min(100, (int) pct * 100)
def roundedPct = Math.round(pct * 100)
result.value = Math.min(100, roundedPct)
result.descriptionText = "{{ device.displayName }} battery was {{ value }}%"
}
}
@@ -403,39 +404,21 @@ def refresh() {
if (device.getDataValue("manufacturer") == "SmartThings") {
log.debug "Refreshing Values for manufacturer: SmartThings "
refreshCmds = refreshCmds + [
/* These values of Motion Threshold Multiplier(01) and Motion Threshold (7602)
seem to be giving pretty accurate results for the XYZ co-ordinates for this manufacturer.
Separating these out in a separate if-else because I do not want to touch Centralite part
as of now.
*/
"zcl mfg-code ${manufacturerCode}", "delay 200",
"zcl global write 0xFC02 0 0x20 {01}", "delay 200",
"send 0x${device.deviceNetworkId} 1 1", "delay 400",
"zcl mfg-code ${manufacturerCode}", "delay 200",
"zcl global write 0xFC02 2 0x21 {7602}", "delay 200",
"send 0x${device.deviceNetworkId} 1 1", "delay 400",
]
/* These values of Motion Threshold Multiplier(0x01) and Motion Threshold (0x0276)
seem to be giving pretty accurate results for the XYZ co-ordinates for this manufacturer.
Separating these out in a separate if-else because I do not want to touch Centralite part
as of now.
*/
refreshCmds += zigbee.writeAttribute(0xFC02, 0x0000, 0x20, 0x01, [mfgCode: manufacturerCode])
refreshCmds += zigbee.writeAttribute(0xFC02, 0x0002, 0x21, 0x0276, [mfgCode: manufacturerCode])
} else {
refreshCmds = refreshCmds + [
/* sensitivity - default value (8) */
"zcl mfg-code ${manufacturerCode}", "delay 200",
"zcl global write 0xFC02 0 0x20 {02}", "delay 200",
"send 0x${device.deviceNetworkId} 1 1", "delay 400",
]
refreshCmds += zigbee.writeAttribute(0xFC02, 0x0000, 0x20, 0x02, [mfgCode: manufacturerCode])
}
//Common refresh commands
refreshCmds = refreshCmds + [
"st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200",
"st rattr 0x${device.deviceNetworkId} 1 1 0x20", "delay 200",
"zcl mfg-code ${manufacturerCode}", "delay 200",
"zcl global read 0xFC02 0x0010",
"send 0x${device.deviceNetworkId} 1 1","delay 400"
]
refreshCmds += zigbee.readAttribute(0x0402, 0x0000) +
zigbee.readAttribute(0x0001, 0x0020) +
zigbee.readAttribute(0xFC02, 0x0010, [mfgCode: manufacturerCode])
return refreshCmds + enrollResponse()
}
@@ -443,38 +426,15 @@ def refresh() {
def configure() {
sendEvent(name: "checkInterval", value: 7200, displayed: false)
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
log.debug "Configuring Reporting"
def configCmds = [
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "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}", "delay 200", //checkin time 6 hrs
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 200",
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}", "delay 200",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0xFC02 {${device.zigbeeId}} {}", "delay 200",
"zcl mfg-code ${manufacturerCode}", "delay 200",
"zcl global send-me-a-report 0xFC02 0x0010 0x18 10 3600 {01}", "delay 200",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
"zcl mfg-code ${manufacturerCode}", "delay 200",
"zcl global send-me-a-report 0xFC02 0x0012 0x29 1 3600 {0100}", "delay 200",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
"zcl mfg-code ${manufacturerCode}", "delay 200",
"zcl global send-me-a-report 0xFC02 0x0013 0x29 1 3600 {0100}", "delay 200",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
"zcl mfg-code ${manufacturerCode}", "delay 200",
"zcl global send-me-a-report 0xFC02 0x0014 0x29 1 3600 {0100}", "delay 200",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500"
]
def configCmds = enrollResponse() +
zigbee.batteryConfig() +
zigbee.temperatureConfig() +
zigbee.configureReporting(0xFC02, 0x0010, 0x18, 10, 3600, 0x01, [mfgCode: manufacturerCode]) +
zigbee.configureReporting(0xFC02, 0x0012, 0x29, 1, 3600, 0x0001, [mfgCode: manufacturerCode]) +
zigbee.configureReporting(0xFC02, 0x0013, 0x29, 1, 3600, 0x0001, [mfgCode: manufacturerCode]) +
zigbee.configureReporting(0xFC02, 0x0014, 0x29, 1, 3600, 0x0001, [mfgCode: manufacturerCode])
return configCmds + refresh()
}

View File

@@ -16,7 +16,7 @@
//DEPRECATED - Using the smartsense-multi-sensor.groovy DTH for this device. Users need to be moved before deleting this DTH
metadata {
definition (name: "SmartSense Open/Closed Accelerometer Sensor", namespace: "smartthings", author: "SmartThings") {
definition (name: "SmartSense Open/Closed Accelerometer Sensor", namespace: "smartthings", author: "SmartThings", category: "C2") {
capability "Battery"
capability "Configuration"
capability "Contact Sensor"
@@ -234,7 +234,8 @@ def getTemperature(value) {
def minVolts = 2.1
def maxVolts = 3.0
def pct = (volts - minVolts) / (maxVolts - minVolts)
result.value = Math.min(100, (int) pct * 100)
def roundedPct = Math.round(pct * 100)
result.value = Math.min(100, roundedPct)
result.descriptionText = "${linkText} battery was ${result.value}%"
}

View File

@@ -13,32 +13,34 @@
* for the specific language governing permissions and limitations under the License.
*
*/
metadata {
definition (name: "SmartSense Open/Closed Sensor", namespace: "smartthings", author: "SmartThings") {
definition (name: "SmartSense Open/Closed Sensor", namespace: "smartthings", author: "SmartThings", category: "C2") {
capability "Battery"
capability "Configuration"
capability "Contact Sensor"
capability "Refresh"
capability "Temperature Measurement"
capability "Health Check"
capability "Sensor"
command "enrollResponse"
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3300-S"
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3300"
fingerprint inClusters: "0000,0001,0003,0020,0402,0500,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3320-L", deviceJoinName: "Iris Contact Sensor"
}
simulator {
}
preferences {
input title: "Temperature Offset", description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
}
tiles(scale: 2) {
multiAttributeTile(name:"contact", type: "generic", width: 6, height: 4){
tileAttribute ("device.contact", key: "PRIMARY_CONTROL") {
@@ -71,10 +73,10 @@ metadata {
details(["contact","temperature","battery","refresh"])
}
}
def parse(String description) {
log.debug "description: $description"
Map map = [:]
if (description?.startsWith('catchall:')) {
map = parseCatchAllMessage(description)
@@ -88,10 +90,10 @@ def parse(String description) {
else if (description?.startsWith('zone status')) {
map = parseIasMessage(description)
}
log.debug "Parse returned $map"
def result = map ? createEvent(map) : null
if (description?.startsWith('enroll request')) {
List cmds = enrollResponse()
log.debug "enroll response: ${cmds}"
@@ -99,7 +101,7 @@ def parse(String description) {
}
return result
}
private Map parseCatchAllMessage(String description) {
Map resultMap = [:]
def cluster = zigbee.parse(description)
@@ -125,7 +127,7 @@ private Map parseCatchAllMessage(String description) {
private boolean shouldProcessMessage(cluster) {
// 0x0B is default response indicating message got through
// 0x07 is bind message
boolean ignoredMessage = cluster.profileId != 0x0104 ||
boolean ignoredMessage = cluster.profileId != 0x0104 ||
cluster.command == 0x0B ||
cluster.command == 0x07 ||
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)
@@ -135,14 +137,14 @@ private boolean shouldProcessMessage(cluster) {
private int getHumidity(value) {
return Math.round(Double.parseDouble(value))
}
private Map parseReportAttributeMessage(String description) {
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"
Map resultMap = [:]
if (descMap.cluster == "0402" && descMap.attrId == "0000") {
def value = getTemperature(descMap.value)
@@ -151,10 +153,10 @@ private Map parseReportAttributeMessage(String description) {
else if (descMap.cluster == "0001" && descMap.attrId == "0020") {
resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16))
}
return resultMap
}
private Map parseCustomMessage(String description) {
Map resultMap = [:]
if (description?.startsWith('temperature: ')) {
@@ -167,7 +169,7 @@ private Map parseCustomMessage(String description) {
private Map parseIasMessage(String description) {
List parsedMsg = description.split(' ')
String msgCode = parsedMsg[2]
Map resultMap = [:]
switch(msgCode) {
case '0x0020': // Closed/No Motion/Dry
@@ -200,7 +202,7 @@ private Map parseIasMessage(String description) {
}
return resultMap
}
def getTemperature(value) {
def celsius = Integer.parseInt(value, 16).shortValue() / 100
if(getTemperatureScale() == "C"){
@@ -213,11 +215,11 @@ def getTemperature(value) {
private Map getBatteryResult(rawValue) {
log.debug 'Battery'
def linkText = getLinkText(device)
def result = [
name: 'battery'
]
def volts = rawValue / 10
def descriptionText
if (rawValue == 0 || rawValue == 255) {}
@@ -228,7 +230,8 @@ private Map getBatteryResult(rawValue) {
def minVolts = 2.1
def maxVolts = 3.0
def pct = (volts - minVolts) / (maxVolts - minVolts)
result.value = Math.min(100, (int) pct * 100)
def roundedPct = Math.round(pct * 100)
result.value = Math.min(100, roundedPct)
result.descriptionText = "${linkText} battery was ${result.value}%"
}
@@ -273,6 +276,7 @@ def refresh() {
}
def configure() {
sendEvent(name: "checkInterval", value: 7200, displayed: false)
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
log.debug "Configuring Reporting, IAS CIE, and Bindings."

View File

@@ -14,7 +14,7 @@
*
*/
metadata {
definition (name: "SmartSense Temp/Humidity Sensor",namespace: "smartthings", author: "SmartThings") {
definition (name: "SmartSense Temp/Humidity Sensor",namespace: "smartthings", author: "SmartThings", category: "C2") {
capability "Configuration"
capability "Battery"
capability "Refresh"
@@ -205,7 +205,8 @@ private Map getBatteryResult(rawValue) {
def minVolts = 2.1
def maxVolts = 3.0
def pct = (volts - minVolts) / (maxVolts - minVolts)
result.value = Math.min(100, (int) pct * 100)
def roundedPct = Math.round(pct * 100)
result.value = Math.min(100, roundedPct)
result.descriptionText = "${linkText} battery was ${result.value}%"
}

View File

@@ -0,0 +1,42 @@
# Device Tiles Examples and Reference
This package contains examples of Device tiles, organized by tile type.
## Purpose
Each Device Handler shows example usages of a specific tile, and is meant to represent the variety of permutations that a tile can be configured.
The various tiles can be used by QA to test tiles on all supported mobile devices, and by developers as a reference implementation.
## Installation
1. Self-publish the Device Handlers in this package.
2. Self-publish the Device Tile Controller SmartApp. The SmartApp can be found [here](https://github.com/SmartThingsCommunity/SmartThingsPublic/blob/master/smartapps/smartthings/tile-ux/device-tile-controller.src/device-tile-controller.groovy).
3. Install the SmartApp from the Marketplace, under "My Apps".
4. Select the simulated devices you want to install and press "Done".
The simulated devices can then be found in the "Things" view of "My Home" in the mobile app.
You may wish to create a new room for these simulated devices for easy access.
## Usage
Each simulated device can be interacted with like other devices.
You can use the mobile app to interact with the tiles to see how they look and behave.
## Troubleshooting
If you get an error when installing the simulated devices using the controller SmartApp, ensure that you have published all the Device Handlers for yourself.
Also check live logging to see if there is a specific tile that is causing installation issues.
## FAQ
*Question: A tile isn't behaving as expected. What should I do?*
QA should create a JIRA ticket for any issues or inconsistencies of tiles across devices.
Developers may file a support ticket, and reference the specific tile and issue observed.
*Question: I'd like to contribute an example tile usage that would be helpful for testing and reference purposes. Can I do that?*
We recommend that you open an issue in the SmartThingsPublic repository describing the example tile and usage.
That way we can discuss with you the proposed change, and then if appropriate you can create a PR associated to the issue.

View File

@@ -22,7 +22,7 @@ metadata {
tiles(scale: 2) {
valueTile("currentColor", "device.color") {
state "default", label: '${currentValue}'
state "color", label: '${currentValue}', defaultState: true
}
controlTile("rgbSelector", "device.color", "color", height: 6, width: 6, inactiveLabel: false) {
@@ -41,6 +41,13 @@ def parse(String description) {
log.debug "Parsing '${description}'"
}
def setColor(value) {
log.debug "setting color: $value"
if (value.hex) { sendEvent(name: "color", value: value.hex) }
if (value.hue) { sendEvent(name: "hue", value: value.hue) }
if (value.saturation) { sendEvent(name: "saturation", value: value.saturation) }
}
def setSaturation(percent) {
log.debug "Executing 'setSaturation'"
sendEvent(name: "saturation", value: percent)

View File

@@ -39,7 +39,7 @@ metadata {
}
valueTile("rangeValue", "device.rangedLevel", height: 2, width: 2) {
state "default", label:'${currentValue}'
state "range", label:'${currentValue}', defaultState: true
}
controlTile("rangeSliderConstrained", "device.rangedLevel", "slider", height: 2, width: 4, range: "(40..60)") {

View File

@@ -41,17 +41,17 @@ metadata {
// standard flat tile with only a label
standardTile("flatLabel", "device.switch", width: 2, height: 2, decoration: "flat") {
state "default", label: 'On Action', action: "switch.on", backgroundColor: "#ffffff"
state "label", label: 'On Action', action: "switch.on", backgroundColor: "#ffffff", defaultState: true
}
// standard flat tile with icon and label
standardTile("flatIconLabel", "device.switch", width: 2, height: 2, decoration: "flat") {
state "default", label: 'Off Action', action: "switch.off", icon:"st.switches.switch.off", backgroundColor: "#ffffff"
state "iconLabel", label: 'Off Action', action: "switch.off", icon:"st.switches.switch.off", backgroundColor: "#ffffff", defaultState: true
}
// standard flat tile with only icon (Refreh text is IN the icon file)
standardTile("flatIcon", "device.switch", width: 2, height: 2, decoration: "flat") {
state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
state "icon", action:"refresh.refresh", icon:"st.secondary.refresh", defaultState: true
}
// standard with defaultState = true
@@ -74,19 +74,19 @@ metadata {
// utility tiles to fill the spaces
standardTile("empty2x2", "null", width: 2, height: 2, decoration: "flat") {
state "default", label:''
state "emptySmall", label:'', defaultState: true
}
standardTile("empty4x2", "null", width: 4, height: 2, decoration: "flat") {
state "default", label:''
state "emptyBigger", label:'', defaultState: true
}
// multi-line text (explicit newlines)
standardTile("multiLine", "device.multiLine", width: 2, height: 2) {
state "default", label: '${currentValue}'
state "multiLine", label: '${currentValue}', defaultState: true
}
standardTile("multiLineWithIcon", "device.multiLine", width: 2, height: 2) {
state "default", label: '${currentValue}', icon: "st.switches.switch.off"
state "multiLineIcon", label: '${currentValue}', icon: "st.switches.switch.off", defaultState: true
}
main("actionRings")

View File

@@ -22,68 +22,68 @@ metadata {
tiles(scale: 2) {
valueTile("text", "device.text", width: 2, height: 2) {
state "default", label:'${currentValue}'
state "val", label:'${currentValue}', defaultState: true
}
valueTile("longText", "device.longText", width: 2, height: 2) {
state "default", label:'${currentValue}'
state "val", label:'${currentValue}', defaultState: true
}
valueTile("integer", "device.integer", width: 2, height: 2) {
state "default", label:'${currentValue}'
state "val", label:'${currentValue}', defaultState: true
}
valueTile("integerFloat", "device.integerFloat", width: 2, height: 2) {
state "default", label:'${currentValue}'
state "val", label:'${currentValue}', defaultState: true
}
valueTile("pi", "device.pi", width: 2, height: 2) {
state "default", label:'${currentValue}'
state "val", label:'${currentValue}', defaultState: true
}
valueTile("floatAsText", "device.floatAsText", width: 2, height: 2) {
state "default", label:'${currentValue}'
state "val", label:'${currentValue}', defaultState: true
}
valueTile("bgColor", "device.integer", width: 2, height: 2) {
state "default", label:'${currentValue}', backgroundColor: "#e86d13"
state "val", label:'${currentValue}', backgroundColor: "#e86d13", defaultState: true
}
valueTile("bgColorRange", "device.integer", width: 2, height: 2) {
state "default", label:'${currentValue}', backgroundColors: [
state "val", label:'${currentValue}', defaultState: true, backgroundColors: [
[value: 10, color: "#ff0000"],
[value: 90, color: "#0000ff"]
]
}
valueTile("bgColorRangeSingleItem", "device.integer", width: 2, height: 2) {
state "default", label:'${currentValue}', backgroundColors: [
state "val", label:'${currentValue}', defaultState: true, backgroundColors: [
[value: 10, color: "#333333"]
]
}
valueTile("bgColorRangeConflict", "device.integer", width: 2, height: 2) {
state "default", label:'${currentValue}', backgroundColors: [
state "valWithConflict", label:'${currentValue}', defaultState: true, backgroundColors: [
[value: 10, color: "#990000"],
[value: 10, color: "#000099"]
]
}
valueTile("noValue", "device.nada", width: 4, height: 2) {
state "default", label:'${currentValue}'
state "noval", label:'${currentValue}', defaultState: true
}
valueTile("multiLine", "device.multiLine", width: 3, height: 2) {
state "default", label: '${currentValue}'
state "val", label: '${currentValue}', defaultState: true
}
valueTile("multiLineWithIcon", "device.multiLine", width: 3, height: 2) {
state "default", label: '${currentValue}', icon: "st.switches.switch.off"
state "val", label: '${currentValue}', icon: "st.switches.switch.off", defaultState: true
}
main("text")
details([
"text", "longText", "integer",
"text", "longText", "integer",
"integerFloat", "pi", "floatAsText",
"bgColor", "bgColorRange", "bgColorRangeSingleItem",
"bgColorRangeConflict", "noValue",

View File

@@ -39,15 +39,15 @@ metadata {
attributeState "turningOff", label:'${name}', backgroundColor:"#ffffff", nextState:"turningOn"
}
tileAttribute("device.level", key: "SECONDARY_CONTROL") {
attributeState "default", icon: 'st.Weather.weather1', action:"randomizeLevel"
attributeState "level", icon: 'st.Weather.weather1', action:"randomizeLevel", defaultState: true
}
tileAttribute("device.level", key: "SLIDER_CONTROL") {
attributeState "default", action:"switch level.setLevel"
attributeState "level", action:"switch level.setLevel", defaultState: true
}
}
multiAttributeTile(name:"valueTile", type:"generic", width:6, height:4) {
tileAttribute("device.level", key: "PRIMARY_CONTROL") {
attributeState "default", label:'${currentValue}', backgroundColors:[
attributeState "level", label:'${currentValue}', defaultState: true, backgroundColors:[
[value: 0, color: "#ff0000"],
[value: 20, color: "#ffff00"],
[value: 40, color: "#00ff00"],
@@ -69,34 +69,34 @@ metadata {
}
multiAttributeTile(name:"lengthyTile", type:"generic", width:6, height:4) {
tileAttribute("device.lengthyText", key: "PRIMARY_CONTROL") {
attributeState "default", label:'The value of this tile is long and should wrap to two lines', backgroundColor:"#79b821"
attributeState "lengthyText", label:'The value of this tile is long and should wrap to two lines', backgroundColor:"#79b821", defaultState: true
}
tileAttribute("device.lengthyText", key: "SECONDARY_CONTROL") {
attributeState "default", label:'The value of this tile is long and should wrap to two lines', backgroundColor:"#79b821"
attributeState "lengthyText", label:'The value of this tile is long and should wrap to two lines', backgroundColor:"#79b821", defaultState: true
}
}
multiAttributeTile(name:"multilineTile", type:"generic", width:6, height:4) {
tileAttribute("device.multilineText", key: "PRIMARY_CONTROL") {
attributeState "default", label:'Line 1 YES\nLine 2 YES\nLine 3 NO', backgroundColor:"#79b821"
attributeState "multiLineText", label:'Line 1 YES\nLine 2 YES\nLine 3 NO', backgroundColor:"#79b821", defaultState: true
}
tileAttribute("device.multilineText", key: "SECONDARY_CONTROL") {
attributeState "default", label:'Line 1 YES\nLine 2 YES\nLine 3 NO', backgroundColor:"#79b821"
attributeState "multiLineText", label:'Line 1 YES\nLine 2 YES\nLine 3 NO', backgroundColor:"#79b821", defaultState: true
}
}
multiAttributeTile(name:"lengthyTileWithIcon", type:"generic", width:6, height:4) {
tileAttribute("device.lengthyText", key: "PRIMARY_CONTROL") {
attributeState "default", label:'The value of this tile is long and should wrap to two lines', backgroundColor:"#79b821", icon: "st.switches.switch.on"
attributeState "lengthyText", label:'The value of this tile is long and should wrap to two lines', backgroundColor:"#79b821", icon: "st.switches.switch.on", defaultState: true
}
tileAttribute("device.lengthyText", key: "SECONDARY_CONTROL") {
attributeState "default", label:'The value of this tile is long and should wrap to two lines', backgroundColor:"#79b821", icon: "st.switches.switch.on"
attributeState "lengthyText", label:'The value of this tile is long and should wrap to two lines', backgroundColor:"#79b821", icon: "st.switches.switch.on", defaultState: true
}
}
multiAttributeTile(name:"multilineTileWithIcon", type:"generic", width:6, height:4) {
tileAttribute("device.multilineText", key: "PRIMARY_CONTROL") {
attributeState "default", label:'Line 1 YES\nLine 2 YES\nLine 3 NO', backgroundColor:"#79b821", icon: "st.switches.switch.on"
attributeState "multilineText", label:'Line 1 YES\nLine 2 YES\nLine 3 NO', backgroundColor:"#79b821", icon: "st.switches.switch.on", defaultState: true
}
tileAttribute("device.multilineText", key: "SECONDARY_CONTROL") {
attributeState "default", label:'Line 1 YES\nLine 2 YES\nLine 3 NO', backgroundColor:"#79b821", icon: "st.switches.switch.on"
attributeState "multilineText", label:'Line 1 YES\nLine 2 YES\nLine 3 NO', backgroundColor:"#79b821", icon: "st.switches.switch.on", defaultState: true
}
}

View File

@@ -96,10 +96,10 @@ metadata {
}
standardTile("reset", "device.reset", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "default", label:"Reset Color", action:"reset", icon:"st.lights.philips.hue-single"
state "reset", label:"Reset Color", action:"reset", icon:"st.lights.philips.hue-single", defaultState: true
}
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
state "refresh", label:"", action:"refresh.refresh", icon:"st.secondary.refresh", defaultState: true
}
main(["switch"])
@@ -173,7 +173,6 @@ def setColor(value) {
def reset() {
log.debug "Executing 'reset'"
setAdjustedColor([level:100, hex:"#90C638", saturation:56, hue:23])
//parent.poll()
}
def setAdjustedColor(value) {
@@ -189,7 +188,6 @@ def setAdjustedColor(value) {
def refresh() {
log.debug "Executing 'refresh'"
//parent.manualRefresh()
}
def adjustOutgoingHue(percent) {
@@ -208,4 +206,3 @@ def adjustOutgoingHue(percent) {
log.info "percent: $percent, adjusted: $adjusted"
adjusted
}

View File

@@ -37,10 +37,10 @@ metadata {
attributeState("stopped", label:"Stopped", action:"music Player.play", nextState: "playing")
}
tileAttribute("device.status", key: "PREVIOUS_TRACK") {
attributeState("default", action:"music Player.previousTrack")
attributeState("status", action:"music Player.previousTrack", defaultState: true)
}
tileAttribute("device.status", key: "NEXT_TRACK") {
attributeState("default", action:"music Player.nextTrack")
attributeState("status", action:"music Player.nextTrack", defaultState: true)
}
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
attributeState("level", action:"music Player.setLevel")
@@ -50,7 +50,7 @@ metadata {
attributeState("muted", action:"music Player.unmute", nextState: "unmuted")
}
tileAttribute("device.trackDescription", key: "MARQUEE") {
attributeState("default", label:"${currentValue}")
attributeState("trackDescription", label:"${currentValue}", defaultState: true)
}
}

View File

@@ -32,14 +32,14 @@ metadata {
tiles(scale: 2) {
multiAttributeTile(name:"thermostatFull", type:"thermostat", width:6, height:4) {
tileAttribute("device.temperature", key: "PRIMARY_CONTROL") {
attributeState("default", label:'${currentValue}', unit:"dF")
attributeState("temp", label:'${currentValue}', unit:"dF", defaultState: true)
}
tileAttribute("device.temperature", key: "VALUE_CONTROL") {
attributeState("VALUE_UP", action: "tempUp")
attributeState("VALUE_DOWN", action: "tempDown")
}
tileAttribute("device.humidity", key: "SECONDARY_CONTROL") {
attributeState("default", label:'${currentValue}%', unit:"%")
attributeState("humidity", label:'${currentValue}%', unit:"%", defaultState: true)
}
tileAttribute("device.thermostatOperatingState", key: "OPERATING_STATE") {
attributeState("idle", backgroundColor:"#44b621")
@@ -53,15 +53,16 @@ metadata {
attributeState("auto", label:'${name}')
}
tileAttribute("device.heatingSetpoint", key: "HEATING_SETPOINT") {
attributeState("default", label:'${currentValue}', unit:"dF")
attributeState("heatingSetpoint", label:'${currentValue}', unit:"dF", defaultState: true)
}
tileAttribute("device.coolingSetpoint", key: "COOLING_SETPOINT") {
attributeState("default", label:'${currentValue}', unit:"dF")
attributeState("coolingSetpoint", label:'${currentValue}', unit:"dF", defaultState: true)
}
}
multiAttributeTile(name:"thermostatNoHumidity", type:"thermostat", width:6, height:4) {
tileAttribute("device.temperature", key: "PRIMARY_CONTROL") {
attributeState("default", label:'${currentValue}', unit:"dF")
attributeState("coolingSetpoint", label:'${currentValue}', unit:"dF", defaultState: true)
attributeState("temp", label:'${currentValue}', unit:"dF")
}
tileAttribute("device.temperature", key: "VALUE_CONTROL") {
attributeState("VALUE_UP", action: "tempUp")
@@ -79,15 +80,16 @@ metadata {
attributeState("auto", label:'${name}')
}
tileAttribute("device.heatingSetpoint", key: "HEATING_SETPOINT") {
attributeState("default", label:'${currentValue}', unit:"dF")
attributeState("coolingSetpoint", label:'${currentValue}', unit:"dF", defaultState: true)
attributeState("heatingSetpoint", label:'${currentValue}', unit:"dF")
}
tileAttribute("device.coolingSetpoint", key: "COOLING_SETPOINT") {
attributeState("default", label:'${currentValue}', unit:"dF")
attributeState("coolingSetpoint", label:'${currentValue}', unit:"dF", defaultState: true)
}
}
multiAttributeTile(name:"thermostatBasic", type:"thermostat", width:6, height:4) {
tileAttribute("device.temperature", key: "PRIMARY_CONTROL") {
attributeState("default", label:'${currentValue}', unit:"dF",
attributeState("temp", label:'${currentValue}', unit:"dF", defaultState: true,
backgroundColors:[
[value: 31, color: "#153591"],
[value: 44, color: "#1e9cbb"],
@@ -118,30 +120,30 @@ metadata {
)
}
standardTile("tempDown", "device.temperature", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
state "default", label:'down', action:"tempDown"
state "tempDown", label:'down', action:"tempDown", defaultState: true
}
standardTile("tempUp", "device.temperature", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
state "default", label:'up', action:"tempUp"
state "tempUp", label:'up', action:"tempUp", defaultState: true
}
valueTile("heatingSetpoint", "device.heatingSetpoint", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
state "heat", label:'${currentValue} heat', unit: "F", backgroundColor:"#ffffff"
}
standardTile("heatDown", "device.temperature", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
state "default", label:'down', action:"heatDown"
state "heatDown", label:'down', action:"heatDown", defaultState: true
}
standardTile("heatUp", "device.temperature", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
state "default", label:'up', action:"heatUp"
state "heatUp", label:'up', action:"heatUp", defaultState: true
}
valueTile("coolingSetpoint", "device.coolingSetpoint", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
state "cool", label:'${currentValue} cool', unit:"F", backgroundColor:"#ffffff"
}
standardTile("coolDown", "device.temperature", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
state "default", label:'down', action:"coolDown"
state "coolDown", label:'down', action:"coolDown", defaultState: true
}
standardTile("coolUp", "device.temperature", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
state "default", label:'up', action:"coolUp"
state "coolUp", label:'up', action:"coolUp", defaultState: true
}
standardTile("mode", "device.thermostatMode", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {

View File

@@ -13,7 +13,7 @@
* for the specific language governing permissions and limitations under the License.
*
*/
metadata {
definition (name: "Tyco Door/Window Sensor", namespace: "smartthings", author: "SmartThings") {
capability "Battery"
@@ -21,28 +21,28 @@ metadata {
capability "Contact Sensor"
capability "Refresh"
capability "Temperature Measurement"
command "enrollResponse"
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "Visonic", model: "MCT-340 SMA"
}
simulator {
}
preferences {
input title: "Temperature Offset", description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
}
tiles {
standardTile("contact", "device.contact", width: 2, height: 2) {
state("open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e")
state("closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821")
}
valueTile("temperature", "device.temperature", inactiveLabel: false) {
state "temperature", label:'${currentValue}°',
backgroundColors:[
@@ -58,23 +58,23 @@ metadata {
valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false) {
state "battery", label:'${currentValue}% battery', unit:""
}
standardTile("refresh", "device.refresh", 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"
}
main (["contact", "temperature"])
details(["contact","temperature","battery","refresh","configure"])
}
}
def parse(String description) {
log.debug "description: $description"
Map map = [:]
if (description?.startsWith('catchall:')) {
map = parseCatchAllMessage(description)
@@ -88,10 +88,10 @@ def parse(String description) {
else if (description?.startsWith('zone status')) {
map = parseIasMessage(description)
}
log.debug "Parse returned $map"
def result = map ? createEvent(map) : null
if (description?.startsWith('enroll request')) {
List cmds = enrollResponse()
log.debug "enroll response: ${cmds}"
@@ -99,7 +99,7 @@ def parse(String description) {
}
return result
}
private Map parseCatchAllMessage(String description) {
Map resultMap = [:]
def cluster = zigbee.parse(description)
@@ -125,20 +125,20 @@ private Map parseCatchAllMessage(String description) {
private boolean shouldProcessMessage(cluster) {
// 0x0B is default response indicating message got through
// 0x07 is bind message
boolean ignoredMessage = cluster.profileId != 0x0104 ||
boolean ignoredMessage = cluster.profileId != 0x0104 ||
cluster.command == 0x0B ||
cluster.command == 0x07 ||
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)
return !ignoredMessage
}
private Map parseReportAttributeMessage(String description) {
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"
Map resultMap = [:]
if (descMap.cluster == "0402" && descMap.attrId == "0000") {
def value = getTemperature(descMap.value)
@@ -147,10 +147,10 @@ private Map parseReportAttributeMessage(String description) {
else if (descMap.cluster == "0001" && descMap.attrId == "0020") {
resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16))
}
return resultMap
}
private Map parseCustomMessage(String description) {
Map resultMap = [:]
if (description?.startsWith('temperature: ')) {
@@ -163,7 +163,7 @@ private Map parseCustomMessage(String description) {
private Map parseIasMessage(String description) {
List parsedMsg = description.split(' ')
String msgCode = parsedMsg[2]
Map resultMap = [:]
switch(msgCode) {
case '0x0020': // Closed/No Motion/Dry
@@ -196,7 +196,7 @@ private Map parseIasMessage(String description) {
}
return resultMap
}
def getTemperature(value) {
def celsius = Integer.parseInt(value, 16).shortValue() / 100
if(getTemperatureScale() == "C"){
@@ -209,11 +209,11 @@ def getTemperature(value) {
private Map getBatteryResult(rawValue) {
log.debug 'Battery'
def linkText = getLinkText(device)
def result = [
name: 'battery'
]
def volts = rawValue / 10
def descriptionText
if (volts > 3.5) {
@@ -223,7 +223,8 @@ private Map getBatteryResult(rawValue) {
def minVolts = 2.1
def maxVolts = 3.0
def pct = (volts - minVolts) / (maxVolts - minVolts)
result.value = Math.min(100, (int) pct * 100)
def roundedPct = Math.round(pct * 100)
result.value = Math.min(100, roundedPct)
result.descriptionText = "${linkText} battery was ${result.value}%"
}
@@ -261,7 +262,7 @@ def refresh()
{
log.debug "Refreshing Temperature and Battery"
[
"st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200",
"st rattr 0x${device.deviceNetworkId} 1 1 0x20"
@@ -274,24 +275,24 @@ def configure() {
log.debug "Configuring Reporting, IAS CIE, and Bindings."
def configCmds = [
"delay 1000",
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
"zcl global send-me-a-report 1 0x20 0x20 600 3600 {01}", "delay 200",
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
"zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}", "delay 200",
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
//"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"
]
return configCmds + enrollResponse() + refresh() // send refresh cmds as part of config
@@ -299,11 +300,11 @@ def configure() {
def enrollResponse() {
log.debug "Sending enroll response"
[
[
"raw 0x500 {01 23 00 00 00}", "delay 200",
"send 0x${device.deviceNetworkId} 1 1"
]
}
private hex(value) {

View File

@@ -28,7 +28,6 @@ metadata {
fingerprint deviceId: "0x0701", inClusters: "0x5E,0x98"
fingerprint deviceId: "0x0701", inClusters: "0x5E,0x86,0x72,0x98", outClusters: "0x5A,0x82"
fingerprint deviceId: "0x0701", inClusters: "0x5E,0x80,0x71,0x85,0x70,0x72,0x86,0x30,0x31,0x84,0x59,0x73,0x5A,0x8F,0x98,0x7A", outClusters:"0x20" // Philio multi+
fingerprint deviceId: "0x0701", inClusters: "0x5E,0x72,0x5A,0x80,0x73,0x86,0x84,0x85,0x59,0x71,0x70,0x7A,0x98" // Vision door/window
}
// simulator metadata
@@ -36,6 +35,7 @@ metadata {
// status messages
status "open": "command: 2001, payload: FF"
status "closed": "command: 2001, payload: 00"
status "wake up": "command: 8407, payload: "
}
// UI tile definitions
@@ -81,7 +81,7 @@ def updated() {
def cmds = []
if (!state.MSR) {
cmds = [
zwave.manufacturerSpecificV2.manufacturerSpecificGet().format(),
command(zwave.manufacturerSpecificV2.manufacturerSpecificGet()),
"delay 1200",
zwave.wakeUpV1.wakeUpNoMoreInformation().format()
]
@@ -94,9 +94,9 @@ def updated() {
}
def configure() {
delayBetween([
zwave.manufacturerSpecificV2.manufacturerSpecificGet().format(),
batteryGetCommand()
commands([
zwave.manufacturerSpecificV2.manufacturerSpecificGet(),
zwave.batteryV1.batteryGet()
], 6000)
}
@@ -147,12 +147,11 @@ def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cm
result << sensorValueEvent(1)
} else if (cmd.event == 0x03) {
result << createEvent(descriptionText: "$device.displayName covering was removed", isStateChange: true)
result << response(zwave.wakeUpV1.wakeUpIntervalSet(seconds:4*3600, nodeid:zwaveHubNodeId))
if(!state.MSR) result << response(zwave.manufacturerSpecificV2.manufacturerSpecificGet())
if(!state.MSR) result << response(command(zwave.manufacturerSpecificV2.manufacturerSpecificGet()))
} else if (cmd.event == 0x05 || cmd.event == 0x06) {
result << createEvent(descriptionText: "$device.displayName detected glass breakage", isStateChange: true)
} else if (cmd.event == 0x07) {
if(!state.MSR) result << response(zwave.manufacturerSpecificV2.manufacturerSpecificGet())
if(!state.MSR) result << response(command(zwave.manufacturerSpecificV2.manufacturerSpecificGet()))
result << createEvent(name: "motion", value: "active", descriptionText:"$device.displayName detected motion")
}
} else if (cmd.notificationType) {
@@ -170,12 +169,11 @@ def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd)
def event = createEvent(descriptionText: "${device.displayName} woke up", isStateChange: false)
def cmds = []
if (!state.MSR) {
cmds << zwave.wakeUpV1.wakeUpIntervalSet(seconds:4*3600, nodeid:zwaveHubNodeId).format()
cmds << zwave.manufacturerSpecificV2.manufacturerSpecificGet().format()
cmds << command(zwave.manufacturerSpecificV2.manufacturerSpecificGet())
cmds << "delay 1200"
}
if (!state.lastbat || now() - state.lastbat > 53*60*60*1000) {
cmds << batteryGetCommand()
cmds << command(zwave.batteryV1.batteryGet())
} else {
cmds << zwave.wakeUpV1.wakeUpNoMoreInformation().format()
}
@@ -212,7 +210,7 @@ def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerS
if (msr == "0086-0102-0059") {
result << response(zwave.securityV1.securityMessageEncapsulation().encapsulate(zwave.batteryV1.batteryGet()).format())
} else {
result << response(batteryGetCommand())
result << response(command(zwave.batteryV1.batteryGet()))
}
}
@@ -220,7 +218,7 @@ def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerS
}
def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) {
def encapsulatedCommand = cmd.encapsulatedCommand([0x20: 1, 0x85: 2, 0x70: 1])
def encapsulatedCommand = cmd.encapsulatedCommand([0x20: 1, 0x25: 1, 0x30: 1, 0x31: 5, 0x80: 1, 0x84: 1, 0x71: 3, 0x9C: 1])
// log.debug "encapsulated: $encapsulatedCommand"
if (encapsulatedCommand) {
state.sec = 1
@@ -232,12 +230,16 @@ def zwaveEvent(physicalgraph.zwave.Command cmd) {
createEvent(descriptionText: "$device.displayName: $cmd", displayed: false)
}
def batteryGetCommand() {
def cmd = zwave.batteryV1.batteryGet()
if (state.sec) {
cmd = zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd)
private command(physicalgraph.zwave.Command cmd) {
if (state.sec == 1) {
zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format()
} else {
cmd.format()
}
cmd.format()
}
private commands(commands, delay=200) {
delayBetween(commands.collect{ command(it) }, delay)
}
def retypeBasedOnMSR() {
@@ -260,8 +262,12 @@ def retypeBasedOnMSR() {
setDeviceType("3-in-1 Multisensor Plus (SG)")
break
case "0109-2001-0106": // Vision door/window
log.debug "Changing device type to Door / Window Sensor Plus (SG)"
setDeviceType("Door / Window Sensor Plus (SG)")
log.debug "Changing device type to Z-Wave Plus Door/Window Sensor"
setDeviceType("Z-Wave Plus Door/Window Sensor")
break
case "0109-2002-0205": // Vision Motion
log.debug "Changing device type to Z-Wave Plus Motion/Temp Sensor"
setDeviceType("Z-Wave Plus Motion/Temp Sensor")
break
}
}

View File

@@ -21,6 +21,13 @@ metadata {
capability "Motion Sensor"
capability "Sensor"
capability "Battery"
fingerprint mfr: "011F", prod: "0001", model: "0001", deviceJoinName: "Schlage Motion Sensor" // Schlage motion
fingerprint mfr: "014A", prod: "0001", model: "0001", deviceJoinName: "Ecolink Motion Sensor" // Ecolink motion
fingerprint mfr: "014A", prod: "0004", model: "0001", deviceJoinName: "Ecolink Motion Sensor" // Ecolink motion +
fingerprint mfr: "0060", prod: "0001", model: "0002", deviceJoinName: "Everspring Motion Sensor" // Everspring SP814
fingerprint mfr: "0060", prod: "0001", model: "0003", deviceJoinName: "Everspring Motion Sensor" // Everspring HSP02
fingerprint mfr: "011A", prod: "0601", model: "0901", deviceJoinName: "Enerwave Motion Sensor" // Enerwave ZWN-BPC
}
simulator {
@@ -125,9 +132,9 @@ def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd)
}
if (!state.lastbat || (new Date().time) - state.lastbat > 53*60*60*1000) {
result << response(zwave.batteryV1.batteryGet())
result << response("delay 1200")
} else {
result << response(zwave.wakeUpV1.wakeUpNoMoreInformation())
}
result << response(zwave.wakeUpV1.wakeUpNoMoreInformation())
result
}

View File

@@ -0,0 +1,270 @@
/**
* Copyright 2016 SmartThings
* Copyright 2015 AstraLink
*
* 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.
*
* Z-Wave Plus Door/Window Sensor, ZD2102*-5
*
*/
metadata {
definition (name: "Z-Wave Plus Door/Window Sensor", namespace: "smartthings", author: "SmartThings") {
capability "Contact Sensor"
capability "Configuration"
capability "Battery"
capability "Sensor"
// for Astralink
attribute "ManufacturerCode", "string"
attribute "ProduceTypeCode", "string"
attribute "ProductCode", "string"
attribute "WakeUp", "string"
attribute "WirelessConfig", "string"
fingerprint deviceId: "0x0701", inClusters: "0x5E, 0x98, 0x86, 0x72, 0x5A, 0x85, 0x59, 0x73, 0x80, 0x71, 0x70, 0x84, 0x7A"
fingerprint type:"8C07", inClusters: "5E,98,86,72,5A,71"
fingerprint mfr:"0109", prod:"2001", model:"0106" // not using deviceJoinName because it's sold under different brand names
}
tiles(scale: 2) {
multiAttributeTile(name:"contact", type: "generic", width: 6, height: 4){
tileAttribute ("device.contact", key: "PRIMARY_CONTROL") {
attributeState "open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e"
attributeState "closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821"
}
}
valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) {
state "battery", label:'${currentValue}% battery', unit:""
}
main (["contact"])
details(["contact","battery"])
}
simulator {
// messages the device returns in response to commands it receives
status "open (basic)" : "command: 9881, payload: 00 20 01 FF"
status "closed (basic)" : "command: 9881 payload: 00 20 01 00"
status "open (notification)" : "command: 9881, payload: 00 71 05 06 FF 00 FF 06 16 00 00"
status "closed (notification)" : "command: 9881, payload: 00 71 05 06 00 00 FF 06 17 00 00"
status "tamper: enclosure opened" : "command: 9881, payload: 00 71 05 07 FF 00 FF 07 03 00 00"
status "tamper: enclosure replaced" : "command: 9881, payload: 00 71 05 07 00 00 FF 07 00 00 00"
status "wake up" : "command: 9881, payload: 00 84 07"
status "battery (100%)" : "command: 9881, payload: 00 80 03 64"
status "battery low" : "command: 9881, payload: 00 80 03 FF"
}
}
def configure() {
log.debug "configure()"
def cmds = []
if (state.sec != 1) {
// secure inclusion may not be complete yet
cmds << "delay 1000"
}
cmds += secureSequence([
zwave.manufacturerSpecificV2.manufacturerSpecificGet(),
zwave.batteryV1.batteryGet(),
], 500)
cmds << "delay 8000"
cmds << secure(zwave.wakeUpV1.wakeUpNoMoreInformation())
return cmds
}
private getCommandClassVersions() {
[
0x71: 3, // Notification
0x5E: 2, // ZwaveplusInfo
0x59: 1, // AssociationGrpInfo
0x85: 2, // Association
0x20: 1, // Basic
0x80: 1, // Battery
0x70: 1, // Configuration
0x5A: 1, // DeviceResetLocally
0x7A: 2, // FirmwareUpdateMd
0x72: 2, // ManufacturerSpecific
0x73: 1, // Powerlevel
0x98: 1, // Security
0x84: 2, // WakeUp
0x86: 1, // Version
]
}
// Parse incoming device messages to generate events
def parse(String description) {
def result = []
def cmd
if (description.startsWith("Err 106")) {
state.sec = 0
result = createEvent( name: "secureInclusion", value: "failed", eventType: "ALERT",
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.startsWith("Err")) {
result = createEvent(descriptionText: "$device.displayName $description", isStateChange: true)
} else {
cmd = zwave.parse(description, commandClassVersions)
if (cmd) {
result = zwaveEvent(cmd)
}
}
if (result instanceof List) {
result = result.flatten()
}
log.debug "Parsed '$description' to $result"
return result
}
def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) {
def encapsulatedCommand = cmd.encapsulatedCommand(commandClassVersions)
log.debug "encapsulated: $encapsulatedCommand"
if (encapsulatedCommand) {
state.sec = 1
return zwaveEvent(encapsulatedCommand)
} else {
log.warn "Unable to extract encapsulated cmd from $cmd"
return [createEvent(descriptionText: cmd.toString())]
}
}
def sensorValueEvent(value) {
if (value) {
createEvent(name: "contact", value: "open", descriptionText: "$device.displayName is open")
} else {
createEvent(name: "contact", value: "closed", descriptionText: "$device.displayName is closed")
}
}
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) {
return sensorValueEvent(cmd.value)
}
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) {
return sensorValueEvent(cmd.value)
}
def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv2.SensorBinaryReport cmd) {
return sensorValueEvent(cmd.sensorValue)
}
def zwaveEvent(physicalgraph.zwave.commands.sensoralarmv1.SensorAlarmReport cmd) {
return sensorValueEvent(cmd.sensorState)
}
def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) {
def result = []
if (cmd.notificationType == 0x06 && cmd.event == 0x16) {
result << sensorValueEvent(1)
} else if (cmd.notificationType == 0x06 && cmd.event == 0x17) {
result << sensorValueEvent(0)
} else if (cmd.notificationType == 0x07) {
if (cmd.event == 0x00) {
if (cmd.eventParametersLength == 0 || cmd.eventParameter[0] != 3) {
result << createEvent(descriptionText: "$device.displayName covering replaced", isStateChange: true, displayed: false)
} else {
result << sensorValueEvent(0)
}
} else if (cmd.event == 0x01 || cmd.event == 0x02) {
result << sensorValueEvent(1)
} else if (cmd.event == 0x03) {
result << createEvent(descriptionText: "$device.displayName covering was removed", isStateChange: true)
if (!device.currentState("ManufacturerCode")) {
result << response(secure(zwave.manufacturerSpecificV2.manufacturerSpecificGet()))
}
} else if (cmd.event == 0x05 || cmd.event == 0x06) {
result << createEvent(descriptionText: "$device.displayName detected glass breakage", isStateChange: true)
} else {
result << createEvent(descriptionText: "$device.displayName event $cmd.event ${cmd.eventParameter.inspect()}", isStateChange: true, displayed: false)
}
} else if (cmd.notificationType) {
result << createEvent(descriptionText: "$device.displayName notification $cmd.notificationType event $cmd.event ${cmd.eventParameter.inspect()}", isStateChange: true, displayed: false)
} else {
def value = cmd.v1AlarmLevel == 255 ? "active" : cmd.v1AlarmLevel ?: "inactive"
result << createEvent(name: "alarm $cmd.v1AlarmType", value: value, isStateChange: true, displayed: false)
}
return result
}
def zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpNotification cmd) {
def event = createEvent(name: "WakeUp", value: "wakeup", descriptionText: "${device.displayName} woke up", isStateChange: true, displayed: false) // for Astralink
def cmds = []
if (!device.currentState("ManufacturerCode")) {
cmds << secure(zwave.manufacturerSpecificV2.manufacturerSpecificGet())
cmds << "delay 2000"
}
if (!state.lastbat || now() - state.lastbat > 10*60*60*1000) {
event.descriptionText += ", requesting battery"
cmds << secure(zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:1, scale:1))
cmds << "delay 800"
cmds << secure(zwave.batteryV1.batteryGet())
cmds << "delay 2000"
} else {
log.debug "not checking battery, was updated ${(now() - state.lastbat)/60000 as int} min ago"
}
cmds << secure(zwave.wakeUpV1.wakeUpNoMoreInformation())
return [event, response(cmds)]
}
def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) {
def map = [ name: "battery", unit: "%" ]
if (cmd.batteryLevel == 0xFF) {
map.value = 1
map.descriptionText = "${device.displayName} has a low battery"
map.isStateChange = true
} else {
map.value = cmd.batteryLevel
}
def event = createEvent(map)
// Save at least one battery report in events list every few days
if (!event.isStateChange && (now() - 3*24*60*60*1000) > device.latestState("battery")?.date?.time) {
map.isStateChange = true
}
state.lastbat = now()
return [event]
}
def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) {
def result = []
def manufacturerCode = String.format("%04X", cmd.manufacturerId)
def productTypeCode = String.format("%04X", cmd.productTypeId)
def productCode = String.format("%04X", cmd.productId)
def wirelessConfig = "ZWP"
log.debug "MSR ${manufacturerCode} ${productTypeCode} ${productCode}"
result << createEvent(name: "ManufacturerCode", value: manufacturerCode)
result << createEvent(name: "ProduceTypeCode", value: productTypeCode)
result << createEvent(name: "ProductCode", value: productCode)
result << createEvent(name: "WirelessConfig", value: wirelessConfig)
return result
}
def zwaveEvent(physicalgraph.zwave.Command cmd) {
return [createEvent(descriptionText: "$device.displayName: $cmd", displayed: false)]
}
private secure(physicalgraph.zwave.Command cmd) {
if (state.sec == 0) { // default to secure
cmd.format()
} else {
zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format()
}
}
private secureSequence(commands, delay=200) {
delayBetween(commands.collect{ secure(it) }, delay)
}

View File

@@ -0,0 +1,327 @@
/**
* Copyright 2016 SmartThings
* Copyright 2015 AstraLink
*
* 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.
*
* Z-Wave Plus Motion Sensor with Temperature Measurement, ZP3102*-5
*
*/
metadata {
definition (name: "Z-Wave Plus Motion/Temp Sensor", namespace: "smartthings", author: "SmartThings") {
capability "Motion Sensor"
capability "Temperature Measurement"
capability "Configuration"
capability "Battery"
capability "Sensor"
// for Astralink
attribute "ManufacturerCode", "string"
attribute "ProduceTypeCode", "string"
attribute "ProductCode", "string"
attribute "WakeUp", "string"
attribute "WirelessConfig", "string"
fingerprint deviceId: "0x0701", inClusters: "0x5E, 0x98, 0x86, 0x72, 0x5A, 0x85, 0x59, 0x73, 0x80, 0x71, 0x31, 0x70, 0x84, 0x7A"
fingerprint type:"8C07", inClusters: "5E,98,86,72,5A,31,71"
fingerprint mfr:"0109", prod:"2002", model:"0205" // not using deviceJoinName because it's sold under different brand names
}
tiles {
standardTile("motion", "device.motion", width: 3, 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"
}
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:'${currentValue}% battery', unit:"%"
}
main(["motion", "temperature"])
details(["motion", "temperature", "battery"])
}
}
def updated() {
if (!device.currentState("ManufacturerCode")) {
response(secure(zwave.manufacturerSpecificV2.manufacturerSpecificGet()))
}
}
def configure() {
log.debug "configure()"
def cmds = []
if (state.sec != 1) {
// secure inclusion may not be complete yet
cmds << "delay 1000"
}
cmds += secureSequence([
zwave.manufacturerSpecificV2.manufacturerSpecificGet(),
zwave.batteryV1.batteryGet(),
zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:1, scale:1)
], 500)
cmds << "delay 8000"
cmds << secure(zwave.wakeUpV1.wakeUpNoMoreInformation())
return cmds
}
private getCommandClassVersions() {
[
0x71: 3, // Notification
0x5E: 2, // ZwaveplusInfo
0x59: 1, // AssociationGrpInfo
0x85: 2, // Association
0x20: 1, // Basic
0x80: 1, // Battery
0x70: 1, // Configuration
0x5A: 1, // DeviceResetLocally
0x7A: 2, // FirmwareUpdateMd
0x72: 2, // ManufacturerSpecific
0x73: 1, // Powerlevel
0x98: 1, // Security
0x31: 5, // SensorMultilevel
0x84: 2 // WakeUp
]
}
// Parse incoming device messages to generate events
def parse(String description) {
def result = []
def cmd
if (description.startsWith("Err 106")) {
state.sec = 0
result = createEvent( name: "secureInclusion", value: "failed", eventType: "ALERT",
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.startsWith("Err")) {
result = createEvent(descriptionText: "$device.displayName $description", isStateChange: true)
} else {
cmd = zwave.parse(description, commandClassVersions)
if (cmd) {
result = zwaveEvent(cmd)
}
}
if (result instanceof List) {
result = result.flatten()
}
log.debug "Parsed '$description' to $result"
return result
}
def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) {
def encapsulatedCommand = cmd.encapsulatedCommand(commandClassVersions)
log.debug "encapsulated: $encapsulatedCommand"
if (encapsulatedCommand) {
state.sec = 1
return zwaveEvent(encapsulatedCommand)
} else {
log.warn "Unable to extract encapsulated cmd from $cmd"
return [createEvent(descriptionText: cmd.toString())]
}
}
def sensorValueEvent(value) {
def result = []
if (value) {
log.debug "sensorValueEvent($value) : active"
result << createEvent(name: "motion", value: "active", descriptionText: "$device.displayName detected motion")
} else {
log.debug "sensorValueEvent($value) : inactive"
result << createEvent(name: "motion", value: "inactive", descriptionText: "$device.displayName motion has stopped")
}
return result
}
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) {
return sensorValueEvent(cmd.value)
}
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) {
return sensorValueEvent(cmd.value)
}
def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) {
return sensorValueEvent(cmd.value)
}
def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv2.SensorBinaryReport cmd) {
return sensorValueEvent(cmd.sensorValue)
}
def zwaveEvent(physicalgraph.zwave.commands.sensoralarmv1.SensorAlarmReport cmd) {
return sensorValueEvent(cmd.sensorState)
}
def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) {
def result = []
if (cmd.notificationType == 0x07) {
if (cmd.event == 0x01 || cmd.event == 0x02) {
result << sensorValueEvent(1)
} else if (cmd.event == 0x03) {
result << createEvent(descriptionText: "$device.displayName covering was removed", isStateChange: true)
result << response(secure(zwave.manufacturerSpecificV2.manufacturerSpecificGet()))
} else if (cmd.event == 0x05 || cmd.event == 0x06) {
result << createEvent(descriptionText: "$device.displayName detected glass breakage", isStateChange: true)
} else if (cmd.event == 0x07) {
result << sensorValueEvent(1)
} else if (cmd.event == 0x08) {
result << sensorValueEvent(1)
} else if (cmd.event == 0x00) {
if (cmd.eventParametersLength && cmd.eventParameter[0] == 3) {
result << createEvent(descriptionText: "$device.displayName covering replaced", isStateChange: true, displayed: false)
} else {
result << sensorValueEvent(0)
}
} else if (cmd.event == 0xFF) {
result << sensorValueEvent(1)
} else {
result << createEvent(descriptionText: "$device.displayName sent event $cmd.event")
}
} else if (cmd.notificationType) {
def text = "Notification $cmd.notificationType: event ${([cmd.event] + cmd.eventParameter).join(", ")}"
result << createEvent(name: "notification$cmd.notificationType", value: "$cmd.event", descriptionText: text, displayed: false)
} else {
def value = cmd.v1AlarmLevel == 255 ? "active" : cmd.v1AlarmLevel ?: "inactive"
result << createEvent(name: "alarm $cmd.v1AlarmType", value: value, displayed: false)
}
return result
}
def zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpNotification cmd) {
def event = createEvent(name: "WakeUp", value: "wakeup", descriptionText: "${device.displayName} woke up", isStateChange: true, displayed: false) // for Astralink
def cmds = []
if (!device.currentState("ManufacturerCode")) {
cmds << secure(zwave.manufacturerSpecificV2.manufacturerSpecificGet())
cmds << "delay 2000"
}
if (!state.lastbat || now() - state.lastbat > 10*60*60*1000) {
event.descriptionText += ", requesting battery"
cmds << secure(zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:1, scale:1))
cmds << "delay 800"
cmds << secure(zwave.batteryV1.batteryGet())
cmds << "delay 2000"
} else {
log.debug "not checking battery, was updated ${(now() - state.lastbat)/60000 as int} min ago"
}
cmds << secure(zwave.wakeUpV1.wakeUpNoMoreInformation())
return [event, response(cmds)]
}
def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) {
def result = []
def map = [ name: "battery", unit: "%" ]
if (cmd.batteryLevel == 0xFF) {
map.value = 1
map.descriptionText = "${device.displayName} has a low battery"
map.isStateChange = true
} else {
map.value = cmd.batteryLevel
}
def event = createEvent(map)
// Save at least one battery report in events list every few days
if (!event.isStateChange && (now() - 3*24*60*60*1000) > device.latestState("battery")?.date?.time) {
map.isStateChange = true
}
state.lastbat = now()
return [event]
}
def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) {
def result = []
def map = [:]
switch (cmd.sensorType) {
case 1:
def cmdScale = cmd.scale == 1 ? "F" : "C"
map.name = "temperature"
map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, cmd.precision)
map.unit = getTemperatureScale()
break;
case 3:
map.name = "illuminance"
map.value = cmd.scaledSensorValue.toInteger().toString()
map.unit = "lux"
break;
case 5:
map.name = "humidity"
map.value = cmd.scaledSensorValue.toInteger().toString()
map.unit = cmd.scale == 0 ? "%" : ""
break;
case 0x1E:
map.name = "loudness"
map.unit = cmd.scale == 1 ? "dBA" : "dB"
map.value = cmd.scaledSensorValue.toString()
break;
default:
map.descriptionText = cmd.toString()
}
result << createEvent(map)
return result
}
def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) {
def result = []
def manufacturerCode = String.format("%04X", cmd.manufacturerId)
def productTypeCode = String.format("%04X", cmd.productTypeId)
def productCode = String.format("%04X", cmd.productId)
def wirelessConfig = "ZWP"
log.debug "MSR ${manufacturerCode} ${productTypeCode} ${productCode}"
result << createEvent(name: "ManufacturerCode", value: manufacturerCode)
result << createEvent(name: "ProduceTypeCode", value: productTypeCode)
result << createEvent(name: "ProductCode", value: productCode)
result << createEvent(name: "WirelessConfig", value: wirelessConfig)
if (manufacturerCode == "0109" && productTypeCode == "2002") {
result << response(secureSequence([
// Change re-trigger duration to 1 minute
zwave.configurationV1.configurationSet(parameterNumber: 1, configurationValue: [1], size: 1),
zwave.batteryV1.batteryGet(),
zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:1, scale:1)
], 400))
}
return result
}
def zwaveEvent(physicalgraph.zwave.Command cmd) {
return [createEvent(descriptionText: "$device.displayName: $cmd", displayed: false)]
}
private secure(physicalgraph.zwave.Command cmd) {
if (state.sec == 0) { // default to secure
cmd.format()
} else {
zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format()
}
}
private secureSequence(commands, delay=200) {
delayBetween(commands.collect{ secure(it) }, delay)
}

View File

@@ -95,11 +95,17 @@ def zwaveEvent(physicalgraph.zwave.commands.hailv1.Hail cmd) {
}
def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) {
if (state.manufacturer != cmd.manufacturerName) {
updateDataValue("manufacturer", cmd.manufacturerName)
}
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)
updateDataValue("manufacturer", cmd.manufacturerName)
createEvent([descriptionText: "$device.displayName MSR: $msr", isStateChange: false])
}
def zwaveEvent(physicalgraph.zwave.Command cmd) {
// Handles all Z-Wave commands we aren't interested in
[:]

View File

@@ -0,0 +1,252 @@
/**
* Gidjit Hub
*
* Copyright 2016 Matthew Page
*
* 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: "Gidjit Hub",
namespace: "com.gidjit.smartthings.hub",
author: "Matthew Page",
description: "Act as an endpoint so user's of Gidjit can quickly access and control their devices and execute routines. Users can do this quickly as Gidjit filters these actions based on their environment",
category: "Convenience",
iconUrl: "http://www.gidjit.com/appicon.png",
iconX2Url: "http://www.gidjit.com/appicon@2x.png",
iconX3Url: "http://www.gidjit.com/appicon@3x.png",
oauth: [displayName: "Gidjit", displayLink: "www.gidjit.com"])
preferences {
section ("Allow Gidjit to have access, there by allowing you to quickly control and monitor the following devices") {
input "switches", "capability.switch", title: "Control/Monitor your switches", multiple: true, required: false
input "thermostats", "capability.thermostat", title: "Control/Monitor your thermostats", multiple: true, required: false
input "windowShades", "capability.windowShade", title: "Control/Monitor your window shades", multiple: true, required: false //windowShade
//input "bulbs", "capability.colorControl", title: "Control your lights", multiple: true, required: false //windowShade
}
}
mappings {
path("/structureinfo") {
action: [
GET: "structureInfo"
]
}
path("/helloactions") {
action: [
GET: "helloActions"
]
}
path("/helloactions/:label") {
action: [
PUT: "executeAction"
]
}
path("/switch/:id/:command") {
action: [
PUT: "updateSwitch"
]
}
path("/thermostat/:id/:command") {
action: [
PUT: "updateThermostat"
]
}
path("/windowshade/:id/:command") {
action: [
PUT: "updateWindowShade"
]
}
path("/acquiredata/:id") {
action: [
GET: "acquiredata"
]
}
}
def installed() {
log.debug "Installed with settings: ${settings}"
initialize()
}
def updated() {
log.debug "Updated with settings: ${settings}"
unsubscribe()
initialize()
}
def initialize() {
// subscribe to attributes, devices, locations, etc.
}
def helloActions() {
def actions = location.helloHome?.getPhrases()*.label
if(!actions) {
return []
}
return actions
}
def executeAction() {
def actions = location.helloHome?.getPhrases()*.label
def a = actions?.find() { it == params.label }
if (!a) {
httpError(400, "invalid label $params.label")
return
}
location.helloHome?.execute(params.label)
}
/* this is the primary function called to query at the structure and its devices */
def structureInfo() { //list all devices
def list = [:]
def currId = location.id
list[currId] = [:]
list[currId].name = location.name
list[currId].id = location.id
list[currId].temperatureScale = location.temperatureScale
list[currId].devices = [:]
def setValues = {
if (params.brief) {
return [id: it.id, name: it.displayName]
}
def newList = [id: it.id, name: it.displayName, suppCapab: it.capabilities.collect {
"$it.name"
}, suppAttributes: it.supportedAttributes.collect {
"$it.name"
}, suppCommands: it.supportedCommands.collect {
"$it.name"
}]
return newList
}
switches?.each {
list[currId].devices[it.id] = setValues(it)
}
thermostats?.each {
list[currId].devices[it.id] = setValues(it)
}
windowShades?.each {
list[currId].devices[it.id] = setValues(it)
}
return list
}
/* This function returns all of the current values of the specified Devices attributes */
def acquiredata() {
def resp = [:]
if (!params.id) {
httpError(400, "invalid id $params.id")
return
}
def dev = switches.find() { it.id == params.id } ?: windowShades.find() { it.id == params.id } ?:
thermostats.find() { it.id == params.id }
if (!dev) {
httpError(400, "invalid id $params.id")
return
}
def att = dev.supportedAttributes
att.each {
resp[it.name] = dev.currentValue("$it.name")
}
return resp
}
void updateSwitch() {
// use the built-in request object to get the command parameter
def command = params.command
def sw = switches.find() { it.id == params.id }
if (!sw) {
httpError(400, "invalid id $params.id")
return
}
switch(command) {
case "on":
if ( sw.currentSwitch != "on" ) {
sw.on()
}
break
case "off":
if ( sw.currentSwitch != "off" ) {
sw.off()
}
break
default:
httpError(400, "$command is not a valid")
}
}
void updateThermostat() {
// use the built-in request object to get the command parameter
def command = params.command
def therm = thermostats.find() { it.id == params.id }
if (!therm || !command) {
httpError(400, "invalid id $params.id")
return
}
def passComm = [
"off",
"heat",
"emergencyHeat",
"cool",
"fanOn",
"fanAuto",
"fanCirculate",
"auto"
]
def passNumParamComm = [
"setHeatingSetpoint",
"setCoolingSetpoint",
]
def passStringParamComm = [
"setThermostatMode",
"setThermostatFanMode",
]
if (command in passComm) {
therm."$command"()
} else if (command in passNumParamComm && params.p1 && params.p1.isFloat()) {
therm."$command"(Float.parseFloat(params.p1))
} else if (command in passStringParamComm && params.p1) {
therm."$command"(params.p1)
} else {
httpError(400, "$command is not a valid command")
}
}
void updateWindowShade() {
// use the built-in request object to get the command parameter
def command = params.command
def ws = windowShades.find() { it.id == params.id }
if (!ws || !command) {
httpError(400, "invalid id $params.id")
return
}
def passComm = [
"open",
"close",
"presetPosition",
]
if (command in passComm) {
ws."$command"()
} else {
httpError(400, "$command is not a valid command")
}
}
// TODO: implement event handlers

View File

@@ -4,29 +4,33 @@
import java.text.DecimalFormat
import groovy.json.JsonSlurper
private apiUrl() { "https://api.netatmo.com" }
private getVendorName() { "netatmo" }
private getVendorAuthPath() { "https://api.netatmo.com/oauth2/authorize?" }
private getVendorTokenPath(){ "https://api.netatmo.com/oauth2/token" }
private getApiUrl() { "https://api.netatmo.com" }
private getVendorName() { "netatmo" }
private getVendorAuthPath() { "${apiUrl}/oauth2/authorize?" }
private getVendorTokenPath(){ "${apiUrl}/oauth2/token" }
private getVendorIcon() { "https://s3.amazonaws.com/smartapp-icons/Partner/netamo-icon-1%402x.png" }
private getClientId() { appSettings.clientId }
private getClientSecret() { appSettings.clientSecret }
private getServerUrl() { "https://graph.api.smartthings.com" }
private getClientId() { appSettings.clientId }
private getClientSecret() { appSettings.clientSecret }
private getServerUrl() { appSettings.serverUrl }
private getShardUrl() { return getApiServerUrl() }
private getCallbackUrl() { "${serverUrl}/oauth/callback" }
private getBuildRedirectUrl() { "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${state.accessToken}&apiServerUrl=${shardUrl}" }
// Automatically generated. Make future change here.
definition(
name: "Netatmo (Connect)",
namespace: "dianoga",
author: "Brian Steere",
description: "Netatmo Integration",
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,
singleInstance: true
name: "Netatmo (Connect)",
namespace: "dianoga",
author: "Brian Steere",
description: "Netatmo Integration",
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,
singleInstance: true
){
appSetting "clientId"
appSetting "clientSecret"
appSetting "serverUrl"
}
preferences {
@@ -35,35 +39,52 @@ preferences {
}
mappings {
path("/receivedToken"){action: [POST: "receivedToken", GET: "receivedToken"]}
path("/receiveToken"){action: [POST: "receiveToken", GET: "receiveToken"]}
path("/auth"){action: [GET: "auth"]}
path("/oauth/initialize") {action: [GET: "oauthInitUrl"]}
path("/oauth/callback") {action: [GET: "callback"]}
}
def authPage() {
log.debug "In authPage"
if(canInstallLabs()) {
def description = null
if (state.vendorAccessToken == null) {
log.debug "About to create access token."
def description
def uninstallAllowed = false
def oauthTokenProvided = false
createAccessToken()
description = "Tap to enter Credentials."
if (!state.accessToken) {
log.debug "About to create access token."
state.accessToken = createAccessToken()
}
return dynamicPage(name: "Credentials", title: "Authorize Connection", nextPage:"listDevices", uninstall: true, install:false) {
section { href url:buildRedirectUrl("auth"), style:"embedded", required:false, title:"Connect to ${getVendorName()}:", description:description }
if (canInstallLabs()) {
def redirectUrl = getBuildRedirectUrl()
log.debug "Redirect url = ${redirectUrl}"
if (state.authToken) {
description = "Tap 'Next' to proceed"
uninstallAllowed = true
oauthTokenProvided = true
} else {
description = "Click to enter Credentials."
}
if (!oauthTokenProvided) {
log.debug "Show the login page"
return dynamicPage(name: "Credentials", title: "Authorize Connection", nextPage:"listDevices", uninstall: uninstallAllowed, install:false) {
section() {
paragraph "Tap below to log in to the netatmo and authorize SmartThings access."
href url:redirectUrl, style:"embedded", required:false, title:"Connect to ${getVendorName()}:", description:description
}
}
} else {
description = "Tap 'Next' to proceed"
return dynamicPage(name: "Credentials", title: "Credentials Accepted!", nextPage:"listDevices", uninstall: true, install:false) {
section { href url: buildRedirectUrl("receivedToken"), style:"embedded", required:false, title:"${getVendorName()} is now connected to SmartThings!", description:description }
log.debug "Show the devices page"
return dynamicPage(name: "Credentials", title: "Credentials Accepted!", nextPage:"listDevices", uninstall: uninstallAllowed, install:false) {
section() {
input(name:"Devices", style:"embedded", required:false, title:"${getVendorName()} is now connected to SmartThings!", description:description)
}
}
}
}
else
{
} 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"."""
@@ -78,229 +99,175 @@ To update your Hub, access Location Settings in the Main Menu (tap the gear next
}
}
def auth() {
redirect location: oauthInitUrl()
}
def oauthInitUrl() {
log.debug "In oauthInitUrl"
/* OAuth Step 1: Request access code with our client ID */
state.oauthInitState = UUID.randomUUID().toString()
def oauthParams = [ response_type: "code",
client_id: getClientId(),
state: state.oauthInitState,
redirect_uri: buildRedirectUrl("receiveToken") ,
scope: "read_station"
]
return getVendorAuthPath() + toQueryString(oauthParams)
}
def buildRedirectUrl(endPoint) {
log.debug "In buildRedirectUrl"
return getServerUrl() + "/api/token/${state.accessToken}/smartapps/installations/${app.id}/${endPoint}"
}
def receiveToken() {
log.debug "In receiveToken"
def oauthParams = [
client_secret: getClientSecret(),
response_type: "code",
client_id: getClientId(),
grant_type: "authorization_code",
redirect_uri: buildRedirectUrl('receiveToken'),
code: params.code,
client_secret: getClientSecret(),
state: state.oauthInitState,
redirect_uri: getCallbackUrl(),
scope: "read_station"
]
def tokenUrl = getVendorTokenPath()
def params = [
uri: tokenUrl,
contentType: 'application/x-www-form-urlencoded',
body: oauthParams,
]
log.debug params
log.debug "REDIRECT URL: ${getVendorAuthPath() + toQueryString(oauthParams)}"
/* OAuth Step 2: Request access token with our client Secret and OAuth "Code" */
try {
httpPost(params) { response ->
log.debug response.data
def slurper = new JsonSlurper();
redirect (location: getVendorAuthPath() + toQueryString(oauthParams))
}
response.data.each {key, value ->
def data = slurper.parseText(key);
log.debug "Data: $data"
def callback() {
log.debug "callback()>> params: $params, params.code ${params.code}"
state.vendorRefreshToken = data.refresh_token
state.vendorAccessToken = data.access_token
state.vendorTokenExpires = now() + (data.expires_in * 1000)
return
def code = params.code
def oauthState = params.state
if (oauthState == state.oauthInitState) {
def tokenParams = [
client_secret: getClientSecret(),
client_id : getClientId(),
grant_type: "authorization_code",
redirect_uri: getCallbackUrl(),
code: code,
scope: "read_station"
]
log.debug "TOKEN URL: ${getVendorTokenPath() + toQueryString(tokenParams)}"
def tokenUrl = getVendorTokenPath()
def params = [
uri: tokenUrl,
contentType: 'application/x-www-form-urlencoded',
body: tokenParams
]
log.debug "PARAMS: ${params}"
httpPost(params) { resp ->
def slurper = new JsonSlurper()
resp.data.each { key, value ->
def data = slurper.parseText(key)
state.refreshToken = data.refresh_token
state.authToken = data.access_token
state.tokenExpires = now() + (data.expires_in * 1000)
log.debug "swapped token: $resp.data"
}
}
} catch (Exception e) {
log.debug "Error: $e"
// Handle success and failure here, and render stuff accordingly
if (state.authToken) {
success()
} else {
fail()
}
} else {
log.error "callback() failed oauthState != state.oauthInitState"
}
}
log.debug "State: $state"
def success() {
log.debug "in success"
def message = """
<p>We have located your """ + getVendorName() + """ account.</p>
<p>Tap 'Done' to continue to Devices.</p>
"""
connectionStatus(message)
}
if ( !state.vendorAccessToken ) { //We didn't get an access token, bail on install
return
def fail() {
log.debug "in fail"
def message = """
<p>The connection could not be established!</p>
<p>Click 'Done' to return to the menu.</p>
"""
connectionStatus(message)
}
def connectionStatus(message, redirectUrl = null) {
def redirectHtml = ""
if (redirectUrl) {
redirectHtml = """
<meta http-equiv="refresh" content="3; url=${redirectUrl}" />
"""
}
/* OAuth Step 3: Use the access token to call into the vendor API throughout your code using state.vendorAccessToken. */
def html = """
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${getVendorName()} Connection</title>
<style type="text/css">
* { box-sizing: border-box; }
@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: 100%;
padding: 40px;
/*background: #eee;*/
text-align: center;
}
img {
vertical-align: middle;
}
img:nth-child(2) {
margin: 0 30px;
}
p {
font-size: 2.2em;
font-family: 'Swiss 721 W01 Thin';
text-align: center;
color: #666666;
margin-bottom: 0;
}
/*
p:last-child {
margin-top: 0px;
}
*/
span {
font-family: 'Swiss 721 W01 Light';
}
</style>
</head>
<body>
<div class="container">
<img src=""" + getVendorIcon() + """ alt="Vendor 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>We have located your """ + getVendorName() + """ account.</p>
<p>Tap 'Done' to process your credentials.</p>
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${getVendorName()} Connection</title>
<style type="text/css">
* { box-sizing: border-box; }
@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: 100%;
padding: 40px;
/*background: #eee;*/
text-align: center;
}
img {
vertical-align: middle;
}
img:nth-child(2) {
margin: 0 30px;
}
p {
font-size: 2.2em;
font-family: 'Swiss 721 W01 Thin';
text-align: center;
color: #666666;
margin-bottom: 0;
}
/*
p:last-child {
margin-top: 0px;
}
*/
span {
font-family: 'Swiss 721 W01 Light';
}
</style>
</head>
<body>
<div class="container">
<img src=""" + getVendorIcon() + """ alt="Vendor 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" />
${message}
</div>
</body>
</html>
"""
"""
render contentType: 'text/html', data: html
}
def receivedToken() {
log.debug "In receivedToken"
def html = """
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Withings Connection</title>
<style type="text/css">
* { box-sizing: border-box; }
@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;
}
img:nth-child(2) {
margin: 0 30px;
}
p {
font-size: 2.2em;
font-family: 'Swiss 721 W01 Thin';
text-align: center;
color: #666666;
margin-bottom: 0;
}
/*
p:last-child {
margin-top: 0px;
}
*/
span {
font-family: 'Swiss 721 W01 Light';
}
</style>
</head>
<body>
<div class="container">
<img src=""" + getVendorIcon() + """ alt="Vendor 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>Tap 'Done' to continue to Devices.</p>
</div>
</body>
</html>
"""
render contentType: 'text/html', data: html
}
// "
def refreshToken() {
log.debug "In refreshToken"
@@ -308,8 +275,8 @@ def refreshToken() {
client_secret: getClientSecret(),
client_id: getClientId(),
grant_type: "refresh_token",
refresh_token: state.vendorRefreshToken
]
refresh_token: state.refreshToken
]
def tokenUrl = getVendorTokenPath()
def params = [
@@ -318,7 +285,7 @@ def refreshToken() {
body: oauthParams,
]
/* OAuth Step 2: Request access token with our client Secret and OAuth "Code" */
// OAuth Step 2: Request access token with our client Secret and OAuth "Code"
try {
httpPost(params) { response ->
def slurper = new JsonSlurper();
@@ -327,9 +294,9 @@ def refreshToken() {
def data = slurper.parseText(key);
log.debug "Data: $data"
state.vendorRefreshToken = data.refresh_token
state.vendorAccessToken = data.access_token
state.vendorTokenExpires = now() + (data.expires_in * 1000)
state.refreshToken = data.refresh_token
state.accessToken = data.access_token
state.tokenExpires = now() + (data.expires_in * 1000)
return true
}
@@ -338,9 +305,8 @@ def refreshToken() {
log.debug "Error: $e"
}
log.debug "State: $state"
if ( !state.vendorAccessToken ) { //We didn't get an access token
// We didn't get an access token
if ( !state.accessToken ) {
return false
}
}
@@ -371,10 +337,10 @@ def initialize() {
settings.devices.each {
def deviceId = it
def detail = state.deviceDetail[deviceId]
def detail = state?.deviceDetail[deviceId]
try {
switch(detail.type) {
switch(detail?.type) {
case 'NAMain':
log.debug "Base station"
createChildDevice("Netatmo Basestation", deviceId, "${detail.type}.${deviceId}", detail.module_name)
@@ -482,13 +448,13 @@ def listDevices() {
}
def apiGet(String path, Map query, Closure callback) {
if(now() >= state.vendorTokenExpires) {
if(now() >= state.tokenExpires) {
refreshToken();
}
query['access_token'] = state.vendorAccessToken
query['access_token'] = state.accessToken
def params = [
uri: apiUrl(),
uri: getApiUrl(),
path: path,
'query': query
]
@@ -521,12 +487,12 @@ def poll() {
log.debug "State: ${state.deviceState}"
settings.devices.each { deviceId ->
def detail = state.deviceDetail[deviceId]
def data = state.deviceState[deviceId]
def child = children.find { it.deviceNetworkId == deviceId }
def detail = state?.deviceDetail[deviceId]
def data = state?.deviceState[deviceId]
def child = children?.find { it.deviceNetworkId == deviceId }
log.debug "Update: $child";
switch(detail.type) {
switch(detail?.type) {
case 'NAMain':
log.debug "Updating NAMain $data"
child?.sendEvent(name: 'temperature', value: cToPref(data['Temperature']) as float, unit: getTemperatureScale())

View File

@@ -370,9 +370,7 @@ def parse_api_response(resp, message) {
}
}
def getServerUrl() {
return "https://graph.api.smartthings.com"
}
def getServerUrl() { return getApiServerUrl() }
def debugEvent(message, displayEvent) {
def results = [

View File

@@ -21,6 +21,12 @@ preferences()
section("Allow Simple Control to Monitor and Control These Things...")
{
input "switches", "capability.switch", title: "Which Switches?", multiple: true, required: false
input "locks", "capability.lock", title: "Which Locks?", multiple: true, required: false
input "thermostats", "capability.thermostat", title: "Which Thermostats?", multiple: true, required: false
input "doorControls", "capability.doorControl", title: "Which Door Controls?", multiple: true, required: false
input "colorControls", "capability.colorControl", title: "Which Color Controllers?", multiple: true, required: false
input "musicPlayers", "capability.musicPlayer", title: "Which Music Players?", multiple: true, required: false
input "switchLevels", "capability.switchLevel", title: "Which Adjustable Switches?", multiple: true, required: false
}
page(name: "mainPage", title: "Simple Control Setup", content: "mainPage", refreshTimeout: 5)
@@ -31,12 +37,17 @@ preferences()
mappings {
path("/devices") {
action: [
GET: "getDevices"
]
}
path("/:deviceType/devices") {
action: [
GET: "getDevices",
POST: "handleDevicesWithIDs"
]
}
path("/device/:id") {
}
path("/device/:deviceType/:id") {
action: [
GET: "getDevice",
POST: "updateDevice"
@@ -93,33 +104,40 @@ def handleDevicesWithIDs()
//log.debug("ids: ${ids}")
def command = data?.command
def arguments = data?.arguments
def type = params?.deviceType
//log.debug("device type: ${type}")
if (command)
{
def success = false
def statusCode = 404
//log.debug("command ${command}, arguments ${arguments}")
for (devId in ids)
{
def device = allDevices.find { it.id == devId }
if (device) {
if (arguments) {
//log.debug("device: ${device}")
// Check if we have a device that responds to the specified command
if (validateCommand(device, type, command)) {
if (arguments) {
device."$command"(*arguments)
} else {
device."$command"()
}
success = true
}
else {
device."$command"()
}
statusCode = 200
} else {
//log.debug("device not found ${devId}")
statusCode = 403
}
}
if (success)
def responseData = "{}"
switch (statusCode)
{
render status: 200, data: "{}"
}
else
{
render status: 404, data: '{"msg": "Device not found"}'
case 403:
responseData = '{"msg": "Access denied. This command is not supported by current capability."}'
break
case 404:
responseData = '{"msg": "Device not found"}'
break
}
render status: statusCode, data: responseData
}
else
{
@@ -164,25 +182,101 @@ def updateDevice()
def data = request.JSON
def command = data?.command
def arguments = data?.arguments
def type = params?.deviceType
//log.debug("device type: ${type}")
//log.debug("updateDevice, params: ${params}, request: ${data}")
if (!command) {
render status: 400, data: '{"msg": "command is required"}'
} else {
def statusCode = 404
def device = allDevices.find { it.id == params.id }
if (device) {
if (arguments) {
device."$command"(*arguments)
// Check if we have a device that responds to the specified command
if (validateCommand(device, type, command)) {
if (arguments) {
device."$command"(*arguments)
}
else {
device."$command"()
}
statusCode = 200
} else {
device."$command"()
statusCode = 403
}
render status: 204, data: "{}"
} else {
render status: 404, data: '{"msg": "Device not found"}'
}
def responseData = "{}"
switch (statusCode)
{
case 403:
responseData = '{"msg": "Access denied. This command is not supported by current capability."}'
break
case 404:
responseData = '{"msg": "Device not found"}'
break
}
render status: statusCode, data: responseData
}
}
/**
* Validating the command passed by the user based on capability.
* @return boolean
*/
def validateCommand(device, deviceType, command) {
//log.debug("validateCommand ${command}")
def capabilityCommands = getDeviceCapabilityCommands(device.capabilities)
//log.debug("capabilityCommands: ${capabilityCommands}")
def currentDeviceCapability = getCapabilityName(deviceType)
//log.debug("currentDeviceCapability: ${currentDeviceCapability}")
if (capabilityCommands[currentDeviceCapability]) {
return command in capabilityCommands[currentDeviceCapability] ? true : false
} else {
// Handling other device types here, which don't accept commands
httpError(400, "Bad request.")
}
}
/**
* Need to get the attribute name to do the lookup. Only
* doing it for the device types which accept commands
* @return attribute name of the device type
*/
def getCapabilityName(type) {
switch(type) {
case "switches":
return "Switch"
case "locks":
return "Lock"
case "thermostats":
return "Thermostat"
case "doorControls":
return "Door Control"
case "colorControls":
return "Color Control"
case "musicPlayers":
return "Music Player"
case "switchLevels":
return "Switch Level"
default:
return type
}
}
/**
* Constructing the map over here of
* supported commands by device capability
* @return a map of device capability -> supported commands
*/
def getDeviceCapabilityCommands(deviceCapabilities) {
def map = [:]
deviceCapabilities.collect {
map[it.name] = it.commands.collect{ it.name.toString() }
}
return map
}
def listSubscriptions()
{
//log.debug "listSubscriptions()"
@@ -361,7 +455,13 @@ def agentDiscovery(params=[:])
}
section("Allow Simple Control to Monitor and Control These Things...")
{
input "switches", "capability.switch", title: "Which Switches?", multiple: true, required: false
input "switches", "capability.switch", title: "Which Switches?", multiple: true, required: false
input "locks", "capability.lock", title: "Which Locks?", multiple: true, required: false
input "thermostats", "capability.thermostat", title: "Which Thermostats?", multiple: true, required: false
input "doorControls", "capability.doorControl", title: "Which Door Controls?", multiple: true, required: false
input "colorControls", "capability.colorControl", title: "Which Color Controllers?", multiple: true, required: false
input "musicPlayers", "capability.musicPlayer", title: "Which Music Players?", multiple: true, required: false
input "switchLevels", "capability.switchLevel", title: "Which Adjustable Switches?", multiple: true, required: false
}
}
}
@@ -672,5 +772,3 @@ def List getRealHubFirmwareVersions()
}

View File

@@ -297,8 +297,8 @@ private getTimeOk() {
def result = true
if (starting && ending) {
def currTime = now()
def start = timeToday(starting).time
def stop = timeToday(ending).time
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"

View File

@@ -39,6 +39,7 @@ preferences {
page(name: "completionPage")
page(name: "numbersPage")
page(name: "controllerExplanationPage")
page(name: "unsupportedDevicesPage")
}
def rootPage() {
@@ -47,6 +48,9 @@ def rootPage() {
section("What to dim") {
input(name: "dimmers", type: "capability.switchLevel", title: "Dimmers", description: null, multiple: true, required: true, submitOnChange: true)
if (dimmers) {
if (dimmersContainUnsupportedDevices()) {
href(name: "toUnsupportedDevicesPage", page: "unsupportedDevicesPage", title: "Some of your selected dimmers don't seem to be supported", description: "Tap here to fix it", required: true)
}
href(name: "toNumbersPage", page: "numbersPage", title: "Duration & Direction", description: numbersPageHrefDescription(), state: "complete")
}
}
@@ -71,6 +75,31 @@ def rootPage() {
}
}
def unsupportedDevicesPage() {
def unsupportedDimmers = dimmers.findAll { !hasSetLevelCommand(it) }
dynamicPage(name: "unsupportedDevicesPage") {
if (unsupportedDimmers) {
section("These devices do not support the setLevel command") {
unsupportedDimmers.each {
paragraph deviceLabel(it)
}
}
section {
input(name: "dimmers", type: "capability.sensor", title: "Please remove the above devices from this list.", submitOnChange: true, multiple: true)
}
section {
paragraph "If you think there is a mistake here, please contact support."
}
} else {
section {
paragraph "You're all set. You can hit the back button, now. Thanks for cleaning up your settings :)"
}
}
}
}
def controllerExplanationPage() {
dynamicPage(name: "controllerExplanationPage", title: "How To Control Gentle Wake Up") {
@@ -208,7 +237,7 @@ def completionPage() {
}
section("Notifications") {
input("recipients", "contact", title: "Send notifications to") {
input("recipients", "contact", title: "Send notifications to", required: false) {
input(name: "completionPhoneNumber", type: "phone", title: "Text This Number", description: "Phone number", required: false)
input(name: "completionPush", type: "bool", title: "Send A Push Notification", description: "Phone number", required: false)
}
@@ -528,14 +557,16 @@ def updateDimmers(percentComplete) {
} else {
def shouldChangeColors = (colorize && colorize != "false")
def canChangeColors = hasSetColorCommand(dimmer)
log.debug "Setting ${deviceLabel(dimmer)} to ${nextLevel}"
if (shouldChangeColors && canChangeColors) {
dimmer.setColor([hue: getHue(dimmer, nextLevel), saturation: 100, level: nextLevel])
} else {
if (shouldChangeColors && hasSetColorCommand(dimmer)) {
def hue = getHue(dimmer, nextLevel)
log.debug "Setting ${deviceLabel(dimmer)} level to ${nextLevel} and hue to ${hue}"
dimmer.setColor([hue: hue, saturation: 100, level: nextLevel])
} else if (hasSetLevelCommand(dimmer)) {
log.debug "Setting ${deviceLabel(dimmer)} level to ${nextLevel}"
dimmer.setLevel(nextLevel)
} else {
log.warn "${deviceLabel(dimmer)} does not have setColor or setLevel commands."
}
}
@@ -689,7 +720,7 @@ def completionPercentage() {
def now = new Date().getTime()
def timeElapsed = now - atomicState.start
def totalRunTime = totalRunTimeMillis()
def totalRunTime = totalRunTimeMillis() ?: 1
def percentComplete = timeElapsed / totalRunTime * 100
log.debug "percentComplete: ${percentComplete}"
@@ -817,24 +848,21 @@ private getRedHue(level) {
if (level >= 96) return 17
}
private dimmersContainUnsupportedDevices() {
def found = dimmers.find { hasSetLevelCommand(it) == false }
return found != null
}
private hasSetLevelCommand(device) {
def isDimmer = false
device.supportedCommands.each {
if (it.name.contains("setLevel")) {
isDimmer = true
}
}
return isDimmer
return hasCommand(device, "setLevel")
}
private hasSetColorCommand(device) {
def hasColor = false
device.supportedCommands.each {
if (it.name.contains("setColor")) {
hasColor = true
}
}
return hasColor
return hasCommand(device, "setColor")
}
private hasCommand(device, String command) {
return (device.supportedCommands.find { it.name == command } != null)
}
private dimmersWithSetColorCommand() {
@@ -1073,4 +1101,4 @@ def hasStartLevel() {
def hasEndLevel() {
return (endLevel != null && endLevel != "")
}
}

View File

@@ -30,6 +30,7 @@ definition(
preferences {
page(name:"mainPage", title:"Hue Device Setup", content:"mainPage", refreshTimeout:5)
page(name:"bridgeDiscovery", title:"Hue Bridge Discovery", content:"bridgeDiscovery", refreshTimeout:5)
page(name:"bridgeDiscoveryFailed", title:"Bridge Discovery Failed", content:"bridgeDiscoveryFailed", refreshTimeout:0)
page(name:"bridgeBtnPush", title:"Linking with your Hue", content:"bridgeLinking", refreshTimeout:5)
page(name:"bulbDiscovery", title:"Hue Device Setup", content:"bulbDiscovery", refreshTimeout:5)
}
@@ -53,12 +54,21 @@ def bridgeDiscovery(params=[:])
def options = bridges ?: []
def numFound = options.size() ?: 0
if (numFound == 0 && state.bridgeRefreshCount > 25) {
log.trace "Cleaning old bridges memory"
state.bridges = [:]
state.bridgeRefreshCount = 0
app.updateSetting("selectedHue", "")
}
if (numFound == 0) {
if (state.bridgeRefreshCount == 25) {
log.trace "Cleaning old bridges memory"
state.bridges = [:]
app.updateSetting("selectedHue", "")
} else if (state.bridgeRefreshCount > 100) {
// five minutes have passed, give up
// there seems to be a problem going back from discovey failed page in some instances (compared to pressing next)
// however it is probably a SmartThings settings issue
state.bridges = [:]
app.updateSetting("selectedHue", "")
state.bridgeRefreshCount = 0
return bridgeDiscoveryFailed()
}
}
ssdpSubscribe()
@@ -79,6 +89,13 @@ def bridgeDiscovery(params=[:])
}
}
def bridgeDiscoveryFailed() {
return dynamicPage(name:"bridgeDiscoveryFailed", title: "Bridge Discovery Failed", nextPage: "bridgeDiscovery") {
section("Failed to discover any Hue Bridges. Please confirm that the Hue Bridge is connected to the same network as your SmartThings Hub, and that it has power.") {
}
}
}
def bridgeLinking()
{
int linkRefreshcount = !state.linkRefreshcount ? 0 : state.linkRefreshcount as int
@@ -88,19 +105,15 @@ def bridgeLinking()
def nextPage = ""
def title = "Linking with your Hue"
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!"
paragraphText = "Linking to your hub was a success! Please click 'Next'!"
hueimage = null
}
if((linkRefreshcount % 2) == 0 && !state.username) {
@@ -110,8 +123,6 @@ def bridgeLinking()
return dynamicPage(name:"bridgeBtnPush", title:title, nextPage:nextPage, refreshInterval:refreshInterval) {
section("") {
paragraph """${paragraphText}"""
if (hueimage != null)
image "${hueimage}"
}
}
}
@@ -135,13 +146,14 @@ def bulbDiscovery() {
if((bulbRefreshCount % 5) == 0) {
discoverHueBulbs()
}
def selectedBridge = state.bridges.find { key, value -> value?.serialNumber?.equalsIgnoreCase(selectedHue) }
def title = selectedBridge?.value?.name ?: "Find bridges"
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:bulboptions
}
section {
def title = getBridgeIP() ? "Hue bridge (${getBridgeIP()})" : "Find bridges"
href "bridgeDiscovery", title: title, description: "", state: selectedHue ? "complete" : "incomplete", params: [override: true]
}
@@ -323,6 +335,8 @@ private getDeviceType(hueType) {
return "Hue Bulb"
else if (hueType?.equalsIgnoreCase("Color Light"))
return "Hue Bloom"
else if (hueType?.equalsIgnoreCase("Color Temperature Light"))
return "Hue White Ambiance Bulb"
else
return null
}
@@ -346,26 +360,29 @@ def addBulbs() {
def newHueBulb
if (bulbs instanceof java.util.Map) {
newHueBulb = bulbs.find { (app.id + "/" + it.value.id) == dni }
if (newHueBulb != null) {
d = addChildBulb(dni, newHueBulb?.value?.type, newHueBulb?.value?.name, newHueBulb?.value?.hub)
if (newHueBulb != null) {
d = addChildBulb(dni, newHueBulb?.value?.type, newHueBulb?.value?.name, newHueBulb?.value?.hub)
if (d) {
log.debug "created ${d.displayName} with id $dni"
d.completedSetup = true
d.refresh()
}
} else {
log.debug "$dni in not longer paired to the Hue Bridge or ID changed"
}
} else {
log.debug "$dni in not longer paired to the Hue Bridge or ID changed"
}
} else {
//backwards compatable
//backwards compatable
newHueBulb = bulbs.find { (app.id + "/" + it.id) == dni }
d = addChildBulb(dni, "Extended Color Light", newHueBulb?.value?.name, newHueBulb?.value?.hub)
d?.completedSetup = true
d?.refresh()
}
} else {
log.debug "found ${d.displayName} with id $dni already exists, type: '$d.typeName'"
if (bulbs instanceof java.util.Map) {
// Update device type if incorrect
def newHueBulb = bulbs.find { (app.id + "/" + it.value.id) == dni }
def newHueBulb = bulbs.find { (app.id + "/" + it.value.id) == dni }
upgradeDeviceType(d, newHueBulb?.value?.type)
}
}
@@ -397,6 +414,7 @@ def addBridge() {
}
if (newbridge) {
d = addChildDevice("smartthings", "Hue Bridge", selectedHue, vbridge.value.hub)
d?.completedSetup = true
log.debug "created ${d.displayName} with id ${d.deviceNetworkId}"
def childDevice = getChildDevice(d.deviceNetworkId)
childDevice.sendEvent(name: "serialNumber", value: vbridge.value.serialNumber)
@@ -484,7 +502,21 @@ void bridgeDescriptionHandler(physicalgraph.device.HubResponse hubResponse) {
def bridges = getHueBridges()
def bridge = bridges.find {it?.key?.contains(body?.device?.UDN?.text())}
if (bridge) {
bridge.value << [name:body?.device?.friendlyName?.text(), serialNumber:body?.device?.serialNumber?.text(), verified: true]
// serialNumber from API is in format of 0017882413ad (mac address), however on the actual bridge only last six
// characters are printed on the back so using that to identify bridge
def idNumber = body?.device?.serialNumber?.text()
if (idNumber?.size() >= 6)
idNumber = idNumber[-6..-1].toUpperCase()
// usually in form of bridge name followed by (ip), i.e. defaults to Philips Hue (192.168.1.2)
// replace IP with serial number to make it easier for user to identify
def name = body?.device?.friendlyName?.text()
def index = name?.indexOf('(')
if (index != -1) {
name = name.substring(0,index)
name += " ($idNumber)"
}
bridge.value << [name:name, serialNumber:body?.device?.serialNumber?.text(), verified: true]
} else {
log.error "/description.xml returned a bridge that didn't exist"
}
@@ -657,7 +689,7 @@ def parse(childDevice, description) {
log.warn "Parsing Body failed - trying again..."
poll()
}
if (body instanceof java.util.HashMap) {
if (body instanceof java.util.Map) {
//poll response
def bulbs = getChildDevices()
for (bulb in body) {
@@ -798,22 +830,22 @@ def setColorTemperature(childDevice, huesettings) {
def setColor(childDevice, huesettings) {
log.debug "Executing 'setColor($huesettings)'"
def value = [:]
def hue = null
def sat = null
def xy = null
if (huesettings.hex != null) {
value.xy = getHextoXY(huesettings.hex)
} else {
if (huesettings.hue != null)
value.hue = Math.min(Math.round(huesettings.hue * 65535 / 100), 65535)
if (huesettings.saturation != null)
if (huesettings.saturation != null)
value.sat = Math.min(Math.round(huesettings.saturation * 255 / 100), 255)
}
// Default behavior is to turn light on
// Default behavior is to turn light on
value.on = true
if (huesettings.level != null) {
@@ -821,7 +853,7 @@ def setColor(childDevice, huesettings) {
value.on = false
else if (huesettings.level == 1)
value.bri = 1
else
else
value.bri = Math.min(Math.round(huesettings.level * 255 / 100), 255)
}
value.alert = huesettings.alert ? huesettings.alert : "none"

View File

@@ -73,7 +73,8 @@ def temperatureHandler(evt) {
// TODO: Send "Temperature back to normal" SMS, turn switch off
} else {
log.debug "Temperature dropped below $tooCold: sending SMS to $phone1 and activating $mySwitch"
send("${temperatureSensor1.displayName} is too cold, reporting a temperature of ${evt.value}${evt.unit?:"F"}")
def tempScale = location.temperatureScale ?: "F"
send("${temperatureSensor1.displayName} is too cold, reporting a temperature of ${evt.value}${evt.unit?:tempScale}")
switch1?.on()
}
}

View File

@@ -73,7 +73,8 @@ def temperatureHandler(evt) {
// TODO: Send "Temperature back to normal" SMS, turn switch off
} else {
log.debug "Temperature rose above $tooHot: sending SMS to $phone1 and activating $mySwitch"
send("${temperatureSensor1.displayName} is too hot, reporting a temperature of ${evt.value}${evt.unit?:"F"}")
def tempScale = location.temperatureScale ?: "F"
send("${temperatureSensor1.displayName} is too hot, reporting a temperature of ${evt.value}${evt.unit?:tempScale}")
switch1?.on()
}
}

View File

@@ -27,7 +27,6 @@ definition(
) {
appSetting "clientId"
appSetting "clientSecret"
appSetting "serverUrl"
}
preferences {
@@ -192,7 +191,7 @@ def getSmartThingsClientId() {
return "pREqugabRetre4EstetherufrePumamExucrEHuc"
}
def getServerUrl() { appSettings.serverUrl }
def getServerUrl() { getApiServerUrl() }
def buildRedirectUrl()
{

View File

@@ -339,7 +339,7 @@ def initialize() {
state.aux = 0
if (selectedhubs || selectedactivities) {
addDevice()
runEvery5Minutes("discovery")
runEvery5Minutes("poll")
}
}
@@ -394,9 +394,9 @@ def discovery() {
}
} catch (java.net.SocketTimeoutException e) {
log.warn "Connection to the hub timed out. Please restart the hub and try again."
state.resethub = true
state.resethub = true
} catch (e) {
log.warn "Hostname in certificate didn't match. Please try again later."
log.info "Logitech Harmony - Error: $e"
}
return null
}
@@ -474,7 +474,7 @@ def activity(dni,mode) {
def poll() {
// GET THE LIST OF ACTIVITIES
if (state.HarmonyAccessToken) {
getActivityList()
getActivityList()
def Params = [auth: state.HarmonyAccessToken]
def url = "https://home.myharmony.com/cloudapi/state?${toQueryString(Params)}"
try {
@@ -520,14 +520,17 @@ def poll() {
return "Poll completed $map - $state.hubs"
}
} catch (groovyx.net.http.HttpResponseException e) {
if (e.statusCode == 401) { // token is expired
state.remove("HarmonyAccessToken")
return "Harmony Access token has expired"
}
} catch(Exception e) {
log.trace e
}
}
if (e.statusCode == 401) { // token is expired
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.info "Logitech Harmony - Error: $e"
}
}
}
@@ -655,29 +658,73 @@ def updateDevice() {
def data = request.JSON
def command = data.command
def arguments = data.arguments
log.debug "updateDevice, params: ${params}, request: ${data}"
if (!command) {
render status: 400, data: '{"msg": "command is required"}'
} else {
def device = allDevices.find { it.id == params.id }
if (device) {
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"}'
}
} else {
render status: 404, data: '{"msg": "Device not found"}'
}
if (device) {
if (validateCommand(device, command)) {
if (arguments) {
device."$command"(*arguments)
} else {
device."$command"()
}
render status: 204, data: "{}"
} else {
render status: 403, data: '{"msg": "Access denied. This command is not supported by current capability."}'
}
} else {
render status: 404, data: '{"msg": "Device not found"}'
}
}
}
/**
* Validating the command passed by the user based on capability.
* @return boolean
*/
def validateCommand(device, command) {
def capabilityCommands = getDeviceCapabilityCommands(device.capabilities)
def currentDeviceCapability = getCapabilityName(device)
if (currentDeviceCapability != "" && capabilityCommands[currentDeviceCapability]) {
return (command in capabilityCommands[currentDeviceCapability] || (currentDeviceCapability == "Switch" && command == "setLevel" && device.hasCommand("setLevel"))) ? true : false
} else {
// Handling other device types here, which don't accept commands
httpError(400, "Bad request.")
}
}
/**
* Need to get the attribute name to do the lookup. Only
* doing it for the device types which accept commands
* @return attribute name of the device type
*/
def getCapabilityName(device) {
def capName = ""
if (switches.find{it.id == device.id})
capName = "Switch"
else if (alarms.find{it.id == device.id})
capName = "Alarm"
else if (locks.find{it.id == device.id})
capName = "Lock"
log.trace "Device: $device - Capability Name: $capName"
return capName
}
/**
* Constructing the map over here of
* supported commands by device capability
* @return a map of device capability -> supported commands
*/
def getDeviceCapabilityCommands(deviceCapabilities) {
def map = [:]
deviceCapabilities.collect {
map[it.name] = it.commands.collect{ it.name.toString() }
}
return map
}
def listSubscriptions() {
log.debug "listSubscriptions()"
app.subscriptions?.findAll { it.device?.device && it.device.id }?.collect {
@@ -776,18 +823,33 @@ def deviceHandler(evt) {
}
def sendToHarmony(evt, String callbackUrl) {
def callback = new URI(callbackUrl)
def host = callback.port != -1 ? "${callback.host}:${callback.port}" : callback.host
def path = callback.query ? "${callback.path}?${callback.query}".toString() : callback.path
sendHubCommand(new physicalgraph.device.HubAction(
method: "POST",
path: path,
headers: [
"Host": host,
"Content-Type": "application/json"
],
body: [evt: [deviceId: evt.deviceId, name: evt.name, value: evt.value]]
))
def callback = new URI(callbackUrl)
if (callback.port != -1) {
def host = callback.port != -1 ? "${callback.host}:${callback.port}" : callback.host
def path = callback.query ? "${callback.path}?${callback.query}".toString() : callback.path
sendHubCommand(new physicalgraph.device.HubAction(
method: "POST",
path: path,
headers: [
"Host": host,
"Content-Type": "application/json"
],
body: [evt: [deviceId: evt.deviceId, name: evt.name, value: evt.value]]
))
} else {
def params = [
uri: callbackUrl,
body: [evt: [deviceId: evt.deviceId, name: evt.name, value: evt.value]]
]
try {
log.debug "Sending data to Harmony Cloud: $params"
httpPostJson(params) { resp ->
log.debug "Harmony Cloud - Response: ${resp.status}"
}
} catch (e) {
log.error "Harmony Cloud - Something went wrong: $e"
}
}
}
def listHubs() {

View File

@@ -107,8 +107,8 @@ mappings {
path("/locks") {
action: [
GET: "listLocks",
PUT: "updateLock",
POST: "updateLock"
PUT: "updateLocks",
POST: "updateLocks"
]
}
path("/locks/:id") {
@@ -442,31 +442,87 @@ def executePhrase() {
}
private void updateAll(devices) {
def type = params.param1
def command = request.JSON?.command
if (command)
{
command = command.toLowerCase()
devices."$command"()
if (!devices) {
httpError(404, "Devices not found")
}
if (command){
devices.each { device ->
executeCommand(device, type, command)
}
}
}
private void update(devices) {
log.debug "update, request: ${request.JSON}, params: ${params}, devices: $devices.id"
//def command = request.JSON?.command
def command = params.command
if (command)
{
command = command.toLowerCase()
def device = devices.find { it.id == params.id }
if (!device)
{
def type = params.param1
def command = request.JSON?.command
def device = devices?.find { it.id == params.id }
if (!device) {
httpError(404, "Device not found")
}
else
{
device."$command"()
}
}
if (command) {
executeCommand(device, type, command)
}
}
/**
* Validating the command passed by the user based on capability.
* @return boolean
*/
def validateCommand(device, deviceType, command) {
def capabilityCommands = getDeviceCapabilityCommands(device.capabilities)
def currentDeviceCapability = getCapabilityName(deviceType)
if (capabilityCommands[currentDeviceCapability]) {
return command in capabilityCommands[currentDeviceCapability] ? true : false
} else {
// Handling other device types here, which don't accept commands
httpError(400, "Bad request.")
}
}
/**
* Need to get the attribute name to do the lookup. Only
* doing it for the device types which accept commands
* @return attribute name of the device type
*/
def getCapabilityName(type) {
switch(type) {
case "switches":
return "Switch"
case "locks":
return "Lock"
default:
return type
}
}
/**
* Constructing the map over here of
* supported commands by device capability
* @return a map of device capability -> supported commands
*/
def getDeviceCapabilityCommands(deviceCapabilities) {
def map = [:]
deviceCapabilities.collect {
map[it.name] = it.commands.collect{ it.name.toString() }
}
return map
}
/**
* Validates and executes the command
* on the device or devices
*/
def executeCommand(device, type, command) {
if (validateCommand(device, type, command)) {
device."$command"()
} else {
httpError(403, "Access denied. This command is not supported by current capability.")
}
}
private show(devices, type) {

View File

@@ -92,22 +92,87 @@ void updateLock() {
private void updateAll(devices) {
def command = request.JSON?.command
if (command) {
devices."$command"()
def type = params.param1
if (!devices) {
httpError(404, "Devices not found")
}
if (command){
devices.each { device ->
executeCommand(device, type, command)
}
}
}
private void update(devices) {
log.debug "update, request: ${request.JSON}, params: ${params}, devices: $devices.id"
def command = request.JSON?.command
if (command) {
def device = devices.find { it.id == params.id }
if (!device) {
httpError(404, "Device not found")
} else {
device."$command"()
}
def type = params.param1
def device = devices?.find { it.id == params.id }
if (!device) {
httpError(404, "Device not found")
}
if (command) {
executeCommand(device, type, command)
}
}
/**
* Validating the command passed by the user based on capability.
* @return boolean
*/
def validateCommand(device, deviceType, command) {
def capabilityCommands = getDeviceCapabilityCommands(device.capabilities)
def currentDeviceCapability = getCapabilityName(deviceType)
if (capabilityCommands[currentDeviceCapability]) {
return command in capabilityCommands[currentDeviceCapability] ? true : false
} else {
// Handling other device types here, which don't accept commands
httpError(400, "Bad request.")
}
}
/**
* Need to get the attribute name to do the lookup. Only
* doing it for the device types which accept commands
* @return attribute name of the device type
*/
def getCapabilityName(type) {
switch(type) {
case "switches":
return "Switch"
case "locks":
return "Lock"
default:
return type
}
}
/**
* Constructing the map over here of
* supported commands by device capability
* @return a map of device capability -> supported commands
*/
def getDeviceCapabilityCommands(deviceCapabilities) {
def map = [:]
deviceCapabilities.collect {
map[it.name] = it.commands.collect{ it.name.toString() }
}
return map
}
/**
* Validates and executes the command
* on the device or devices
*/
def executeCommand(device, type, command) {
if (validateCommand(device, type, command)) {
device."$command"()
} else {
httpError(403, "Access denied. This command is not supported by current capability.")
}
}
private show(devices, name) {