mirror of
https://github.com/mtan93/SmartThingsPublic.git
synced 2026-03-09 05:11:52 +00:00
Compare commits
275 Commits
MSA-933-1
...
MSA-1327-2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce8a629cef | ||
|
|
031a15ec86 | ||
|
|
fc2db2575d | ||
|
|
fd549631e6 | ||
|
|
417c246d61 | ||
|
|
0f1781c02e | ||
|
|
4ce6ee0890 | ||
|
|
f8050a5cd5 | ||
|
|
d44dac448b | ||
|
|
67c20abcba | ||
|
|
d56e132896 | ||
|
|
009ec2539d | ||
|
|
ff0860cbe1 | ||
|
|
ecfb99974b | ||
|
|
aa3a18f421 | ||
|
|
c2f18a91be | ||
|
|
fc6b14b85e | ||
|
|
0c1208928f | ||
|
|
02d9963fab | ||
|
|
f131fb71cf | ||
|
|
9c27ed6cb7 | ||
|
|
35edaa19c7 | ||
|
|
dc09201866 | ||
|
|
6afcbf8f70 | ||
|
|
2894d52efa | ||
|
|
a21f9f177c | ||
|
|
02f968b8cb | ||
|
|
32f0385e30 | ||
|
|
b105d9d80e | ||
|
|
9bfad5d6a4 | ||
|
|
85a335d365 | ||
|
|
40e6778e31 | ||
|
|
feba6643a6 | ||
|
|
49c893771d | ||
|
|
d73f4c2ded | ||
|
|
13a056ec8d | ||
|
|
bd0ccd0c21 | ||
|
|
d34508c19d | ||
|
|
a8e118fe83 | ||
|
|
072cc066b6 | ||
|
|
17562c96ae | ||
|
|
f55452d1c6 | ||
|
|
14b5fd41b8 | ||
|
|
0f4d7bd520 | ||
|
|
a95fbf2612 | ||
|
|
0f60ca61cb | ||
|
|
293a73136e | ||
|
|
3b56fb4a2f | ||
|
|
26a0f6f939 | ||
|
|
8de3276ce6 | ||
|
|
f77c5810c6 | ||
|
|
9fc5f14dd7 | ||
|
|
55d7a4a263 | ||
|
|
cca1eccce6 | ||
|
|
56eef9cf22 | ||
|
|
b33d621696 | ||
|
|
45b78eff8d | ||
|
|
a133406b6e | ||
|
|
bd62962ee1 | ||
|
|
f1e54c8a5c | ||
|
|
a5da182bf4 | ||
|
|
a4a48fddd2 | ||
|
|
2bd18859b9 | ||
|
|
32f8d2d944 | ||
|
|
e1de599668 | ||
|
|
0e01cbed06 | ||
|
|
566425c531 | ||
|
|
bf476940e9 | ||
|
|
ab2ba8104d | ||
|
|
a219f37035 | ||
|
|
8821c68e9c | ||
|
|
27c05f4e5b | ||
|
|
7571c1b980 | ||
|
|
bb65c4ce14 | ||
|
|
69ae9973da | ||
|
|
36c0af82fe | ||
|
|
eba1f16ee1 | ||
|
|
f397691fdb | ||
|
|
9a5be2c5db | ||
|
|
39ac9f9a8c | ||
|
|
c353eeae17 | ||
|
|
467c6ff055 | ||
|
|
5beacf0ef2 | ||
|
|
e7448e7908 | ||
|
|
973c16f088 | ||
|
|
77f880af6e | ||
|
|
1c4386a67b | ||
|
|
88dd510e72 | ||
|
|
e278a3b57d | ||
|
|
7786df3262 | ||
|
|
5ac08e5a92 | ||
|
|
b05d956d95 | ||
|
|
6cdb80db1f | ||
|
|
4db99824af | ||
|
|
d9f224fa6e | ||
|
|
72b51d50bc | ||
|
|
b2e245bd85 | ||
|
|
9a9854cf92 | ||
|
|
1e27ff5d4a | ||
|
|
37f1726ee6 | ||
|
|
d17cadc4c7 | ||
|
|
c7e8079ff1 | ||
|
|
481d13a571 | ||
|
|
9d83b850ca | ||
|
|
84de336a1a | ||
|
|
8b465b03b4 | ||
|
|
2f81964479 | ||
|
|
d5ea735df7 | ||
|
|
6428719c79 | ||
|
|
327f8dfb00 | ||
|
|
e150ea4a59 | ||
|
|
cdbcab6dad | ||
|
|
dd99a024c2 | ||
|
|
810f3645d9 | ||
|
|
2dcbcc84fc | ||
|
|
33ef75091b | ||
|
|
1bcad614ec | ||
|
|
3ab83350f3 | ||
|
|
0c75c8806e | ||
|
|
d91fea89df | ||
|
|
d28d27c4ed | ||
|
|
235e3f5507 | ||
|
|
e7c1d88285 | ||
|
|
74c334a0f7 | ||
|
|
6881f469f5 | ||
|
|
be8c84306a | ||
|
|
a6105188ea | ||
|
|
22f218c072 | ||
|
|
7c000dc61a | ||
|
|
8b543d399b | ||
|
|
bb21b9a612 | ||
|
|
e366a2686f | ||
|
|
820405a3ab | ||
|
|
69875becae | ||
|
|
5c90091e36 | ||
|
|
af19cee795 | ||
|
|
55ed08d5e7 | ||
|
|
2dec6f69c8 | ||
|
|
381fcfdd31 | ||
|
|
b23d7ccf2e | ||
|
|
237d6a79e9 | ||
|
|
fd29fe2b2a | ||
|
|
c259af4312 | ||
|
|
82214e29eb | ||
|
|
bf00284c74 | ||
|
|
8de5ed77f4 | ||
|
|
4d31f8dbe8 | ||
|
|
9d378ce9a1 | ||
|
|
8abe4ac29f | ||
|
|
104fa8d616 | ||
|
|
5b5e185ef0 | ||
|
|
3a0c9c1298 | ||
|
|
51fb7fc7a9 | ||
|
|
4a096fc884 | ||
|
|
babc0206df | ||
|
|
d419fb8606 | ||
|
|
70aae0d76c | ||
|
|
1cd5c68e68 | ||
|
|
9e427d4108 | ||
|
|
a8628b7343 | ||
|
|
1736caebfe | ||
|
|
0fa363fa1a | ||
|
|
0c5840087b | ||
|
|
a6ee53641f | ||
|
|
c6818c8c2b | ||
|
|
6ac174c2f3 | ||
|
|
eb8d5ed4c9 | ||
|
|
7b5d618de8 | ||
|
|
c024e09fb8 | ||
|
|
e5841fb3cb | ||
|
|
805b870447 | ||
|
|
fe92f7ad19 | ||
|
|
10245315ee | ||
|
|
0b239d4686 | ||
|
|
9374290d64 | ||
|
|
ffcacb9da5 | ||
|
|
c45129170a | ||
|
|
53406ada8e | ||
|
|
ffd0dd1545 | ||
|
|
5c1236a21a | ||
|
|
0911651f71 | ||
|
|
9cc92b1987 | ||
|
|
1e27dc1824 | ||
|
|
4bf3679942 | ||
|
|
c714720578 | ||
|
|
281fc939ac | ||
|
|
03c2dec425 | ||
|
|
633bef2ac5 | ||
|
|
38d0ca6170 | ||
|
|
836dd608c6 | ||
|
|
43e4db28eb | ||
|
|
df421a51ac | ||
|
|
3affdd21fc | ||
|
|
a8357e7644 | ||
|
|
42865abc55 | ||
|
|
024a6cb698 | ||
|
|
dc1c78391e | ||
|
|
e279172383 | ||
|
|
0deb26810d | ||
|
|
d4fb75cc47 | ||
|
|
8a5f0af0e2 | ||
|
|
2276748a91 | ||
|
|
8d423e7c4b | ||
|
|
0dfbddee38 | ||
|
|
40f88fa436 | ||
|
|
c3c8bafef4 | ||
|
|
1b9f758bd6 | ||
|
|
ef1b04c08a | ||
|
|
9cece36d69 | ||
|
|
be220e02b2 | ||
|
|
131cc7b016 | ||
|
|
dd1e76e95a | ||
|
|
8aff9e78f6 | ||
|
|
6fbef3b297 | ||
|
|
0b5779528c | ||
|
|
63d25528ae | ||
|
|
d8fe639a51 | ||
|
|
92fea16beb | ||
|
|
25ae1306c4 | ||
|
|
321389aee3 | ||
|
|
106f09445b | ||
|
|
410e9f40cc | ||
|
|
6f173981e4 | ||
|
|
a94d8f2378 | ||
|
|
1578c48440 | ||
|
|
2126d85f6e | ||
|
|
9218b40e25 | ||
|
|
7c326ec7c3 | ||
|
|
74598ec943 | ||
|
|
96a82797de | ||
|
|
c8e4eef66b | ||
|
|
5effd158fc | ||
|
|
3a4b4d6345 | ||
|
|
27808c3996 | ||
|
|
bd0d636b8c | ||
|
|
7526d2b445 | ||
|
|
caf761c015 | ||
|
|
62a965d90b | ||
|
|
fb9f1dee47 | ||
|
|
3a433d3865 | ||
|
|
fbb248dc31 | ||
|
|
a53e506538 | ||
|
|
c15a09a077 | ||
|
|
2e4036d694 | ||
|
|
49b6fb02df | ||
|
|
e2ab965e89 | ||
|
|
3824ccb5e1 | ||
|
|
089ab00ab7 | ||
|
|
a7b2e6a6cd | ||
|
|
361eca6b15 | ||
|
|
56d991b8d2 | ||
|
|
20e112f4f8 | ||
|
|
f593f08c5e | ||
|
|
2d7300d9e5 | ||
|
|
f31c3bf5ee | ||
|
|
0799722bdb | ||
|
|
7fdb99524e | ||
|
|
04941dfa21 | ||
|
|
8f25ff4434 | ||
|
|
1af43681a5 | ||
|
|
b211b298c0 | ||
|
|
da029000c9 | ||
|
|
515b268374 | ||
|
|
23a76fa72b | ||
|
|
a46f09a84a | ||
|
|
aff8dec3ce | ||
|
|
9b87d39fe8 | ||
|
|
e61be4ff9c | ||
|
|
6123fbeea5 | ||
|
|
a103d437c2 | ||
|
|
376779598a | ||
|
|
bdd88deb99 | ||
|
|
783a30fa5f | ||
|
|
177c816348 | ||
|
|
91c01dc643 |
76
build.gradle
76
build.gradle
@@ -1,15 +1,18 @@
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.nio.file.Paths
|
||||
import com.smartthings.deployment.slack.FileUpload
|
||||
import com.smartthings.deployment.slack.Message
|
||||
|
||||
apply plugin: 'groovy'
|
||||
apply plugin: 'smartthings-executable-deployment'
|
||||
apply plugin: 'smartthings-hipchat'
|
||||
apply plugin: 'smartthings-slack'
|
||||
|
||||
buildscript {
|
||||
dependencies {
|
||||
classpath "com.smartthings.deployment:executable-deployment-scripts:1.0.3"
|
||||
classpath "com.smartthings.deployment:executable-deployment-scripts:1.0.8"
|
||||
}
|
||||
repositories {
|
||||
mavenLocal()
|
||||
jcenter()
|
||||
maven {
|
||||
credentials {
|
||||
@@ -29,7 +32,43 @@ repositories {
|
||||
dependencies {
|
||||
}
|
||||
|
||||
hipchatShareFile {
|
||||
slackSendMessage {
|
||||
String branch = project.hasProperty('branch') ? project.property('branch') : 'unknown'
|
||||
String token = project.hasProperty('slackToken') ? project.property('slackToken') : null
|
||||
String webhookUrl = project.hasProperty('slackWebhookUrl') ? project.property('slackWebhookUrl') : null
|
||||
String channel = project.hasProperty('slackChannel') ? project.property('slackChannel') : null
|
||||
String drinks = 'https://dl.dropboxusercontent.com/s/m1z5mpd3c83lwev/minion_beer.jpeg?dl=0'
|
||||
String wolverine = 'https://dl.dropboxusercontent.com/s/4lbjqzvm2v033u9/minion_wolverine.jpg?dl=0'
|
||||
String beach = 'https://dl.dropboxusercontent.com/s/rqrfgxk53gfng69/minion_beach.png?dl=0'
|
||||
String iconUrl
|
||||
String color
|
||||
String messageText
|
||||
String username
|
||||
switch (branch) {
|
||||
case 'master':
|
||||
username = 'Hickory'
|
||||
iconUrl = wolverine
|
||||
color = '#35D0F2'
|
||||
messageText = 'Began deployment of _SmartThingsPublic[master]_ branch to the _Dev_ environments.'
|
||||
break
|
||||
case 'staging':
|
||||
username = 'Dickory'
|
||||
iconUrl = beach
|
||||
color = '#FFDE20'
|
||||
messageText = 'Began deployment of _SmartThingsPublic[staging]_ branch to the _Staging_ environments.'
|
||||
break
|
||||
case 'production':
|
||||
username = 'Dock'
|
||||
iconUrl = drinks
|
||||
color = '#FF1D23'
|
||||
messageText = 'Began deployment of _SmartThingsPublic[production]_ branch to the _Prod_ environments.'
|
||||
break
|
||||
default:
|
||||
username = 'Hickory'
|
||||
iconUrl = wolverine
|
||||
color = '#35D0F2'
|
||||
messageText = "Began deployment of an _SmartThingsPublic[${branch}]_ branch. Have no idea what's going on."
|
||||
}
|
||||
List<String> archives = []
|
||||
File rootDir = new File("${project.buildDir}/archives")
|
||||
if (rootDir.exists()) {
|
||||
@@ -42,16 +81,25 @@ hipchatShareFile {
|
||||
}
|
||||
}
|
||||
}
|
||||
Date date = new Date()
|
||||
String fileDate = date.format('yyyy-MM-dd_HH-mm-ss', TimeZone.getTimeZone('GMT'))
|
||||
|
||||
// Set task properties
|
||||
data = archives.join('\n').getBytes(StandardCharsets.UTF_8)
|
||||
fileName = 'deployment-notes.txt'
|
||||
contentType = 'text/html'
|
||||
}
|
||||
|
||||
hipchatSendNotification {
|
||||
String branch = project.hasProperty('branch') ? project.property('branch') : 'unknown'
|
||||
message = "Began executable deploy of SmartThingsPublic(${branch})."
|
||||
color = branch == 'master' ? 'yellow' : 'red'
|
||||
notify = true
|
||||
// Required Task Arguments.
|
||||
file = new FileUpload(
|
||||
data: archives.join('\n').getBytes(StandardCharsets.UTF_8),
|
||||
filename: "deployment-notes-${fileDate}.txt",
|
||||
title: 'Deployment Notes',
|
||||
channels: channel,
|
||||
token: token,
|
||||
color: color
|
||||
)
|
||||
message = new Message(
|
||||
webhookUrl: webhookUrl,
|
||||
username: username,
|
||||
asUser: true,
|
||||
iconUrl: iconUrl,
|
||||
channel: channel,
|
||||
fallback: 'Deployment Notification',
|
||||
text: messageText
|
||||
)
|
||||
}
|
||||
|
||||
10
circle.yml
10
circle.yml
@@ -15,13 +15,11 @@ deployment:
|
||||
develop:
|
||||
branch: master
|
||||
commands:
|
||||
- ./gradlew deployArchives -PsmartThingsArtifactoryUserName=$ARTIFACTORY_USERNAME -PsmartThingsArtifactoryPassword=$ARTIFACTORY_PASSWORD -Ps3BucketName=$S3_BUCKET_NAME_PREPROD_DEV
|
||||
- ./gradlew hipchatSendNotification -PsmartThingsArtifactoryUserName=$ARTIFACTORY_USERNAME -PsmartThingsArtifactoryPassword=$ARTIFACTORY_PASSWORD -Pbranch=$CIRCLE_BRANCH
|
||||
- ./gradlew hipchatShareFile -PsmartThingsArtifactoryUserName=$ARTIFACTORY_USERNAME -PsmartThingsArtifactoryPassword=$ARTIFACTORY_PASSWORD
|
||||
- ./gradlew deployArchives -PsmartThingsArtifactoryUserName="$ARTIFACTORY_USERNAME" -PsmartThingsArtifactoryPassword="$ARTIFACTORY_PASSWORD" -Ps3Buckets="$S3_BUCKETS_DEV"
|
||||
- ./gradlew slackSendMessage -PsmartThingsArtifactoryUserName="$ARTIFACTORY_USERNAME" -PsmartThingsArtifactoryPassword="$ARTIFACTORY_PASSWORD" -Pbranch="$CIRCLE_BRANCH" -PslackToken="$SLACK_TOKEN" -PslackWebhookUrl="$SLACK_WEBHOOK_URL" -PslackChannel="$SLACK_CHANNEL" --stacktrace
|
||||
|
||||
stage:
|
||||
branch: staging
|
||||
commands:
|
||||
- ./gradlew deployArchives -PsmartThingsArtifactoryUserName=$ARTIFACTORY_USERNAME -PsmartThingsArtifactoryPassword=$ARTIFACTORY_PASSWORD -Ps3BucketName=$S3_BUCKET_NAME_PREPROD_STAGING
|
||||
- ./gradlew hipchatSendNotification -PsmartThingsArtifactoryUserName=$ARTIFACTORY_USERNAME -PsmartThingsArtifactoryPassword=$ARTIFACTORY_PASSWORD -Pbranch=$CIRCLE_BRANCH
|
||||
- ./gradlew hipchatShareFile -PsmartThingsArtifactoryUserName=$ARTIFACTORY_USERNAME -PsmartThingsArtifactoryPassword=$ARTIFACTORY_PASSWORD
|
||||
- ./gradlew deployArchives -PsmartThingsArtifactoryUserName="$ARTIFACTORY_USERNAME" -PsmartThingsArtifactoryPassword="$ARTIFACTORY_PASSWORD" -Ps3Buckets="$S3_BUCKETS_STAGE"
|
||||
- ./gradlew slackSendMessage -PsmartThingsArtifactoryUserName="$ARTIFACTORY_USERNAME" -PsmartThingsArtifactoryPassword="$ARTIFACTORY_PASSWORD" -Pbranch="$CIRCLE_BRANCH" -PslackToken="$SLACK_TOKEN" -PslackWebhookUrl="$SLACK_WEBHOOK_URL" -PslackChannel="$SLACK_CHANNEL" --stacktrace
|
||||
|
||||
107
devicetypes/com-obycode/beaconthing.src/beaconthing.groovy
Normal file
107
devicetypes/com-obycode/beaconthing.src/beaconthing.groovy
Normal file
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* BeaconThing
|
||||
*
|
||||
* Copyright 2015 obycode
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
import groovy.json.JsonSlurper
|
||||
|
||||
metadata {
|
||||
definition (name: "BeaconThing", namespace: "com.obycode", author: "obycode") {
|
||||
capability "Beacon"
|
||||
capability "Presence Sensor"
|
||||
capability "Sensor"
|
||||
|
||||
attribute "inRange", "json_object"
|
||||
attribute "inRangeFriendly", "string"
|
||||
|
||||
command "setPresence", ["string"]
|
||||
command "arrived", ["string"]
|
||||
command "left", ["string"]
|
||||
}
|
||||
|
||||
simulator {
|
||||
status "present": "presence: 1"
|
||||
status "not present": "presence: 0"
|
||||
}
|
||||
|
||||
tiles {
|
||||
standardTile("presence", "device.presence", width: 2, height: 2, canChangeBackground: true) {
|
||||
state("present", labelIcon:"st.presence.tile.present", backgroundColor:"#53a7c0")
|
||||
state("not present", labelIcon:"st.presence.tile.not-present", backgroundColor:"#ffffff")
|
||||
}
|
||||
valueTile("inRange", "device.inRangeFriendly", inactiveLabel: true, height:1, width:3, decoration: "flat") {
|
||||
state "default", label:'${currentValue}', backgroundColor:"#ffffff"
|
||||
}
|
||||
main "presence"
|
||||
details (["presence","inRange"])
|
||||
}
|
||||
}
|
||||
|
||||
// parse events into attributes
|
||||
def parse(String description) {
|
||||
log.debug "Parsing '${description}'"
|
||||
}
|
||||
|
||||
def installed() {
|
||||
sendEvent(name: "presence", value: "not present")
|
||||
def emptyList = []
|
||||
def json = new groovy.json.JsonBuilder(emptyList)
|
||||
sendEvent(name:"inRange", value:json.toString())
|
||||
}
|
||||
|
||||
def setPresence(status) {
|
||||
log.debug "Status is $status"
|
||||
sendEvent(name:"presence", value:status)
|
||||
}
|
||||
|
||||
def arrived(id) {
|
||||
log.debug "$id has arrived"
|
||||
def theList = device.latestValue("inRange")
|
||||
def inRangeList = new JsonSlurper().parseText(theList)
|
||||
if (inRangeList.contains(id)) {
|
||||
return
|
||||
}
|
||||
inRangeList += id
|
||||
def json = new groovy.json.JsonBuilder(inRangeList)
|
||||
log.debug "Now in range: ${json.toString()}"
|
||||
sendEvent(name:"inRange", value:json.toString())
|
||||
|
||||
// Generate human friendly string for tile
|
||||
def friendlyList = "Nearby: " + inRangeList.join(", ")
|
||||
sendEvent(name:"inRangeFriendly", value:friendlyList)
|
||||
|
||||
if (inRangeList.size() == 1) {
|
||||
setPresence("present")
|
||||
}
|
||||
}
|
||||
|
||||
def left(id) {
|
||||
log.debug "$id has left"
|
||||
def theList = device.latestValue("inRange")
|
||||
def inRangeList = new JsonSlurper().parseText(theList)
|
||||
inRangeList -= id
|
||||
def json = new groovy.json.JsonBuilder(inRangeList)
|
||||
log.debug "Now in range: ${json.toString()}"
|
||||
sendEvent(name:"inRange", value:json.toString())
|
||||
|
||||
// Generate human friendly string for tile
|
||||
def friendlyList = "Nearby: " + inRangeList.join(", ")
|
||||
|
||||
if (inRangeList.empty) {
|
||||
setPresence("not present")
|
||||
friendlyList = "No one is nearby"
|
||||
}
|
||||
|
||||
sendEvent(name:"inRangeFriendly", value:friendlyList)
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
/**
|
||||
* Fibaro Door/Window Sensor ZW5
|
||||
*
|
||||
* Copyright 2016 Fibar Group S.A.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "Fibaro Door/Window Sensor ZW5 with Temperature", namespace: "fibargroup", author: "Fibar Group S.A.") {
|
||||
capability "Battery"
|
||||
capability "Contact Sensor"
|
||||
capability "Sensor"
|
||||
capability "Configuration"
|
||||
capability "Tamper Alert"
|
||||
|
||||
capability "Temperature Measurement"
|
||||
|
||||
fingerprint deviceId: "0x0701", inClusters: "0x5E, 0x85, 0x59, 0x22, 0x20, 0x80, 0x70, 0x56, 0x5A, 0x7A, 0x72, 0x8E, 0x71, 0x73, 0x98, 0x2B, 0x9C, 0x30, 0x31, 0x86", outClusters: ""
|
||||
fingerprint deviceId: "0x0701", inClusters: "0x5E, 0x85, 0x59, 0x22, 0x20, 0x80, 0x70, 0x56, 0x5A, 0x7A, 0x72, 0x8E, 0x71, 0x73, 0x98, 0x2B, 0x9C, 0x30, 0x31, 0x86, 0x84", outClusters: ""//actual NIF
|
||||
}
|
||||
|
||||
simulator {
|
||||
|
||||
}
|
||||
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"FGK", type:"lighting", width:6, height:4) {//with generic type secondary control text is not displayed in Android app
|
||||
tileAttribute("device.contact", key:"PRIMARY_CONTROL") {
|
||||
attributeState("open", icon:"st.contact.contact.open", backgroundColor:"#ffa81e")
|
||||
attributeState("closed", icon:"st.contact.contact.closed", backgroundColor:"#79b821")
|
||||
}
|
||||
|
||||
tileAttribute("device.tamper", key:"SECONDARY_CONTROL") {
|
||||
attributeState("active", label:'tamper active', backgroundColor:"#53a7c0")
|
||||
attributeState("inactive", label:'tamper inactive', backgroundColor:"#ffffff")
|
||||
}
|
||||
}
|
||||
|
||||
valueTile("battery", "device.battery", inactiveLabel: false, , width: 2, height: 2, decoration: "flat") {
|
||||
state "battery", label:'${currentValue}% battery', unit:""
|
||||
}
|
||||
|
||||
valueTile("temperature", "device.temperature", inactiveLabel: false, width: 2, height: 2) {
|
||||
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"]
|
||||
]
|
||||
}
|
||||
|
||||
main "FGK"
|
||||
details(["FGK","battery", "temperature"])
|
||||
}
|
||||
}
|
||||
|
||||
// parse events into attributes
|
||||
def parse(String description) {
|
||||
log.debug "Parsing '${description}'"
|
||||
def result = []
|
||||
|
||||
if (description.startsWith("Err 106")) {
|
||||
if (state.sec) {
|
||||
result = createEvent(descriptionText:description, displayed:false)
|
||||
} else {
|
||||
result = createEvent(
|
||||
descriptionText: "FGK failed to complete the network security key exchange. If you are unable to receive data from it, you must remove it from your network and add it again.",
|
||||
eventType: "ALERT",
|
||||
name: "secureInclusion",
|
||||
value: "failed",
|
||||
displayed: true,
|
||||
)
|
||||
}
|
||||
} else if (description == "updated") {
|
||||
return null
|
||||
} else {
|
||||
def cmd = zwave.parse(description, [0x31: 5, 0x56: 1, 0x71: 3, 0x72: 2, 0x80: 1, 0x84: 2, 0x85: 2, 0x86: 1, 0x98: 1])
|
||||
|
||||
if (cmd) {
|
||||
log.debug "Parsed '${cmd}'"
|
||||
zwaveEvent(cmd)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//security
|
||||
def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) {
|
||||
def encapsulatedCommand = cmd.encapsulatedCommand([0x71: 3, 0x84: 2, 0x85: 2, 0x98: 1])
|
||||
if (encapsulatedCommand) {
|
||||
return zwaveEvent(encapsulatedCommand)
|
||||
} else {
|
||||
log.warn "Unable to extract encapsulated cmd from $cmd"
|
||||
createEvent(descriptionText: cmd.toString())
|
||||
}
|
||||
}
|
||||
|
||||
//crc16
|
||||
def zwaveEvent(physicalgraph.zwave.commands.crc16encapv1.Crc16Encap cmd)
|
||||
{
|
||||
def versions = [0x31: 5, 0x72: 2, 0x80: 1, 0x86: 1]
|
||||
def version = versions[cmd.commandClass as Integer]
|
||||
def ccObj = version ? zwave.commandClass(cmd.commandClass, version) : zwave.commandClass(cmd.commandClass)
|
||||
def encapsulatedCommand = ccObj?.command(cmd.command)?.parse(cmd.data)
|
||||
if (!encapsulatedCommand) {
|
||||
log.debug "Could not extract command from $cmd"
|
||||
} else {
|
||||
zwaveEvent(encapsulatedCommand)
|
||||
}
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) {
|
||||
//it is assumed that default notification events are used
|
||||
//(parameter 20 was not changed before device's re-inclusion)
|
||||
def map = [:]
|
||||
if (cmd.notificationType == 6) {
|
||||
switch (cmd.event) {
|
||||
case 22:
|
||||
map.name = "contact"
|
||||
map.value = "open"
|
||||
map.descriptionText = "${device.displayName}: is open"
|
||||
break
|
||||
|
||||
case 23:
|
||||
map.name = "contact"
|
||||
map.value = "closed"
|
||||
map.descriptionText = "${device.displayName}: is closed"
|
||||
break
|
||||
}
|
||||
} else if (cmd.notificationType == 7) {
|
||||
switch (cmd.event) {
|
||||
case 0:
|
||||
map.name = "tamper"
|
||||
map.value = "inactive"
|
||||
map.descriptionText = "${device.displayName}: tamper alarm has been deactivated"
|
||||
break
|
||||
|
||||
case 3:
|
||||
map.name = "tamper"
|
||||
map.value = "active"
|
||||
map.descriptionText = "${device.displayName}: tamper alarm activated"
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
createEvent(map)
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) {
|
||||
def map = [:]
|
||||
map.name = "battery"
|
||||
map.value = cmd.batteryLevel == 255 ? 1 : cmd.batteryLevel.toString()
|
||||
map.unit = "%"
|
||||
map.displayed = true
|
||||
createEvent(map)
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpNotification cmd) {
|
||||
def event = createEvent(descriptionText: "${device.displayName} woke up", displayed: false)
|
||||
def cmds = []
|
||||
cmds << encap(zwave.batteryV1.batteryGet())
|
||||
cmds << "delay 500"
|
||||
cmds << encap(zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 1, scale: 0))
|
||||
cmds << "delay 1200"
|
||||
cmds << encap(zwave.wakeUpV1.wakeUpNoMoreInformation())
|
||||
[event, response(cmds)]
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) {
|
||||
log.debug "manufacturerId: ${cmd.manufacturerId}"
|
||||
log.debug "manufacturerName: ${cmd.manufacturerName}"
|
||||
log.debug "productId: ${cmd.productId}"
|
||||
log.debug "productTypeId: ${cmd.productTypeId}"
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.DeviceSpecificReport cmd) {
|
||||
log.debug "deviceIdData: ${cmd.deviceIdData}"
|
||||
log.debug "deviceIdDataFormat: ${cmd.deviceIdDataFormat}"
|
||||
log.debug "deviceIdDataLengthIndicator: ${cmd.deviceIdDataLengthIndicator}"
|
||||
log.debug "deviceIdType: ${cmd.deviceIdType}"
|
||||
|
||||
if (cmd.deviceIdType == 1 && cmd.deviceIdDataFormat == 1) {//serial number in binary format
|
||||
String serialNumber = "h'"
|
||||
|
||||
cmd.deviceIdData.each{ data ->
|
||||
serialNumber += "${String.format("%02X", data)}"
|
||||
}
|
||||
|
||||
updateDataValue("serialNumber", serialNumber)
|
||||
log.debug "${device.displayName} - serial number: ${serialNumber}"
|
||||
}
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd) {
|
||||
updateDataValue("version", "${cmd.applicationVersion}.${cmd.applicationSubVersion}")
|
||||
log.debug "applicationVersion: ${cmd.applicationVersion}"
|
||||
log.debug "applicationSubVersion: ${cmd.applicationSubVersion}"
|
||||
log.debug "zWaveLibraryType: ${cmd.zWaveLibraryType}"
|
||||
log.debug "zWaveProtocolVersion: ${cmd.zWaveProtocolVersion}"
|
||||
log.debug "zWaveProtocolSubVersion: ${cmd.zWaveProtocolSubVersion}"
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) {
|
||||
def map = [:]
|
||||
if (cmd.sensorType == 1) {
|
||||
// temperature
|
||||
def cmdScale = cmd.scale == 1 ? "F" : "C"
|
||||
map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, cmd.precision)
|
||||
map.unit = getTemperatureScale()
|
||||
map.name = "temperature"
|
||||
map.displayed = true
|
||||
}
|
||||
|
||||
createEvent(map)
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.deviceresetlocallyv1.DeviceResetLocallyNotification cmd) {
|
||||
log.info "${device.displayName}: received command: $cmd - device has reset itself"
|
||||
}
|
||||
|
||||
def configure() {
|
||||
log.debug "Executing 'configure'"
|
||||
|
||||
def cmds = []
|
||||
|
||||
cmds += zwave.wakeUpV2.wakeUpIntervalSet(seconds:21600, nodeid: zwaveHubNodeId)//FGK's default wake up interval
|
||||
cmds += zwave.manufacturerSpecificV2.manufacturerSpecificGet()
|
||||
cmds += zwave.manufacturerSpecificV2.deviceSpecificGet()
|
||||
cmds += zwave.versionV1.versionGet()
|
||||
cmds += zwave.batteryV1.batteryGet()
|
||||
cmds += zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 1, scale: 0)
|
||||
cmds += zwave.associationV2.associationSet(groupingIdentifier:1, nodeId: [zwaveHubNodeId])
|
||||
cmds += zwave.wakeUpV2.wakeUpNoMoreInformation()
|
||||
|
||||
encapSequence(cmds, 500)
|
||||
}
|
||||
|
||||
private secure(physicalgraph.zwave.Command cmd) {
|
||||
zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format()
|
||||
}
|
||||
|
||||
private crc16(physicalgraph.zwave.Command cmd) {
|
||||
//zwave.crc16EncapV1.crc16Encap().encapsulate(cmd).format()
|
||||
"5601${cmd.format()}0000"
|
||||
}
|
||||
|
||||
private encapSequence(commands, delay=200) {
|
||||
delayBetween(commands.collect{ encap(it) }, delay)
|
||||
}
|
||||
|
||||
private encap(physicalgraph.zwave.Command cmd) {
|
||||
def secureClasses = [0x20, 0x2B, 0x30, 0x5A, 0x70, 0x71, 0x84, 0x85, 0x8E, 0x9C]
|
||||
|
||||
//todo: check if secure inclusion was successful
|
||||
//if not do not send security-encapsulated command
|
||||
if (secureClasses.find{ it == cmd.commandClassId }) {
|
||||
secure(cmd)
|
||||
} else {
|
||||
crc16(cmd)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
/**
|
||||
* Fibaro Door/Window Sensor ZW5
|
||||
*
|
||||
* Copyright 2016 Fibar Group S.A.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "Fibaro Door/Window Sensor ZW5", namespace: "fibargroup", author: "Fibar Group S.A.") {
|
||||
capability "Battery"
|
||||
capability "Contact Sensor"
|
||||
capability "Sensor"
|
||||
capability "Configuration"
|
||||
capability "Tamper Alert"
|
||||
|
||||
fingerprint deviceId: "0x0701", inClusters: "0x5E, 0x85, 0x59, 0x22, 0x20, 0x80, 0x70, 0x56, 0x5A, 0x7A, 0x72, 0x8E, 0x71, 0x73, 0x98, 0x2B, 0x9C, 0x30, 0x86, 0x84", outClusters: ""
|
||||
}
|
||||
|
||||
simulator {
|
||||
|
||||
}
|
||||
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"FGK", type:"lighting", width:6, height:4) {//with generic type secondary control text is not displayed in Android app
|
||||
tileAttribute("device.contact", key:"PRIMARY_CONTROL") {
|
||||
attributeState("open", icon:"st.contact.contact.open", backgroundColor:"#ffa81e")
|
||||
attributeState("closed", icon:"st.contact.contact.closed", backgroundColor:"#79b821")
|
||||
}
|
||||
|
||||
tileAttribute("device.tamper", key:"SECONDARY_CONTROL") {
|
||||
attributeState("active", label:'tamper active', backgroundColor:"#53a7c0")
|
||||
attributeState("inactive", label:'tamper inactive', backgroundColor:"#ffffff")
|
||||
}
|
||||
}
|
||||
|
||||
valueTile("battery", "device.battery", inactiveLabel: false, , width: 2, height: 2, decoration: "flat") {
|
||||
state "battery", label:'${currentValue}% battery', unit:""
|
||||
}
|
||||
|
||||
main "FGK"
|
||||
details(["FGK","battery"])
|
||||
}
|
||||
}
|
||||
|
||||
// parse events into attributes
|
||||
def parse(String description) {
|
||||
log.debug "Parsing '${description}'"
|
||||
def result = []
|
||||
|
||||
if (description.startsWith("Err 106")) {
|
||||
if (state.sec) {
|
||||
result = createEvent(descriptionText:description, displayed:false)
|
||||
} else {
|
||||
result = createEvent(
|
||||
descriptionText: "FGK failed to complete the network security key exchange. If you are unable to receive data from it, you must remove it from your network and add it again.",
|
||||
eventType: "ALERT",
|
||||
name: "secureInclusion",
|
||||
value: "failed",
|
||||
displayed: true,
|
||||
)
|
||||
}
|
||||
} else if (description == "updated") {
|
||||
return null
|
||||
} else {
|
||||
def cmd = zwave.parse(description, [0x56: 1, 0x71: 3, 0x72: 2, 0x80: 1, 0x84: 2, 0x85: 2, 0x86: 1, 0x98: 1])
|
||||
|
||||
if (cmd) {
|
||||
log.debug "Parsed '${cmd}'"
|
||||
zwaveEvent(cmd)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//security
|
||||
def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) {
|
||||
def encapsulatedCommand = cmd.encapsulatedCommand([0x71: 3, 0x84: 2, 0x85: 2, 0x98: 1])
|
||||
if (encapsulatedCommand) {
|
||||
return zwaveEvent(encapsulatedCommand)
|
||||
} else {
|
||||
log.warn "Unable to extract encapsulated cmd from $cmd"
|
||||
createEvent(descriptionText: cmd.toString())
|
||||
}
|
||||
}
|
||||
|
||||
//crc16
|
||||
def zwaveEvent(physicalgraph.zwave.commands.crc16encapv1.Crc16Encap cmd)
|
||||
{
|
||||
def versions = [0x72: 2, 0x80: 1, 0x86: 1]
|
||||
def version = versions[cmd.commandClass as Integer]
|
||||
def ccObj = version ? zwave.commandClass(cmd.commandClass, version) : zwave.commandClass(cmd.commandClass)
|
||||
def encapsulatedCommand = ccObj?.command(cmd.command)?.parse(cmd.data)
|
||||
if (!encapsulatedCommand) {
|
||||
log.debug "Could not extract command from $cmd"
|
||||
} else {
|
||||
zwaveEvent(encapsulatedCommand)
|
||||
}
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) {
|
||||
//it is assumed that default notification events are used
|
||||
//(parameter 20 was not changed before device's re-inclusion)
|
||||
def map = [:]
|
||||
if (cmd.notificationType == 6) {
|
||||
switch (cmd.event) {
|
||||
case 22:
|
||||
map.name = "contact"
|
||||
map.value = "open"
|
||||
map.descriptionText = "${device.displayName}: is open"
|
||||
break
|
||||
|
||||
case 23:
|
||||
map.name = "contact"
|
||||
map.value = "closed"
|
||||
map.descriptionText = "${device.displayName}: is closed"
|
||||
break
|
||||
}
|
||||
} else if (cmd.notificationType == 7) {
|
||||
switch (cmd.event) {
|
||||
case 0:
|
||||
map.name = "tamper"
|
||||
map.value = "inactive"
|
||||
map.descriptionText = "${device.displayName}: tamper alarm has been deactivated"
|
||||
break
|
||||
|
||||
case 3:
|
||||
map.name = "tamper"
|
||||
map.value = "active"
|
||||
map.descriptionText = "${device.displayName}: tamper alarm activated"
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
createEvent(map)
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) {
|
||||
def map = [:]
|
||||
map.name = "battery"
|
||||
map.value = cmd.batteryLevel == 255 ? 1 : cmd.batteryLevel.toString()
|
||||
map.unit = "%"
|
||||
map.displayed = true
|
||||
createEvent(map)
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpNotification cmd) {
|
||||
def event = createEvent(descriptionText: "${device.displayName} woke up", displayed: false)
|
||||
def cmds = []
|
||||
cmds << encap(zwave.batteryV1.batteryGet())
|
||||
cmds << "delay 1200"
|
||||
cmds << encap(zwave.wakeUpV1.wakeUpNoMoreInformation())
|
||||
[event, response(cmds)]
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) {
|
||||
log.debug "manufacturerId: ${cmd.manufacturerId}"
|
||||
log.debug "manufacturerName: ${cmd.manufacturerName}"
|
||||
log.debug "productId: ${cmd.productId}"
|
||||
log.debug "productTypeId: ${cmd.productTypeId}"
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.DeviceSpecificReport cmd) {
|
||||
log.debug "deviceIdData: ${cmd.deviceIdData}"
|
||||
log.debug "deviceIdDataFormat: ${cmd.deviceIdDataFormat}"
|
||||
log.debug "deviceIdDataLengthIndicator: ${cmd.deviceIdDataLengthIndicator}"
|
||||
log.debug "deviceIdType: ${cmd.deviceIdType}"
|
||||
|
||||
if (cmd.deviceIdType == 1 && cmd.deviceIdDataFormat == 1) {//serial number in binary format
|
||||
String serialNumber = "h'"
|
||||
|
||||
cmd.deviceIdData.each{ data ->
|
||||
serialNumber += "${String.format("%02X", data)}"
|
||||
}
|
||||
|
||||
updateDataValue("serialNumber", serialNumber)
|
||||
log.debug "${device.displayName} - serial number: ${serialNumber}"
|
||||
}
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd) {
|
||||
updateDataValue("version", "${cmd.applicationVersion}.${cmd.applicationSubVersion}")
|
||||
log.debug "applicationVersion: ${cmd.applicationVersion}"
|
||||
log.debug "applicationSubVersion: ${cmd.applicationSubVersion}"
|
||||
log.debug "zWaveLibraryType: ${cmd.zWaveLibraryType}"
|
||||
log.debug "zWaveProtocolVersion: ${cmd.zWaveProtocolVersion}"
|
||||
log.debug "zWaveProtocolSubVersion: ${cmd.zWaveProtocolSubVersion}"
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.deviceresetlocallyv1.DeviceResetLocallyNotification cmd) {
|
||||
log.info "${device.displayName}: received command: $cmd - device has reset itself"
|
||||
}
|
||||
|
||||
def configure() {
|
||||
log.debug "Executing 'configure'"
|
||||
|
||||
def cmds = []
|
||||
|
||||
cmds += zwave.wakeUpV2.wakeUpIntervalSet(seconds:21600, nodeid: zwaveHubNodeId)//FGK's default wake up interval
|
||||
cmds += zwave.manufacturerSpecificV2.manufacturerSpecificGet()
|
||||
cmds += zwave.manufacturerSpecificV2.deviceSpecificGet()
|
||||
cmds += zwave.versionV1.versionGet()
|
||||
cmds += zwave.batteryV1.batteryGet()
|
||||
cmds += zwave.associationV2.associationSet(groupingIdentifier:1, nodeId: [zwaveHubNodeId])
|
||||
cmds += zwave.wakeUpV2.wakeUpNoMoreInformation()
|
||||
|
||||
encapSequence(cmds, 500)
|
||||
}
|
||||
|
||||
private secure(physicalgraph.zwave.Command cmd) {
|
||||
zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format()
|
||||
}
|
||||
|
||||
private crc16(physicalgraph.zwave.Command cmd) {
|
||||
//zwave.crc16EncapV1.crc16Encap().encapsulate(cmd).format()
|
||||
"5601${cmd.format()}0000"
|
||||
}
|
||||
|
||||
private encapSequence(commands, delay=200) {
|
||||
delayBetween(commands.collect{ encap(it) }, delay)
|
||||
}
|
||||
|
||||
private encap(physicalgraph.zwave.Command cmd) {
|
||||
def secureClasses = [0x20, 0x2B, 0x30, 0x5A, 0x70, 0x71, 0x84, 0x85, 0x8E, 0x9C]
|
||||
|
||||
//todo: check if secure inclusion was successful
|
||||
//if not do not send security-encapsulated command
|
||||
if (secureClasses.find{ it == cmd.commandClassId }) {
|
||||
secure(cmd)
|
||||
} else {
|
||||
crc16(cmd)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
/**
|
||||
* Fibaro Flood Sensor ZW5
|
||||
*
|
||||
* Copyright 2016 Fibar Group S.A.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "Fibaro Flood Sensor ZW5", namespace: "fibargroup", author: "Fibar Group S.A.") {
|
||||
capability "Battery"
|
||||
capability "Configuration"
|
||||
capability "Sensor"
|
||||
capability "Tamper Alert"
|
||||
capability "Temperature Measurement"
|
||||
capability "Water Sensor"
|
||||
|
||||
fingerprint deviceId: "0x0701", inClusters: "0x5E, 0x22, 0x85, 0x59, 0x20, 0x80, 0x70, 0x56, 0x5A, 0x7A, 0x72, 0x8E, 0x71, 0x73, 0x98, 0x9C, 0x31, 0x86", outClusters: ""
|
||||
}
|
||||
|
||||
simulator {
|
||||
|
||||
}
|
||||
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"FGFS", type:"lighting", width:6, height:4) {//with generic type secondary control text is not displayed in Android app
|
||||
tileAttribute("device.water", key:"PRIMARY_CONTROL") {
|
||||
attributeState("dry", icon:"st.alarm.water.dry", backgroundColor:"#79b821")
|
||||
attributeState("wet", icon:"st.alarm.water.wet", backgroundColor:"#ffa81e")
|
||||
}
|
||||
|
||||
tileAttribute("device.tamper", key:"SECONDARY_CONTROL") {
|
||||
attributeState("active", label:'tamper active', backgroundColor:"#53a7c0")
|
||||
attributeState("inactive", label:'tamper inactive', backgroundColor:"#ffffff")
|
||||
}
|
||||
}
|
||||
|
||||
valueTile("temperature", "device.temperature", inactiveLabel: false, width: 2, height: 2) {
|
||||
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", width: 2, height: 2) {
|
||||
state "battery", label:'${currentValue}% battery', unit:""
|
||||
}
|
||||
|
||||
main "FGFS"
|
||||
details(["FGFS","battery", "temperature"])
|
||||
}
|
||||
}
|
||||
|
||||
// parse events into attributes
|
||||
def parse(String description) {
|
||||
log.debug "Parsing '${description}'"
|
||||
def result = []
|
||||
|
||||
if (description.startsWith("Err 106")) {
|
||||
if (state.sec) {
|
||||
result = createEvent(descriptionText:description, displayed:false)
|
||||
} else {
|
||||
result = createEvent(
|
||||
descriptionText: "FGFS failed to complete the network security key exchange. If you are unable to receive data from it, you must remove it from your network and add it again.",
|
||||
eventType: "ALERT",
|
||||
name: "secureInclusion",
|
||||
value: "failed",
|
||||
displayed: true,
|
||||
)
|
||||
}
|
||||
} else if (description == "updated") {
|
||||
return null
|
||||
} else {
|
||||
def cmd = zwave.parse(description, [0x31: 5, 0x56: 1, 0x71: 3, 0x72:2, 0x80: 1, 0x84: 2, 0x85: 2, 0x86: 1, 0x98: 1])
|
||||
|
||||
if (cmd) {
|
||||
log.debug "Parsed '${cmd}'"
|
||||
zwaveEvent(cmd)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//security
|
||||
def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) {
|
||||
def encapsulatedCommand = cmd.encapsulatedCommand([0x71: 3, 0x84: 2, 0x85: 2, 0x86: 1, 0x98: 1])
|
||||
if (encapsulatedCommand) {
|
||||
return zwaveEvent(encapsulatedCommand)
|
||||
} else {
|
||||
log.warn "Unable to extract encapsulated cmd from $cmd"
|
||||
createEvent(descriptionText: cmd.toString())
|
||||
}
|
||||
}
|
||||
|
||||
//crc16
|
||||
def zwaveEvent(physicalgraph.zwave.commands.crc16encapv1.Crc16Encap cmd)
|
||||
{
|
||||
def versions = [0x31: 5, 0x72: 2, 0x80: 1]
|
||||
def version = versions[cmd.commandClass as Integer]
|
||||
def ccObj = version ? zwave.commandClass(cmd.commandClass, version) : zwave.commandClass(cmd.commandClass)
|
||||
def encapsulatedCommand = ccObj?.command(cmd.command)?.parse(cmd.data)
|
||||
if (!encapsulatedCommand) {
|
||||
log.debug "Could not extract command from $cmd"
|
||||
} else {
|
||||
zwaveEvent(encapsulatedCommand)
|
||||
}
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpNotification cmd)
|
||||
{
|
||||
def event = createEvent(descriptionText: "${device.displayName} woke up", displayed: false)
|
||||
def cmds = []
|
||||
cmds << encap(zwave.batteryV1.batteryGet())
|
||||
cmds << "delay 500"
|
||||
cmds << encap(zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 1, scale: 0))
|
||||
cmds << "delay 1200"
|
||||
cmds << encap(zwave.wakeUpV1.wakeUpNoMoreInformation())
|
||||
[event, response(cmds)]
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) {
|
||||
log.debug "manufacturerId: ${cmd.manufacturerId}"
|
||||
log.debug "manufacturerName: ${cmd.manufacturerName}"
|
||||
log.debug "productId: ${cmd.productId}"
|
||||
log.debug "productTypeId: ${cmd.productTypeId}"
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.DeviceSpecificReport cmd) {
|
||||
log.debug "deviceIdData: ${cmd.deviceIdData}"
|
||||
log.debug "deviceIdDataFormat: ${cmd.deviceIdDataFormat}"
|
||||
log.debug "deviceIdDataLengthIndicator: ${cmd.deviceIdDataLengthIndicator}"
|
||||
log.debug "deviceIdType: ${cmd.deviceIdType}"
|
||||
|
||||
if (cmd.deviceIdType == 1 && cmd.deviceIdDataFormat == 1) {//serial number in binary format
|
||||
String serialNumber = "h'"
|
||||
|
||||
cmd.deviceIdData.each{ data ->
|
||||
serialNumber += "${String.format("%02X", data)}"
|
||||
}
|
||||
|
||||
updateDataValue("serialNumber", serialNumber)
|
||||
log.debug "${device.displayName} - serial number: ${serialNumber}"
|
||||
}
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd) {
|
||||
updateDataValue("version", "${cmd.applicationVersion}.${cmd.applicationSubVersion}")
|
||||
log.debug "applicationVersion: ${cmd.applicationVersion}"
|
||||
log.debug "applicationSubVersion: ${cmd.applicationSubVersion}"
|
||||
log.debug "zWaveLibraryType: ${cmd.zWaveLibraryType}"
|
||||
log.debug "zWaveProtocolVersion: ${cmd.zWaveProtocolVersion}"
|
||||
log.debug "zWaveProtocolSubVersion: ${cmd.zWaveProtocolSubVersion}"
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) {
|
||||
def map = [:]
|
||||
map.name = "battery"
|
||||
map.value = cmd.batteryLevel == 255 ? 1 : cmd.batteryLevel.toString()
|
||||
map.unit = "%"
|
||||
map.displayed = true
|
||||
createEvent(map)
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) {
|
||||
def map = [:]
|
||||
if (cmd.notificationType == 5) {
|
||||
switch (cmd.event) {
|
||||
case 2:
|
||||
map.name = "water"
|
||||
map.value = "wet"
|
||||
map.descriptionText = "${device.displayName} is ${map.value}"
|
||||
break
|
||||
|
||||
case 0:
|
||||
map.name = "water"
|
||||
map.value = "dry"
|
||||
map.descriptionText = "${device.displayName} is ${map.value}"
|
||||
break
|
||||
}
|
||||
} else if (cmd.notificationType == 7) {
|
||||
switch (cmd.event) {
|
||||
case 0:
|
||||
map.name = "tamper"
|
||||
map.value = "inactive"
|
||||
map.descriptionText = "${device.displayName}: tamper alarm has been deactivated"
|
||||
break
|
||||
|
||||
case 3:
|
||||
map.name = "tamper"
|
||||
map.value = "active"
|
||||
map.descriptionText = "${device.displayName}: tamper alarm activated"
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
createEvent(map)
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) {
|
||||
def map = [:]
|
||||
if (cmd.sensorType == 1) {
|
||||
// temperature
|
||||
def cmdScale = cmd.scale == 1 ? "F" : "C"
|
||||
map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, cmd.precision)
|
||||
map.unit = getTemperatureScale()
|
||||
map.name = "temperature"
|
||||
map.displayed = true
|
||||
}
|
||||
|
||||
createEvent(map)
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.deviceresetlocallyv1.DeviceResetLocallyNotification cmd) {
|
||||
log.info "${device.displayName}: received command: $cmd - device has reset itself"
|
||||
}
|
||||
|
||||
def configure() {
|
||||
log.debug "Executing 'configure'"
|
||||
|
||||
def cmds = []
|
||||
|
||||
cmds += zwave.wakeUpV2.wakeUpIntervalSet(seconds:21600, nodeid: zwaveHubNodeId)//FGFS' default wake up interval
|
||||
cmds += zwave.manufacturerSpecificV2.manufacturerSpecificGet()
|
||||
cmds += zwave.manufacturerSpecificV2.deviceSpecificGet()
|
||||
cmds += zwave.versionV1.versionGet()
|
||||
cmds += zwave.batteryV1.batteryGet()
|
||||
cmds += zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 1, scale: 0)
|
||||
cmds += zwave.associationV2.associationSet(groupingIdentifier:1, nodeId: [zwaveHubNodeId])
|
||||
cmds += zwave.wakeUpV2.wakeUpNoMoreInformation()
|
||||
|
||||
encapSequence(cmds, 500)
|
||||
}
|
||||
|
||||
private secure(physicalgraph.zwave.Command cmd) {
|
||||
zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format()
|
||||
}
|
||||
|
||||
private crc16(physicalgraph.zwave.Command cmd) {
|
||||
//zwave.crc16EncapV1.crc16Encap().encapsulate(cmd).format()
|
||||
"5601${cmd.format()}0000"
|
||||
}
|
||||
|
||||
private encapSequence(commands, delay=200) {
|
||||
delayBetween(commands.collect{ encap(it) }, delay)
|
||||
}
|
||||
|
||||
private encap(physicalgraph.zwave.Command cmd) {
|
||||
def secureClasses = [0x20, 0x5A, 0x70, 0x71, 0x84, 0x85, 0x8E, 0x9C]
|
||||
|
||||
//todo: check if secure inclusion was successful
|
||||
//if not do not send security-encapsulated command
|
||||
if (secureClasses.find{ it == cmd.commandClassId }) {
|
||||
secure(cmd)
|
||||
} else {
|
||||
crc16(cmd)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
/**
|
||||
* Fibaro Motion Sensor ZW5
|
||||
*
|
||||
* Copyright 2016 Fibar Group S.A.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "Fibaro Motion Sensor ZW5", namespace: "fibargroup", author: "Fibar Group S.A.") {
|
||||
capability "Battery"
|
||||
capability "Configuration"
|
||||
capability "Illuminance Measurement"
|
||||
capability "Motion Sensor"
|
||||
capability "Sensor"
|
||||
capability "Tamper Alert"
|
||||
capability "Temperature Measurement"
|
||||
|
||||
fingerprint deviceId: "0x0701", inClusters: "0x5E, 0x20, 0x86, 0x72, 0x5A, 0x59, 0x85, 0x73, 0x84, 0x80, 0x71, 0x56, 0x70, 0x31, 0x8E, 0x22, 0x30, 0x9C, 0x98, 0x7A", outClusters: ""
|
||||
}
|
||||
|
||||
simulator {
|
||||
|
||||
}
|
||||
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"FGMS", type:"lighting", width:6, height:4) {//with generic type secondary control text is not displayed in Android app
|
||||
tileAttribute("device.motion", key:"PRIMARY_CONTROL") {
|
||||
attributeState("inactive", icon:"st.motion.motion.inactive", backgroundColor:"#79b821")
|
||||
attributeState("active", icon:"st.motion.motion.active", backgroundColor:"#ffa81e")
|
||||
}
|
||||
|
||||
tileAttribute("device.tamper", key:"SECONDARY_CONTROL") {
|
||||
attributeState("active", label:'tamper active', backgroundColor:"#53a7c0")
|
||||
attributeState("inactive", label:'tamper inactive', backgroundColor:"#ffffff")
|
||||
}
|
||||
}
|
||||
|
||||
valueTile("temperature", "device.temperature", inactiveLabel: false, width: 2, height: 2) {
|
||||
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("illuminance", "device.illuminance", inactiveLabel: false, width: 2, height: 2) {
|
||||
state "luminosity", label:'${currentValue} ${unit}', unit:"lux"
|
||||
}
|
||||
|
||||
valueTile("battery", "device.battery", inactiveLabel: false, width: 2, height: 2, decoration: "flat") {
|
||||
state "battery", label:'${currentValue}% battery', unit:""
|
||||
}
|
||||
|
||||
main "FGMS"
|
||||
details(["FGMS","battery","temperature","illuminance"])
|
||||
}
|
||||
}
|
||||
|
||||
// parse events into attributes
|
||||
def parse(String description) {
|
||||
log.debug "Parsing '${description}'"
|
||||
def result = []
|
||||
|
||||
if (description.startsWith("Err 106")) {
|
||||
if (state.sec) {
|
||||
result = createEvent(descriptionText:description, displayed:false)
|
||||
} else {
|
||||
result = createEvent(
|
||||
descriptionText: "FGK failed to complete the network security key exchange. If you are unable to receive data from it, you must remove it from your network and add it again.",
|
||||
eventType: "ALERT",
|
||||
name: "secureInclusion",
|
||||
value: "failed",
|
||||
displayed: true,
|
||||
)
|
||||
}
|
||||
} else if (description == "updated") {
|
||||
return null
|
||||
} else {
|
||||
def cmd = zwave.parse(description, [0x31: 5, 0x56: 1, 0x71: 3, 0x72: 2, 0x80: 1, 0x84: 2, 0x85: 2, 0x86: 1, 0x98: 1])
|
||||
|
||||
if (cmd) {
|
||||
log.debug "Parsed '${cmd}'"
|
||||
zwaveEvent(cmd)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//security
|
||||
def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) {
|
||||
def encapsulatedCommand = cmd.encapsulatedCommand([0x71: 3, 0x84: 2, 0x85: 2, 0x86: 1, 0x98: 1])
|
||||
if (encapsulatedCommand) {
|
||||
return zwaveEvent(encapsulatedCommand)
|
||||
} else {
|
||||
log.warn "Unable to extract encapsulated cmd from $cmd"
|
||||
createEvent(descriptionText: cmd.toString())
|
||||
}
|
||||
}
|
||||
|
||||
//crc16
|
||||
def zwaveEvent(physicalgraph.zwave.commands.crc16encapv1.Crc16Encap cmd)
|
||||
{
|
||||
def versions = [0x31: 5, 0x71: 3, 0x72: 2, 0x80: 1, 0x84: 2, 0x85: 2, 0x86: 1]
|
||||
def version = versions[cmd.commandClass as Integer]
|
||||
def ccObj = version ? zwave.commandClass(cmd.commandClass, version) : zwave.commandClass(cmd.commandClass)
|
||||
def encapsulatedCommand = ccObj?.command(cmd.command)?.parse(cmd.data)
|
||||
if (!encapsulatedCommand) {
|
||||
log.debug "Could not extract command from $cmd"
|
||||
} else {
|
||||
zwaveEvent(encapsulatedCommand)
|
||||
}
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) {
|
||||
def map = [ displayed: true ]
|
||||
switch (cmd.sensorType) {
|
||||
case 1:
|
||||
map.name = "temperature"
|
||||
map.unit = cmd.scale == 1 ? "F" : "C"
|
||||
map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, map.unit, cmd.precision)
|
||||
break
|
||||
case 3:
|
||||
map.name = "illuminance"
|
||||
map.value = cmd.scaledSensorValue.toInteger().toString()
|
||||
map.unit = "lux"
|
||||
break
|
||||
}
|
||||
|
||||
createEvent(map)
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) {
|
||||
def map = [:]
|
||||
if (cmd.notificationType == 7) {
|
||||
switch (cmd.event) {
|
||||
case 0:
|
||||
if (cmd.eventParameter[0] == 3) {
|
||||
map.name = "tamper"
|
||||
map.value = "inactive"
|
||||
map.descriptionText = "${device.displayName}: tamper alarm has been deactivated"
|
||||
}
|
||||
if (cmd.eventParameter[0] == 8) {
|
||||
map.name = "motion"
|
||||
map.value = "inactive"
|
||||
map.descriptionText = "${device.displayName}: motion has stopped"
|
||||
}
|
||||
break
|
||||
|
||||
case 3:
|
||||
map.name = "tamper"
|
||||
map.value = "active"
|
||||
map.descriptionText = "${device.displayName}: tamper alarm activated"
|
||||
break
|
||||
|
||||
case 8:
|
||||
map.name = "motion"
|
||||
map.value = "active"
|
||||
map.descriptionText = "${device.displayName}: motion detected"
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
createEvent(map)
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) {
|
||||
def map = [:]
|
||||
map.name = "battery"
|
||||
map.value = cmd.batteryLevel == 255 ? 1 : cmd.batteryLevel.toString()
|
||||
map.unit = "%"
|
||||
map.displayed = true
|
||||
createEvent(map)
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpNotification cmd)
|
||||
{
|
||||
def event = createEvent(descriptionText: "${device.displayName} woke up", displayed: false)
|
||||
def cmds = []
|
||||
cmds << encap(zwave.batteryV1.batteryGet())
|
||||
cmds << "delay 500"
|
||||
cmds << encap(zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 1, scale: 0))
|
||||
cmds << "delay 500"
|
||||
cmds << encap(zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 3, scale: 1))
|
||||
cmds << "delay 1200"
|
||||
cmds << encap(zwave.wakeUpV1.wakeUpNoMoreInformation())
|
||||
[event, response(cmds)]
|
||||
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) {
|
||||
log.debug "manufacturerId: ${cmd.manufacturerId}"
|
||||
log.debug "manufacturerName: ${cmd.manufacturerName}"
|
||||
log.debug "productId: ${cmd.productId}"
|
||||
log.debug "productTypeId: ${cmd.productTypeId}"
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.DeviceSpecificReport cmd) {
|
||||
log.debug "deviceIdData: ${cmd.deviceIdData}"
|
||||
log.debug "deviceIdDataFormat: ${cmd.deviceIdDataFormat}"
|
||||
log.debug "deviceIdDataLengthIndicator: ${cmd.deviceIdDataLengthIndicator}"
|
||||
log.debug "deviceIdType: ${cmd.deviceIdType}"
|
||||
|
||||
if (cmd.deviceIdType == 1 && cmd.deviceIdDataFormat == 1) {//serial number in binary format
|
||||
String serialNumber = "h'"
|
||||
|
||||
cmd.deviceIdData.each{ data ->
|
||||
serialNumber += "${String.format("%02X", data)}"
|
||||
}
|
||||
|
||||
updateDataValue("serialNumber", serialNumber)
|
||||
log.debug "${device.displayName} - serial number: ${serialNumber}"
|
||||
}
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd) {
|
||||
updateDataValue("version", "${cmd.applicationVersion}.${cmd.applicationSubVersion}")
|
||||
log.debug "applicationVersion: ${cmd.applicationVersion}"
|
||||
log.debug "applicationSubVersion: ${cmd.applicationSubVersion}"
|
||||
log.debug "zWaveLibraryType: ${cmd.zWaveLibraryType}"
|
||||
log.debug "zWaveProtocolVersion: ${cmd.zWaveProtocolVersion}"
|
||||
log.debug "zWaveProtocolSubVersion: ${cmd.zWaveProtocolSubVersion}"
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.deviceresetlocallyv1.DeviceResetLocallyNotification cmd) {
|
||||
log.info "${device.displayName}: received command: $cmd - device has reset itself"
|
||||
}
|
||||
|
||||
def configure() {
|
||||
log.debug "Executing 'configure'"
|
||||
|
||||
def cmds = []
|
||||
|
||||
cmds += zwave.wakeUpV2.wakeUpIntervalSet(seconds: 7200, nodeid: zwaveHubNodeId)//FGMS' default wake up interval
|
||||
cmds += zwave.manufacturerSpecificV2.manufacturerSpecificGet()
|
||||
cmds += zwave.manufacturerSpecificV2.deviceSpecificGet()
|
||||
cmds += zwave.versionV1.versionGet()
|
||||
cmds += zwave.associationV2.associationSet(groupingIdentifier:1, nodeId:[zwaveHubNodeId])
|
||||
cmds += zwave.batteryV1.batteryGet()
|
||||
cmds += zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 1, scale: 0)
|
||||
cmds += zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 3, scale: 1)
|
||||
cmds += zwave.wakeUpV2.wakeUpNoMoreInformation()
|
||||
|
||||
encapSequence(cmds, 500)
|
||||
}
|
||||
|
||||
private secure(physicalgraph.zwave.Command cmd) {
|
||||
zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format()
|
||||
}
|
||||
|
||||
private crc16(physicalgraph.zwave.Command cmd) {
|
||||
//zwave.crc16encapV1.crc16Encap().encapsulate(cmd).format()
|
||||
"5601${cmd.format()}0000"
|
||||
}
|
||||
|
||||
private encapSequence(commands, delay=200) {
|
||||
delayBetween(commands.collect{ encap(it) }, delay)
|
||||
}
|
||||
|
||||
private encap(physicalgraph.zwave.Command cmd) {
|
||||
def secureClasses = [0x20, 0x30, 0x5A, 0x70, 0x71, 0x84, 0x85, 0x8E, 0x9C]
|
||||
|
||||
//todo: check if secure inclusion was successful
|
||||
//if not do not send security-encapsulated command
|
||||
if (secureClasses.find{ it == cmd.commandClassId }) {
|
||||
secure(cmd)
|
||||
} else {
|
||||
crc16(cmd)
|
||||
}
|
||||
}
|
||||
795
devicetypes/johnrucker/coopboss-h3vx.src/coopboss-h3vx.groovy
Normal file
795
devicetypes/johnrucker/coopboss-h3vx.src/coopboss-h3vx.groovy
Normal file
@@ -0,0 +1,795 @@
|
||||
/**
|
||||
* CoopBoss H3Vx
|
||||
* 02/29/16 Fixed app crash with Android by changing the syntax of default state in tile definition.
|
||||
* Fixed null value errors during join process. Added 3 new commands to refresh data.
|
||||
*
|
||||
* 01/18/16 Masked invalid temperature reporting when TempProbe1 is below 0C
|
||||
* Added setBaseCurrentNE, readBaseCurrentNE, commands as well as baseCurrentNE attribute.
|
||||
*
|
||||
* Copyright 2016 John Rucker
|
||||
*
|
||||
* 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.
|
||||
* Icon location = http://scripts.3dgo.net/smartthings/icons/
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "CoopBoss H3Vx", namespace: "JohnRucker", author: "John.Rucker@Solar-Current.com") {
|
||||
capability "Refresh"
|
||||
capability "Polling"
|
||||
capability "Sensor"
|
||||
capability "Actuator"
|
||||
capability "Configuration"
|
||||
capability "Temperature Measurement"
|
||||
capability "Door Control"
|
||||
capability "Switch"
|
||||
|
||||
command "closeDoor"
|
||||
command "closeDoorHiI"
|
||||
command "openDoor"
|
||||
command "autoCloseOn"
|
||||
command "autoCloseOff"
|
||||
command "autoOpenOn"
|
||||
command "autoOpenOff"
|
||||
command "setCloseLevelTo"
|
||||
command "setOpenLevelTo"
|
||||
command "setSensitivityLevel"
|
||||
command "Aux1On"
|
||||
command "Aux1Off"
|
||||
command "Aux2On"
|
||||
command "Aux2Off"
|
||||
command "updateTemp1"
|
||||
command "updateTemp2"
|
||||
command "updateSun"
|
||||
command "setNewBaseCurrent"
|
||||
command "setNewPhotoCalibration"
|
||||
command "readNewPhotoCalibration"
|
||||
command "readBaseCurrentNE"
|
||||
command "setBaseCurrentNE"
|
||||
command "updateSensitivity"
|
||||
command "updateCloseLightLevel"
|
||||
command "updateOpenLightLevel"
|
||||
|
||||
attribute "doorState","string"
|
||||
attribute "currentLightLevel","number"
|
||||
attribute "closeLightLevel","number"
|
||||
attribute "openLightLevel","number"
|
||||
attribute "autoCloseEnable","string"
|
||||
attribute "autoOpenEnable","string"
|
||||
attribute "TempProb1","number"
|
||||
attribute "TempProb2","number"
|
||||
attribute "dayOrNight","string"
|
||||
attribute "doorSensitivity","number"
|
||||
attribute "doorCurrent","number"
|
||||
attribute "doorVoltage","number"
|
||||
attribute "Aux1","string"
|
||||
attribute "Aux2","string"
|
||||
attribute "coopStatus","string"
|
||||
attribute "baseDoorCurrent","number"
|
||||
attribute "photoCalibration","number"
|
||||
attribute "baseCurrentNE","string"
|
||||
|
||||
fingerprint profileId: "0104", inClusters: "0000,0101,0402", manufacturer: "Solar-Current", model: "Coop Boss"
|
||||
|
||||
}
|
||||
|
||||
// simulator metadata
|
||||
simulator {
|
||||
}
|
||||
|
||||
|
||||
preferences {
|
||||
input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
|
||||
input "tempOffsetCoop", "number", title: "Coop Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
|
||||
input "tempOffsetOutside", "number", title: "Outside Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
|
||||
}
|
||||
|
||||
|
||||
// UI tile definitions
|
||||
tiles(scale: 2){
|
||||
multiAttributeTile(name:"doorCtrl", type:"generic", width:6, height:4) {tileAttribute("device.doorState", key: "PRIMARY_CONTROL")
|
||||
{
|
||||
attributeState "unknown", label: '${name}', action:"openDoor", icon: "st.Outdoor.outdoor20", nextState:"Sent"
|
||||
attributeState "open", label: '${name}', action:"closeDoor", icon: "st.Outdoor.outdoor20", backgroundColor: "#0000ff" , nextState:"Sent"
|
||||
attributeState "opening", label: '${name}', action:"closeDoor", icon: "st.Outdoor.outdoor20", backgroundColor: "#ffa81e"
|
||||
attributeState "closed", label: '${name}', action:"openDoor", icon: "st.Outdoor.outdoor20", backgroundColor: "#79b821", nextState:"Sent"
|
||||
attributeState "closing", label: '${name}', action:"openDoor", icon: "st.Outdoor.outdoor20", backgroundColor: "#ffa81e"
|
||||
attributeState "jammed", label: '${name}', action:"closeDoorHiI", icon: "st.Outdoor.outdoor20", backgroundColor: "#ff0000", nextState:"Sent"
|
||||
attributeState "forced close", label: 'forced\rclose', action:"openDoor", icon: "st.Outdoor.outdoor20", backgroundColor: "#ff8000", nextState:"Sent"
|
||||
attributeState "fault", label: 'FAULT', action:"openDoor", icon: "st.Outdoor.outdoor20", backgroundColor: "#ff0000", nextState:"Sent"
|
||||
attributeState "Sent", label: 'wait', icon: "st.motion.motion.active", backgroundColor: "#ffa81e"
|
||||
}
|
||||
tileAttribute ("device.coopStatus", key: "SECONDARY_CONTROL") {
|
||||
attributeState "device.coopStatus", label:'${currentValue}'
|
||||
}
|
||||
}
|
||||
|
||||
multiAttributeTile(name:"dtlsDoorCtrl", type:"generic", width:6, height:4) {tileAttribute("device.doorState", key: "PRIMARY_CONTROL")
|
||||
{
|
||||
attributeState "unknown", label: '${name}', action:"openDoor", icon: "st.secondary.tools", nextState:"Sent"
|
||||
attributeState "open", label: '${name}', action:"closeDoor", icon: "st.doors.garage.garage-open", backgroundColor: "#0000ff", nextState:"Sent"
|
||||
attributeState "opening", label: '${name}', action:"closeDoor", icon: "st.doors.garage.garage-opening", backgroundColor: "#ffa81e"
|
||||
attributeState "closed", label: '${name}', action:"openDoor", icon: "st.doors.garage.garage-closed", backgroundColor: "#79b821", nextState:"Sent"
|
||||
attributeState "closing", label: '${name}', action:"openDoor", icon: "st.doors.garage.garage-closing", backgroundColor: "#ffa81e"
|
||||
attributeState "jammed", label: '${name}', action:"closeDoorHiI", icon: "st.doors.garage.garage-open", backgroundColor: "#ff0000", nextState:"Sent"
|
||||
attributeState "forced close", label: "forced", action:"openDoor", icon: "st.doors.garage.garage-closed", backgroundColor: "#ff8000", nextState:"Sent"
|
||||
attributeState "fault", label: 'FAULT', action:"openDoor", icon: "st.secondary.tools", backgroundColor: "#ff0000", nextState:"Sent"
|
||||
attributeState "Sent", label: 'wait', icon: "st.motion.motion.active", backgroundColor: "#ffa81e"
|
||||
}
|
||||
tileAttribute ("device.doorState", key: "SECONDARY_CONTROL") {
|
||||
attributeState "unknown", label: 'Door is in unknown state. Push to open.'
|
||||
attributeState "open", label: 'Coop door is open. Push to close.'
|
||||
attributeState "opening", label: 'Caution, door is opening!'
|
||||
attributeState "closed", label: 'Coop door is closed. Push to open.'
|
||||
attributeState "closing", label: 'Caution, door is closing!'
|
||||
attributeState "jammed", label: 'Door open! Push for high-force close'
|
||||
attributeState "forced close", label: "Door is closed. Push to open."
|
||||
attributeState "fault", label: 'Door fault check electrical connection.'
|
||||
attributeState "Sent", label: 'Command sent to CoopBoss...'
|
||||
}
|
||||
}
|
||||
|
||||
standardTile("autoClose", "device.autoCloseEnable", width: 2, height: 2){
|
||||
state "on", label: 'Auto', action:"autoCloseOff", icon: "st.doors.garage.garage-closing", backgroundColor: "#79b821", nextState:"Sent"
|
||||
state "off", label: 'Auto', action:"autoCloseOn", icon: "st.doors.garage.garage-closing", nextState:"Sent"
|
||||
state "Sent", label: 'wait', icon: "st.motion.motion.active", backgroundColor: "#ffa81e"
|
||||
}
|
||||
|
||||
standardTile("autoOpen", "device.autoOpenEnable", width: 2, height: 2){
|
||||
state "on", label: 'Auto', action:"autoOpenOff", icon: "st.doors.garage.garage-opening", backgroundColor: "#79b821", nextState:"Sent"
|
||||
state "off", label: 'Auto', action:"autoOpenOn", icon: "st.doors.garage.garage-opening", nextState:"Sent"
|
||||
state "Sent", label: 'wait', icon: "st.motion.motion.active", backgroundColor: "#ffa81e"
|
||||
}
|
||||
|
||||
valueTile("TempProb1", "device.TempProb1", width: 2, height: 2, decoration: "flat"){
|
||||
state "default", label:'Coop\r${currentValue}°', unit:"F", action:"updateTemp1"}
|
||||
|
||||
valueTile("TempProb2", "device.TempProb2", width: 2, height: 2, decoration: "flat"){
|
||||
state "default", label:'Outside\r${currentValue}°', unit:"F", action:"updateTemp2"}
|
||||
|
||||
valueTile("currentLevel", "device.currentLightLevel", width: 2, height: 2, decoration: "flat") {
|
||||
state "default", label:'Sun\r${currentValue}', action:"updateSun"}
|
||||
|
||||
valueTile("dayOrNight", "device.dayOrNight", decoration: "flat", inactiveLabel: false, width: 2, height: 2) {
|
||||
state "default", label:'${currentValue}.'
|
||||
}
|
||||
|
||||
controlTile("SetClSlider", "device.closeLightLevel", "slider", height: 2, width: 4, inactiveLabel: false, range:"(1..100)") {
|
||||
state "closeLightLevel", action:"setCloseLevelTo", backgroundColor:"#d04e00"
|
||||
}
|
||||
|
||||
valueTile("SetClValue", "device.closeLightLevel", decoration: "flat", inactiveLabel: false, width: 2, height: 2) {
|
||||
state "default", label:'Close\nSunlight\n${currentValue}', action:'updateCloseLightLevel'
|
||||
}
|
||||
|
||||
controlTile("SetOpSlider", "device.openLightLevel", "slider", height: 2, width: 4, inactiveLabel: false, range:"(1..100)") {
|
||||
state "openLightLevel", action:"setOpenLevelTo", backgroundColor:"#d04e00"
|
||||
}
|
||||
|
||||
valueTile("SetOpValue", "device.openLightLevel", decoration: "flat", inactiveLabel: false, width: 2, height: 2) {
|
||||
state "default", label:'Open\nSunlight\n${currentValue}', action:'updateOpenLightLevel'
|
||||
}
|
||||
|
||||
controlTile("SetSensitivitySlider", "device.doorSensitivity", "slider", height: 2, width: 4, inactiveLabel: false, range:"(1..100)") {
|
||||
state "openLightLevel", action:"setSensitivityLevel", backgroundColor:"#d04e00"
|
||||
}
|
||||
|
||||
valueTile("SetSensitivityValue", "device.doorSensitivity", decoration: "flat", inactiveLabel: false, width: 2, height: 2) {
|
||||
state "default", label:'Door\nSensitivity\n${currentValue}', action:'updateSensitivity'
|
||||
}
|
||||
|
||||
standardTile("refresh", "device.refresh", width: 2, height: 2, decoration: "flat", inactiveLabel: false) {
|
||||
state "default", label:'All', action:"refresh.refresh", icon:"st.secondary.refresh-icon"
|
||||
}
|
||||
|
||||
standardTile("aux1", "device.Aux1", width: 2, height: 2, canChangeIcon: true) {
|
||||
state "off", label:'Aux 1', action:"Aux1On", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"Sent"
|
||||
state "on", label:'Aux 1', action:"Aux1Off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"Sent"
|
||||
state "Sent", label: 'wait', icon: "st.motion.motion.active", backgroundColor: "#ffa81e"
|
||||
}
|
||||
|
||||
standardTile("aux2", "device.Aux2", width: 2, height: 2, canChangeIcon: true) {
|
||||
state "off", label:'Aux 2', action:"Aux2On", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"Sent"
|
||||
state "on", label:'Aux 2', action:"Aux2Off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"Sent"
|
||||
state "Sent", label: 'wait', icon: "st.motion.motion.active", backgroundColor: "#ffa81e"
|
||||
}
|
||||
|
||||
main "doorCtrl"
|
||||
details (["dtlsDoorCtrl", "TempProb1", "TempProb2", "currentLevel", "autoClose", "autoOpen", "dayOrNight",
|
||||
"SetClSlider", "SetClValue", "SetOpSlider", "SetOpValue", "SetSensitivitySlider", "SetSensitivityValue",
|
||||
"aux1", "aux2", "refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
// Parse incoming device messages to generate events def parse(String description) {
|
||||
def parse(String description) {
|
||||
log.debug "description: $description"
|
||||
Map map = [:]
|
||||
if (description?.startsWith('catchall:')) {
|
||||
map = parseCatchAllMessage(description)
|
||||
}
|
||||
else if (description?.startsWith('read attr -')) {
|
||||
map = parseReportAttributeMessage(description)
|
||||
}
|
||||
else if (description?.startsWith('temperature: ') || description?.startsWith('humidity: ')) {
|
||||
map = parseCustomMessage(description)
|
||||
}
|
||||
log.debug map
|
||||
//return map ? createEvent(map) : null
|
||||
sendEvent(map)
|
||||
callUpdateStatusTxt()
|
||||
}
|
||||
|
||||
private Map parseCatchAllMessage(String description) {
|
||||
Map resultMap = [:]
|
||||
def cluster = zigbee.parse(description)
|
||||
log.debug cluster
|
||||
if (cluster.clusterId == 0x0402) {
|
||||
switch(cluster.sourceEndpoint) {
|
||||
|
||||
case 0x39: // Endpoint 0x39 is the temperature of probe 1
|
||||
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
|
||||
resultMap.name = "TempProb1"
|
||||
def celsius = Integer.valueOf(temp,16).shortValue()
|
||||
if (celsius == -32768){ // This number is used to indicate an error in the temperature reading
|
||||
resultMap.value = "---"
|
||||
}else{
|
||||
celsius = celsius / 100 // Temperature value is sent X 100.
|
||||
resultMap.value = celsiusToFahrenheit(celsius)
|
||||
if (tempOffsetOutside) {
|
||||
def offset = tempOffsetOutside as int
|
||||
resultMap.value = resultMap.value + offset
|
||||
}
|
||||
}
|
||||
sendEvent(name: "temperature", value: resultMap.value, displayed: false) // set the temperatureMeasurment capability to temperature
|
||||
break
|
||||
|
||||
case 0x40: // Endpoint 0x40 is the temperature of probe 2
|
||||
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
|
||||
resultMap.name = "TempProb2"
|
||||
def celsius = Integer.valueOf(temp,16).shortValue()
|
||||
//resultMap.descriptionText = "Prob2 celsius value = ${celsius}"
|
||||
if (celsius == -32768){ // This number is used to indicate an error in the temperature reading
|
||||
resultMap.value = "---"
|
||||
}else{
|
||||
celsius = celsius / 100 // Temperature value is sent X 100.
|
||||
resultMap.value = celsiusToFahrenheit(celsius)
|
||||
if (tempOffsetCoop) {
|
||||
def offset = tempOffsetCoop as int
|
||||
resultMap.value = resultMap.value + offset
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (cluster.clusterId == 0x0101 && cluster.command == 0x0b) { // This is a default response to a command sent to cluster 0x0101 door control
|
||||
//log.debug "Default Response Data = $cluster.data"
|
||||
switch(cluster.data) {
|
||||
|
||||
case "[10, 0]": // 0x0a turn auto close on command verified
|
||||
resultMap.name = "autoCloseEnable"
|
||||
resultMap.value = "on"
|
||||
break
|
||||
|
||||
case "[11, 0]": // 0x0b turn auto close off command verified
|
||||
resultMap.name = "autoCloseEnable"
|
||||
resultMap.value = "off"
|
||||
break
|
||||
|
||||
case "[12, 0]": // 0x0C turn auto open on command verified
|
||||
resultMap.name = "autoOpenEnable"
|
||||
resultMap.value = "on"
|
||||
break
|
||||
|
||||
case "[13, 0]": // 0x0d turn auto open off command verified
|
||||
resultMap.name = "autoOpenEnable"
|
||||
resultMap.value = "off"
|
||||
break
|
||||
|
||||
|
||||
case "[20, 0]": // 0x14 Aux1 On command verified
|
||||
log.info "verified Aux1 On"
|
||||
sendEvent(name: "switch", value: "on", displayed: false)
|
||||
resultMap.name = "Aux1"
|
||||
resultMap.value = "on"
|
||||
break
|
||||
|
||||
case "[21, 0]": // 0x15 Aux1 Off command verified
|
||||
log.info "verified Aux1 Off"
|
||||
sendEvent(name: "switch", value: "off", displayed: false)
|
||||
resultMap.name = "Aux1"
|
||||
resultMap.value = "off"
|
||||
break
|
||||
|
||||
case "[22, 0]": // 0x16 Aux2 On command verified
|
||||
log.info "verified Aux2 On"
|
||||
resultMap.name = "Aux2"
|
||||
resultMap.value = "on"
|
||||
break
|
||||
|
||||
case "[23, 0]": // 0x17 Aux2 Off command verified
|
||||
log.info "verified Aux2 Off"
|
||||
resultMap.name = "Aux2"
|
||||
resultMap.value = "off"
|
||||
break
|
||||
|
||||
}
|
||||
}
|
||||
return resultMap
|
||||
}
|
||||
|
||||
private Map parseReportAttributeMessage(String description) {
|
||||
Map resultMap = [:]
|
||||
def descMap = parseDescriptionAsMap(description)
|
||||
//log.debug "read attr descMap --> $descMap"
|
||||
if (descMap.cluster == "0101" && descMap.attrId == "0003") {
|
||||
resultMap.name = "doorState"
|
||||
if (descMap.value == "00"){
|
||||
resultMap.value = "unknown"
|
||||
sendEvent(name: "door", value: "unknown", displayed: false)
|
||||
}else if(descMap.value == "01"){
|
||||
resultMap.value = "closed"
|
||||
sendEvent(name: "door", value: "closed", displayed: false)
|
||||
}else if(descMap.value == "02"){
|
||||
resultMap.value = "open"
|
||||
sendEvent(name: "door", value: "open", displayed: false)
|
||||
}else if(descMap.value == "03"){
|
||||
resultMap.value = "jammed"
|
||||
}else if(descMap.value == "04"){
|
||||
resultMap.value = "forced close"
|
||||
}else if(descMap.value == "05"){
|
||||
resultMap.value = "forced close"
|
||||
}else if(descMap.value == "06"){
|
||||
resultMap.value = "closing"
|
||||
sendEvent(name: "door", value: "closing", displayed: false)
|
||||
}else if(descMap.value == "07"){
|
||||
resultMap.value = "opening"
|
||||
sendEvent(name: "door", value: "opening", displayed: false)
|
||||
}else if(descMap.value == "08"){
|
||||
resultMap.value = "fault"
|
||||
}else {
|
||||
resultMap.value = "unknown"
|
||||
}
|
||||
resultMap.descriptionText = "Door State Changed to ${resultMap.value}"
|
||||
|
||||
} else if (descMap.cluster == "0101" && descMap.attrId == "0400") {
|
||||
resultMap.name = "currentLightLevel"
|
||||
resultMap.value = (Integer.parseInt(descMap.value, 16))
|
||||
resultMap.displayed = false
|
||||
|
||||
} else if (descMap.cluster == "0101" && descMap.attrId == "0401") {
|
||||
resultMap.name = "closeLightLevel"
|
||||
resultMap.value = (Integer.parseInt(descMap.value, 16))
|
||||
|
||||
} else if (descMap.cluster == "0101" && descMap.attrId == "0402") {
|
||||
resultMap.name = "openLightLevel"
|
||||
resultMap.value = (Integer.parseInt(descMap.value, 16))
|
||||
|
||||
} else if (descMap.cluster == "0101" && descMap.attrId == "0403") {
|
||||
resultMap.name = "autoCloseEnable"
|
||||
if (descMap.value == "01"){resultMap.value = "on"}
|
||||
else{resultMap.value = "off"}
|
||||
|
||||
} else if (descMap.cluster == "0101" && descMap.attrId == "0404") {
|
||||
resultMap.name = "autoOpenEnable"
|
||||
if (descMap.value == "01"){resultMap.value = "on"}
|
||||
else{resultMap.value = "off"}
|
||||
|
||||
} else if (descMap.cluster == "0101" && descMap.attrId == "0405") {
|
||||
resultMap.name = "doorCurrent"
|
||||
resultMap.value = (Integer.parseInt(descMap.value, 16))
|
||||
resultMap.value = resultMap.value * 0.001
|
||||
|
||||
} else if (descMap.cluster == "0101" && descMap.attrId == "0408") {
|
||||
resultMap.name = "doorSensitivity"
|
||||
resultMap.value = (100 - Integer.parseInt(descMap.value, 16))
|
||||
|
||||
} else if (descMap.cluster == "0101" && descMap.attrId == "0409") {
|
||||
resultMap.name = "baseDoorCurrent"
|
||||
resultMap.value = (Integer.parseInt(descMap.value, 16))
|
||||
resultMap.value = resultMap.value * 0.001
|
||||
|
||||
} else if (descMap.cluster == "0101" && descMap.attrId == "040a") {
|
||||
resultMap.name = "doorVoltage"
|
||||
resultMap.value = (Integer.parseInt(descMap.value, 16))
|
||||
resultMap.value = resultMap.value * 0.001
|
||||
|
||||
} else if (descMap.cluster == "0101" && descMap.attrId == "040b") {
|
||||
resultMap.name = "Aux1"
|
||||
if(descMap.value == "01"){
|
||||
resultMap.value = "on"
|
||||
sendEvent(name: "switch", value: "on", displayed: false)
|
||||
}else{
|
||||
resultMap.value = "off"
|
||||
sendEvent(name: "switch", value: "off", displayed: false)
|
||||
}
|
||||
|
||||
} else if (descMap.cluster == "0101" && descMap.attrId == "040c") {
|
||||
resultMap.name = "Aux2"
|
||||
if(descMap.value == "01"){
|
||||
resultMap.value = "on"
|
||||
}else{
|
||||
resultMap.value = "off"
|
||||
}
|
||||
} else if (descMap.cluster == "0101" && descMap.attrId == "040d") {
|
||||
resultMap.name = "photoCalibration"
|
||||
resultMap.value = (Integer.parseInt(descMap.value, 16))
|
||||
} else if (descMap.cluster == "0101" && descMap.attrId == "040e") {
|
||||
resultMap.name = "baseCurrentNE"
|
||||
resultMap.value = (Integer.parseInt(descMap.value, 16))
|
||||
}
|
||||
return resultMap
|
||||
}
|
||||
|
||||
private Map parseCustomMessage(String description) {
|
||||
//log.info "ParseCustomMessage called with ${description}"
|
||||
Map resultMap = [:]
|
||||
if (description?.startsWith('temperature: ')) {
|
||||
resultMap.name = "temperature"
|
||||
def rawT = (description - "temperature: ").trim()
|
||||
resultMap.descriptionText = "Temperature celsius value = ${rawT}"
|
||||
def rawTint = Float.parseFloat(rawT)
|
||||
if (rawTint > 65){
|
||||
resultMap.name = null
|
||||
resultMap.value = null
|
||||
resultMap.descriptionText = "Temperature celsius value = ${rawT} is invalid not updating"
|
||||
log.warn "Invalid temperature value detected! rawT = ${rawT}, description = ${description}"
|
||||
}else if (rawT == -32768){ // This number is used to indicate an error in the temperature reading
|
||||
resultMap.value = "ERR"
|
||||
}else{
|
||||
resultMap.value = celsiusToFahrenheit(rawT.toFloat()) as Float
|
||||
sendEvent(name: "TempProb1", value: resultMap.value, displayed: false) // Workaround for lack of access to endpoint information for Temperature report
|
||||
}
|
||||
}
|
||||
resultMap.displayed = false
|
||||
log.info "Temperature reported = ${resultMap.value}"
|
||||
return resultMap
|
||||
}
|
||||
|
||||
def parseDescriptionAsMap(description) {
|
||||
(description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||
def nameAndValue = param.split(":")
|
||||
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
|
||||
}
|
||||
}
|
||||
|
||||
// Added for Temeperature parse
|
||||
def getFahrenheit(value) {
|
||||
def celsius = Integer.parseInt(value, 16)
|
||||
return celsiusToFahrenheit(celsius) as Integer
|
||||
}
|
||||
|
||||
// Private methods
|
||||
def callUpdateStatusTxt(){
|
||||
def cTemp = device.currentState("TempProb1")?.value
|
||||
def cLight = 0
|
||||
def testNull = device.currentState("currentLightLevel")?.value
|
||||
if (testNull != null){
|
||||
cLight = device.currentState("currentLightLevel")?.value as int
|
||||
}
|
||||
updateStatusTxt(cTemp, cLight)
|
||||
}
|
||||
|
||||
def updateStatusTxt(currentTemp, currentLight){
|
||||
//log.info "called updateStatusTxt with ${currentTemp}, ${currentLight}"
|
||||
def cTmp = currentTemp
|
||||
def cLL = 10
|
||||
def oLL = 10
|
||||
|
||||
def testNull = device.currentState("closeLightLevel")?.value
|
||||
if (testNull != null){
|
||||
cLL = device.currentState("closeLightLevel")?.value as int
|
||||
}
|
||||
|
||||
testNull = device.currentState("openLightLevel")?.value
|
||||
if (testNull != null){
|
||||
oLL = device.currentState("openLightLevel")?.value as int
|
||||
}
|
||||
|
||||
def aOpnEn = device.currentState("autoOpenEnable")?.value
|
||||
def aClsEn = device.currentState("autoCloseEnable")?.value
|
||||
|
||||
if (currentLight < cLL){
|
||||
if (aOpnEn == "on"){
|
||||
sendEvent(name: "dayOrNight", value: "Sun must be > ${oLL} to auto open", displayed: false)
|
||||
sendEvent(name: "coopStatus", value: "Sunlight ${currentLight} open at ${oLL}. Coop ${cTmp}°", displayed: false)
|
||||
}else{
|
||||
sendEvent(name: "dayOrNight", value: "Auto Open is turned off.", displayed: false)
|
||||
sendEvent(name: "coopStatus", value: "Sunlight ${currentLight} auto open off. Coop ${cTmp}°", displayed: false)
|
||||
}
|
||||
}else {
|
||||
if (aClsEn == "on"){
|
||||
sendEvent(name: "dayOrNight", value: "Sun must be < ${cLL} to auto close", displayed: false)
|
||||
sendEvent(name: "coopStatus", value: "Sunlight ${currentLight} close at ${cLL}. Coop ${cTmp}°", displayed: false)
|
||||
}else{
|
||||
sendEvent(name: "dayOrNight", value: "Auto Close is turned off.", displayed: false)
|
||||
sendEvent(name: "coopStatus", value: "Sunlight ${currentLight} auto close off. Coop ${cTmp}°", displayed: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Commands to device
|
||||
def on() {
|
||||
log.debug "on calling Aux1On"
|
||||
Aux1On()
|
||||
}
|
||||
|
||||
def off() {
|
||||
log.debug "off calling Aux1Off"
|
||||
Aux1Off()
|
||||
}
|
||||
|
||||
def close() {
|
||||
log.debug "close calling closeDoor"
|
||||
closeDoor()
|
||||
}
|
||||
|
||||
def open() {
|
||||
log.debug "open calling openDoor"
|
||||
openDoor()
|
||||
}
|
||||
|
||||
def Aux1On(){
|
||||
log.debug "Sending Aux1 = on command"
|
||||
"st cmd 0x${device.deviceNetworkId} 0x38 0x0101 0x14 {}"
|
||||
}
|
||||
|
||||
def Aux1Off(){
|
||||
log.debug "Sending Aux1 = off command"
|
||||
"st cmd 0x${device.deviceNetworkId} 0x38 0x0101 0x15 {}"
|
||||
}
|
||||
|
||||
def Aux2On(){
|
||||
log.debug "Sending Aux2 = on command"
|
||||
"st cmd 0x${device.deviceNetworkId} 0x38 0x0101 0x16 {}"
|
||||
}
|
||||
|
||||
def Aux2Off(){
|
||||
log.debug "Sending Aux2 = off command"
|
||||
"st cmd 0x${device.deviceNetworkId} 0x38 0x0101 0x17 {}"
|
||||
}
|
||||
|
||||
def openDoor() {
|
||||
log.debug "Sending Open command"
|
||||
"st cmd 0x${device.deviceNetworkId} 0x38 0x0101 0x1 {}"
|
||||
}
|
||||
|
||||
def closeDoor() {
|
||||
log.debug "Sending Close command"
|
||||
"st cmd 0x${device.deviceNetworkId} 0x38 0x0101 0x0 {}"
|
||||
}
|
||||
|
||||
def closeDoorHiI() {
|
||||
log.debug "Sending High Current Close command"
|
||||
"st cmd 0x${device.deviceNetworkId} 0x38 0x0101 0x4 {}"
|
||||
}
|
||||
|
||||
def autoOpenOn() {
|
||||
log.debug "Setting Auto Open On"
|
||||
"st cmd 0x${device.deviceNetworkId} 0x38 0x0101 0x0C {}"
|
||||
}
|
||||
|
||||
def autoOpenOff() {
|
||||
log.debug "Setting Auto Open Off"
|
||||
"st cmd 0x${device.deviceNetworkId} 0x38 0x0101 0x0D {}"
|
||||
}
|
||||
|
||||
def autoCloseOn() {
|
||||
log.debug "Setting Auto Close On"
|
||||
"st cmd 0x${device.deviceNetworkId} 0x38 0x0101 0x0A {}"
|
||||
}
|
||||
|
||||
def autoCloseOff() {
|
||||
log.debug "Setting Auto Close Off"
|
||||
"st cmd 0x${device.deviceNetworkId} 0x38 0x0101 0x0B {}"
|
||||
}
|
||||
|
||||
def setOpenLevelTo(cValue) {
|
||||
def cX = cValue
|
||||
log.debug "Setting Open Light Level to ${cX} Hex = 0x${Integer.toHexString(cX)}"
|
||||
|
||||
def cmd = []
|
||||
cmd << "st wattr 0x${device.deviceNetworkId} 0x38 0x0101 0x402 0x23 {${Integer.toHexString(cX)}}"
|
||||
cmd << "delay 150"
|
||||
cmd << "st rattr 0x${device.deviceNetworkId} 0x38 0x0101 0x402" // Read light value
|
||||
cmd
|
||||
}
|
||||
|
||||
def setCloseLevelTo(cValue) {
|
||||
def cX = cValue
|
||||
log.debug "Setting Close Light Level to ${cX} Hex = 0x${Integer.toHexString(cX)}"
|
||||
|
||||
def cmd = []
|
||||
cmd << "st wattr 0x${device.deviceNetworkId} 0x38 0x0101 0x401 0x23 {${Integer.toHexString(cX)}}"
|
||||
cmd << "delay 150"
|
||||
cmd << "st rattr 0x${device.deviceNetworkId} 0x38 0x0101 0x401" // Read light value
|
||||
cmd
|
||||
|
||||
}
|
||||
|
||||
def setSensitivityLevel(cValue) {
|
||||
def cX = 100 - cValue
|
||||
log.debug "Setting Door sensitivity level to ${cX} Hex = 0x${Integer.toHexString(cX)}"
|
||||
|
||||
def cmd = []
|
||||
cmd << "st wattr 0x${device.deviceNetworkId} 0x38 0x0101 0x408 0x23 {${Integer.toHexString(cX)}}" // Write attribute. 0x23 is a 32 bit integer value.
|
||||
cmd << "delay 150"
|
||||
cmd << "st rattr 0x${device.deviceNetworkId} 0x38 0x0101 0x408" // Read attribute
|
||||
cmd
|
||||
}
|
||||
|
||||
def setNewBaseCurrent(cValue) {
|
||||
def cX = cValue as int
|
||||
log.info "Setting new BaseCurrent to ${cX} Hex = 0x${Integer.toHexString(cX)}"
|
||||
|
||||
def cmd = []
|
||||
cmd << "st wattr 0x${device.deviceNetworkId} 0x38 0x0101 0x409 0x23 {${Integer.toHexString(cX)}}" // Write attribute. 0x23 is a 32 bit integer value.
|
||||
cmd << "delay 150"
|
||||
cmd << "st rattr 0x${device.deviceNetworkId} 0x38 0x0101 0x409" // Read attribute
|
||||
cmd
|
||||
}
|
||||
|
||||
def setNewPhotoCalibration(cValue) {
|
||||
def cX = cValue as int
|
||||
log.info "Setting new Photoresister calibration to ${cX} Hex = 0x${Integer.toHexString(cX)}"
|
||||
|
||||
def cmd = []
|
||||
cmd << "st wattr 0x${device.deviceNetworkId} 0x38 0x0101 0x40D 0x2B {${Integer.toHexString(cX)}}" // Write attribute. 0x2B is a 32 bit signed integer value.
|
||||
cmd << "st rattr 0x${device.deviceNetworkId} 0x38 0x0101 0x40D" // Read attribute
|
||||
cmd
|
||||
}
|
||||
|
||||
def readNewPhotoCalibration() {
|
||||
log.info "Requesting current Photoresister calibration "
|
||||
|
||||
def cmd = []
|
||||
cmd << "st rattr 0x${device.deviceNetworkId} 0x38 0x0101 0x40D" // Read attribute
|
||||
cmd
|
||||
}
|
||||
|
||||
def readBaseCurrentNE() {
|
||||
log.info "Requesting base current never exceed setting "
|
||||
|
||||
def cmd = []
|
||||
cmd << "st rattr 0x${device.deviceNetworkId} 0x38 0x0101 0x40E" // Read attribute
|
||||
cmd
|
||||
}
|
||||
|
||||
def setBaseCurrentNE(cValue) {
|
||||
def cX = cValue as int
|
||||
log.info "Setting new base Current Never Exceed to ${cX} Hex = 0x${Integer.toHexString(cX)}"
|
||||
|
||||
def cmd = []
|
||||
cmd << "st wattr 0x${device.deviceNetworkId} 0x38 0x0101 0x40E 0x23 {${Integer.toHexString(cX)}}" // Write attribute. 0x23 is a 32 bit unsigned integer value.
|
||||
cmd << "st rattr 0x${device.deviceNetworkId} 0x38 0x0101 0x40E" // Read attribute
|
||||
cmd
|
||||
}
|
||||
|
||||
def poll(){
|
||||
log.debug "Polling Device"
|
||||
def cmd = []
|
||||
cmd << "st rattr 0x${device.deviceNetworkId} 0x38 0x0101 0x0003" // Read Door State
|
||||
cmd << "delay 150"
|
||||
|
||||
cmd << "st rattr 0x${device.deviceNetworkId} 0x38 0x0101 0x0400" // Read Current Light Level
|
||||
cmd << "delay 150"
|
||||
|
||||
cmd << "st rattr 0x${device.deviceNetworkId} 0x39 0x0402 0x0000" // Read probe 1 Temperature
|
||||
cmd << "delay 150"
|
||||
|
||||
cmd << "st rattr 0x${device.deviceNetworkId} 0x40 0x0402 0x0000" // Read probe 2 Temperature
|
||||
|
||||
cmd
|
||||
}
|
||||
|
||||
def updateTemp1() {
|
||||
log.debug "Sending attribute read request for Temperature Probe1"
|
||||
def cmd = []
|
||||
cmd << "st rattr 0x${device.deviceNetworkId} 0x39 0x0402 0x0000" // Read Current Temperature from Coop Probe 1
|
||||
cmd
|
||||
}
|
||||
|
||||
def updateTemp2() {
|
||||
log.debug "Sending attribute read request for Temperature Probe2"
|
||||
def cmd = []
|
||||
cmd << "st rattr 0x${device.deviceNetworkId} 0x40 0x0402 0x0000" // Read Current Temperature from Coop Probe 2
|
||||
cmd
|
||||
}
|
||||
|
||||
|
||||
def updateSun() {
|
||||
log.debug "Sending attribute read request for Sun Light Level"
|
||||
def cmd = []
|
||||
cmd << "st rattr 0x${device.deviceNetworkId} 0x38 0x0101 0x0400" // Read Current Light Level
|
||||
cmd
|
||||
}
|
||||
|
||||
def updateSensitivity() {
|
||||
log.debug "Sending attribute read request for door sensitivity"
|
||||
def cmd = []
|
||||
cmd << "st rattr 0x${device.deviceNetworkId} 0x38 0x0101 0x0408" // Read Door sensitivity
|
||||
cmd
|
||||
}
|
||||
|
||||
def updateCloseLightLevel() {
|
||||
log.debug "Sending attribute read close light level"
|
||||
def cmd = []
|
||||
cmd << "st rattr 0x${device.deviceNetworkId} 0x38 0x0101 0x0401"
|
||||
cmd
|
||||
}
|
||||
|
||||
def updateOpenLightLevel() {
|
||||
log.debug "Sending attribute read open light level"
|
||||
def cmd = []
|
||||
cmd << "st rattr 0x${device.deviceNetworkId} 0x38 0x0101 0x0402"
|
||||
cmd
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
log.debug "sending refresh command"
|
||||
def cmd = []
|
||||
cmd << "st rattr 0x${device.deviceNetworkId} 0x38 0x0101 0x0003" // Read Door State
|
||||
cmd << "delay 150"
|
||||
|
||||
cmd << "st rattr 0x${device.deviceNetworkId} 0x38 0x0101 0x0400" // Read Current Light Level
|
||||
cmd << "delay 150"
|
||||
|
||||
cmd << "st rattr 0x${device.deviceNetworkId} 0x38 0x0101 0x0401" // Read Door Close Light Level
|
||||
cmd << "delay 150"
|
||||
|
||||
cmd << "st rattr 0x${device.deviceNetworkId} 0x38 0x0101 0x0402" // Read Door Open Light Level
|
||||
cmd << "delay 150"
|
||||
|
||||
cmd << "st rattr 0x${device.deviceNetworkId} 0x38 0x0101 0x0403" // Read Auto Door Close Settings
|
||||
cmd << "delay 150"
|
||||
|
||||
cmd << "st rattr 0x${device.deviceNetworkId} 0x38 0x0101 0x0404" // Read Auto Door Open Settings
|
||||
cmd << "delay 150"
|
||||
|
||||
cmd << "st rattr 0x${device.deviceNetworkId} 0x39 0x0402 0x0000" // Read Current Temperature from Coop Probe 1
|
||||
cmd << "delay 150"
|
||||
|
||||
cmd << "st rattr 0x${device.deviceNetworkId} 0x38 0x0101 0x0408" // Object detection sensitivity
|
||||
cmd << "delay 150"
|
||||
|
||||
cmd << "st rattr 0x${device.deviceNetworkId} 0x40 0x0402 0x0000" // Read Current Temperature from Coop Probe 2
|
||||
cmd << "delay 150"
|
||||
|
||||
cmd << "st rattr 0x${device.deviceNetworkId} 0x38 0x0101 0x0405" // Current required to close door
|
||||
cmd << "delay 150"
|
||||
|
||||
cmd << "st rattr 0x${device.deviceNetworkId} 0x38 0x0101 0x040B" // Aux1 Status
|
||||
cmd << "delay 150"
|
||||
|
||||
cmd << "st rattr 0x${device.deviceNetworkId} 0x38 0x0101 0x040C" // Aux2 Status
|
||||
cmd << "delay 150"
|
||||
|
||||
cmd << "st rattr 0x${device.deviceNetworkId} 0x38 0x0101 0x409" // Read Base current
|
||||
|
||||
cmd
|
||||
}
|
||||
|
||||
def configure() {
|
||||
log.debug "Binding SEP 0x38 DEP 0x01 Cluster 0x0101 Lock cluster to hub"
|
||||
log.debug "Binding SEP 0x39 DEP 0x01 Cluster 0x0402 Temperature cluster to hub"
|
||||
log.debug "Binding SEP 0x40 DEP 0x01 Cluster 0x0402 Temperature cluster to hub"
|
||||
|
||||
def cmd = []
|
||||
cmd << "zdo bind 0x${device.deviceNetworkId} 0x38 0x01 0x0101 {${device.zigbeeId}} {}" // Bind to end point 0x38 and the lock cluster
|
||||
cmd << "delay 150"
|
||||
cmd << "zdo bind 0x${device.deviceNetworkId} 0x39 0x01 0x0402 {${device.zigbeeId}} {}" // Bind to end point 0x39 and the temperature cluster
|
||||
cmd << "delay 150"
|
||||
cmd << "zdo bind 0x${device.deviceNetworkId} 0x40 0x01 0x0402 {${device.zigbeeId}} {}" // Bind to end point 0x40 and the temperature cluster
|
||||
cmd << "delay 1500"
|
||||
|
||||
log.info "Sending ZigBee Configuration Commands to Coop Control"
|
||||
return cmd + refresh()
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* Simple Sync
|
||||
*
|
||||
* Copyright 2015 Roomie Remote, Inc.
|
||||
*
|
||||
* Date: 2015-09-22
|
||||
*/
|
||||
metadata
|
||||
{
|
||||
definition (name: "Simple Sync", namespace: "roomieremote-agent", author: "Roomie Remote, Inc.")
|
||||
{
|
||||
capability "Media Controller"
|
||||
}
|
||||
|
||||
// simulator metadata
|
||||
simulator
|
||||
{
|
||||
}
|
||||
|
||||
// UI tile definitions
|
||||
tiles
|
||||
{
|
||||
standardTile("mainTile", "device.status", width: 1, height: 1, icon: "st.Entertainment.entertainment11")
|
||||
{
|
||||
state "default", label: "Simple Sync", icon: "st.Home.home2", backgroundColor: "#55A7FF"
|
||||
}
|
||||
|
||||
def detailTiles = ["mainTile"]
|
||||
|
||||
main "mainTile"
|
||||
details(detailTiles)
|
||||
}
|
||||
}
|
||||
|
||||
def parse(String description)
|
||||
{
|
||||
def results = []
|
||||
|
||||
try
|
||||
{
|
||||
def msg = parseLanMessage(description)
|
||||
|
||||
if (msg.headers && msg.body)
|
||||
{
|
||||
switch (msg.headers["X-Roomie-Echo"])
|
||||
{
|
||||
case "getAllActivities":
|
||||
handleGetAllActivitiesResponse(msg)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Throwable t)
|
||||
{
|
||||
sendEvent(name: "parseError", value: "$t", description: description)
|
||||
throw t
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
def handleGetAllActivitiesResponse(response)
|
||||
{
|
||||
def body = parseJson(response.body)
|
||||
|
||||
if (body.status == "success")
|
||||
{
|
||||
def json = new groovy.json.JsonBuilder()
|
||||
def root = json activities: body.data
|
||||
def data = json.toString()
|
||||
|
||||
sendEvent(name: "activities", value: data)
|
||||
}
|
||||
}
|
||||
|
||||
def getAllActivities(evt)
|
||||
{
|
||||
def host = getHostAddress(device.deviceNetworkId)
|
||||
|
||||
def action = new physicalgraph.device.HubAction(method: "GET",
|
||||
path: "/api/v1/activities",
|
||||
headers: [HOST: host, "X-Roomie-Echo": "getAllActivities"])
|
||||
|
||||
action
|
||||
}
|
||||
|
||||
def startActivity(evt)
|
||||
{
|
||||
def uuid = evt
|
||||
def host = getHostAddress(device.deviceNetworkId)
|
||||
def activity = new groovy.json.JsonSlurper().parseText(device.currentValue('activities') ?: "{ 'activities' : [] }").activities.find { it.uuid == uuid }
|
||||
def toggle = activity["toggle"]
|
||||
def jsonMap = ["activity_uuid": uuid]
|
||||
|
||||
if (toggle != null)
|
||||
{
|
||||
jsonMap << ["toggle_state": toggle ? "on" : "off"]
|
||||
}
|
||||
|
||||
def json = new groovy.json.JsonBuilder(jsonMap)
|
||||
def jsonBody = json.toString()
|
||||
def headers = [HOST: host, "Content-Type": "application/json"]
|
||||
|
||||
def action = new physicalgraph.device.HubAction(method: "POST",
|
||||
path: "/api/v1/runactivity",
|
||||
body: jsonBody,
|
||||
headers: headers)
|
||||
|
||||
action
|
||||
}
|
||||
|
||||
def getHostAddress(d)
|
||||
{
|
||||
def parts = d.split(":")
|
||||
def ip = convertHexToIP(parts[0])
|
||||
def port = convertHexToInt(parts[1])
|
||||
return ip + ":" + port
|
||||
}
|
||||
|
||||
def String convertHexToIP(hex)
|
||||
{
|
||||
[convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
|
||||
}
|
||||
|
||||
def Integer convertHexToInt(hex)
|
||||
{
|
||||
Integer.parseInt(hex,16)
|
||||
}
|
||||
@@ -11,6 +11,7 @@
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
metadata {
|
||||
definition (name: "Arrival Sensor HA", namespace: "smartthings", author: "SmartThings") {
|
||||
capability "Tone"
|
||||
@@ -32,7 +33,7 @@ metadata {
|
||||
])
|
||||
}
|
||||
section {
|
||||
input "checkInterval", "enum", title: "Presence timeout (minutes)",
|
||||
input "checkInterval", "enum", title: "Presence timeout (minutes)", description: "Tap to set",
|
||||
defaultValue:"2", options: ["2", "3", "5"], displayDuringSetup: false
|
||||
}
|
||||
}
|
||||
@@ -82,7 +83,6 @@ def parse(String description) {
|
||||
|
||||
private handleReportAttributeMessage(String description) {
|
||||
def descMap = zigbee.parseDescriptionAsMap(description)
|
||||
|
||||
if (descMap.clusterInt == 0x0001 && descMap.attrInt == 0x0020) {
|
||||
handleBatteryEvent(Integer.parseInt(descMap.value, 16))
|
||||
}
|
||||
@@ -94,6 +94,7 @@ private handleReportAttributeMessage(String description) {
|
||||
* @param volts Battery voltage in .1V increments
|
||||
*/
|
||||
private handleBatteryEvent(volts) {
|
||||
def descriptionText
|
||||
if (volts == 0 || volts == 255) {
|
||||
log.debug "Ignoring invalid value for voltage (${volts/10}V)"
|
||||
}
|
||||
@@ -107,15 +108,17 @@ private handleBatteryEvent(volts) {
|
||||
volts = minVolts
|
||||
else if (volts > maxVolts)
|
||||
volts = maxVolts
|
||||
def pct = batteryMap[volts]
|
||||
if (pct != null) {
|
||||
def value = batteryMap[volts]
|
||||
if (value != null) {
|
||||
def linkText = getLinkText(device)
|
||||
descriptionText = '{{ linkText }} battery was {{ value }}'
|
||||
def eventMap = [
|
||||
name: 'battery',
|
||||
value: pct,
|
||||
descriptionText: "${linkText} battery was ${pct}%"
|
||||
value: value,
|
||||
descriptionText: descriptionText,
|
||||
translatable: true
|
||||
]
|
||||
log.debug "Creating battery event for voltage=${volts/10}V: ${eventMap}"
|
||||
log.debug "Creating battery event for voltage=${volts/10}V: ${linkText} ${eventMap.name} is ${eventMap.value}%"
|
||||
sendEvent(eventMap)
|
||||
}
|
||||
}
|
||||
@@ -131,13 +134,19 @@ private handlePresenceEvent(present) {
|
||||
stopTimer()
|
||||
}
|
||||
def linkText = getLinkText(device)
|
||||
def descriptionText
|
||||
if ( present )
|
||||
descriptionText = "{{ linkText }} has arrived"
|
||||
else
|
||||
descriptionText = "{{ linkText }} has left"
|
||||
def eventMap = [
|
||||
name: "presence",
|
||||
value: present ? "present" : "not present",
|
||||
linkText: linkText,
|
||||
descriptionText: "${linkText} has ${present ? 'arrived' : 'left'}",
|
||||
descriptionText: descriptionText,
|
||||
translatable: true
|
||||
]
|
||||
log.debug "Creating presence event: ${eventMap}"
|
||||
log.debug "Creating presence event: ${device.displayName} ${eventMap.name} is ${eventMap.value}"
|
||||
sendEvent(eventMap)
|
||||
}
|
||||
|
||||
@@ -158,4 +167,4 @@ def checkPresenceCallback() {
|
||||
if (timeSinceLastCheckin >= theCheckInterval) {
|
||||
handlePresenceEvent(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
# Copyright 2016 SmartThings
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
# use this file except in compliance with the License. You may obtain a copy
|
||||
# of the License at:
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
# Korean (ko)
|
||||
# Device Preferences
|
||||
'''Give your device a name'''.ko=기기 이름 설정
|
||||
'''Set Device Image'''.ko=기기 이미지 설정
|
||||
'''Presence timeout (minutes)'''.ko=알람 유예 시간 설정 (분)
|
||||
'''Tap to set'''.ko=눌러서 설정
|
||||
'''Arrival Sensor'''.ko=도착알림 센서
|
||||
'''${currentValue}% battery'''.ko=${currentValue}% 배터리
|
||||
# Events / Notifications
|
||||
'''{{ linkText }} battery was {{ value }}'''.ko={{ linkText }}의 남은 배터리 {{ value }}
|
||||
'''{{ linkText }} has arrived'''.ko={{ linkText }} 귀가
|
||||
'''{{ linkText }} has left'''.ko={{ linkText }} 외출
|
||||
#==============================================================================
|
||||
@@ -13,12 +13,13 @@
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
metadata {
|
||||
definition (name: "Cree Bulb", namespace: "smartthings", author: "SmartThings") {
|
||||
|
||||
capability "Actuator"
|
||||
capability "Configuration"
|
||||
capability "Polling"
|
||||
capability "Refresh"
|
||||
capability "Switch"
|
||||
capability "Switch Level"
|
||||
@@ -88,6 +89,10 @@ def refresh() {
|
||||
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.onOffConfig() + zigbee.levelConfig()
|
||||
}
|
||||
|
||||
def poll() {
|
||||
zigbee.onOffRefresh() + zigbee.levelRefresh()
|
||||
}
|
||||
|
||||
def configure() {
|
||||
log.debug "Configuring Reporting and Bindings."
|
||||
zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh()
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ metadata {
|
||||
capability "Temperature Measurement"
|
||||
capability "Motion Sensor"
|
||||
capability "Refresh"
|
||||
capability "Polling"
|
||||
}
|
||||
|
||||
tiles {
|
||||
@@ -68,6 +67,6 @@ def refresh() {
|
||||
|
||||
void poll() {
|
||||
log.debug "Executing 'poll' using parent SmartApp"
|
||||
parent.pollChild(this)
|
||||
parent.pollChild()
|
||||
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ metadata {
|
||||
capability "Actuator"
|
||||
capability "Thermostat"
|
||||
capability "Temperature Measurement"
|
||||
capability "Polling"
|
||||
capability "Sensor"
|
||||
capability "Refresh"
|
||||
capability "Relative Humidity Measurement"
|
||||
@@ -134,9 +133,7 @@ def refresh() {
|
||||
|
||||
void poll() {
|
||||
log.debug "Executing 'poll' using parent SmartApp"
|
||||
|
||||
def results = parent.pollChild(this)
|
||||
generateEvent(results) //parse received message from parent
|
||||
parent.pollChild()
|
||||
}
|
||||
|
||||
def generateEvent(Map results) {
|
||||
@@ -382,12 +379,16 @@ def getDataByName(String name) {
|
||||
state[name] ?: device.getDataValue(name)
|
||||
}
|
||||
|
||||
def setThermostatMode(String value) {
|
||||
log.debug "setThermostatMode({$value})"
|
||||
def setThermostatMode(String mode) {
|
||||
log.debug "setThermostatMode($mode)"
|
||||
mode = mode.toLowerCase()
|
||||
switchToMode(mode)
|
||||
}
|
||||
|
||||
def setThermostatFanMode(String value) {
|
||||
log.debug "setThermostatFanMode({$value})"
|
||||
def setThermostatFanMode(String mode) {
|
||||
log.debug "setThermostatFanMode($mode)"
|
||||
mode = mode.toLowerCase()
|
||||
switchToFanMode(mode)
|
||||
}
|
||||
|
||||
def generateModeEvent(mode) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* GE Link Bulb
|
||||
*
|
||||
* Copyright 2014 SmartThings
|
||||
* Copyright 2016 SmartThings
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
@@ -36,155 +36,73 @@
|
||||
* Slider range from 0..100
|
||||
* Change 9: 2015-03-06 (Juan Risso)
|
||||
* Setlevel -> value to integer (to prevent smartapp calling this function from not working).
|
||||
* Change 10: 2016-03-06 (Vinay Rao/Tom Manley)
|
||||
* changed 2/3rds of the file to clean up code and add zigbee library improvements
|
||||
*
|
||||
*/
|
||||
|
||||
metadata {
|
||||
definition (name: "GE Link Bulb", namespace: "smartthings", author: "SmartThings") {
|
||||
definition (name: "GE Link Bulb", namespace: "smartthings", author: "SmartThings") {
|
||||
|
||||
capability "Actuator"
|
||||
capability "Actuator"
|
||||
capability "Configuration"
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
capability "Sensor"
|
||||
capability "Switch"
|
||||
capability "Switch Level"
|
||||
capability "Switch Level"
|
||||
capability "Polling"
|
||||
|
||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,1000", outClusters: "0019", manufacturer: "GE_Appliances", model: "ZLL Light", deviceJoinName: "GE Link Bulb"
|
||||
}
|
||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,1000", outClusters: "0019", manufacturer: "GE", model: "SoftWhite", deviceJoinName: "GE Link Soft White Bulb"
|
||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,1000", outClusters: "0019", manufacturer: "GE", model: "Daylight", deviceJoinName: "GE Link Daylight Bulb"
|
||||
}
|
||||
|
||||
// UI tile definitions
|
||||
tiles {
|
||||
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||
state "on", label: '${name}', action: "switch.off", icon: "st.switches.light.on", backgroundColor: "#79b821", nextState:"turningOff"
|
||||
state "off", label: '${name}', action: "switch.on", icon: "st.switches.light.off", backgroundColor: "#ffffff", nextState:"turningOn"
|
||||
state "turningOn", label:'${name}', action: "switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
state "turningOff", label:'${name}', action: "switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
}
|
||||
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 3, inactiveLabel: false, range:"(0..100)") {
|
||||
state "level", action:"switch level.setLevel"
|
||||
}
|
||||
valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") {
|
||||
state "level", label: 'Level ${currentValue}%'
|
||||
}
|
||||
|
||||
main(["switch"])
|
||||
details(["switch", "level", "levelSliderControl", "refresh"])
|
||||
}
|
||||
|
||||
preferences {
|
||||
|
||||
input("dimRate", "enum", title: "Dim Rate", options: ["Instant", "Normal", "Slow", "Very Slow"], defaultValue: "Normal", required: false, displayDuringSetup: true)
|
||||
input("dimOnOff", "enum", title: "Dim transition for On/Off commands?", options: ["Yes", "No"], defaultValue: "No", required: false, displayDuringSetup: true)
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){
|
||||
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
|
||||
attributeState "on", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
attributeState "off", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
}
|
||||
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
|
||||
attributeState "level", action:"switch level.setLevel"
|
||||
}
|
||||
}
|
||||
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
main "switch"
|
||||
details(["switch", "refresh"])
|
||||
}
|
||||
|
||||
preferences {
|
||||
input("dimRate", "enum", title: "Dim Rate", options: ["Instant", "Normal", "Slow", "Very Slow"], defaultValue: "Normal", required: false, displayDuringSetup: true)
|
||||
input("dimOnOff", "enum", title: "Dim transition for On/Off commands?", options: ["Yes", "No"], defaultValue: "No", required: false, displayDuringSetup: true)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse incoming device messages to generate events
|
||||
def parse(String description) {
|
||||
log.trace description
|
||||
|
||||
if (description?.startsWith("on/off:")) {
|
||||
log.debug "The bulb was sent a command to do something just now..."
|
||||
if (description[-1] == "1") {
|
||||
def result = createEvent(name: "switch", value: "on")
|
||||
log.debug "On command was sent maybe from manually turning on? : Parse returned ${result?.descriptionText}"
|
||||
return result
|
||||
} else if (description[-1] == "0") {
|
||||
def result = createEvent(name: "switch", value: "off")
|
||||
log.debug "Off command was sent : Parse returned ${result?.descriptionText}"
|
||||
return result
|
||||
def resultMap = zigbee.getEvent(description)
|
||||
if (resultMap) {
|
||||
if ((resultMap.name == "level" && state.trigger == "setLevel") || resultMap.name != "level") { //doing this to account for weird level reporting bug with GE Link Bulbs
|
||||
sendEvent(resultMap)
|
||||
}
|
||||
}
|
||||
|
||||
def msg = zigbee.parse(description)
|
||||
|
||||
if (description?.startsWith("catchall:")) {
|
||||
// log.trace msg
|
||||
// log.trace "data: $msg.data"
|
||||
|
||||
def x = description[-4..-1]
|
||||
// log.debug x
|
||||
|
||||
switch (x)
|
||||
{
|
||||
|
||||
case "0000":
|
||||
|
||||
def result = createEvent(name: "switch", value: "off")
|
||||
log.debug "${result?.descriptionText}"
|
||||
return result
|
||||
break
|
||||
|
||||
case "1000":
|
||||
|
||||
def result = createEvent(name: "switch", value: "off")
|
||||
log.debug "${result?.descriptionText}"
|
||||
return result
|
||||
break
|
||||
|
||||
case "0100":
|
||||
|
||||
def result = createEvent(name: "switch", value: "on")
|
||||
log.debug "${result?.descriptionText}"
|
||||
return result
|
||||
break
|
||||
|
||||
case "1001":
|
||||
|
||||
def result = createEvent(name: "switch", value: "on")
|
||||
log.debug "${result?.descriptionText}"
|
||||
return result
|
||||
break
|
||||
}
|
||||
else {
|
||||
log.debug "DID NOT PARSE MESSAGE for description : $description"
|
||||
log.debug zigbee.parseDescriptionAsMap(description)
|
||||
}
|
||||
|
||||
if (description?.startsWith("read attr")) {
|
||||
|
||||
// log.trace description[27..28]
|
||||
// log.trace description[-2..-1]
|
||||
|
||||
if (description[27..28] == "0A") {
|
||||
|
||||
// log.debug description[-2..-1]
|
||||
def i = Math.round(convertHexToInt(description[-2..-1]) / 256 * 100 )
|
||||
sendEvent( name: "level", value: i )
|
||||
sendEvent( name: "switch.setLevel", value: i) //added to help subscribers
|
||||
|
||||
}
|
||||
|
||||
else {
|
||||
|
||||
if (description[-2..-1] == "00" && state.trigger == "setLevel") {
|
||||
// log.debug description[-2..-1]
|
||||
def i = Math.round(convertHexToInt(description[-2..-1]) / 256 * 100 )
|
||||
sendEvent( name: "level", value: i )
|
||||
sendEvent( name: "switch.setLevel", value: i) //added to help subscribers
|
||||
}
|
||||
|
||||
if (description[-2..-1] == state.lvl) {
|
||||
// log.debug description[-2..-1]
|
||||
def i = Math.round(convertHexToInt(description[-2..-1]) / 256 * 100 )
|
||||
sendEvent( name: "level", value: i )
|
||||
sendEvent( name: "switch.setLevel", value: i) //added to help subscribers
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def poll() {
|
||||
|
||||
[
|
||||
"st rattr 0x${device.deviceNetworkId} 1 6 0", "delay 500",
|
||||
"st rattr 0x${device.deviceNetworkId} 1 8 0", "delay 500",
|
||||
"st wattr 0x${device.deviceNetworkId} 1 8 0x10 0x21 {${state?.dOnOff ?: '0000'}}"
|
||||
def refreshCmds = [
|
||||
"st wattr 0x${device.deviceNetworkId} 1 8 0x10 0x21 {${state?.dOnOff ?: '0000'}}", "delay 500"
|
||||
]
|
||||
|
||||
return refreshCmds + zigbee.onOffRefresh() + zigbee.levelRefresh()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
@@ -270,109 +188,63 @@ def updated() {
|
||||
}
|
||||
|
||||
def on() {
|
||||
state.lvl = "00"
|
||||
state.trigger = "on/off"
|
||||
|
||||
// log.debug "on()"
|
||||
sendEvent(name: "switch", value: "on")
|
||||
"st cmd 0x${device.deviceNetworkId} 1 6 1 {}"
|
||||
zigbee.on()
|
||||
}
|
||||
|
||||
def off() {
|
||||
state.lvl = "00"
|
||||
state.trigger = "on/off"
|
||||
|
||||
// log.debug "off()"
|
||||
sendEvent(name: "switch", value: "off")
|
||||
"st cmd 0x${device.deviceNetworkId} 1 6 0 {}"
|
||||
zigbee.off()
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
|
||||
[
|
||||
"st rattr 0x${device.deviceNetworkId} 1 6 0", "delay 500",
|
||||
"st rattr 0x${device.deviceNetworkId} 1 8 0", "delay 500",
|
||||
"st wattr 0x${device.deviceNetworkId} 1 8 0x10 0x21 {${state?.dOnOff ?: '0000'}}"
|
||||
def refreshCmds = [
|
||||
"st wattr 0x${device.deviceNetworkId} 1 8 0x10 0x21 {${state?.dOnOff ?: '0000'}}", "delay 500"
|
||||
]
|
||||
poll()
|
||||
|
||||
return refreshCmds + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.onOffConfig()
|
||||
}
|
||||
|
||||
def setLevel(value) {
|
||||
|
||||
def cmds = []
|
||||
value = value as Integer
|
||||
if (value == 0) {
|
||||
sendEvent(name: "switch", value: "off")
|
||||
cmds << "st cmd 0x${device.deviceNetworkId} 1 8 0 {0000 ${state.rate}}"
|
||||
}
|
||||
else if (device.latestValue("switch") == "off") {
|
||||
sendEvent(name: "switch", value: "on")
|
||||
}
|
||||
|
||||
sendEvent(name: "level", value: value)
|
||||
value = (value * 255 / 100)
|
||||
def level = hex(value);
|
||||
|
||||
state.trigger = "setLevel"
|
||||
state.lvl = "${level}"
|
||||
|
||||
def cmd
|
||||
def delayForRefresh = 500
|
||||
if (dimRate && (state?.rate != null)) {
|
||||
cmds << "st cmd 0x${device.deviceNetworkId} 1 8 4 {${level} ${state.rate}}"
|
||||
def computedRate = convertRateValue(state.rate)
|
||||
cmd = zigbee.setLevel(value, computedRate)
|
||||
delayForRefresh += computedRate * 100 //converting tenth of second to milliseconds
|
||||
}
|
||||
else {
|
||||
cmds << "st cmd 0x${device.deviceNetworkId} 1 8 4 {${level} 1500}"
|
||||
cmd = zigbee.setLevel(value, 20)
|
||||
delayForRefresh += 2000
|
||||
}
|
||||
cmd + ["delay $delayForRefresh"] + zigbee.levelRefresh()
|
||||
}
|
||||
|
||||
log.debug cmds
|
||||
cmds
|
||||
int convertRateValue(rate) {
|
||||
int convertedRate = 0
|
||||
switch (rate)
|
||||
{
|
||||
case "0000":
|
||||
convertedRate = 0
|
||||
break
|
||||
|
||||
case "1500":
|
||||
convertedRate = 20 //0015 hex in int is 2.1
|
||||
break
|
||||
|
||||
case "2500":
|
||||
convertedRate = 35 //0025 hex in int is 3.7
|
||||
break
|
||||
|
||||
case "3500":
|
||||
convertedRate = 50 //0035 hex in int is 5.1
|
||||
break
|
||||
}
|
||||
convertedRate
|
||||
}
|
||||
|
||||
def configure() {
|
||||
|
||||
log.debug "Configuring Reporting and Bindings."
|
||||
def configCmds = [
|
||||
|
||||
//Switch Reporting
|
||||
"zcl global send-me-a-report 6 0 0x10 0 3600 {01}", "delay 500",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 1000",
|
||||
|
||||
//Level Control Reporting
|
||||
"zcl global send-me-a-report 8 0 0x20 5 3600 {0010}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 6 {${device.zigbeeId}} {}", "delay 1000",
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 8 {${device.zigbeeId}} {}", "delay 500",
|
||||
]
|
||||
return configCmds + refresh() // send refresh cmds as part of config
|
||||
log.debug "Configuring Reporting and Bindings."
|
||||
return zigbee.onOffConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh()
|
||||
}
|
||||
|
||||
private hex(value, width=2) {
|
||||
def s = new BigInteger(Math.round(value).toString()).toString(16)
|
||||
while (s.size() < width) {
|
||||
s = "0" + s
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
private Integer convertHexToInt(hex) {
|
||||
Integer.parseInt(hex,16)
|
||||
}
|
||||
|
||||
private String swapEndianHex(String hex) {
|
||||
reverseArray(hex.decodeHex()).encodeHex()
|
||||
}
|
||||
|
||||
private byte[] reverseArray(byte[] array) {
|
||||
int i = 0;
|
||||
int j = array.length - 1;
|
||||
byte tmp;
|
||||
while (j > i) {
|
||||
tmp = array[j];
|
||||
array[j] = array[i];
|
||||
array[i] = tmp;
|
||||
j--;
|
||||
i++;
|
||||
}
|
||||
return array
|
||||
}
|
||||
224
devicetypes/smartthings/hue-bloom.src/hue-bloom.groovy
Normal file
224
devicetypes/smartthings/hue-bloom.src/hue-bloom.groovy
Normal file
@@ -0,0 +1,224 @@
|
||||
/**
|
||||
* Hue Bloom
|
||||
*
|
||||
* Philips Hue Type "Color Light"
|
||||
*
|
||||
* Author: SmartThings
|
||||
*/
|
||||
|
||||
// for the UI
|
||||
metadata {
|
||||
// Automatically generated. Make future change here.
|
||||
definition (name: "Hue Bloom", namespace: "smartthings", author: "SmartThings") {
|
||||
capability "Switch Level"
|
||||
capability "Actuator"
|
||||
capability "Color Control"
|
||||
capability "Switch"
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
|
||||
command "setAdjustedColor"
|
||||
command "reset"
|
||||
command "refresh"
|
||||
}
|
||||
|
||||
simulator {
|
||||
// TODO: define status and reply messages here
|
||||
}
|
||||
|
||||
tiles (scale: 2){
|
||||
multiAttributeTile(name:"rich-control", type: "lighting", width: 6, height: 4, canChangeIcon: true){
|
||||
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
|
||||
attributeState "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
attributeState "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
}
|
||||
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
|
||||
attributeState "level", action:"switch level.setLevel", range:"(0..100)"
|
||||
}
|
||||
tileAttribute ("device.color", key: "COLOR_CONTROL") {
|
||||
attributeState "color", action:"setAdjustedColor"
|
||||
}
|
||||
}
|
||||
|
||||
standardTile("reset", "device.reset", height: 2, width: 2, inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:"Reset Color", action:"reset", icon:"st.lights.philips.hue-single"
|
||||
}
|
||||
|
||||
standardTile("refresh", "device.refresh", height: 2, width: 2, inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
main(["rich-control"])
|
||||
details(["rich-control", "colorTempSliderControl", "colorTemp", "reset", "refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
// parse events into attributes
|
||||
def parse(description) {
|
||||
log.debug "parse() - $description"
|
||||
def results = []
|
||||
|
||||
def map = description
|
||||
if (description instanceof String) {
|
||||
log.debug "Hue Bulb stringToMap - ${map}"
|
||||
map = stringToMap(description)
|
||||
}
|
||||
|
||||
if (map?.name && map?.value) {
|
||||
results << createEvent(name: "${map?.name}", value: "${map?.value}")
|
||||
}
|
||||
results
|
||||
}
|
||||
|
||||
// handle commands
|
||||
void on() {
|
||||
log.trace parent.on(this)
|
||||
sendEvent(name: "switch", value: "on")
|
||||
}
|
||||
|
||||
void off() {
|
||||
log.trace parent.off(this)
|
||||
sendEvent(name: "switch", value: "off")
|
||||
}
|
||||
|
||||
void nextLevel() {
|
||||
def level = device.latestValue("level") as Integer ?: 0
|
||||
if (level <= 100) {
|
||||
level = Math.min(25 * (Math.round(level / 25) + 1), 100) as Integer
|
||||
}
|
||||
else {
|
||||
level = 25
|
||||
}
|
||||
setLevel(level)
|
||||
}
|
||||
|
||||
void setLevel(percent) {
|
||||
log.debug "Executing 'setLevel'"
|
||||
if (verifyPercent(percent)) {
|
||||
parent.setLevel(this, percent)
|
||||
sendEvent(name: "level", value: percent, descriptionText: "Level has changed to ${percent}%")
|
||||
sendEvent(name: "switch", value: "on")
|
||||
}
|
||||
}
|
||||
|
||||
void setSaturation(percent) {
|
||||
log.debug "Executing 'setSaturation'"
|
||||
if (verifyPercent(percent)) {
|
||||
parent.setSaturation(this, percent)
|
||||
sendEvent(name: "saturation", value: percent, displayed: false)
|
||||
}
|
||||
}
|
||||
|
||||
void setHue(percent) {
|
||||
log.debug "Executing 'setHue'"
|
||||
if (verifyPercent(percent)) {
|
||||
parent.setHue(this, percent)
|
||||
sendEvent(name: "hue", value: percent, displayed: false)
|
||||
}
|
||||
}
|
||||
|
||||
void setColor(value) {
|
||||
log.debug "setColor: ${value}, $this"
|
||||
def events = []
|
||||
def validValues = [:]
|
||||
|
||||
if (verifyPercent(value.hue)) {
|
||||
events << createEvent(name: "hue", value: value.hue, displayed: false)
|
||||
validValues.hue = value.hue
|
||||
}
|
||||
if (verifyPercent(value.saturation)) {
|
||||
events << createEvent(name: "saturation", value: value.saturation, displayed: false)
|
||||
validValues.saturation = value.saturation
|
||||
}
|
||||
if (value.hex != null) {
|
||||
if (value.hex ==~ /^\#([A-Fa-f0-9]){6}$/) {
|
||||
events << createEvent(name: "color", value: value.hex)
|
||||
validValues.hex = value.hex
|
||||
} else {
|
||||
log.warn "$value.hex is not a valid color"
|
||||
}
|
||||
}
|
||||
if (verifyPercent(value.level)) {
|
||||
events << createEvent(name: "level", value: value.level, descriptionText: "Level has changed to ${value.level}%")
|
||||
validValues.level = value.level
|
||||
}
|
||||
if (value.switch == "off" || (value.level != null && value.level <= 0)) {
|
||||
events << createEvent(name: "switch", value: "off")
|
||||
validValues.switch = "off"
|
||||
} else {
|
||||
events << createEvent(name: "switch", value: "on")
|
||||
validValues.switch = "on"
|
||||
}
|
||||
if (!events.isEmpty()) {
|
||||
parent.setColor(this, validValues)
|
||||
}
|
||||
events.each {
|
||||
sendEvent(it)
|
||||
}
|
||||
}
|
||||
|
||||
void reset() {
|
||||
log.debug "Executing 'reset'"
|
||||
def value = [level:100, saturation:56, hue:23]
|
||||
setAdjustedColor(value)
|
||||
parent.poll()
|
||||
}
|
||||
|
||||
void setAdjustedColor(value) {
|
||||
if (value) {
|
||||
log.trace "setAdjustedColor: ${value}"
|
||||
def adjusted = value + [:]
|
||||
adjusted.hue = adjustOutgoingHue(value.hue)
|
||||
// Needed because color picker always sends 100
|
||||
adjusted.level = null
|
||||
setColor(adjusted)
|
||||
} else {
|
||||
log.warn "Invalid color input"
|
||||
}
|
||||
}
|
||||
|
||||
void setColorTemperature(value) {
|
||||
if (value) {
|
||||
log.trace "setColorTemperature: ${value}k"
|
||||
parent.setColorTemperature(this, value)
|
||||
sendEvent(name: "colorTemperature", value: value)
|
||||
sendEvent(name: "switch", value: "on")
|
||||
} else {
|
||||
log.warn "Invalid color temperature"
|
||||
}
|
||||
}
|
||||
|
||||
void refresh() {
|
||||
log.debug "Executing 'refresh'"
|
||||
parent.manualRefresh()
|
||||
}
|
||||
|
||||
def adjustOutgoingHue(percent) {
|
||||
def adjusted = percent
|
||||
if (percent > 31) {
|
||||
if (percent < 63.0) {
|
||||
adjusted = percent + (7 * (percent -30 ) / 32)
|
||||
}
|
||||
else if (percent < 73.0) {
|
||||
adjusted = 69 + (5 * (percent - 62) / 10)
|
||||
}
|
||||
else {
|
||||
adjusted = percent + (2 * (100 - percent) / 28)
|
||||
}
|
||||
}
|
||||
log.info "percent: $percent, adjusted: $adjusted"
|
||||
adjusted
|
||||
}
|
||||
|
||||
def verifyPercent(percent) {
|
||||
if (percent == null)
|
||||
return false
|
||||
else if (percent >= 0 && percent <= 100) {
|
||||
return true
|
||||
} else {
|
||||
log.warn "$percent is not 0-100"
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -17,16 +17,13 @@ metadata {
|
||||
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"rich-control"){
|
||||
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
|
||||
tileAttribute ("", key: "PRIMARY_CONTROL") {
|
||||
attributeState "default", label: "Hue Bridge", action: "", icon: "st.Lighting.light99-hue", backgroundColor: "#F3C200"
|
||||
}
|
||||
}
|
||||
tileAttribute ("serialNumber", key: "SECONDARY_CONTROL") {
|
||||
attributeState "default", label:'SN: ${currentValue}'
|
||||
}
|
||||
}
|
||||
standardTile("icon", "icon", width: 1, height: 1, canChangeIcon: false, inactiveLabel: true, canChangeBackground: false) {
|
||||
state "default", label: "Hue Bridge", action: "", icon: "st.Lighting.light99-hue", backgroundColor: "#FFFFFF"
|
||||
}
|
||||
}
|
||||
}
|
||||
valueTile("serialNumber", "device.serialNumber", decoration: "flat", height: 1, width: 2, inactiveLabel: false) {
|
||||
state "default", label:'SN: ${currentValue}'
|
||||
}
|
||||
@@ -34,7 +31,7 @@ metadata {
|
||||
state "default", label:'${currentValue}', height: 1, width: 2, inactiveLabel: false
|
||||
}
|
||||
|
||||
main (["icon"])
|
||||
main (["rich-control"])
|
||||
details(["rich-control", "networkAddress"])
|
||||
}
|
||||
}
|
||||
@@ -75,6 +72,7 @@ def parse(description) {
|
||||
}
|
||||
else if (contentType?.contains("xml")) {
|
||||
log.debug "HUE BRIDGE ALREADY PRESENT"
|
||||
parent.hubVerification(device.hub.id, msg.body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
/**
|
||||
* Hue Bulb
|
||||
*
|
||||
* Philips Hue Type "Extended Color Light"
|
||||
*
|
||||
* Author: SmartThings
|
||||
*/
|
||||
|
||||
// for the UI
|
||||
metadata {
|
||||
// Automatically generated. Make future change here.
|
||||
@@ -35,24 +38,15 @@ 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"
|
||||
}
|
||||
}
|
||||
|
||||
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||
state "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
state "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
state "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
state "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
}
|
||||
|
||||
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", width: 4, height: 2, inactiveLabel: false, range:"(2000..6500)") {
|
||||
state "colorTemperature", action:"color temperature.setColorTemperature"
|
||||
}
|
||||
|
||||
valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "colorTemperature", label: '${currentValue} K'
|
||||
}
|
||||
@@ -60,29 +54,12 @@ metadata {
|
||||
standardTile("reset", "device.reset", height: 2, width: 2, inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:"Reset Color", action:"reset", icon:"st.lights.philips.hue-single"
|
||||
}
|
||||
standardTile("refresh", "device.switch", height: 2, width: 2, inactiveLabel: false, decoration: "flat") {
|
||||
|
||||
standardTile("refresh", "device.refresh", height: 2, width: 2, inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false, range:"(0..100)") {
|
||||
state "level", action:"switch level.setLevel"
|
||||
}
|
||||
valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") {
|
||||
state "level", label: 'Level ${currentValue}%'
|
||||
}
|
||||
controlTile("saturationSliderControl", "device.saturation", "slider", height: 1, width: 2, inactiveLabel: false) {
|
||||
state "saturation", action:"color control.setSaturation"
|
||||
}
|
||||
valueTile("saturation", "device.saturation", inactiveLabel: false, decoration: "flat") {
|
||||
state "saturation", label: 'Sat ${currentValue} '
|
||||
}
|
||||
controlTile("hueSliderControl", "device.hue", "slider", height: 1, width: 2, inactiveLabel: false) {
|
||||
state "hue", action:"color control.setHue"
|
||||
}
|
||||
valueTile("hue", "device.hue", inactiveLabel: false, decoration: "flat") {
|
||||
state "hue", label: 'Hue ${currentValue} '
|
||||
}
|
||||
|
||||
main(["switch"])
|
||||
main(["rich-control"])
|
||||
details(["rich-control", "colorTempSliderControl", "colorTemp", "reset", "refresh"])
|
||||
}
|
||||
}
|
||||
@@ -91,11 +68,13 @@ metadata {
|
||||
def parse(description) {
|
||||
log.debug "parse() - $description"
|
||||
def results = []
|
||||
|
||||
def map = description
|
||||
if (description instanceof String) {
|
||||
log.debug "Hue Bulb stringToMap - ${map}"
|
||||
map = stringToMap(description)
|
||||
}
|
||||
|
||||
if (map?.name && map?.value) {
|
||||
results << createEvent(name: "${map?.name}", value: "${map?.value}")
|
||||
}
|
||||
@@ -125,62 +104,104 @@ void nextLevel() {
|
||||
}
|
||||
|
||||
void setLevel(percent) {
|
||||
log.debug "Executing 'setLevel'"
|
||||
parent.setLevel(this, percent)
|
||||
sendEvent(name: "level", value: percent)
|
||||
log.debug "Executing 'setLevel'"
|
||||
if (verifyPercent(percent)) {
|
||||
parent.setLevel(this, percent)
|
||||
sendEvent(name: "level", value: percent, descriptionText: "Level has changed to ${percent}%")
|
||||
sendEvent(name: "switch", value: "on")
|
||||
}
|
||||
}
|
||||
|
||||
void setSaturation(percent) {
|
||||
log.debug "Executing 'setSaturation'"
|
||||
parent.setSaturation(this, percent)
|
||||
sendEvent(name: "saturation", value: percent)
|
||||
log.debug "Executing 'setSaturation'"
|
||||
if (verifyPercent(percent)) {
|
||||
parent.setSaturation(this, percent)
|
||||
sendEvent(name: "saturation", value: percent, displayed: false)
|
||||
}
|
||||
}
|
||||
|
||||
void setHue(percent) {
|
||||
log.debug "Executing 'setHue'"
|
||||
parent.setHue(this, percent)
|
||||
sendEvent(name: "hue", value: percent)
|
||||
log.debug "Executing 'setHue'"
|
||||
if (verifyPercent(percent)) {
|
||||
parent.setHue(this, percent)
|
||||
sendEvent(name: "hue", value: percent, displayed: false)
|
||||
}
|
||||
}
|
||||
|
||||
void setColor(value) {
|
||||
log.debug "setColor: ${value}, $this"
|
||||
parent.setColor(this, value)
|
||||
if (value.hue) { sendEvent(name: "hue", value: value.hue)}
|
||||
if (value.saturation) { sendEvent(name: "saturation", value: value.saturation)}
|
||||
if (value.hex) { sendEvent(name: "color", value: value.hex)}
|
||||
if (value.level) { sendEvent(name: "level", value: value.level)}
|
||||
if (value.switch) { sendEvent(name: "switch", value: value.switch)}
|
||||
log.debug "setColor: ${value}, $this"
|
||||
def events = []
|
||||
def validValues = [:]
|
||||
|
||||
if (verifyPercent(value.hue)) {
|
||||
events << createEvent(name: "hue", value: value.hue, displayed: false)
|
||||
validValues.hue = value.hue
|
||||
}
|
||||
if (verifyPercent(value.saturation)) {
|
||||
events << createEvent(name: "saturation", value: value.saturation, displayed: false)
|
||||
validValues.saturation = value.saturation
|
||||
}
|
||||
if (value.hex != null) {
|
||||
if (value.hex ==~ /^\#([A-Fa-f0-9]){6}$/) {
|
||||
events << createEvent(name: "color", value: value.hex)
|
||||
validValues.hex = value.hex
|
||||
} else {
|
||||
log.warn "$value.hex is not a valid color"
|
||||
}
|
||||
}
|
||||
if (verifyPercent(value.level)) {
|
||||
events << createEvent(name: "level", value: value.level, descriptionText: "Level has changed to ${value.level}%")
|
||||
validValues.level = value.level
|
||||
}
|
||||
if (value.switch == "off" || (value.level != null && value.level <= 0)) {
|
||||
events << createEvent(name: "switch", value: "off")
|
||||
validValues.switch = "off"
|
||||
} else {
|
||||
events << createEvent(name: "switch", value: "on")
|
||||
validValues.switch = "on"
|
||||
}
|
||||
if (!events.isEmpty()) {
|
||||
parent.setColor(this, validValues)
|
||||
}
|
||||
events.each {
|
||||
sendEvent(it)
|
||||
}
|
||||
}
|
||||
|
||||
void reset() {
|
||||
log.debug "Executing 'reset'"
|
||||
def value = [level:100, hex:"#90C638", saturation:56, hue:23]
|
||||
log.debug "Executing 'reset'"
|
||||
def value = [level:100, saturation:56, hue:23]
|
||||
setAdjustedColor(value)
|
||||
parent.poll()
|
||||
parent.poll()
|
||||
}
|
||||
|
||||
void setAdjustedColor(value) {
|
||||
if (value) {
|
||||
if (value) {
|
||||
log.trace "setAdjustedColor: ${value}"
|
||||
def adjusted = value + [:]
|
||||
adjusted.hue = adjustOutgoingHue(value.hue)
|
||||
// Needed because color picker always sends 100
|
||||
adjusted.level = null
|
||||
setColor(adjusted)
|
||||
} else {
|
||||
log.warn "Invalid color input"
|
||||
}
|
||||
}
|
||||
|
||||
void setColorTemperature(value) {
|
||||
if (value) {
|
||||
if (value) {
|
||||
log.trace "setColorTemperature: ${value}k"
|
||||
parent.setColorTemperature(this, value)
|
||||
sendEvent(name: "colorTemperature", value: value)
|
||||
}
|
||||
sendEvent(name: "switch", value: "on")
|
||||
} else {
|
||||
log.warn "Invalid color temperature"
|
||||
}
|
||||
}
|
||||
|
||||
void refresh() {
|
||||
log.debug "Executing 'refresh'"
|
||||
parent.manualRefresh()
|
||||
log.debug "Executing 'refresh'"
|
||||
parent.manualRefresh()
|
||||
}
|
||||
|
||||
def adjustOutgoingHue(percent) {
|
||||
@@ -199,3 +220,14 @@ def adjustOutgoingHue(percent) {
|
||||
log.info "percent: $percent, adjusted: $adjusted"
|
||||
adjusted
|
||||
}
|
||||
|
||||
def verifyPercent(percent) {
|
||||
if (percent == null)
|
||||
return false
|
||||
else if (percent >= 0 && percent <= 100) {
|
||||
return true
|
||||
} else {
|
||||
log.warn "$percent is not 0-100"
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
/**
|
||||
* Hue Lux Bulb
|
||||
*
|
||||
* Philips Hue Type "Dimmable Light"
|
||||
*
|
||||
* Author: SmartThings
|
||||
*/
|
||||
// for the UI
|
||||
@@ -9,7 +11,6 @@ metadata {
|
||||
definition (name: "Hue Lux Bulb", namespace: "smartthings", author: "SmartThings") {
|
||||
capability "Switch Level"
|
||||
capability "Actuator"
|
||||
capability "Color Temperature"
|
||||
capability "Switch"
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
@@ -32,35 +33,18 @@ 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}%'
|
||||
}
|
||||
}
|
||||
|
||||
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||
state "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
state "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
state "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
state "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
}
|
||||
|
||||
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false, range:"(0..100)") {
|
||||
state "level", action:"switch level.setLevel"
|
||||
}
|
||||
|
||||
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.switch", inactiveLabel: false, decoration: "flat") {
|
||||
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
main(["switch"])
|
||||
details(["rich-control", "colorTempSliderControl", "colorTemp", "refresh"])
|
||||
main(["rich-control"])
|
||||
details(["rich-control", "refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,27 +67,24 @@ def parse(description) {
|
||||
|
||||
// handle commands
|
||||
void on() {
|
||||
parent.on(this)
|
||||
log.trace parent.on(this)
|
||||
sendEvent(name: "switch", value: "on")
|
||||
}
|
||||
|
||||
void off() {
|
||||
parent.off(this)
|
||||
log.trace parent.off(this)
|
||||
sendEvent(name: "switch", value: "off")
|
||||
}
|
||||
|
||||
void setLevel(percent) {
|
||||
log.debug "Executing 'setLevel'"
|
||||
parent.setLevel(this, percent)
|
||||
sendEvent(name: "level", value: percent)
|
||||
}
|
||||
|
||||
void setColorTemperature(value) {
|
||||
if (value) {
|
||||
log.trace "setColorTemperature: ${value}k"
|
||||
parent.setColorTemperature(this, value)
|
||||
sendEvent(name: "colorTemperature", value: value)
|
||||
}
|
||||
if (percent != null && percent >= 0 && percent <= 100) {
|
||||
parent.setLevel(this, percent)
|
||||
sendEvent(name: "level", value: percent)
|
||||
sendEvent(name: "switch", value: "on")
|
||||
} else {
|
||||
log.warn "$percent is not 0-100"
|
||||
}
|
||||
}
|
||||
|
||||
void refresh() {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#==============================================================================
|
||||
# Copyright 2016 SmartThings
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
@@ -12,20 +11,12 @@
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#==============================================================================
|
||||
# Purpose: Mobile Presence i18n Translation File
|
||||
#
|
||||
# Filename: mobile-presence.src/i18n/messages.properties
|
||||
#
|
||||
# Change History:
|
||||
# 1. 20160205 TW Initial release with informal Korean translation.
|
||||
#==============================================================================
|
||||
# Korean (ko)
|
||||
# Device Preferences
|
||||
'''Give your device a name'''.ko=기기 이름 바꾸기
|
||||
'''Set Device Image'''.ko=디바이스 이미지 설정
|
||||
'''Give your device a name'''.ko=기기 이름 설정
|
||||
'''Set Device Image'''.ko=기기 이미지 설정
|
||||
# Events / Notifications
|
||||
'''{{ linkText }} has left'''.ko={{ linkText }}님이 나갔습니다
|
||||
'''{{ linkText }} has arrived'''.ko={{ linkText }}님이 도착했습니다
|
||||
'''{{ linkText }} has left'''.ko={{ linkText }} 외출
|
||||
'''{{ linkText }} has arrived'''.ko={{ linkText }} 귀가
|
||||
'''present'''.ko=집안
|
||||
'''not present'''.ko=부재중
|
||||
'''not present'''.ko=외출
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
/**
|
||||
* Copyright 2015 SmartThings
|
||||
/*
|
||||
* Copyright 2016 SmartThings
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
* 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.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
metadata {
|
||||
definition (name: "Mobile Presence", namespace: "smartthings", author: "SmartThings") {
|
||||
capability "Presence Sensor"
|
||||
@@ -41,6 +44,7 @@ def parse(String description) {
|
||||
def isStateChange = isStateChange(device, name, value)
|
||||
|
||||
def results = [
|
||||
translatable: true,
|
||||
name: name,
|
||||
value: value,
|
||||
unit: null,
|
||||
@@ -72,8 +76,8 @@ private String parseValue(String description) {
|
||||
|
||||
private parseDescriptionText(String linkText, String value, String description) {
|
||||
switch(value) {
|
||||
case "present": return "$linkText has arrived"
|
||||
case "not present": return "$linkText has left"
|
||||
case "present": return "{{ linkText }} has arrived"
|
||||
case "not present": return "{{ linkText }} has left"
|
||||
default: return value
|
||||
}
|
||||
}
|
||||
@@ -84,4 +88,4 @@ private getState(String value) {
|
||||
case "not present": return "left"
|
||||
default: return value
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ metadata {
|
||||
command "enrollResponse"
|
||||
|
||||
|
||||
fingerprint inClusters: "0000,0001,0003,0500,0020", manufacturer: "NYCE", model: "3010", deviceJoinName: "NYCE Door Hinge Sensor"
|
||||
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3011", deviceJoinName: "NYCE Door/Window Sensor"
|
||||
fingerprint inClusters: "0000,0001,0003,0500,0020", manufacturer: "NYCE", model: "3011", deviceJoinName: "NYCE Door/Window Sensor"
|
||||
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3014", deviceJoinName: "NYCE Tilt Sensor"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# Copyright 2016 SmartThings
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
# use this file except in compliance with the License. You may obtain a copy
|
||||
# of the License at:
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
# Korean (ko)
|
||||
# Device Preferences
|
||||
'''Give your device a name'''.ko=기기 이름 설정
|
||||
'''Outlet'''.ko= 스마트 플러그
|
||||
# Events descriptionText
|
||||
'''{{ device.displayName }} is On'''.ko={{ device.displayName }} 켜짐
|
||||
'''{{ device.displayName }} is Off'''.ko={{ device.displayName }} 꺼짐
|
||||
'''{{ device.displayName }} power is {{ value }} Watts'''.ko={{ device.displayName }} 전력은 {{ value }}와트입니다.
|
||||
'''On'''.ko= 켜짐
|
||||
'''Off'''.ko=꺼짐
|
||||
'''Turning On'''.ko=켜는 중
|
||||
'''Turning Off'''.ko=끄는 중
|
||||
#==============================================================================
|
||||
@@ -1,32 +1,29 @@
|
||||
/**
|
||||
* Copyright 2015 SmartThings
|
||||
/*
|
||||
* Copyright 2016 SmartThings
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
* 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.
|
||||
*
|
||||
* SmartPower Outlet (CentraLite)
|
||||
*
|
||||
* Author: SmartThings
|
||||
* Date: 2015-08-23
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
metadata {
|
||||
// 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"
|
||||
capability "Configuration"
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
|
||||
// indicates that device keeps track of heartbeat (in state.heartbeat)
|
||||
attribute "heartbeat", "string"
|
||||
capability "Health Check"
|
||||
|
||||
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"
|
||||
@@ -58,10 +55,10 @@ metadata {
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){
|
||||
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
|
||||
attributeState "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821", nextState: "turningOff"
|
||||
attributeState "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff", nextState: "turningOn"
|
||||
attributeState "turningOn", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821", nextState: "turningOff"
|
||||
attributeState "turningOff", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff", nextState: "turningOn"
|
||||
attributeState "on", label: 'On', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821", nextState: "turningOff"
|
||||
attributeState "off", label: 'Off', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff", nextState: "turningOn"
|
||||
attributeState "turningOn", label: 'Turning On', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821", nextState: "turningOff"
|
||||
attributeState "turningOff", label: 'Turning Off', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff", nextState: "turningOn"
|
||||
}
|
||||
tileAttribute ("power", key: "SECONDARY_CONTROL") {
|
||||
attributeState "power", label:'${currentValue} W'
|
||||
@@ -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
|
||||
@@ -91,22 +85,22 @@ def parse(String description) {
|
||||
finalResult = getPowerDescription(zigbee.parseDescriptionAsMap(description))
|
||||
|
||||
if (finalResult) {
|
||||
log.info finalResult
|
||||
log.info "final result = $finalResult"
|
||||
if (finalResult.type == "update") {
|
||||
log.info "$device updates: ${finalResult.value}"
|
||||
}
|
||||
else if (finalResult.type == "power") {
|
||||
def powerValue = (finalResult.value as Integer)/10
|
||||
sendEvent(name: "power", value: powerValue)
|
||||
sendEvent(name: "power", value: powerValue, descriptionText: '{{ device.displayName }} power is {{ value }} Watts', translatable: true )
|
||||
/*
|
||||
Dividing by 10 as the Divisor is 10000 and unit is kW for the device. AttrId: 0302 and 0300. Simplifying to 10
|
||||
|
||||
power level is an integer. The exact power level with correct units needs to be handled in the device type
|
||||
to account for the different Divisor value (AttrId: 0302) and POWER Unit (AttrId: 0300). CLUSTER for simple metering is 0702
|
||||
*/
|
||||
}
|
||||
else {
|
||||
sendEvent(name: finalResult.type, value: finalResult.value)
|
||||
def descriptionText = finalResult.value == "on" ? '{{ device.displayName }} is On' : '{{ device.displayName }} is Off'
|
||||
sendEvent(name: finalResult.type, value: finalResult.value, descriptionText: descriptionText, translatable: true)
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -124,11 +118,11 @@ def on() {
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
sendEvent(name: "heartbeat", value: "alive", displayed:false)
|
||||
zigbee.onOffRefresh() + zigbee.refreshData("0x0B04", "0x050B")
|
||||
}
|
||||
|
||||
def configure() {
|
||||
sendEvent(name: "checkInterval", value: 1200, displayed: false)
|
||||
zigbee.onOffConfig() + powerConfig() + refresh()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,35 @@
|
||||
|
||||
# Generated on Wed Feb 24 14:28:26 CST 2016 by dylan
|
||||
'''Adjust temperature by this many degrees'''.ko=몇 도씩 온도를 조절하십시오
|
||||
'''Degrees'''.ko=온도
|
||||
'''Temperature Offset'''.ko=온도 직접 설정
|
||||
'''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'.'''.ko=기준 온도를 원하는대로 몇 도 올리거나 내려서 설정할 수 있습니다.
|
||||
'''battery'''.ko=배터리
|
||||
# Copyright 2016 SmartThings
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
# use this file except in compliance with the License. You may obtain a copy
|
||||
# of the License at:
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
# Korean (ko)
|
||||
# Device Preferences
|
||||
'''Dry'''.ko=건조
|
||||
'''Wet'''.ko=누수
|
||||
'''dry'''.ko=건조
|
||||
'''wet'''.ko=누수
|
||||
'''{{ device.displayName }} battery has too much power: (> 3.5) volts.'''.ko={{ device.displayName }} 배터리 전력이 너무 높습니다(3.5볼트 초과).
|
||||
'''{{ device.displayName }} battery was {{ value }}'''.ko={{ device.displayName }} 배터리가 {{ value }}였습니다
|
||||
'''{{ device.displayName }} is {{ value | translate }}'''.ko={{ device.displayName }}이(가) {{ value | translate }}입니다
|
||||
'''{{ device.displayName }} was {{ value }}°C'''.ko={{ device.displayName }}이(가) {{ value }}°C였습니다
|
||||
'''battery'''.ko=배터리
|
||||
'''Temperature Offset'''.ko=온도 직접 설정
|
||||
'''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'.'''.ko=기준 온도를 원하는대로 몇 도 올리거나 내려서 설정할 수 있습니다.
|
||||
'''Degrees'''.ko=온도
|
||||
'''Adjust temperature by this many degrees'''.ko=몇 도씩 온도를 조절하십시오
|
||||
'''Give your device a name'''.ko=기기 이름 설정
|
||||
'''Water Leak Sensor'''.ko=누수감지 센서
|
||||
'''${currentValue}% battery'''.ko=${currentValue}% 배터리
|
||||
# Events descriptionText
|
||||
'''{{ device.displayName }} is dry'''.ko={{ device.displayName }}가 건조
|
||||
'''{{ device.displayName }} is wet'''.ko={{ device.displayName }}누수
|
||||
'''{{ device.displayName }} was {{ value }}°C'''.ko={{ device.displayName }}에서 {{ value }}°C 감지
|
||||
'''{{ device.displayName }} was {{ value }}°F'''.ko={{ device.displayName }}이(가) {{ value }}°F였습니다
|
||||
'''{{ device.displayName }} battery has too much power: (> 3.5) volts.'''.ko={{ device.displayName }} 배터리 전력이 너무 높습니다(3.5볼트 초과).
|
||||
'''{{ device.displayName }} battery was {{ value }}%'''.ko={{ device.displayName }}의 남은 배터리 {{ value }}%
|
||||
#==============================================================================
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
/**
|
||||
* SmartSense Moisture Sensor
|
||||
/*
|
||||
* Copyright 2016 SmartThings
|
||||
*
|
||||
* Copyright 2014 SmartThings
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
* 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.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
metadata {
|
||||
definition (name: "SmartSense Moisture Sensor",namespace: "smartthings", author: "SmartThings") {
|
||||
definition (name: "SmartSense Moisture Sensor",namespace: "smartthings", author: "SmartThings", category: "C2") {
|
||||
capability "Configuration"
|
||||
capability "Battery"
|
||||
capability "Refresh"
|
||||
capability "Temperature Measurement"
|
||||
capability "Water Sensor"
|
||||
capability "Health Check"
|
||||
|
||||
command "enrollResponse"
|
||||
|
||||
@@ -43,7 +45,7 @@ metadata {
|
||||
])
|
||||
}
|
||||
section {
|
||||
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 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
|
||||
}
|
||||
}
|
||||
@@ -116,16 +118,16 @@ private Map parseCatchAllMessage(String description) {
|
||||
resultMap = getBatteryResult(cluster.data.last())
|
||||
break
|
||||
|
||||
case 0x0402:
|
||||
// temp is last 2 data values. reverse to swap endian
|
||||
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
|
||||
def value = getTemperature(temp)
|
||||
resultMap = getTemperatureResult(value)
|
||||
break
|
||||
}
|
||||
}
|
||||
case 0x0402:
|
||||
// temp is last 2 data values. reverse to swap endian
|
||||
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
|
||||
def value = getTemperature(temp)
|
||||
resultMap = getTemperatureResult(value)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return resultMap
|
||||
return resultMap
|
||||
}
|
||||
|
||||
private boolean shouldProcessMessage(cluster) {
|
||||
@@ -220,7 +222,8 @@ private Map getBatteryResult(rawValue) {
|
||||
|
||||
def result = [
|
||||
name: 'battery',
|
||||
value: '--'
|
||||
value: '--',
|
||||
translatable: true
|
||||
]
|
||||
|
||||
def volts = rawValue / 10
|
||||
@@ -228,7 +231,7 @@ private Map getBatteryResult(rawValue) {
|
||||
if (rawValue == 0 || rawValue == 255) {}
|
||||
else {
|
||||
if (volts > 3.5) {
|
||||
result.descriptionText = "${linkText} battery has too much power (${volts} volts)."
|
||||
result.descriptionText = "{{ device.displayName }} battery has too much power: (> 3.5) volts."
|
||||
}
|
||||
else {
|
||||
if (device.getDataValue("manufacturer") == "SmartThings") {
|
||||
@@ -245,7 +248,7 @@ private Map getBatteryResult(rawValue) {
|
||||
def pct = batteryMap[volts]
|
||||
if (pct != null) {
|
||||
result.value = pct
|
||||
result.descriptionText = "${linkText} battery was ${result.value}%"
|
||||
result.descriptionText = "{{ device.displayName }} battery was {{ value }}%"
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -253,7 +256,7 @@ private Map getBatteryResult(rawValue) {
|
||||
def maxVolts = 3.0
|
||||
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
||||
result.value = Math.min(100, (int) pct * 100)
|
||||
result.descriptionText = "${linkText} battery was ${result.value}%"
|
||||
result.descriptionText = "{{ device.displayName }} battery was {{ value }}%"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -263,27 +266,37 @@ private Map getBatteryResult(rawValue) {
|
||||
|
||||
private Map getTemperatureResult(value) {
|
||||
log.debug 'TEMP'
|
||||
def linkText = getLinkText(device)
|
||||
if (tempOffset) {
|
||||
def offset = tempOffset as int
|
||||
def v = value as int
|
||||
value = v + offset
|
||||
}
|
||||
def descriptionText = "${linkText} was ${value}°${temperatureScale}"
|
||||
def descriptionText
|
||||
if ( temperatureScale == 'C' )
|
||||
descriptionText = '{{ device.displayName }} was {{ value }}°C'
|
||||
else
|
||||
descriptionText = '{{ device.displayName }} was {{ value }}°F'
|
||||
|
||||
return [
|
||||
name: 'temperature',
|
||||
value: value,
|
||||
descriptionText: descriptionText
|
||||
descriptionText: descriptionText,
|
||||
translatable: true
|
||||
]
|
||||
}
|
||||
|
||||
private Map getMoistureResult(value) {
|
||||
log.debug 'water'
|
||||
String descriptionText = "${device.displayName} is ${value}"
|
||||
log.debug "water"
|
||||
def descriptionText
|
||||
if ( value == "wet" )
|
||||
descriptionText = '{{ device.displayName }} is wet'
|
||||
else
|
||||
descriptionText = '{{ device.displayName }} is dry'
|
||||
return [
|
||||
name: 'water',
|
||||
value: value,
|
||||
descriptionText: descriptionText
|
||||
descriptionText: descriptionText,
|
||||
translatable: true
|
||||
]
|
||||
}
|
||||
|
||||
@@ -298,6 +311,8 @@ 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."
|
||||
def configCmds = [
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
.st-ignore
|
||||
README.md
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,13 +1,33 @@
|
||||
|
||||
# Generated on Wed Feb 24 14:28:26 CST 2016 by dylan
|
||||
'''Adjust temperature by this many degrees'''.ko=몇 도씩 온도를 조절하십시오
|
||||
'''Degrees'''.ko=온도
|
||||
'''Temperature Offset'''.ko=온도 직접 설정
|
||||
'''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'.'''.ko=기준 온도를 원하는대로 몇 도 올리거나 내려서 설정할 수 있습니다.
|
||||
# Copyright 2016 SmartThings
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
# use this file except in compliance with the License. You may obtain a copy
|
||||
# of the License at:
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
# Korean (ko)
|
||||
# Device Preferences
|
||||
'''battery'''.ko=배터리
|
||||
'''{{ device.displayName }} battery has too much power: (> 3.5) volts.'''.ko={{ device.displayName }} 배터리 전력이 너무 높습니다(3.5볼트 초과).
|
||||
'''{{ device.displayName }} battery was {{ value }}'''.ko={{ device.displayName }} 배터리가 {{ value }}였습니다
|
||||
'''{{ device.displayName }} detected motion'''.ko={{ device.displayName }}가 움직임을 감지하였습니다.
|
||||
'''{{ device.displayName }} motion has stopped'''.ko={{ device.displayName }} 동작이 중단되었습니다
|
||||
'''{{ device.displayName }} was {{ value }}°C'''.ko={{ device.displayName }}이(가) {{ value }}°C였습니다
|
||||
'''Temperature Offset'''.ko=온도 직접 설정
|
||||
'''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'.'''.ko=기준 온도를 원하는대로 몇 도 올리거나 내려서 설정할 수 있습니다.
|
||||
'''Degrees'''.ko=온도
|
||||
'''Adjust temperature by this many degrees'''.ko=몇 도씩 온도를 조절하십시오
|
||||
'''Give your device a name'''.ko=기기 이름 설정
|
||||
'''Motion Sensor'''.ko=모션 센서
|
||||
'''motion'''.ko= 동작 감지
|
||||
'''no motion'''.ko=동작 없음
|
||||
'''${currentValue}% battery'''.ko=${currentValue}% 배터리
|
||||
# Events descriptionText
|
||||
'''{{ device.displayName }} detected motion'''.ko={{ device.displayName }}에서 움직임이 감지되었습니다.
|
||||
'''{{ device.displayName }} motion has stopped'''.ko={{ device.displayName }}에서 움직임이 중단되었습니다.
|
||||
'''{{ device.displayName }} was {{ value }}°C'''.ko={{ device.displayName }}에서 {{ value }}°C 감지
|
||||
'''{{ device.displayName }} was {{ value }}°F'''.ko={{ device.displayName }}이(가) {{ value }}°F였습니다
|
||||
'''{{ device.displayName }} battery has too much power: (> 3.5) volts.'''.ko={{ device.displayName }} 배터리 전력이 너무 높습니다(3.5볼트 초과).
|
||||
'''{{ device.displayName }} battery was {{ value }}%'''.ko={{ device.displayName }}의 남은 배터리 {{ value }}%
|
||||
#==============================================================================
|
||||
|
||||
@@ -1,26 +1,27 @@
|
||||
/**
|
||||
* SmartSense Motion/Temp Sensor
|
||||
/*
|
||||
* Copyright 2016 SmartThings
|
||||
*
|
||||
* Copyright 2014 SmartThings
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
* 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.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
metadata {
|
||||
definition (name: "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"
|
||||
capability "Temperature Measurement"
|
||||
capability "Refresh"
|
||||
capability "Health Check"
|
||||
|
||||
command "enrollResponse"
|
||||
|
||||
@@ -47,7 +48,7 @@ metadata {
|
||||
])
|
||||
}
|
||||
section {
|
||||
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 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
|
||||
}
|
||||
}
|
||||
@@ -236,7 +237,8 @@ private Map getBatteryResult(rawValue) {
|
||||
|
||||
def result = [
|
||||
name: 'battery',
|
||||
value: '--'
|
||||
value: '--',
|
||||
translatable: true
|
||||
]
|
||||
|
||||
def volts = rawValue / 10
|
||||
@@ -244,7 +246,7 @@ private Map getBatteryResult(rawValue) {
|
||||
if (rawValue == 0 || rawValue == 255) {}
|
||||
else {
|
||||
if (volts > 3.5) {
|
||||
result.descriptionText = "${linkText} battery has too much power (${volts} volts)."
|
||||
result.descriptionText = "{{ device.displayName }} battery has too much power: (> 3.5) volts."
|
||||
}
|
||||
else {
|
||||
if (device.getDataValue("manufacturer") == "SmartThings") {
|
||||
@@ -261,7 +263,8 @@ private Map getBatteryResult(rawValue) {
|
||||
def pct = batteryMap[volts]
|
||||
if (pct != null) {
|
||||
result.value = pct
|
||||
result.descriptionText = "${linkText} battery was ${result.value}%"
|
||||
def value = pct
|
||||
result.descriptionText = "{{ device.displayName }} battery was {{ value }}%"
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -269,7 +272,7 @@ private Map getBatteryResult(rawValue) {
|
||||
def maxVolts = 3.0
|
||||
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
||||
result.value = Math.min(100, (int) pct * 100)
|
||||
result.descriptionText = "${linkText} battery was ${result.value}%"
|
||||
result.descriptionText = "{{ device.displayName }} battery was {{ value }}%"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -279,28 +282,33 @@ private Map getBatteryResult(rawValue) {
|
||||
|
||||
private Map getTemperatureResult(value) {
|
||||
log.debug 'TEMP'
|
||||
def linkText = getLinkText(device)
|
||||
if (tempOffset) {
|
||||
def offset = tempOffset as int
|
||||
def v = value as int
|
||||
value = v + offset
|
||||
}
|
||||
def descriptionText = "${linkText} was ${value}°${temperatureScale}"
|
||||
def descriptionText
|
||||
if ( temperatureScale == 'C' )
|
||||
descriptionText = '{{ device.displayName }} was {{ value }}°C'
|
||||
else
|
||||
descriptionText = '{{ device.displayName }} was {{ value }}°F'
|
||||
|
||||
return [
|
||||
name: 'temperature',
|
||||
value: value,
|
||||
descriptionText: descriptionText
|
||||
descriptionText: descriptionText,
|
||||
translatable: true
|
||||
]
|
||||
}
|
||||
|
||||
private Map getMotionResult(value) {
|
||||
log.debug 'motion'
|
||||
String linkText = getLinkText(device)
|
||||
String descriptionText = value == 'active' ? "${linkText} detected motion" : "${linkText} motion has stopped"
|
||||
String descriptionText = value == 'active' ? "{{ device.displayName }} detected motion" : "{{ device.displayName }} motion has stopped"
|
||||
return [
|
||||
name: 'motion',
|
||||
value: value,
|
||||
descriptionText: descriptionText
|
||||
descriptionText: descriptionText,
|
||||
translatable: true
|
||||
]
|
||||
}
|
||||
|
||||
@@ -315,6 +323,8 @@ 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."
|
||||
|
||||
|
||||
@@ -1,20 +1,41 @@
|
||||
|
||||
# Generated on Wed Feb 24 14:28:26 CST 2016 by dylan
|
||||
'''Adjust temperature by this many degrees'''.ko=몇 도씩 온도를 조절하십시오
|
||||
'''Degrees'''.ko=온도
|
||||
'''Do you want to use this sensor on a garage door?'''.ko=차고 문의 센서 사용 설정하기
|
||||
# Copyright 2016 SmartThings
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
# use this file except in compliance with the License. You may obtain a copy
|
||||
# of the License at:
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
# Korean (ko)
|
||||
# Device Preferences
|
||||
'''Yes'''.ko=예
|
||||
'''No'''.ko=아니요
|
||||
'''Tap to set'''.ko=눌러서 설정
|
||||
'''battery'''.ko=배터리
|
||||
'''Temperature Offset'''.ko=온도 직접 설정
|
||||
'''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'.'''.ko=기준 온도를 원하는대로 몇 도 올리거나 내려서 설정할 수 있습니다.
|
||||
'''Degrees'''.ko=온도
|
||||
'''Adjust temperature by this many degrees'''.ko=몇 도씩 온도를 조절하십시오
|
||||
'''Do you want to use this sensor on a garage door?'''.ko=차고 문의 센서 사용 설정하기
|
||||
'''Tap to set'''.ko=눌러서 설정
|
||||
'''Give your device a name'''.ko=기기 이름 설정
|
||||
'''Multipurpose Sensor'''.ko=문 및 창 센서
|
||||
# Events descriptionText
|
||||
'''{{ device.displayName }} was opened'''.ko={{ device.displayName }}에서 열림이 감지되었습니다.
|
||||
'''{{ device.displayName }} was closed'''.ko={{ device.displayName }}에서 닫힘이 감지되었습니다.
|
||||
'''{{ device.displayName }} was active'''.ko={{ device.displayName }} 활성화
|
||||
'''{{ device.displayName }} was inactive'''.ko={{ device.displayName }} 비활성화
|
||||
'''{{ device.displayName }} was {{ value }}°C'''.ko={{ device.displayName }}에서 {{ value }}°C 감지
|
||||
'''{{ device.displayName }} was {{ value }}°F'''.ko={{ device.displayName }}이(가) {{ value }}°F였습니다
|
||||
'''{{ device.displayName }} battery was {{ value }}%'''.ko={{ device.displayName }}의 남은 배터리 {{ value }}%
|
||||
'''Updating device to garage sensor'''.ko=기기-차고 센서 업데이트 중
|
||||
'''Updating device to open/close sensor'''.ko=기기-열림/닫힘 센서 업데이트 중
|
||||
'''Yes'''.ko=예
|
||||
'''{{ device.displayName }} status was closed'''.ko={{ device.displayName }}은(는) 닫힌 상태입니다
|
||||
'''{{ device.displayName }} status was opened'''.ko={{ device.displayName }}은(는) 열린 상태입니다
|
||||
'''{{ device.displayName }} was active'''.ko={{ device.displayName }}이(가) 활성화되었습니다
|
||||
'''{{ device.displayName }} was closed'''.ko={{ device.displayName }}이(가) 닫혔습니다
|
||||
'''{{ device.displayName }} was inactive'''.ko={{ device.displayName }}이(가) 비활성화되었습니다
|
||||
'''{{ device.displayName }} was opened'''.ko={{ device.displayName }}이(가) 열렸습니다
|
||||
'''{{ device.displayName }} was {{ value }}°C'''.ko={{ device.displayName }}이(가) {{ value }}°C였습니다
|
||||
'''{{ device.displayName }} was {{ value }}°F'''.ko={{ device.displayName }}이(가) {{ value }}°F였습니다
|
||||
'''Inactive'''.ko=비활성 상태
|
||||
'''Active'''.ko=활성 상태
|
||||
'''Open'''.ko= 열림이 감지될 때
|
||||
'''Closed'''.ko=닫힘
|
||||
'''${currentValue}% battery'''.ko=${currentValue}% 배터리
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
/**
|
||||
* SmartSense Multi
|
||||
/*
|
||||
* Copyright 2016 SmartThings
|
||||
*
|
||||
* Copyright 2015 SmartThings
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
* 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.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
metadata {
|
||||
definition (name: "SmartSense Multi Sensor", namespace: "smartthings", author: "SmartThings") {
|
||||
definition (name: "SmartSense Multi Sensor", namespace: "smartthings", author: "SmartThings", category: "C2") {
|
||||
|
||||
capability "Three Axis"
|
||||
capability "Battery"
|
||||
@@ -25,6 +25,7 @@ metadata {
|
||||
capability "Acceleration Sensor"
|
||||
capability "Refresh"
|
||||
capability "Temperature Measurement"
|
||||
capability "Health Check"
|
||||
|
||||
command "enrollResponse"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3320"
|
||||
@@ -62,30 +63,30 @@ metadata {
|
||||
])
|
||||
}
|
||||
section {
|
||||
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 title: "Temperature Offset", description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter '-5'. If 3 degrees too cold, enter '+3'.", displayDuringSetup: false, type: "paragraph", element: "paragraph"
|
||||
input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
|
||||
}
|
||||
section {
|
||||
input("garageSensor", "enum", title: "Do you want to use this sensor on a garage door?", options: ["Yes", "No"], defaultValue: "No", required: false, displayDuringSetup: false)
|
||||
input("garageSensor", "enum", title: "Do you want to use this sensor on a garage door?", description: "Tap to set", options: ["Yes", "No"], defaultValue: "No", required: false, displayDuringSetup: false)
|
||||
}
|
||||
}
|
||||
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"status", type: "generic", width: 6, height: 4){
|
||||
tileAttribute ("device.status", 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"
|
||||
attributeState "open", label:'Open', icon:"st.contact.contact.open", backgroundColor:"#ffa81e"
|
||||
attributeState "closed", label:'Closed', icon:"st.contact.contact.closed", backgroundColor:"#79b821"
|
||||
attributeState "garage-open", label:'Open', icon:"st.doors.garage.garage-open", backgroundColor:"#ffa81e"
|
||||
attributeState "garage-closed", label:'Closed', icon:"st.doors.garage.garage-closed", backgroundColor:"#79b821"
|
||||
}
|
||||
}
|
||||
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")
|
||||
state("open", label:'Open', icon:"st.contact.contact.open", backgroundColor:"#ffa81e")
|
||||
state("closed", label:'Closed', icon:"st.contact.contact.closed", backgroundColor:"#79b821")
|
||||
}
|
||||
standardTile("acceleration", "device.acceleration", width: 2, height: 2) {
|
||||
state("active", label:'${name}', icon:"st.motion.acceleration.active", backgroundColor:"#53a7c0")
|
||||
state("inactive", label:'${name}', icon:"st.motion.acceleration.inactive", backgroundColor:"#ffffff")
|
||||
state("active", label:'Active', icon:"st.motion.acceleration.active", backgroundColor:"#53a7c0")
|
||||
state("inactive", label:'Inactive', icon:"st.motion.acceleration.inactive", backgroundColor:"#ffffff")
|
||||
}
|
||||
valueTile("temperature", "device.temperature", width: 2, height: 2) {
|
||||
state("temperature", label:'${currentValue}°',
|
||||
@@ -273,19 +274,19 @@ def updated() {
|
||||
if (garageSensor == "Yes") {
|
||||
def descriptionText = "Updating device to garage sensor"
|
||||
if (device.latestValue("status") == "open") {
|
||||
sendEvent(name: 'status', value: 'garage-open', descriptionText: descriptionText)
|
||||
sendEvent(name: 'status', value: 'garage-open', descriptionText: descriptionText, translatable: true)
|
||||
}
|
||||
else if (device.latestValue("status") == "closed") {
|
||||
sendEvent(name: 'status', value: 'garage-closed', descriptionText: descriptionText)
|
||||
sendEvent(name: 'status', value: 'garage-closed', descriptionText: descriptionText, translatable: true)
|
||||
}
|
||||
}
|
||||
else {
|
||||
def descriptionText = "Updating device to open/close sensor"
|
||||
if (device.latestValue("status") == "garage-open") {
|
||||
sendEvent(name: 'status', value: 'open', descriptionText: descriptionText)
|
||||
sendEvent(name: 'status', value: 'open', descriptionText: descriptionText, translatable: true)
|
||||
}
|
||||
else if (device.latestValue("status") == "garage-closed") {
|
||||
sendEvent(name: 'status', value: 'closed', descriptionText: descriptionText)
|
||||
sendEvent(name: 'status', value: 'closed', descriptionText: descriptionText, translatable: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -301,11 +302,11 @@ def getTemperature(value) {
|
||||
|
||||
private Map getBatteryResult(rawValue) {
|
||||
log.debug "Battery rawValue = ${rawValue}"
|
||||
def linkText = getLinkText(device)
|
||||
|
||||
def result = [
|
||||
name: 'battery',
|
||||
value: '--'
|
||||
value: '--',
|
||||
translatable: true
|
||||
]
|
||||
|
||||
def volts = rawValue / 10
|
||||
@@ -313,7 +314,7 @@ private Map getBatteryResult(rawValue) {
|
||||
if (rawValue == 0 || rawValue == 255) {}
|
||||
else {
|
||||
if (volts > 3.5) {
|
||||
result.descriptionText = "${linkText} battery has too much power (${volts} volts)."
|
||||
result.descriptionText = "{{ device.displayName }} battery has too much power: (> 3.5) volts."
|
||||
}
|
||||
else {
|
||||
if (device.getDataValue("manufacturer") == "SmartThings") {
|
||||
@@ -330,7 +331,7 @@ private Map getBatteryResult(rawValue) {
|
||||
def pct = batteryMap[volts]
|
||||
if (pct != null) {
|
||||
result.value = pct
|
||||
result.descriptionText = "${linkText} battery was ${result.value}%"
|
||||
result.descriptionText = "{{ device.displayName }} battery was {{ value }}%"
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -338,7 +339,7 @@ private Map getBatteryResult(rawValue) {
|
||||
def maxVolts = 3.0
|
||||
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
||||
result.value = Math.min(100, (int) pct * 100)
|
||||
result.descriptionText = "${linkText} battery was ${result.value}%"
|
||||
result.descriptionText = "{{ device.displayName }} battery was {{ value }}%"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -348,40 +349,50 @@ private Map getBatteryResult(rawValue) {
|
||||
|
||||
private Map getTemperatureResult(value) {
|
||||
log.debug "Temperature"
|
||||
def linkText = getLinkText(device)
|
||||
if (tempOffset) {
|
||||
def offset = tempOffset as int
|
||||
def v = value as int
|
||||
value = v + offset
|
||||
}
|
||||
def descriptionText = "${linkText} was ${value}°${temperatureScale}"
|
||||
def descriptionText = temperatureScale == 'C' ? '{{ device.displayName }} was {{ value }}°C':
|
||||
'{{ device.displayName }} was {{ value }}°F'
|
||||
|
||||
return [
|
||||
name: 'temperature',
|
||||
value: value,
|
||||
descriptionText: descriptionText
|
||||
descriptionText: descriptionText,
|
||||
translatable: true
|
||||
]
|
||||
}
|
||||
|
||||
private Map getContactResult(value) {
|
||||
log.debug "Contact"
|
||||
def linkText = getLinkText(device)
|
||||
def descriptionText = "${linkText} was ${value == 'open' ? 'opened' : 'closed'}"
|
||||
sendEvent(name: 'contact', value: value, descriptionText: descriptionText, displayed:false)
|
||||
sendEvent(name: 'status', value: value, descriptionText: descriptionText)
|
||||
log.debug "Contact: ${device.displayName} value = ${value}"
|
||||
def descriptionText = value == 'open' ? '{{ device.displayName }} was opened' : '{{ device.displayName }} was closed'
|
||||
sendEvent(name: 'contact', value: value, descriptionText: descriptionText, displayed: false, translatable: true)
|
||||
sendEvent(name: 'status', value: value, descriptionText: descriptionText, translatable: true)
|
||||
}
|
||||
|
||||
private getAccelerationResult(numValue) {
|
||||
log.debug "Acceleration"
|
||||
def name = "acceleration"
|
||||
def value = numValue.endsWith("1") ? "active" : "inactive"
|
||||
def linkText = getLinkText(device)
|
||||
def descriptionText = "$linkText was $value"
|
||||
def value
|
||||
def descriptionText
|
||||
|
||||
if ( numValue.endsWith("1") ) {
|
||||
value = "active"
|
||||
descriptionText = '{{ device.displayName }} was active'
|
||||
} else {
|
||||
value = "inactive"
|
||||
descriptionText = '{{ device.displayName }} was inactive'
|
||||
}
|
||||
|
||||
def isStateChange = isStateChange(device, name, value)
|
||||
[
|
||||
return [
|
||||
name: name,
|
||||
value: value,
|
||||
descriptionText: descriptionText,
|
||||
isStateChange: isStateChange
|
||||
isStateChange: isStateChange,
|
||||
translatable: true
|
||||
]
|
||||
}
|
||||
|
||||
@@ -430,6 +441,8 @@ def refresh() {
|
||||
}
|
||||
|
||||
def configure() {
|
||||
sendEvent(name: "checkInterval", value: 7200, displayed: false)
|
||||
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
log.debug "Configuring Reporting"
|
||||
|
||||
@@ -451,15 +464,15 @@ def configure() {
|
||||
"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 {01}", "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 {01}", "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 {01}", "delay 200",
|
||||
"zcl global send-me-a-report 0xFC02 0x0014 0x29 1 3600 {0100}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500"
|
||||
]
|
||||
|
||||
@@ -528,10 +541,9 @@ def garageEvent(zValue) {
|
||||
garageValue = 'garage-open'
|
||||
}
|
||||
if (contactValue != null){
|
||||
def linkText = getLinkText(device)
|
||||
def descriptionText = "${linkText} was ${contactValue == 'open' ? 'opened' : 'closed'}"
|
||||
sendEvent(name: 'contact', value: contactValue, descriptionText: descriptionText, displayed:false)
|
||||
sendEvent(name: 'status', value: garageValue, descriptionText: descriptionText)
|
||||
def descriptionText = contactValue == 'open' ? '{{ device.displayName }} was opened' :'{{ device.displayName }} was closed'
|
||||
sendEvent(name: 'contact', value: contactValue, descriptionText: descriptionText, displayed:false, translatable: true)
|
||||
sendEvent(name: 'status', value: garageValue, descriptionText: descriptionText, translatable: true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@ metadata {
|
||||
capability "Battery"
|
||||
|
||||
fingerprint profileId: "FC01", deviceId: "0139"
|
||||
|
||||
attribute "status", "string"
|
||||
}
|
||||
|
||||
simulator {
|
||||
@@ -99,7 +101,7 @@ def parse(String description) {
|
||||
|
||||
}
|
||||
|
||||
private Map parseSingleMessage(description) {
|
||||
private List parseSingleMessage(description) {
|
||||
|
||||
def name = parseName(description)
|
||||
def value = parseValue(description)
|
||||
@@ -108,8 +110,9 @@ private Map parseSingleMessage(description) {
|
||||
def handlerName = value == 'open' ? 'opened' : value
|
||||
def isStateChange = isStateChange(device, name, value)
|
||||
|
||||
def results = [
|
||||
name: name,
|
||||
def results = []
|
||||
results << createEvent(
|
||||
name: "contact",
|
||||
value: value,
|
||||
unit: null,
|
||||
linkText: linkText,
|
||||
@@ -117,8 +120,18 @@ private Map parseSingleMessage(description) {
|
||||
handlerName: handlerName,
|
||||
isStateChange: isStateChange,
|
||||
displayed: displayed(description, isStateChange)
|
||||
]
|
||||
log.debug "Parse results for $device: $results"
|
||||
)
|
||||
|
||||
results << createEvent(
|
||||
name: "status",
|
||||
value: value,
|
||||
unit: null,
|
||||
linkText: linkText,
|
||||
descriptionText: descriptionText,
|
||||
handlerName: handlerName,
|
||||
isStateChange: isStateChange,
|
||||
displayed: displayed(description, isStateChange)
|
||||
)
|
||||
|
||||
results
|
||||
}
|
||||
@@ -190,7 +203,7 @@ private List parseContactMessage(String description) {
|
||||
parts.each { part ->
|
||||
part = part.trim()
|
||||
if (part.startsWith('contactState:')) {
|
||||
results << getContactResult(part, description)
|
||||
results.addAll(getContactResult(part, description))
|
||||
}
|
||||
else if (part.startsWith('accelerationState:')) {
|
||||
results << getAccelerationResult(part, description)
|
||||
@@ -269,7 +282,7 @@ private List parseRssiLqiMessage(String description) {
|
||||
results
|
||||
}
|
||||
|
||||
private getContactResult(part, description) {
|
||||
private List getContactResult(part, description) {
|
||||
def name = "contact"
|
||||
def value = part.endsWith("1") ? "open" : "closed"
|
||||
def handlerName = value == 'open' ? 'opened' : value
|
||||
@@ -277,19 +290,33 @@ private getContactResult(part, description) {
|
||||
def descriptionText = "$linkText was $handlerName"
|
||||
def isStateChange = isStateChange(device, name, value)
|
||||
|
||||
[
|
||||
name: name,
|
||||
value: value,
|
||||
unit: null,
|
||||
linkText: linkText,
|
||||
descriptionText: descriptionText,
|
||||
handlerName: handlerName,
|
||||
isStateChange: isStateChange,
|
||||
displayed: displayed(description, isStateChange)
|
||||
]
|
||||
def results = []
|
||||
results << createEvent(
|
||||
name: "contact",
|
||||
value: value,
|
||||
unit: null,
|
||||
linkText: linkText,
|
||||
descriptionText: descriptionText,
|
||||
handlerName: handlerName,
|
||||
isStateChange: isStateChange,
|
||||
displayed:false
|
||||
)
|
||||
|
||||
results << createEvent(
|
||||
name: "status",
|
||||
value: value,
|
||||
unit: null,
|
||||
linkText: linkText,
|
||||
descriptionText: descriptionText,
|
||||
handlerName: handlerName,
|
||||
isStateChange: isStateChange,
|
||||
displayed: displayed(description, isStateChange)
|
||||
)
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
private getAccelerationResult(part, description) {
|
||||
private Map getAccelerationResult(part, description) {
|
||||
def name = "acceleration"
|
||||
def value = part.endsWith("1") ? "active" : "inactive"
|
||||
def linkText = getLinkText(device)
|
||||
@@ -308,7 +335,7 @@ private getAccelerationResult(part, description) {
|
||||
]
|
||||
}
|
||||
|
||||
private getTempResult(part, description) {
|
||||
private Map getTempResult(part, description) {
|
||||
def name = "temperature"
|
||||
def temperatureScale = getTemperatureScale()
|
||||
def value = zigbee.parseSmartThingsTemperatureValue(part, "temp: ", temperatureScale)
|
||||
@@ -333,7 +360,7 @@ private getTempResult(part, description) {
|
||||
]
|
||||
}
|
||||
|
||||
private getXyzResult(results, description) {
|
||||
private Map getXyzResult(results, description) {
|
||||
def name = "threeAxis"
|
||||
def value = "${results.x},${results.y},${results.z}"
|
||||
def linkText = getLinkText(device)
|
||||
@@ -352,7 +379,7 @@ private getXyzResult(results, description) {
|
||||
]
|
||||
}
|
||||
|
||||
private getBatteryResult(part, description) {
|
||||
private Map getBatteryResult(part, description) {
|
||||
def batteryDivisor = description.split(",").find {it.split(":")[0].trim() == "batteryDivisor"} ? description.split(",").find {it.split(":")[0].trim() == "batteryDivisor"}.split(":")[1].trim() : null
|
||||
def name = "battery"
|
||||
def value = zigbee.parseSmartThingsBatteryValue(part, batteryDivisor)
|
||||
@@ -373,7 +400,7 @@ private getBatteryResult(part, description) {
|
||||
]
|
||||
}
|
||||
|
||||
private getRssiResult(part, description, lastHop=false) {
|
||||
private Map getRssiResult(part, description, lastHop=false) {
|
||||
def name = lastHop ? "lastHopRssi" : "rssi"
|
||||
def valueString = part.split(":")[1].trim()
|
||||
def value = (Integer.parseInt(valueString) - 128).toString()
|
||||
@@ -404,7 +431,7 @@ private getRssiResult(part, description, lastHop=false) {
|
||||
* Note: To make the signal strength indicator more accurate, we could combine
|
||||
* LQI with RSSI.
|
||||
*/
|
||||
private getLqiResult(part, description, lastHop=false) {
|
||||
private Map getLqiResult(part, description, lastHop=false) {
|
||||
def name = lastHop ? "lastHopLqi" : "lqi"
|
||||
def valueString = part.split(":")[1].trim()
|
||||
def percentageOf = 255
|
||||
@@ -449,6 +476,7 @@ private Boolean isOrientationMessage(String description) {
|
||||
description ==~ /x:.*y:.*z:.*rssi:.*lqi:.*/
|
||||
}
|
||||
|
||||
//Note: Not using this method anymore
|
||||
private String parseName(String description) {
|
||||
if (isSupportedDescription(description)) {
|
||||
return "contact"
|
||||
|
||||
@@ -16,15 +16,16 @@
|
||||
//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"
|
||||
capability "Acceleration Sensor"
|
||||
capability "Refresh"
|
||||
capability "Temperature Measurement"
|
||||
command "enrollResponse"
|
||||
capability "Health Check"
|
||||
|
||||
command "enrollResponse"
|
||||
}
|
||||
|
||||
simulator {
|
||||
@@ -299,6 +300,7 @@ def getTemperature(value) {
|
||||
}
|
||||
|
||||
def configure() {
|
||||
sendEvent(name: "checkInterval", value: 7200, displayed: false)
|
||||
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
||||
|
||||
@@ -14,12 +14,13 @@
|
||||
*
|
||||
*/
|
||||
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"
|
||||
capability "Temperature Measurement"
|
||||
capability "Relative Humidity Measurement"
|
||||
capability "Health Check"
|
||||
|
||||
fingerprint endpointId: "01", inClusters: "0001,0003,0020,0402,0B05,FC45", outClusters: "0019,0003"
|
||||
}
|
||||
@@ -251,6 +252,7 @@ def refresh()
|
||||
}
|
||||
|
||||
def configure() {
|
||||
sendEvent(name: "checkInterval", value: 7200, displayed: false)
|
||||
|
||||
log.debug "Configuring Reporting and Bindings."
|
||||
def configCmds = [
|
||||
|
||||
@@ -32,14 +32,15 @@ metadata {
|
||||
attributeState("default", label:'${currentValue}', unit:"dF")
|
||||
}
|
||||
tileAttribute("device.temperature", key: "VALUE_CONTROL") {
|
||||
attributeState("default", action: "setTemperature")
|
||||
attributeState("VALUE_UP", action: "tempUp")
|
||||
attributeState("VALUE_DOWN", action: "tempDown")
|
||||
}
|
||||
tileAttribute("device.humidity", key: "SECONDARY_CONTROL") {
|
||||
attributeState("default", label:'${currentValue}%', unit:"%")
|
||||
}
|
||||
tileAttribute("device.thermostatOperatingState", key: "OPERATING_STATE") {
|
||||
attributeState("idle", backgroundColor:"#44b621")
|
||||
attributeState("heating", backgroundColor:"#ffa81e")
|
||||
attributeState("heating", backgroundColor:"#ea5462")
|
||||
attributeState("cooling", backgroundColor:"#269bd2")
|
||||
}
|
||||
tileAttribute("device.thermostatMode", key: "THERMOSTAT_MODE") {
|
||||
|
||||
42
devicetypes/smartthings/tile-ux/README.md
Normal file
42
devicetypes/smartthings/tile-ux/README.md
Normal 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.
|
||||
@@ -0,0 +1,225 @@
|
||||
/**
|
||||
* Copyright 2016 SmartThings, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (
|
||||
name: "carouselDeviceTile",
|
||||
namespace: "smartthings/tile-ux",
|
||||
author: "SmartThings") {
|
||||
|
||||
capability "Thermostat"
|
||||
capability "Relative Humidity Measurement"
|
||||
|
||||
command "tempUp"
|
||||
command "tempDown"
|
||||
command "heatUp"
|
||||
command "heatDown"
|
||||
command "coolUp"
|
||||
command "coolDown"
|
||||
command "setTemperature", ["number"]
|
||||
}
|
||||
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"thermostatMulti", type:"thermostat", width:6, height:4) {
|
||||
tileAttribute("device.temperature", key: "PRIMARY_CONTROL") {
|
||||
attributeState("default", label:'${currentValue}', unit:"dF")
|
||||
}
|
||||
tileAttribute("device.temperature", key: "VALUE_CONTROL") {
|
||||
attributeState("default", action: "setTemperature")
|
||||
}
|
||||
tileAttribute("device.humidity", key: "SECONDARY_CONTROL") {
|
||||
attributeState("default", label:'${currentValue}%', unit:"%")
|
||||
}
|
||||
tileAttribute("device.thermostatOperatingState", key: "OPERATING_STATE") {
|
||||
attributeState("idle", backgroundColor:"#44b621")
|
||||
attributeState("heating", backgroundColor:"#ffa81e")
|
||||
attributeState("cooling", backgroundColor:"#269bd2")
|
||||
}
|
||||
tileAttribute("device.thermostatMode", key: "THERMOSTAT_MODE") {
|
||||
attributeState("off", label:'${name}')
|
||||
attributeState("heat", label:'${name}')
|
||||
attributeState("cool", label:'${name}')
|
||||
attributeState("auto", label:'${name}')
|
||||
}
|
||||
tileAttribute("device.heatingSetpoint", key: "HEATING_SETPOINT") {
|
||||
attributeState("default", label:'${currentValue}', unit:"dF")
|
||||
}
|
||||
tileAttribute("device.coolingSetpoint", key: "COOLING_SETPOINT") {
|
||||
attributeState("default", label:'${currentValue}', unit:"dF")
|
||||
}
|
||||
}
|
||||
|
||||
main("thermostatMulti")
|
||||
details([
|
||||
"thermostatMulti"
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
sendEvent(name: "temperature", value: 72, unit: "F")
|
||||
sendEvent(name: "heatingSetpoint", value: 70, unit: "F")
|
||||
sendEvent(name: "thermostatSetpoint", value: 70, unit: "F")
|
||||
sendEvent(name: "coolingSetpoint", value: 76, unit: "F")
|
||||
sendEvent(name: "thermostatMode", value: "off")
|
||||
sendEvent(name: "thermostatFanMode", value: "fanAuto")
|
||||
sendEvent(name: "thermostatOperatingState", value: "idle")
|
||||
sendEvent(name: "humidity", value: 53, unit: "%")
|
||||
}
|
||||
|
||||
def parse(String description) {
|
||||
}
|
||||
|
||||
def evaluate(temp, heatingSetpoint, coolingSetpoint) {
|
||||
log.debug "evaluate($temp, $heatingSetpoint, $coolingSetpoint"
|
||||
def threshold = 1.0
|
||||
def current = device.currentValue("thermostatOperatingState")
|
||||
def mode = device.currentValue("thermostatMode")
|
||||
|
||||
def heating = false
|
||||
def cooling = false
|
||||
def idle = false
|
||||
if (mode in ["heat","emergency heat","auto"]) {
|
||||
if (heatingSetpoint - temp >= threshold) {
|
||||
heating = true
|
||||
sendEvent(name: "thermostatOperatingState", value: "heating")
|
||||
}
|
||||
else if (temp - heatingSetpoint >= threshold) {
|
||||
idle = true
|
||||
}
|
||||
sendEvent(name: "thermostatSetpoint", value: heatingSetpoint)
|
||||
}
|
||||
if (mode in ["cool","auto"]) {
|
||||
if (temp - coolingSetpoint >= threshold) {
|
||||
cooling = true
|
||||
sendEvent(name: "thermostatOperatingState", value: "cooling")
|
||||
}
|
||||
else if (coolingSetpoint - temp >= threshold && !heating) {
|
||||
idle = true
|
||||
}
|
||||
sendEvent(name: "thermostatSetpoint", value: coolingSetpoint)
|
||||
}
|
||||
else {
|
||||
sendEvent(name: "thermostatSetpoint", value: heatingSetpoint)
|
||||
}
|
||||
if (idle && !heating && !cooling) {
|
||||
sendEvent(name: "thermostatOperatingState", value: "idle")
|
||||
}
|
||||
}
|
||||
|
||||
def setHeatingSetpoint(Double degreesF) {
|
||||
log.debug "setHeatingSetpoint($degreesF)"
|
||||
sendEvent(name: "heatingSetpoint", value: degreesF)
|
||||
evaluate(device.currentValue("temperature"), degreesF, device.currentValue("coolingSetpoint"))
|
||||
}
|
||||
|
||||
def setCoolingSetpoint(Double degreesF) {
|
||||
log.debug "setCoolingSetpoint($degreesF)"
|
||||
sendEvent(name: "coolingSetpoint", value: degreesF)
|
||||
evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), degreesF)
|
||||
}
|
||||
|
||||
def setThermostatMode(String value) {
|
||||
sendEvent(name: "thermostatMode", value: value)
|
||||
evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint"))
|
||||
}
|
||||
|
||||
def setThermostatFanMode(String value) {
|
||||
sendEvent(name: "thermostatFanMode", value: value)
|
||||
}
|
||||
|
||||
def off() {
|
||||
sendEvent(name: "thermostatMode", value: "off")
|
||||
evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint"))
|
||||
}
|
||||
|
||||
def heat() {
|
||||
sendEvent(name: "thermostatMode", value: "heat")
|
||||
evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint"))
|
||||
}
|
||||
|
||||
def auto() {
|
||||
sendEvent(name: "thermostatMode", value: "auto")
|
||||
evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint"))
|
||||
}
|
||||
|
||||
def emergencyHeat() {
|
||||
sendEvent(name: "thermostatMode", value: "emergency heat")
|
||||
evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint"))
|
||||
}
|
||||
|
||||
def cool() {
|
||||
sendEvent(name: "thermostatMode", value: "cool")
|
||||
evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint"))
|
||||
}
|
||||
|
||||
def fanOn() {
|
||||
sendEvent(name: "thermostatFanMode", value: "fanOn")
|
||||
}
|
||||
|
||||
def fanAuto() {
|
||||
sendEvent(name: "thermostatFanMode", value: "fanAuto")
|
||||
}
|
||||
|
||||
def fanCirculate() {
|
||||
sendEvent(name: "thermostatFanMode", value: "fanCirculate")
|
||||
}
|
||||
|
||||
def tempUp() {
|
||||
def ts = device.currentState("temperature")
|
||||
def value = ts ? ts.integerValue + 1 : 72
|
||||
sendEvent(name:"temperature", value: value)
|
||||
evaluate(value, device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint"))
|
||||
}
|
||||
|
||||
def tempDown() {
|
||||
def ts = device.currentState("temperature")
|
||||
def value = ts ? ts.integerValue - 1 : 72
|
||||
sendEvent(name:"temperature", value: value)
|
||||
evaluate(value, device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint"))
|
||||
}
|
||||
|
||||
def setTemperature(value) {
|
||||
def ts = device.currentState("temperature")
|
||||
sendEvent(name:"temperature", value: value)
|
||||
evaluate(value, device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint"))
|
||||
}
|
||||
|
||||
def heatUp() {
|
||||
def ts = device.currentState("heatingSetpoint")
|
||||
def value = ts ? ts.integerValue + 1 : 68
|
||||
sendEvent(name:"heatingSetpoint", value: value)
|
||||
evaluate(device.currentValue("temperature"), value, device.currentValue("coolingSetpoint"))
|
||||
}
|
||||
|
||||
def heatDown() {
|
||||
def ts = device.currentState("heatingSetpoint")
|
||||
def value = ts ? ts.integerValue - 1 : 68
|
||||
sendEvent(name:"heatingSetpoint", value: value)
|
||||
evaluate(device.currentValue("temperature"), value, device.currentValue("coolingSetpoint"))
|
||||
}
|
||||
|
||||
|
||||
def coolUp() {
|
||||
def ts = device.currentState("coolingSetpoint")
|
||||
def value = ts ? ts.integerValue + 1 : 76
|
||||
sendEvent(name:"coolingSetpoint", value: value)
|
||||
evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), value)
|
||||
}
|
||||
|
||||
def coolDown() {
|
||||
def ts = device.currentState("coolingSetpoint")
|
||||
def value = ts ? ts.integerValue - 1 : 76
|
||||
sendEvent(name:"coolingSetpoint", value: value)
|
||||
evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), value)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Copyright 2016 SmartThings, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (
|
||||
name: "colorWheelDeviceTile",
|
||||
namespace: "smartthings/tile-ux",
|
||||
author: "SmartThings") {
|
||||
|
||||
capability "Color Control"
|
||||
}
|
||||
|
||||
tiles(scale: 2) {
|
||||
valueTile("currentColor", "device.color") {
|
||||
state "color", label: '${currentValue}', defaultState: true
|
||||
}
|
||||
|
||||
controlTile("rgbSelector", "device.color", "color", height: 6, width: 6, inactiveLabel: false) {
|
||||
state "color", action: "color control.setColor"
|
||||
}
|
||||
|
||||
main("currentColor")
|
||||
details([
|
||||
"rgbSelector"
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
// parse events into attributes
|
||||
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)
|
||||
}
|
||||
|
||||
def setHue(percent) {
|
||||
log.debug "Executing 'setHue'"
|
||||
sendEvent(name: "hue", value: percent)
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Copyright 2016 SmartThings, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (
|
||||
name: "presenceDeviceTile",
|
||||
namespace: "smartthings/tile-ux",
|
||||
author: "SmartThings") {
|
||||
|
||||
capability "Presence Sensor"
|
||||
|
||||
command "arrived"
|
||||
command "departed"
|
||||
}
|
||||
|
||||
tiles(scale: 2) {
|
||||
// You only get a presence tile view when the size is 3x3 otherwise it's a value tile
|
||||
standardTile("presence", "device.presence", width: 3, height: 3, canChangeBackground: true) {
|
||||
state("present", labelIcon:"st.presence.tile.mobile-present", backgroundColor:"#53a7c0")
|
||||
state("not present", labelIcon:"st.presence.tile.mobile-not-present", backgroundColor:"#ebeef2")
|
||||
}
|
||||
|
||||
standardTile("notPresentBtn", "device.fake", width: 3, height: 3, decoration: "flat") {
|
||||
state("not present", label:'not present', backgroundColor:"#ffffff", action:"departed")
|
||||
}
|
||||
|
||||
standardTile("presentBtn", "device.fake", width: 3, height: 3, decoration: "flat") {
|
||||
state("present", label:'present', backgroundColor:"#53a7c0", action:"arrived")
|
||||
}
|
||||
|
||||
main("presence")
|
||||
details([
|
||||
"presence", "presenceControl", "notPresentBtn", "presentBtn"
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
sendEvent(name: "presence", value: "present")
|
||||
}
|
||||
|
||||
def parse(String description) {
|
||||
}
|
||||
|
||||
def arrived() {
|
||||
log.trace "Executing 'arrived'"
|
||||
sendEvent(name: "presence", value: "present")
|
||||
}
|
||||
|
||||
def departed() {
|
||||
log.trace "Executing 'arrived'"
|
||||
sendEvent(name: "presence", value: "not present")
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Copyright 2016 SmartThings, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (
|
||||
name: "sliderDeviceTile",
|
||||
namespace: "smartthings/tile-ux",
|
||||
author: "SmartThings") {
|
||||
|
||||
capability "Switch Level"
|
||||
command "setRangedLevel", ["number"]
|
||||
}
|
||||
|
||||
tiles(scale: 2) {
|
||||
controlTile("tinySlider", "device.level", "slider", height: 2, width: 2, inactiveLabel: false) {
|
||||
state "level", action:"switch level.setLevel"
|
||||
}
|
||||
|
||||
controlTile("mediumSlider", "device.level", "slider", height: 2, width: 4, inactiveLabel: false) {
|
||||
state "level", action:"switch level.setLevel"
|
||||
}
|
||||
|
||||
controlTile("largeSlider", "device.level", "slider", decoration: "flat", height: 2, width: 6, inactiveLabel: false) {
|
||||
state "level", action:"switch level.setLevel"
|
||||
}
|
||||
|
||||
controlTile("rangeSlider", "device.rangedLevel", "slider", height: 2, width: 4, range: "(20..80)") {
|
||||
state "level", action:"setRangedLevel"
|
||||
}
|
||||
|
||||
valueTile("rangeValue", "device.rangedLevel", height: 2, width: 2) {
|
||||
state "range", label:'${currentValue}', defaultState: true
|
||||
}
|
||||
|
||||
controlTile("rangeSliderConstrained", "device.rangedLevel", "slider", height: 2, width: 4, range: "(40..60)") {
|
||||
state "level", action:"setRangedLevel"
|
||||
}
|
||||
|
||||
main("rangeValue")
|
||||
details([
|
||||
"tinySlider", "mediumSlider",
|
||||
"largeSlider",
|
||||
"rangeSlider", "rangeValue",
|
||||
"rangeSliderConstrained"
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
sendEvent(name: "level", value: 63)
|
||||
sendEvent(name: "rangedLevel", value: 47)
|
||||
}
|
||||
|
||||
def parse(String description) {
|
||||
}
|
||||
|
||||
def setLevel(value) {
|
||||
log.debug "setting level to $value"
|
||||
sendEvent(name:"level", value:value)
|
||||
}
|
||||
|
||||
def setRangedLevel(value) {
|
||||
log.debug "setting ranged level to $value"
|
||||
sendEvent(name:"rangedLevel", value:value)
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* Copyright 2016 SmartThings, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (
|
||||
name: "standardDeviceTile",
|
||||
namespace: "smartthings/tile-ux",
|
||||
author: "SmartThings") {
|
||||
|
||||
capability "Switch"
|
||||
}
|
||||
|
||||
tiles(scale: 2) {
|
||||
// standard tile with actions
|
||||
standardTile("actionRings", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||
state "off", label: '${currentValue}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff"
|
||||
state "on", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821"
|
||||
}
|
||||
|
||||
// standard flat tile with actions
|
||||
standardTile("actionFlat", "device.switch", width: 2, height: 2, canChangeIcon: true, decoration: "flat") {
|
||||
state "off", label: '${currentValue}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff"
|
||||
state "on", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821"
|
||||
}
|
||||
|
||||
// standard flat tile without actions
|
||||
standardTile("noActionFlat", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||
state "off", label: '${currentValue}',icon: "st.switches.switch.off", backgroundColor: "#ffffff"
|
||||
state "on", label: '${currentValue}', icon: "st.switches.switch.on", backgroundColor: "#79b821"
|
||||
}
|
||||
|
||||
// standard flat tile with only a label
|
||||
standardTile("flatLabel", "device.switch", width: 2, height: 2, decoration: "flat") {
|
||||
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 "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 "icon", action:"refresh.refresh", icon:"st.secondary.refresh", defaultState: true
|
||||
}
|
||||
|
||||
// standard with defaultState = true
|
||||
standardTile("flatDefaultState", "null", width: 2, height: 2, decoration: "flat") {
|
||||
state "off", label: 'Fail!', icon: "st.switches.switch.off"
|
||||
state "on", label: 'Pass!', icon: "st.switches.switch.on", defaultState: true
|
||||
}
|
||||
|
||||
// standard with implicit defaultState based on order (0 index is selected)
|
||||
standardTile("flatImplicitDefaultState1", "null", width: 2, height: 2, decoration: "flat") {
|
||||
state "on", label: 'Pass!', icon: "st.switches.switch.on"
|
||||
state "off", label: 'Fail!', icon: "st.switches.switch.off"
|
||||
}
|
||||
|
||||
// standard with implicit defaultState based on state.name == default
|
||||
standardTile("flatImplicitDefaultState2", "null", width: 2, height: 2, decoration: "flat") {
|
||||
state "off", label: 'Fail!', icon: "st.switches.switch.off"
|
||||
state "default", label: 'Pass!', icon: "st.switches.switch.on"
|
||||
}
|
||||
|
||||
// utility tiles to fill the spaces
|
||||
standardTile("empty2x2", "null", width: 2, height: 2, decoration: "flat") {
|
||||
state "emptySmall", label:'', defaultState: true
|
||||
}
|
||||
standardTile("empty4x2", "null", width: 4, height: 2, decoration: "flat") {
|
||||
state "emptyBigger", label:'', defaultState: true
|
||||
}
|
||||
|
||||
// multi-line text (explicit newlines)
|
||||
standardTile("multiLine", "device.multiLine", width: 2, height: 2) {
|
||||
state "multiLine", label: '${currentValue}', defaultState: true
|
||||
}
|
||||
|
||||
standardTile("multiLineWithIcon", "device.multiLine", width: 2, height: 2) {
|
||||
state "multiLineIcon", label: '${currentValue}', icon: "st.switches.switch.off", defaultState: true
|
||||
}
|
||||
|
||||
main("actionRings")
|
||||
details([
|
||||
"actionRings", "actionFlat", "noActionFlat",
|
||||
|
||||
"flatLabel", "flatIconLabel", "flatIcon",
|
||||
|
||||
"flatDefaultState", "flatImplicitDefaultState1", "flatImplicitDefaultState2",
|
||||
|
||||
"multiLine", "multiLineWithIcon"
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
sendEvent(name: "switch", value: "off")
|
||||
sendEvent(name: "multiLine", value: "Line 1\nLine 2\nLine 3")
|
||||
}
|
||||
|
||||
def parse(String description) {
|
||||
}
|
||||
|
||||
def on() {
|
||||
log.debug "on()"
|
||||
sendEvent(name: "switch", value: "on")
|
||||
}
|
||||
|
||||
def off() {
|
||||
log.debug "off()"
|
||||
sendEvent(name: "switch", value: "off")
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* Copyright 2016 SmartThings, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (
|
||||
name: "valueDeviceTile",
|
||||
namespace: "smartthings/tile-ux",
|
||||
author: "SmartThings") {
|
||||
|
||||
capability "Sensor"
|
||||
}
|
||||
|
||||
tiles(scale: 2) {
|
||||
valueTile("text", "device.text", width: 2, height: 2) {
|
||||
state "val", label:'${currentValue}', defaultState: true
|
||||
}
|
||||
|
||||
valueTile("longText", "device.longText", width: 2, height: 2) {
|
||||
state "val", label:'${currentValue}', defaultState: true
|
||||
}
|
||||
|
||||
valueTile("integer", "device.integer", width: 2, height: 2) {
|
||||
state "val", label:'${currentValue}', defaultState: true
|
||||
}
|
||||
|
||||
valueTile("integerFloat", "device.integerFloat", width: 2, height: 2) {
|
||||
state "val", label:'${currentValue}', defaultState: true
|
||||
}
|
||||
|
||||
valueTile("pi", "device.pi", width: 2, height: 2) {
|
||||
state "val", label:'${currentValue}', defaultState: true
|
||||
}
|
||||
|
||||
valueTile("floatAsText", "device.floatAsText", width: 2, height: 2) {
|
||||
state "val", label:'${currentValue}', defaultState: true
|
||||
}
|
||||
|
||||
valueTile("bgColor", "device.integer", width: 2, height: 2) {
|
||||
state "val", label:'${currentValue}', backgroundColor: "#e86d13", defaultState: true
|
||||
}
|
||||
|
||||
valueTile("bgColorRange", "device.integer", width: 2, height: 2) {
|
||||
state "val", label:'${currentValue}', defaultState: true, backgroundColors: [
|
||||
[value: 10, color: "#ff0000"],
|
||||
[value: 90, color: "#0000ff"]
|
||||
]
|
||||
}
|
||||
|
||||
valueTile("bgColorRangeSingleItem", "device.integer", width: 2, height: 2) {
|
||||
state "val", label:'${currentValue}', defaultState: true, backgroundColors: [
|
||||
[value: 10, color: "#333333"]
|
||||
]
|
||||
}
|
||||
|
||||
valueTile("bgColorRangeConflict", "device.integer", width: 2, height: 2) {
|
||||
state "valWithConflict", label:'${currentValue}', defaultState: true, backgroundColors: [
|
||||
[value: 10, color: "#990000"],
|
||||
[value: 10, color: "#000099"]
|
||||
]
|
||||
}
|
||||
|
||||
valueTile("noValue", "device.nada", width: 4, height: 2) {
|
||||
state "noval", label:'${currentValue}', defaultState: true
|
||||
}
|
||||
|
||||
valueTile("multiLine", "device.multiLine", width: 3, height: 2) {
|
||||
state "val", label: '${currentValue}', defaultState: true
|
||||
}
|
||||
|
||||
valueTile("multiLineWithIcon", "device.multiLine", width: 3, height: 2) {
|
||||
state "val", label: '${currentValue}', icon: "st.switches.switch.off", defaultState: true
|
||||
}
|
||||
|
||||
main("text")
|
||||
details([
|
||||
"text", "longText", "integer",
|
||||
"integerFloat", "pi", "floatAsText",
|
||||
"bgColor", "bgColorRange", "bgColorRangeSingleItem",
|
||||
"bgColorRangeConflict", "noValue",
|
||||
"multiLine", "multiLineWithIcon"
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
sendEvent(name: "text", value: "Test")
|
||||
sendEvent(name: "longText", value: "The Longer The Text, The Better The Test")
|
||||
sendEvent(name: "integer", value: 47)
|
||||
sendEvent(name: "integerFloat", value: 47.0)
|
||||
sendEvent(name: "pi", value: 3.14159)
|
||||
sendEvent(name: "floatAsText", value: "3.14159")
|
||||
sendEvent(name: "multiLine", value: "Line 1\nLine 2\nLine 3")
|
||||
}
|
||||
|
||||
def parse(String description) {
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
/**
|
||||
* Copyright 2016 SmartThings, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "genericDeviceTile", namespace: "smartthings/tile-ux", author: "SmartThings") {
|
||||
capability "Actuator"
|
||||
capability "Switch"
|
||||
capability "Switch Level"
|
||||
|
||||
command "levelUp"
|
||||
command "levelDown"
|
||||
command "randomizeLevel"
|
||||
}
|
||||
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"basicTile", type:"generic", width:6, height:4) {
|
||||
tileAttribute("device.switch", key: "PRIMARY_CONTROL") {
|
||||
attributeState "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
attributeState "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
}
|
||||
}
|
||||
multiAttributeTile(name:"sliderTile", type:"generic", width:6, height:4) {
|
||||
tileAttribute("device.switch", key: "PRIMARY_CONTROL") {
|
||||
attributeState "on", label:'${name}', backgroundColor:"#79b821", nextState:"turningOff"
|
||||
attributeState "off", label:'${name}', backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
attributeState "turningOn", label:'${name}', backgroundColor:"#79b821", nextState:"turningOff"
|
||||
attributeState "turningOff", label:'${name}', backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
}
|
||||
tileAttribute("device.level", key: "SECONDARY_CONTROL") {
|
||||
attributeState "level", icon: 'st.Weather.weather1', action:"randomizeLevel", defaultState: true
|
||||
}
|
||||
tileAttribute("device.level", key: "SLIDER_CONTROL") {
|
||||
attributeState "level", action:"switch level.setLevel", defaultState: true
|
||||
}
|
||||
}
|
||||
multiAttributeTile(name:"valueTile", type:"generic", width:6, height:4) {
|
||||
tileAttribute("device.level", key: "PRIMARY_CONTROL") {
|
||||
attributeState "level", label:'${currentValue}', defaultState: true, backgroundColors:[
|
||||
[value: 0, color: "#ff0000"],
|
||||
[value: 20, color: "#ffff00"],
|
||||
[value: 40, color: "#00ff00"],
|
||||
[value: 60, color: "#00ffff"],
|
||||
[value: 80, color: "#0000ff"],
|
||||
[value: 100, color: "#ff00ff"]
|
||||
]
|
||||
}
|
||||
tileAttribute("device.switch", key: "SECONDARY_CONTROL") {
|
||||
attributeState "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
attributeState "off", label:'${name}', action:"switch.on", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
attributeState "turningOn", label:'…', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
attributeState "turningOff", label:'…', action:"switch.on", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
}
|
||||
tileAttribute("device.level", key: "VALUE_CONTROL") {
|
||||
attributeState "VALUE_UP", action: "levelUp"
|
||||
attributeState "VALUE_DOWN", action: "levelDown"
|
||||
}
|
||||
}
|
||||
multiAttributeTile(name:"lengthyTile", type:"generic", width:6, height:4) {
|
||||
tileAttribute("device.lengthyText", key: "PRIMARY_CONTROL") {
|
||||
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 "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 "multiLineText", label:'Line 1 YES\nLine 2 YES\nLine 3 NO', backgroundColor:"#79b821", defaultState: true
|
||||
}
|
||||
tileAttribute("device.multilineText", key: "SECONDARY_CONTROL") {
|
||||
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 "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 "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 "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 "multilineText", label:'Line 1 YES\nLine 2 YES\nLine 3 NO', backgroundColor:"#79b821", icon: "st.switches.switch.on", defaultState: true
|
||||
}
|
||||
}
|
||||
|
||||
main(["basicTile"])
|
||||
details(["basicTile", "sliderTile", "valueTile", "lengthyTile", "multilineTile", "lengthyTileWithIcon", "multilineTileWithIcon"])
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
sendEvent(name: "lengthyText", value: "The value of this tile is long and should wrap to two lines")
|
||||
sendEvent(name: "multilineText", value: "Line 1 YES\nLine 2 YES\nLine 3 NO")
|
||||
}
|
||||
|
||||
def parse(String description) {
|
||||
// This is a simulated device. No incoming data to parse.
|
||||
}
|
||||
|
||||
def on() {
|
||||
log.debug "turningOn"
|
||||
sendEvent(name: "switch", value: "on")
|
||||
}
|
||||
|
||||
def off() {
|
||||
log.debug "turningOff"
|
||||
sendEvent(name: "switch", value: "off")
|
||||
}
|
||||
|
||||
def setLevel(percent) {
|
||||
log.debug "setLevel: ${percent}, this"
|
||||
sendEvent(name: "level", value: percent)
|
||||
}
|
||||
|
||||
def randomizeLevel() {
|
||||
def level = Math.round(Math.random() * 100)
|
||||
setLevel(level)
|
||||
}
|
||||
|
||||
def levelUp() {
|
||||
def level = device.latestValue("level") as Integer ?: 0
|
||||
if (level < 100) {
|
||||
level = level + 1
|
||||
}
|
||||
setLevel(level)
|
||||
}
|
||||
|
||||
def levelDown() {
|
||||
def level = device.latestValue("level") as Integer ?: 0
|
||||
if (level > 0) {
|
||||
level = level - 1
|
||||
}
|
||||
setLevel(level)
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
/**
|
||||
* Copyright 2016 SmartThings, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (
|
||||
name: "lightingDeviceTile",
|
||||
namespace: "smartthings/tile-ux",
|
||||
author: "SmartThings") {
|
||||
|
||||
capability "Switch Level"
|
||||
capability "Actuator"
|
||||
capability "Color Control"
|
||||
capability "Power Meter"
|
||||
capability "Switch"
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
|
||||
command "setAdjustedColor"
|
||||
command "reset"
|
||||
command "refresh"
|
||||
}
|
||||
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true) {
|
||||
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
|
||||
attributeState "on", label:'${name}', action:"switch.off", icon:"st.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.power", key: "SECONDARY_CONTROL") {
|
||||
attributeState "power", label:'Power level: ${currentValue}W', icon: "st.Appliances.appliances17"
|
||||
}
|
||||
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
|
||||
attributeState "level", action:"switch level.setLevel"
|
||||
}
|
||||
tileAttribute ("device.color", key: "COLOR_CONTROL") {
|
||||
attributeState "color", action:"setAdjustedColor"
|
||||
}
|
||||
}
|
||||
|
||||
multiAttributeTile(name:"switchNoPower", 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"
|
||||
}
|
||||
tileAttribute ("device.color", key: "COLOR_CONTROL") {
|
||||
attributeState "color", action:"setAdjustedColor"
|
||||
}
|
||||
}
|
||||
|
||||
multiAttributeTile(name:"switchNoSlider", 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.power", key: "SECONDARY_CONTROL") {
|
||||
attributeState "power", label:'The power level is currently: ${currentValue}W', icon: "st.Appliances.appliances17"
|
||||
}
|
||||
tileAttribute ("device.color", key: "COLOR_CONTROL") {
|
||||
attributeState "color", action:"setAdjustedColor"
|
||||
}
|
||||
}
|
||||
|
||||
multiAttributeTile(name:"switchNoSliderOrColor", 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.power", key: "SECONDARY_CONTROL") {
|
||||
attributeState "power", label:'The light is currently consuming this amount of power: ${currentValue}W', icon: "st.Appliances.appliances17"
|
||||
}
|
||||
}
|
||||
|
||||
valueTile("color", "device.color", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "color", label: '${currentValue}'
|
||||
}
|
||||
|
||||
standardTile("reset", "device.reset", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
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 "refresh", label:"", action:"refresh.refresh", icon:"st.secondary.refresh", defaultState: true
|
||||
}
|
||||
|
||||
main(["switch"])
|
||||
details(["switch", "switchNoPower", "switchNoSlider", "switchNoSliderOrColor", "color", "refresh", "reset"])
|
||||
}
|
||||
}
|
||||
|
||||
// parse events into attributes
|
||||
def parse(description) {
|
||||
log.debug "parse() - $description"
|
||||
def results = []
|
||||
def map = description
|
||||
if (description instanceof String) {
|
||||
log.debug "Hue Bulb stringToMap - ${map}"
|
||||
map = stringToMap(description)
|
||||
}
|
||||
if (map?.name && map?.value) {
|
||||
results << createEvent(name: "${map?.name}", value: "${map?.value}")
|
||||
}
|
||||
results
|
||||
}
|
||||
|
||||
// handle commands
|
||||
def on() {
|
||||
//log.trace parent.on(this)
|
||||
sendEvent(name: "switch", value: "on")
|
||||
}
|
||||
|
||||
def off() {
|
||||
//log.trace parent.off(this)
|
||||
sendEvent(name: "switch", value: "off")
|
||||
}
|
||||
|
||||
def nextLevel() {
|
||||
def level = device.latestValue("level") as Integer ?: 0
|
||||
if (level <= 100) {
|
||||
level = Math.min(25 * (Math.round(level / 25) + 1), 100) as Integer
|
||||
}
|
||||
else {
|
||||
level = 25
|
||||
}
|
||||
setLevel(level)
|
||||
}
|
||||
|
||||
def setLevel(percent) {
|
||||
log.debug "setLevel: ${percent}, this"
|
||||
sendEvent(name: "level", value: percent)
|
||||
def power = Math.round(percent / 1.175) * 0.1
|
||||
sendEvent(name: "power", value: power)
|
||||
}
|
||||
|
||||
def setSaturation(percent) {
|
||||
log.debug "setSaturation: ${percent}, $this"
|
||||
sendEvent(name: "saturation", value: percent)
|
||||
}
|
||||
|
||||
def setHue(percent) {
|
||||
log.debug "setHue: ${percent}, $this"
|
||||
sendEvent(name: "hue", value: percent)
|
||||
}
|
||||
|
||||
def setColor(value) {
|
||||
log.debug "setColor: ${value}, $this"
|
||||
if (value.hue) { sendEvent(name: "hue", value: value.hue)}
|
||||
if (value.saturation) { sendEvent(name: "saturation", value: value.saturation)}
|
||||
if (value.hex) { sendEvent(name: "color", value: value.hex)}
|
||||
if (value.level) { sendEvent(name: "level", value: value.level)}
|
||||
if (value.switch) { sendEvent(name: "switch", value: value.switch)}
|
||||
}
|
||||
|
||||
def reset() {
|
||||
log.debug "Executing 'reset'"
|
||||
setAdjustedColor([level:100, hex:"#90C638", saturation:56, hue:23])
|
||||
}
|
||||
|
||||
def setAdjustedColor(value) {
|
||||
if (value) {
|
||||
log.trace "setAdjustedColor: ${value}"
|
||||
def adjusted = value + [:]
|
||||
adjusted.hue = adjustOutgoingHue(value.hue)
|
||||
// Needed because color picker always sends 100
|
||||
adjusted.level = null
|
||||
setColor(adjusted)
|
||||
}
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
log.debug "Executing 'refresh'"
|
||||
}
|
||||
|
||||
def adjustOutgoingHue(percent) {
|
||||
def adjusted = percent
|
||||
if (percent > 31) {
|
||||
if (percent < 63.0) {
|
||||
adjusted = percent + (7 * (percent -30 ) / 32)
|
||||
}
|
||||
else if (percent < 73.0) {
|
||||
adjusted = 69 + (5 * (percent - 62) / 10)
|
||||
}
|
||||
else {
|
||||
adjusted = percent + (2 * (100 - percent) / 28)
|
||||
}
|
||||
}
|
||||
log.info "percent: $percent, adjusted: $adjusted"
|
||||
adjusted
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* Copyright 2016 SmartThings, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (
|
||||
name: "mediaPlayerDeviceTile",
|
||||
namespace: "smartthings/tile-ux",
|
||||
author: "SmartThings") {
|
||||
|
||||
capability "Actuator"
|
||||
capability "Switch"
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
capability "Music Player"
|
||||
}
|
||||
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name: "mediaMulti", type:"mediaPlayer", width:6, height:4) {
|
||||
tileAttribute("device.status", key: "PRIMARY_CONTROL") {
|
||||
attributeState("paused", label:"Paused",)
|
||||
attributeState("playing", label:"Playing")
|
||||
attributeState("stopped", label:"Stopped")
|
||||
}
|
||||
tileAttribute("device.status", key: "MEDIA_STATUS") {
|
||||
attributeState("paused", label:"Paused", action:"music Player.play", nextState: "playing")
|
||||
attributeState("playing", label:"Playing", action:"music Player.pause", nextState: "paused")
|
||||
attributeState("stopped", label:"Stopped", action:"music Player.play", nextState: "playing")
|
||||
}
|
||||
tileAttribute("device.status", key: "PREVIOUS_TRACK") {
|
||||
attributeState("status", action:"music Player.previousTrack", defaultState: true)
|
||||
}
|
||||
tileAttribute("device.status", key: "NEXT_TRACK") {
|
||||
attributeState("status", action:"music Player.nextTrack", defaultState: true)
|
||||
}
|
||||
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
|
||||
attributeState("level", action:"music Player.setLevel")
|
||||
}
|
||||
tileAttribute ("device.mute", key: "MEDIA_MUTED") {
|
||||
attributeState("unmuted", action:"music Player.mute", nextState: "muted")
|
||||
attributeState("muted", action:"music Player.unmute", nextState: "unmuted")
|
||||
}
|
||||
tileAttribute("device.trackDescription", key: "MARQUEE") {
|
||||
attributeState("trackDescription", label:"${currentValue}", defaultState: true)
|
||||
}
|
||||
}
|
||||
|
||||
main "mediaMulti"
|
||||
details(["mediaMulti"])
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
state.tracks = [
|
||||
"Gangnam Style (강남스타일)\nPSY\nPsy 6 (Six Rules), Part 1",
|
||||
"Careless Whisper\nWham!\nMake It Big",
|
||||
"Never Gonna Give You Up\nRick Astley\nWhenever You Need Somebody",
|
||||
"Shake It Off\nTaylor Swift\n1989",
|
||||
"Ironic\nAlanis Morissette\nJagged Little Pill",
|
||||
"Hotline Bling\nDrake\nHotline Bling - Single"
|
||||
]
|
||||
state.currentTrack = 0
|
||||
|
||||
sendEvent(name: "level", value: 72)
|
||||
sendEvent(name: "mute", value: "unmuted")
|
||||
sendEvent(name: "status", value: "stopped")
|
||||
}
|
||||
|
||||
def parse(description) {
|
||||
// No parsing will happen with this simulated device.
|
||||
}
|
||||
|
||||
def play() {
|
||||
sendEvent(name: "status", value: "playing")
|
||||
sendEvent(name: "trackDescription", value: state.tracks[state.currentTrack])
|
||||
}
|
||||
|
||||
def pause() {
|
||||
sendEvent(name: "status", value: "paused")
|
||||
sendEvent(name: "trackDescription", value: state.tracks[state.currentTrack])
|
||||
}
|
||||
|
||||
def stop() {
|
||||
sendEvent(name: "status", value: "stopped")
|
||||
}
|
||||
|
||||
def previousTrack() {
|
||||
state.currentTrack = state.currentTrack - 1
|
||||
if (state.currentTrack < 0)
|
||||
state.currentTrack = state.tracks.size()-1
|
||||
|
||||
sendEvent(name: "trackDescription", value: state.tracks[state.currentTrack])
|
||||
}
|
||||
|
||||
def nextTrack() {
|
||||
state.currentTrack = state.currentTrack + 1
|
||||
if (state.currentTrack == state.tracks.size())
|
||||
state.currentTrack = 0
|
||||
|
||||
sendEvent(name: "trackDescription", value: state.tracks[state.currentTrack])
|
||||
}
|
||||
|
||||
def mute() {
|
||||
sendEvent(name: "mute", value: "muted")
|
||||
}
|
||||
|
||||
def unmute() {
|
||||
sendEvent(name: "mute", value: "unmuted")
|
||||
}
|
||||
|
||||
def setLevel(level) {
|
||||
sendEvent(name: "level", value: level)
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
/**
|
||||
* Copyright 2016 SmartThings, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (
|
||||
name: "thermostatDeviceTile",
|
||||
namespace: "smartthings/tile-ux",
|
||||
author: "SmartThings") {
|
||||
|
||||
capability "Thermostat"
|
||||
capability "Relative Humidity Measurement"
|
||||
|
||||
command "tempUp"
|
||||
command "tempDown"
|
||||
command "heatUp"
|
||||
command "heatDown"
|
||||
command "coolUp"
|
||||
command "coolDown"
|
||||
command "setTemperature", ["number"]
|
||||
}
|
||||
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"thermostatFull", type:"thermostat", width:6, height:4) {
|
||||
tileAttribute("device.temperature", key: "PRIMARY_CONTROL") {
|
||||
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("humidity", label:'${currentValue}%', unit:"%", defaultState: true)
|
||||
}
|
||||
tileAttribute("device.thermostatOperatingState", key: "OPERATING_STATE") {
|
||||
attributeState("idle", backgroundColor:"#44b621")
|
||||
attributeState("heating", backgroundColor:"#ffa81e")
|
||||
attributeState("cooling", backgroundColor:"#269bd2")
|
||||
}
|
||||
tileAttribute("device.thermostatMode", key: "THERMOSTAT_MODE") {
|
||||
attributeState("off", label:'${name}')
|
||||
attributeState("heat", label:'${name}')
|
||||
attributeState("cool", label:'${name}')
|
||||
attributeState("auto", label:'${name}')
|
||||
}
|
||||
tileAttribute("device.heatingSetpoint", key: "HEATING_SETPOINT") {
|
||||
attributeState("heatingSetpoint", label:'${currentValue}', unit:"dF", defaultState: true)
|
||||
}
|
||||
tileAttribute("device.coolingSetpoint", key: "COOLING_SETPOINT") {
|
||||
attributeState("coolingSetpoint", label:'${currentValue}', unit:"dF", defaultState: true)
|
||||
}
|
||||
}
|
||||
multiAttributeTile(name:"thermostatNoHumidity", type:"thermostat", width:6, height:4) {
|
||||
tileAttribute("device.temperature", key: "PRIMARY_CONTROL") {
|
||||
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")
|
||||
attributeState("VALUE_DOWN", action: "tempDown")
|
||||
}
|
||||
tileAttribute("device.thermostatOperatingState", key: "OPERATING_STATE") {
|
||||
attributeState("idle", backgroundColor:"#44b621")
|
||||
attributeState("heating", backgroundColor:"#ffa81e")
|
||||
attributeState("cooling", backgroundColor:"#269bd2")
|
||||
}
|
||||
tileAttribute("device.thermostatMode", key: "THERMOSTAT_MODE") {
|
||||
attributeState("off", label:'${name}')
|
||||
attributeState("heat", label:'${name}')
|
||||
attributeState("cool", label:'${name}')
|
||||
attributeState("auto", label:'${name}')
|
||||
}
|
||||
tileAttribute("device.heatingSetpoint", key: "HEATING_SETPOINT") {
|
||||
attributeState("coolingSetpoint", label:'${currentValue}', unit:"dF", defaultState: true)
|
||||
attributeState("heatingSetpoint", label:'${currentValue}', unit:"dF")
|
||||
}
|
||||
tileAttribute("device.coolingSetpoint", key: "COOLING_SETPOINT") {
|
||||
attributeState("coolingSetpoint", label:'${currentValue}', unit:"dF", defaultState: true)
|
||||
}
|
||||
}
|
||||
multiAttributeTile(name:"thermostatBasic", type:"thermostat", width:6, height:4) {
|
||||
tileAttribute("device.temperature", key: "PRIMARY_CONTROL") {
|
||||
attributeState("temp", label:'${currentValue}', unit:"dF", defaultState: true,
|
||||
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"]
|
||||
])
|
||||
}
|
||||
tileAttribute("device.temperature", key: "VALUE_CONTROL") {
|
||||
attributeState("VALUE_UP", action: "tempUp")
|
||||
attributeState("VALUE_DOWN", action: "tempDown")
|
||||
}
|
||||
}
|
||||
|
||||
valueTile("temperature", "device.temperature", width: 2, height: 2) {
|
||||
state("temperature", label:'${currentValue}', unit:"dF",
|
||||
backgroundColors:[
|
||||
[value: 31, color: "#153591"],
|
||||
[value: 44, color: "#1e9cbb"],
|
||||
[value: 59, color: "#90d2a7"],
|
||||
[value: 74, color: "#44b621"],
|
||||
[value: 84, color: "#f1d801"],
|
||||
[value: 95, color: "#d04e00"],
|
||||
[value: 96, color: "#bc2323"]
|
||||
]
|
||||
)
|
||||
}
|
||||
standardTile("tempDown", "device.temperature", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||
state "tempDown", label:'down', action:"tempDown", defaultState: true
|
||||
}
|
||||
standardTile("tempUp", "device.temperature", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||
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 "heatDown", label:'down', action:"heatDown", defaultState: true
|
||||
}
|
||||
standardTile("heatUp", "device.temperature", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||
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 "coolDown", label:'down', action:"coolDown", defaultState: true
|
||||
}
|
||||
standardTile("coolUp", "device.temperature", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||
state "coolUp", label:'up', action:"coolUp", defaultState: true
|
||||
}
|
||||
|
||||
standardTile("mode", "device.thermostatMode", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||
state "off", label:'${name}', action:"thermostat.heat", backgroundColor:"#ffffff"
|
||||
state "heat", label:'${name}', action:"thermostat.cool", backgroundColor:"#ffa81e"
|
||||
state "cool", label:'${name}', action:"thermostat.auto", backgroundColor:"#269bd2"
|
||||
state "auto", label:'${name}', action:"thermostat.off", backgroundColor:"#79b821"
|
||||
}
|
||||
standardTile("fanMode", "device.thermostatFanMode", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||
state "fanAuto", label:'${name}', action:"thermostat.fanOn", backgroundColor:"#ffffff"
|
||||
state "fanOn", label:'${name}', action:"thermostat.fanCirculate", backgroundColor:"#ffffff"
|
||||
state "fanCirculate", label:'${name}', action:"thermostat.fanAuto", backgroundColor:"#ffffff"
|
||||
}
|
||||
standardTile("operatingState", "device.thermostatOperatingState", width: 2, height: 2) {
|
||||
state "idle", label:'${name}', backgroundColor:"#ffffff"
|
||||
state "heating", label:'${name}', backgroundColor:"#ffa81e"
|
||||
state "cooling", label:'${name}', backgroundColor:"#269bd2"
|
||||
}
|
||||
|
||||
|
||||
main("thermostatFull")
|
||||
details([
|
||||
"thermostatFull", "thermostatNoHumidity", "thermostatBasic",
|
||||
"temperature","tempDown","tempUp",
|
||||
"mode", "fanMode", "operatingState",
|
||||
"heatingSetpoint", "heatDown", "heatUp",
|
||||
"coolingSetpoint", "coolDown", "coolUp"
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
sendEvent(name: "temperature", value: 72, unit: "F")
|
||||
sendEvent(name: "heatingSetpoint", value: 70, unit: "F")
|
||||
sendEvent(name: "thermostatSetpoint", value: 70, unit: "F")
|
||||
sendEvent(name: "coolingSetpoint", value: 76, unit: "F")
|
||||
sendEvent(name: "thermostatMode", value: "off")
|
||||
sendEvent(name: "thermostatFanMode", value: "fanAuto")
|
||||
sendEvent(name: "thermostatOperatingState", value: "idle")
|
||||
sendEvent(name: "humidity", value: 53, unit: "%")
|
||||
}
|
||||
|
||||
def parse(String description) {
|
||||
}
|
||||
|
||||
def evaluate(temp, heatingSetpoint, coolingSetpoint) {
|
||||
log.debug "evaluate($temp, $heatingSetpoint, $coolingSetpoint"
|
||||
def threshold = 1.0
|
||||
def current = device.currentValue("thermostatOperatingState")
|
||||
def mode = device.currentValue("thermostatMode")
|
||||
|
||||
def heating = false
|
||||
def cooling = false
|
||||
def idle = false
|
||||
if (mode in ["heat","emergency heat","auto"]) {
|
||||
if (heatingSetpoint - temp >= threshold) {
|
||||
heating = true
|
||||
sendEvent(name: "thermostatOperatingState", value: "heating")
|
||||
}
|
||||
else if (temp - heatingSetpoint >= threshold) {
|
||||
idle = true
|
||||
}
|
||||
sendEvent(name: "thermostatSetpoint", value: heatingSetpoint)
|
||||
}
|
||||
if (mode in ["cool","auto"]) {
|
||||
if (temp - coolingSetpoint >= threshold) {
|
||||
cooling = true
|
||||
sendEvent(name: "thermostatOperatingState", value: "cooling")
|
||||
}
|
||||
else if (coolingSetpoint - temp >= threshold && !heating) {
|
||||
idle = true
|
||||
}
|
||||
sendEvent(name: "thermostatSetpoint", value: coolingSetpoint)
|
||||
}
|
||||
else {
|
||||
sendEvent(name: "thermostatSetpoint", value: heatingSetpoint)
|
||||
}
|
||||
|
||||
if (mode == "off") {
|
||||
idle = true
|
||||
}
|
||||
|
||||
if (idle && !heating && !cooling) {
|
||||
sendEvent(name: "thermostatOperatingState", value: "idle")
|
||||
}
|
||||
}
|
||||
|
||||
def setHeatingSetpoint(Double degreesF) {
|
||||
log.debug "setHeatingSetpoint($degreesF)"
|
||||
sendEvent(name: "heatingSetpoint", value: degreesF)
|
||||
evaluate(device.currentValue("temperature"), degreesF, device.currentValue("coolingSetpoint"))
|
||||
}
|
||||
|
||||
def setCoolingSetpoint(Double degreesF) {
|
||||
log.debug "setCoolingSetpoint($degreesF)"
|
||||
sendEvent(name: "coolingSetpoint", value: degreesF)
|
||||
evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), degreesF)
|
||||
}
|
||||
|
||||
def setThermostatMode(String value) {
|
||||
sendEvent(name: "thermostatMode", value: value)
|
||||
evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint"))
|
||||
}
|
||||
|
||||
def setThermostatFanMode(String value) {
|
||||
sendEvent(name: "thermostatFanMode", value: value)
|
||||
}
|
||||
|
||||
def off() {
|
||||
sendEvent(name: "thermostatMode", value: "off")
|
||||
evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint"))
|
||||
}
|
||||
|
||||
def heat() {
|
||||
sendEvent(name: "thermostatMode", value: "heat")
|
||||
evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint"))
|
||||
}
|
||||
|
||||
def auto() {
|
||||
sendEvent(name: "thermostatMode", value: "auto")
|
||||
evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint"))
|
||||
}
|
||||
|
||||
def emergencyHeat() {
|
||||
sendEvent(name: "thermostatMode", value: "emergency heat")
|
||||
evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint"))
|
||||
}
|
||||
|
||||
def cool() {
|
||||
sendEvent(name: "thermostatMode", value: "cool")
|
||||
evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint"))
|
||||
}
|
||||
|
||||
def fanOn() {
|
||||
sendEvent(name: "thermostatFanMode", value: "fanOn")
|
||||
}
|
||||
|
||||
def fanAuto() {
|
||||
sendEvent(name: "thermostatFanMode", value: "fanAuto")
|
||||
}
|
||||
|
||||
def fanCirculate() {
|
||||
sendEvent(name: "thermostatFanMode", value: "fanCirculate")
|
||||
}
|
||||
|
||||
def poll() {
|
||||
null
|
||||
}
|
||||
|
||||
def tempUp() {
|
||||
def ts = device.currentState("temperature")
|
||||
def value = ts ? ts.integerValue + 1 : 72
|
||||
sendEvent(name:"temperature", value: value)
|
||||
evaluate(value, device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint"))
|
||||
}
|
||||
|
||||
def tempDown() {
|
||||
def ts = device.currentState("temperature")
|
||||
def value = ts ? ts.integerValue - 1 : 72
|
||||
sendEvent(name:"temperature", value: value)
|
||||
evaluate(value, device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint"))
|
||||
}
|
||||
|
||||
def setTemperature(value) {
|
||||
def ts = device.currentState("temperature")
|
||||
sendEvent(name:"temperature", value: value)
|
||||
evaluate(value, device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint"))
|
||||
}
|
||||
|
||||
def heatUp() {
|
||||
def ts = device.currentState("heatingSetpoint")
|
||||
def value = ts ? ts.integerValue + 1 : 68
|
||||
sendEvent(name:"heatingSetpoint", value: value)
|
||||
evaluate(device.currentValue("temperature"), value, device.currentValue("coolingSetpoint"))
|
||||
}
|
||||
|
||||
def heatDown() {
|
||||
def ts = device.currentState("heatingSetpoint")
|
||||
def value = ts ? ts.integerValue - 1 : 68
|
||||
sendEvent(name:"heatingSetpoint", value: value)
|
||||
evaluate(device.currentValue("temperature"), value, device.currentValue("coolingSetpoint"))
|
||||
}
|
||||
|
||||
|
||||
def coolUp() {
|
||||
def ts = device.currentState("coolingSetpoint")
|
||||
def value = ts ? ts.integerValue + 1 : 76
|
||||
sendEvent(name:"coolingSetpoint", value: value)
|
||||
evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), value)
|
||||
}
|
||||
|
||||
def coolDown() {
|
||||
def ts = device.currentState("coolingSetpoint")
|
||||
def value = ts ? ts.integerValue - 1 : 76
|
||||
sendEvent(name:"coolingSetpoint", value: value)
|
||||
evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), value)
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
/**
|
||||
* Copyright 2016 SmartThings, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (
|
||||
name: "videoPlayerDeviceTile",
|
||||
namespace: "smartthings/tile-ux",
|
||||
author: "SmartThings") {
|
||||
|
||||
capability "Configuration"
|
||||
capability "Video Camera"
|
||||
capability "Video Capture"
|
||||
capability "Refresh"
|
||||
capability "Switch"
|
||||
|
||||
// custom commands
|
||||
command "start"
|
||||
command "stop"
|
||||
command "setProfileHD"
|
||||
command "setProfileSDH"
|
||||
command "setProfileSDL"
|
||||
}
|
||||
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name: "videoPlayer", type: "videoPlayer", width: 6, height: 4) {
|
||||
tileAttribute("device.switch", key: "CAMERA_STATUS") {
|
||||
attributeState("on", label: "Active", icon: "st.camera.dlink-indoor", action: "switch.off", backgroundColor: "#79b821", defaultState: true)
|
||||
attributeState("off", label: "Inactive", icon: "st.camera.dlink-indoor", action: "switch.on", backgroundColor: "#ffffff")
|
||||
attributeState("restarting", label: "Connecting", icon: "st.camera.dlink-indoor", backgroundColor: "#53a7c0")
|
||||
attributeState("unavailable", label: "Unavailable", icon: "st.camera.dlink-indoor", action: "refresh.refresh", backgroundColor: "#F22000")
|
||||
}
|
||||
|
||||
tileAttribute("device.errorMessage", key: "CAMERA_ERROR_MESSAGE") {
|
||||
attributeState("errorMessage", label: "", value: "", defaultState: true)
|
||||
}
|
||||
|
||||
tileAttribute("device.camera", key: "PRIMARY_CONTROL") {
|
||||
attributeState("on", label: "Active", icon: "st.camera.dlink-indoor", backgroundColor: "#79b821", defaultState: true)
|
||||
attributeState("off", label: "Inactive", icon: "st.camera.dlink-indoor", backgroundColor: "#ffffff")
|
||||
attributeState("restarting", label: "Connecting", icon: "st.camera.dlink-indoor", backgroundColor: "#53a7c0")
|
||||
attributeState("unavailable", label: "Unavailable", icon: "st.camera.dlink-indoor", backgroundColor: "#F22000")
|
||||
}
|
||||
|
||||
tileAttribute("device.startLive", key: "START_LIVE") {
|
||||
attributeState("live", action: "start", defaultState: true)
|
||||
}
|
||||
|
||||
tileAttribute("device.stream", key: "STREAM_URL") {
|
||||
attributeState("activeURL", defaultState: true)
|
||||
}
|
||||
|
||||
tileAttribute("device.profile", key: "STREAM_QUALITY") {
|
||||
attributeState("1", label: "720p", action: "setProfileHD", defaultState: true)
|
||||
attributeState("2", label: "h360p", action: "setProfileSDH", defaultState: true)
|
||||
attributeState("3", label: "l360p", action: "setProfileSDL", defaultState: true)
|
||||
}
|
||||
|
||||
tileAttribute("device.betaLogo", key: "BETA_LOGO") {
|
||||
attributeState("betaLogo", label: "", value: "", defaultState: true)
|
||||
}
|
||||
}
|
||||
|
||||
multiAttributeTile(name: "videoPlayerMin", type: "videoPlayer", width: 6, height: 4) {
|
||||
tileAttribute("device.switch", key: "CAMERA_STATUS") {
|
||||
attributeState("on", label: "Active", icon: "st.camera.dlink-indoor", action: "switch.off", backgroundColor: "#79b821", defaultState: true)
|
||||
attributeState("off", label: "Inactive", icon: "st.camera.dlink-indoor", action: "switch.on", backgroundColor: "#ffffff")
|
||||
attributeState("restarting", label: "Connecting", icon: "st.camera.dlink-indoor", backgroundColor: "#53a7c0")
|
||||
attributeState("unavailable", label: "Unavailable", icon: "st.camera.dlink-indoor", action: "refresh.refresh", backgroundColor: "#F22000")
|
||||
}
|
||||
|
||||
tileAttribute("device.errorMessage", key: "CAMERA_ERROR_MESSAGE") {
|
||||
attributeState("errorMessage", label: "", value: "", defaultState: true)
|
||||
}
|
||||
|
||||
tileAttribute("device.camera", key: "PRIMARY_CONTROL") {
|
||||
attributeState("on", label: "Active", icon: "st.camera.dlink-indoor", backgroundColor: "#79b821", defaultState: true)
|
||||
attributeState("off", label: "Inactive", icon: "st.camera.dlink-indoor", backgroundColor: "#ffffff")
|
||||
attributeState("restarting", label: "Connecting", icon: "st.camera.dlink-indoor", backgroundColor: "#53a7c0")
|
||||
attributeState("unavailable", label: "Unavailable", icon: "st.camera.dlink-indoor", backgroundColor: "#F22000")
|
||||
}
|
||||
|
||||
tileAttribute("device.startLive", key: "START_LIVE") {
|
||||
attributeState("live", action: "start", defaultState: true)
|
||||
}
|
||||
|
||||
tileAttribute("device.stream", key: "STREAM_URL") {
|
||||
attributeState("activeURL", defaultState: true)
|
||||
}
|
||||
}
|
||||
|
||||
main("videoPlayer")
|
||||
details([
|
||||
"videoPlayer", "videoPlayerMin"
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
}
|
||||
|
||||
def parse(String description) {
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
log.trace "refresh()"
|
||||
// no-op
|
||||
}
|
||||
|
||||
def on() {
|
||||
log.trace "on()"
|
||||
// no-op
|
||||
}
|
||||
|
||||
def off() {
|
||||
log.trace "off()"
|
||||
// no-op
|
||||
}
|
||||
|
||||
def setProfile(profile) {
|
||||
log.trace "setProfile(): ${profile}"
|
||||
sendEvent(name: "profile", value: profile, displayed: false)
|
||||
}
|
||||
|
||||
def setProfileHD() {
|
||||
setProfile(1)
|
||||
}
|
||||
|
||||
def setProfileSDH() {
|
||||
setProfile(2)
|
||||
}
|
||||
|
||||
def setProfileSDL() {
|
||||
setProfile(3)
|
||||
}
|
||||
|
||||
def start() {
|
||||
log.trace "start()"
|
||||
def dataLiveVideo = [
|
||||
OutHomeURL : "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/bipbop_4x3_variant.m3u8",
|
||||
InHomeURL : "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/bipbop_4x3_variant.m3u8",
|
||||
ThumbnailURL: "http://cdn.device-icons.smartthings.com/camera/dlink-indoor@2x.png",
|
||||
cookie : [key: "key", value: "value"]
|
||||
]
|
||||
|
||||
def event = [
|
||||
name : "stream",
|
||||
value : groovy.json.JsonOutput.toJson(dataLiveVideo).toString(),
|
||||
data : groovy.json.JsonOutput.toJson(dataLiveVideo),
|
||||
descriptionText: "Starting the livestream",
|
||||
eventType : "VIDEO",
|
||||
displayed : false,
|
||||
isStateChange : true
|
||||
]
|
||||
sendEvent(event)
|
||||
}
|
||||
|
||||
def stop() {
|
||||
log.trace "stop()"
|
||||
}
|
||||
@@ -33,7 +33,7 @@ metadata {
|
||||
state "power", label: '${currentValue} W'
|
||||
}
|
||||
|
||||
htmlTile(name: "powerContent", attribute: "powerContent", type: "HTML", whitelist: "www.wattvision.com" , url: '${currentValue}', width: 3, height: 2)
|
||||
htmlTile(name: "powerContent", attribute: "powerContent", type: "HTML", whitelist: ["www.wattvision.com"] , url: '${currentValue}', width: 3, height: 2)
|
||||
|
||||
standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label: '', action: "refresh.refresh", icon: "st.secondary.refresh"
|
||||
|
||||
194
devicetypes/smartthings/zigbee-button.src/zigbee-button.groovy
Normal file
194
devicetypes/smartthings/zigbee-button.src/zigbee-button.groovy
Normal file
@@ -0,0 +1,194 @@
|
||||
/**
|
||||
* Iris Smart Fob
|
||||
*
|
||||
* Copyright 2015 Mitch Pond
|
||||
* Presence code adapted from SmartThings Arrival Sensor HA device type
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "ZigBee Button", namespace: "smartthings", author: "Mitch Pond") {
|
||||
capability "Battery"
|
||||
capability "Button"
|
||||
capability "Configuration"
|
||||
capability "Presence Sensor"
|
||||
capability "Sensor"
|
||||
|
||||
//fingerprint endpointId: "01", profileId: "0104", inClusters: "0000,0001,0003,0007,0020,0B05", outClusters: "0003,0006,0019", model:"3450-L", manufacturer: "CentraLite"
|
||||
}
|
||||
|
||||
preferences{
|
||||
input ("holdTime", "number", title: "Minimum time in seconds for a press to count as \"held\"",
|
||||
defaultValue: 3, displayDuringSetup: false)
|
||||
input "checkInterval", "enum", title: "Presence timeout (minutes)",
|
||||
defaultValue:"2", options: ["2", "3", "5"], displayDuringSetup: false
|
||||
input "logging", "bool", title: "Enable debug logging",
|
||||
defaultValue: false, displayDuringSetup: false
|
||||
}
|
||||
|
||||
tiles(scale: 2) {
|
||||
standardTile("presence", "device.presence", width: 4, height: 4, canChangeBackground: true) {
|
||||
state "present", label: "Present", labelIcon:"st.presence.tile.present", backgroundColor:"#53a7c0"
|
||||
state "not present", labelIcon:"st.presence.tile.not-present", backgroundColor:"#ffffff"
|
||||
}
|
||||
standardTile("button", "device.button", decoration: "flat", width: 2, height: 2) {
|
||||
state "default", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#ffffff"
|
||||
}
|
||||
valueTile("battery", "device.battery", decoration: "flat", width: 2, height: 2) {
|
||||
state "battery", label:'${currentValue}% battery', unit:""
|
||||
}
|
||||
|
||||
main (["presence"])
|
||||
details(["presence","button","battery"])
|
||||
}
|
||||
}
|
||||
|
||||
def parse(String description) {
|
||||
def descMap = zigbee.parseDescriptionAsMap(description)
|
||||
logIt descMap
|
||||
state.lastCheckin = now()
|
||||
logIt "lastCheckin = ${state.lastCheckin}"
|
||||
handlePresenceEvent(true)
|
||||
|
||||
def results = []
|
||||
if (description?.startsWith('catchall:'))
|
||||
results = parseCatchAllMessage(descMap)
|
||||
else if (description?.startsWith('read attr -'))
|
||||
results = parseReportAttributeMessage(descMap)
|
||||
else logIt(descMap, "trace")
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
def updated() {
|
||||
startTimer()
|
||||
configure()
|
||||
}
|
||||
|
||||
def configure(){
|
||||
logIt "Configuring Smart Fob..."
|
||||
[
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 6 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zdo bind 0x${device.deviceNetworkId} 2 1 6 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zdo bind 0x${device.deviceNetworkId} 3 1 6 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zdo bind 0x${device.deviceNetworkId} 4 1 6 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 1 {${device.zigbeeId}} {}", "delay 200"
|
||||
] +
|
||||
zigbee.configureReporting(0x0001,0x0020,0x20,20,20,0x01)
|
||||
}
|
||||
|
||||
def parseCatchAllMessage(descMap) {
|
||||
if (descMap?.clusterId == "0006" && descMap?.command == "01") //button pressed
|
||||
handleButtonPress(descMap.sourceEndpoint as int)
|
||||
else if (descMap?.clusterId == "0006" && descMap?.command == "00") //button released
|
||||
handleButtonRelease(descMap.sourceEndpoint as int)
|
||||
else logIt("Parse: Unhandled message: ${descMap}","trace")
|
||||
}
|
||||
|
||||
def parseReportAttributeMessage(descMap) {
|
||||
if (descMap?.cluster == "0001" && descMap?.attrId == "0020") createBatteryEvent(getBatteryLevel(descMap.value))
|
||||
else logIt descMap
|
||||
}
|
||||
|
||||
private createBatteryEvent(percent) {
|
||||
logIt "Battery level at " + percent
|
||||
return createEvent([name: "battery", value: percent])
|
||||
}
|
||||
|
||||
//this method determines if a press should count as a push or a hold and returns the relevant event type
|
||||
private handleButtonRelease(button) {
|
||||
logIt "lastPress state variable: ${state.lastPress}"
|
||||
def sequenceError = {logIt("Uh oh...missed a message? Dropping this event.", "error"); state.lastPress = null; return []}
|
||||
|
||||
if (!state.lastPress) return sequenceError()
|
||||
else if (state.lastPress.button != button) return sequenceError()
|
||||
|
||||
def currentTime = now()
|
||||
def startOfPress = state.lastPress?.time
|
||||
def timeDif = currentTime - startOfPress
|
||||
def holdTimeMillisec = (settings.holdTime?:3).toInteger() * 1000
|
||||
|
||||
state.lastPress = null //we're done with this. clear it to make error conditions easier to catch
|
||||
|
||||
if (timeDif < 0)
|
||||
//likely a message sequence issue or dropped packet. Drop this press and wait for another.
|
||||
return sequenceError()
|
||||
else if (timeDif < holdTimeMillisec)
|
||||
return createButtonEvent(button,"pushed")
|
||||
else
|
||||
return createButtonEvent(button,"held")
|
||||
}
|
||||
|
||||
private handleButtonPress(button) {
|
||||
state.lastPress = [button: button, time: now()]
|
||||
}
|
||||
|
||||
private createButtonEvent(button,action) {
|
||||
logIt "Button ${button} ${action}"
|
||||
return createEvent([
|
||||
name: "button",
|
||||
value: action,
|
||||
data:[buttonNumber: button],
|
||||
descriptionText: "${device.displayName} button ${button} was ${action}",
|
||||
isStateChange: true,
|
||||
displayed: true])
|
||||
}
|
||||
|
||||
private getBatteryLevel(rawValue) {
|
||||
def intValue = Integer.parseInt(rawValue,16)
|
||||
def min = 2.1
|
||||
def max = 3.0
|
||||
def vBatt = intValue / 10
|
||||
return ((vBatt - min) / (max - min) * 100) as int
|
||||
}
|
||||
|
||||
private handlePresenceEvent(present) {
|
||||
def wasPresent = device.currentState("presence")?.value == "present"
|
||||
if (!wasPresent && present) {
|
||||
logIt "Sensor is present"
|
||||
startTimer()
|
||||
} else if (!present) {
|
||||
logIt "Sensor is not present"
|
||||
stopTimer()
|
||||
}
|
||||
def linkText = getLinkText(device)
|
||||
def eventMap = [
|
||||
name: "presence",
|
||||
value: present ? "present" : "not present",
|
||||
linkText: linkText,
|
||||
descriptionText: "${linkText} has ${present ? 'arrived' : 'left'}",
|
||||
]
|
||||
logIt "Creating presence event: ${eventMap}"
|
||||
sendEvent(eventMap)
|
||||
}
|
||||
|
||||
private startTimer() {
|
||||
logIt "Scheduling periodic timer"
|
||||
schedule("0 * * * * ?", checkPresenceCallback)
|
||||
}
|
||||
|
||||
private stopTimer() {
|
||||
logIt "Stopping periodic timer"
|
||||
unschedule()
|
||||
}
|
||||
|
||||
def checkPresenceCallback() {
|
||||
def timeSinceLastCheckin = (now() - state.lastCheckin) / 1000
|
||||
def theCheckInterval = (checkInterval ? checkInterval as int : 2) * 60
|
||||
logIt "Sensor checked in ${timeSinceLastCheckin} seconds ago"
|
||||
if (timeSinceLastCheckin >= theCheckInterval) {
|
||||
handlePresenceEvent(false)
|
||||
}
|
||||
}
|
||||
|
||||
// ****** Utility functions ******
|
||||
|
||||
private logIt(str, logLevel = 'debug') {if (settings.logging) log."$logLevel"(str) }
|
||||
@@ -56,21 +56,17 @@ metadata {
|
||||
def parse(String description) {
|
||||
log.debug "description is $description"
|
||||
|
||||
def resultMap = zigbee.getKnownDescription(description)
|
||||
if (resultMap) {
|
||||
log.info resultMap
|
||||
if (resultMap.type == "update") {
|
||||
log.info "$device updates: ${resultMap.value}"
|
||||
}
|
||||
else if (resultMap.type == "power") {
|
||||
def powerValue
|
||||
def event = zigbee.getEvent(description)
|
||||
if (event) {
|
||||
log.info event
|
||||
if (event.name == "power") {
|
||||
if (device.getDataValue("manufacturer") != "OSRAM") { //OSRAM devices do not reliably update power
|
||||
powerValue = (resultMap.value as Integer)/10 //TODO: The divisor value needs to be set as part of configuration
|
||||
sendEvent(name: "power", value: powerValue)
|
||||
event.value = (event.value as Integer) / 10 //TODO: The divisor value needs to be set as part of configuration
|
||||
sendEvent(event)
|
||||
}
|
||||
}
|
||||
else {
|
||||
sendEvent(name: resultMap.type, value: resultMap.value)
|
||||
sendEvent(event)
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -51,15 +51,9 @@ metadata {
|
||||
def parse(String description) {
|
||||
log.debug "description is $description"
|
||||
|
||||
def resultMap = zigbee.getKnownDescription(description)
|
||||
if (resultMap) {
|
||||
log.info resultMap
|
||||
if (resultMap.type == "update") {
|
||||
log.info "$device updates: ${resultMap.value}"
|
||||
}
|
||||
else {
|
||||
sendEvent(name: resultMap.type, value: resultMap.value)
|
||||
}
|
||||
def event = zigbee.getEvent(description)
|
||||
if (event) {
|
||||
sendEvent(event)
|
||||
}
|
||||
else {
|
||||
log.warn "DID NOT PARSE MESSAGE for description : $description"
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
//@Deprecated - Moved to zll-rgbw-bulb
|
||||
|
||||
/* Philips Hue (via Zigbee)
|
||||
|
||||
Capabilities:
|
||||
@@ -22,10 +25,10 @@ Capabilities:
|
||||
Sensor
|
||||
Switch
|
||||
Switch Level
|
||||
|
||||
|
||||
Custom Commands:
|
||||
setAdjustedColor
|
||||
|
||||
|
||||
*/
|
||||
|
||||
metadata {
|
||||
@@ -41,7 +44,7 @@ metadata {
|
||||
|
||||
command "setAdjustedColor"
|
||||
|
||||
fingerprint profileId: "C05E", inClusters: "0000,0003,0004,0005,0006,0008,0300,1000", outClusters: "0019"
|
||||
//fingerprint profileId: "C05E", inClusters: "0000,0003,0004,0005,0006,0008,0300,1000", outClusters: "0019"
|
||||
}
|
||||
|
||||
// simulator metadata
|
||||
|
||||
@@ -83,32 +83,19 @@ def uninstalled() {
|
||||
}
|
||||
|
||||
def configure() {
|
||||
/*
|
||||
def cmds =
|
||||
zigbee.configSetup("${CLUSTER_DOORLOCK}", "${DOORLOCK_ATTR_LOCKSTATE}",
|
||||
"${TYPE_ENUM8}", 0, 3600, "{01}") +
|
||||
zigbee.configSetup("${CLUSTER_POWER}", "${POWER_ATTR_BATTERY_PERCENTAGE_REMAINING}",
|
||||
"${TYPE_U8}", 600, 21600, "{01}")
|
||||
*/
|
||||
def zigbeeId = device.zigbeeId
|
||||
def cmds =
|
||||
[
|
||||
"zdo bind 0x${device.deviceNetworkId} 0x${device.endpointId} 1 ${CLUSTER_DOORLOCK} {$zigbeeId} {}", "delay 200",
|
||||
"zcl global send-me-a-report ${CLUSTER_DOORLOCK} ${DOORLOCK_ATTR_LOCKSTATE} ${TYPE_ENUM8} 0 3600 {01}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 0x${device.endpointId}", "delay 200",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 0x${device.endpointId} 1 ${CLUSTER_POWER} {$zigbeeId} {}", "delay 200",
|
||||
"zcl global send-me-a-report ${CLUSTER_POWER} ${POWER_ATTR_BATTERY_PERCENTAGE_REMAINING} ${TYPE_U8} 600 21600 {01}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 0x${device.endpointId}", "delay 200",
|
||||
]
|
||||
zigbee.configureReporting(CLUSTER_DOORLOCK, DOORLOCK_ATTR_LOCKSTATE,
|
||||
TYPE_ENUM8, 0, 3600, null) +
|
||||
zigbee.configureReporting(CLUSTER_POWER, POWER_ATTR_BATTERY_PERCENTAGE_REMAINING,
|
||||
TYPE_U8, 600, 21600, 0x01)
|
||||
log.info "configure() --- cmds: $cmds"
|
||||
return cmds + refresh() // send refresh cmds as part of config
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
def cmds =
|
||||
zigbee.refreshData("${CLUSTER_DOORLOCK}", "${DOORLOCK_ATTR_LOCKSTATE}") +
|
||||
zigbee.refreshData("${CLUSTER_POWER}", "${POWER_ATTR_BATTERY_PERCENTAGE_REMAINING}")
|
||||
zigbee.readAttribute(CLUSTER_DOORLOCK, DOORLOCK_ATTR_LOCKSTATE) +
|
||||
zigbee.readAttribute(CLUSTER_POWER, POWER_ATTR_BATTERY_PERCENTAGE_REMAINING)
|
||||
log.info "refresh() --- cmds: $cmds"
|
||||
return cmds
|
||||
}
|
||||
@@ -121,34 +108,27 @@ def parse(String description) {
|
||||
map = parseReportAttributeMessage(description)
|
||||
}
|
||||
|
||||
log.debug "parse() --- Parse returned $map"
|
||||
def result = map ? createEvent(map) : null
|
||||
log.debug "parse() --- returned: $result"
|
||||
return result
|
||||
}
|
||||
|
||||
// Lock capability commands
|
||||
def lock() {
|
||||
//def cmds = zigbee.zigbeeCommand("${CLUSTER_DOORLOCK}", "${DOORLOCK_CMD_LOCK_DOOR}", "{}")
|
||||
//log.info "lock() -- cmds: $cmds"
|
||||
//return cmds
|
||||
"st cmd 0x${device.deviceNetworkId} 0x${device.endpointId} ${CLUSTER_DOORLOCK} ${DOORLOCK_CMD_LOCK_DOOR} {}"
|
||||
def cmds = zigbee.command(CLUSTER_DOORLOCK, DOORLOCK_CMD_LOCK_DOOR)
|
||||
log.info "lock() -- cmds: $cmds"
|
||||
return cmds
|
||||
}
|
||||
|
||||
def unlock() {
|
||||
//def cmds = zigbee.zigbeeCommand("${CLUSTER_DOORLOCK}", "${DOORLOCK_CMD_UNLOCK_DOOR}", "{}")
|
||||
//log.info "unlock() -- cmds: $cmds"
|
||||
//return cmds
|
||||
"st cmd 0x${device.deviceNetworkId} 0x${device.endpointId} ${CLUSTER_DOORLOCK} ${DOORLOCK_CMD_UNLOCK_DOOR} {}"
|
||||
def cmds = zigbee.command(CLUSTER_DOORLOCK, DOORLOCK_CMD_UNLOCK_DOOR)
|
||||
log.info "unlock() -- cmds: $cmds"
|
||||
return cmds
|
||||
}
|
||||
|
||||
// Private methods
|
||||
private Map parseReportAttributeMessage(String description) {
|
||||
log.trace "parseReportAttributeMessage() --- description: $description"
|
||||
|
||||
Map descMap = zigbee.parseDescriptionAsMap(description)
|
||||
|
||||
log.debug "parseReportAttributeMessage() --- descMap: $descMap"
|
||||
|
||||
Map resultMap = [:]
|
||||
if (descMap.clusterInt == CLUSTER_POWER && descMap.attrInt == POWER_ATTR_BATTERY_PERCENTAGE_REMAINING) {
|
||||
resultMap.name = "battery"
|
||||
@@ -156,18 +136,24 @@ private Map parseReportAttributeMessage(String description) {
|
||||
if (device.getDataValue("manufacturer") == "Yale") { //Handling issue with Yale locks incorrect battery reporting
|
||||
resultMap.value = Integer.parseInt(descMap.value, 16)
|
||||
}
|
||||
log.info "parseReportAttributeMessage() --- battery: ${resultMap.value}"
|
||||
}
|
||||
else if (descMap.clusterInt == CLUSTER_DOORLOCK && descMap.attrInt == DOORLOCK_ATTR_LOCKSTATE) {
|
||||
def value = Integer.parseInt(descMap.value, 16)
|
||||
def linkText = getLinkText(device)
|
||||
resultMap.name = "lock"
|
||||
resultMap.putAll([0:["value":"unknown",
|
||||
"descriptionText":"Not fully locked"],
|
||||
1:["value":"locked"],
|
||||
2:["value":"unlocked"]].get(value,
|
||||
["value":"unknown",
|
||||
"descriptionText":"Unknown lock state"]))
|
||||
log.info "parseReportAttributeMessage() --- lock: ${resultMap.value}"
|
||||
if (value == 0) {
|
||||
resultMap.value = "unknown"
|
||||
resultMap.descriptionText = "${linkText} is not fully locked"
|
||||
} else if (value == 1) {
|
||||
resultMap.value = "locked"
|
||||
resultMap.descriptionText = "${linkText} is locked"
|
||||
} else if (value == 2) {
|
||||
resultMap.value = "unlocked"
|
||||
resultMap.descriptionText = "${linkText} is unlocked"
|
||||
} else {
|
||||
resultMap.value = "unknown"
|
||||
resultMap.descriptionText = "${linkText} is in unknown lock state"
|
||||
}
|
||||
}
|
||||
else {
|
||||
log.debug "parseReportAttributeMessage() --- ignoring attribute"
|
||||
|
||||
@@ -51,22 +51,15 @@ metadata {
|
||||
// Parse incoming device messages to generate events
|
||||
def parse(String description) {
|
||||
log.debug "description is $description"
|
||||
|
||||
def resultMap = zigbee.getKnownDescription(description)
|
||||
if (resultMap) {
|
||||
log.info resultMap
|
||||
if (resultMap.type == "update") {
|
||||
log.info "$device updates: ${resultMap.value}"
|
||||
}
|
||||
else if (resultMap.type == "power") {
|
||||
def event = zigbee.getEvent(description)
|
||||
if (event) {
|
||||
if (event.name == "power") {
|
||||
def powerValue
|
||||
if (device.getDataValue("manufacturer") != "OSRAM") { //OSRAM devices do not reliably update power
|
||||
powerValue = (resultMap.value as Integer)/10 //TODO: The divisor value needs to be set as part of configuration
|
||||
sendEvent(name: "power", value: powerValue)
|
||||
}
|
||||
powerValue = (event.value as Integer)/10 //TODO: The divisor value needs to be set as part of configuration
|
||||
sendEvent(name: "power", value: powerValue)
|
||||
}
|
||||
else {
|
||||
sendEvent(name: resultMap.type, value: resultMap.value)
|
||||
sendEvent(event)
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -53,16 +53,9 @@ metadata {
|
||||
// Parse incoming device messages to generate events
|
||||
def parse(String description) {
|
||||
log.debug "description is $description"
|
||||
|
||||
def resultMap = zigbee.getKnownDescription(description)
|
||||
if (resultMap) {
|
||||
log.info resultMap
|
||||
if (resultMap.type == "update") {
|
||||
log.info "$device updates: ${resultMap.value}"
|
||||
}
|
||||
else {
|
||||
sendEvent(name: resultMap.type, value: resultMap.value)
|
||||
}
|
||||
def event = zigbee.getEvent(description)
|
||||
if (event) {
|
||||
sendEvent(event)
|
||||
}
|
||||
else {
|
||||
log.warn "DID NOT PARSE MESSAGE for description : $description"
|
||||
|
||||
@@ -73,16 +73,9 @@ metadata {
|
||||
// Parse incoming device messages to generate events
|
||||
def parse(String description) {
|
||||
log.debug "description is $description"
|
||||
|
||||
def finalResult = zigbee.getKnownDescription(description)
|
||||
if (finalResult) {
|
||||
log.info finalResult
|
||||
if (finalResult.type == "update") {
|
||||
log.info "$device updates: ${finalResult.value}"
|
||||
}
|
||||
else {
|
||||
sendEvent(name: finalResult.type, value: finalResult.value)
|
||||
}
|
||||
def event = zigbee.getEvent(description)
|
||||
if (event) {
|
||||
sendEvent(event)
|
||||
}
|
||||
else {
|
||||
log.warn "DID NOT PARSE MESSAGE for description : $description"
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* Copyright 2016 SmartThings
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
metadata {
|
||||
definition (name: "ZLL Dimmer Bulb", namespace: "smartthings", author: "SmartThings") {
|
||||
|
||||
capability "Actuator"
|
||||
capability "Configuration"
|
||||
capability "Polling"
|
||||
capability "Refresh"
|
||||
capability "Switch"
|
||||
capability "Switch Level"
|
||||
|
||||
//fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 1000", outClusters: "0000,0019"
|
||||
fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 1000", outClusters: "0019"
|
||||
//fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 1000", outClusters: "0000,0019", manufacturer: "CREE", model: "Connected A-19 60W Equivalent", deviceJoinName: "Cree Connected Bulb"
|
||||
fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 1000, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Classic A60 W clear", deviceJoinName: "OSRAM LIGHTIFY LED Smart Connected Light"
|
||||
fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 1000, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Classic A60 W clear - LIGHTIFY", deviceJoinName: "OSRAM LIGHTIFY LED Smart Connected Light"
|
||||
fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 1000", outClusters: "0019", manufacturer: "Philips", model: "LWB006", deviceJoinName: "Philips Hue White"
|
||||
}
|
||||
|
||||
// simulator metadata
|
||||
simulator {
|
||||
// status messages
|
||||
status "on": "on/off: 1"
|
||||
status "off": "on/off: 0"
|
||||
|
||||
// reply messages
|
||||
reply "zcl on-off on": "on/off: 1"
|
||||
reply "zcl on-off off": "on/off: 0"
|
||||
}
|
||||
|
||||
// UI tile definitions
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){
|
||||
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
|
||||
attributeState "on", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
attributeState "off", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
}
|
||||
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
|
||||
attributeState "level", action:"switch level.setLevel"
|
||||
}
|
||||
}
|
||||
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
main "switch"
|
||||
details(["switch", "refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
// Parse incoming device messages to generate events
|
||||
def parse(String description) {
|
||||
log.debug "description is $description"
|
||||
|
||||
def resultMap = zigbee.getEvent(description)
|
||||
if (resultMap) {
|
||||
sendEvent(resultMap)
|
||||
}
|
||||
else {
|
||||
log.debug "DID NOT PARSE MESSAGE for description : $description"
|
||||
log.debug zigbee.parseDescriptionAsMap(description)
|
||||
}
|
||||
}
|
||||
|
||||
def off() {
|
||||
zigbee.off() + ["delay 1500"] + zigbee.onOffRefresh()
|
||||
}
|
||||
|
||||
def on() {
|
||||
zigbee.on() + ["delay 1500"] + zigbee.onOffRefresh()
|
||||
}
|
||||
|
||||
def setLevel(value) {
|
||||
zigbee.setLevel(value) + ["delay 1500"] + zigbee.levelRefresh() //adding refresh because of ZLL bulb not conforming to send-me-a-report
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
zigbee.onOffRefresh() + zigbee.levelRefresh()
|
||||
}
|
||||
|
||||
def poll() {
|
||||
refresh()
|
||||
}
|
||||
|
||||
def configure() {
|
||||
log.debug "Configuring Reporting and Bindings."
|
||||
zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh()
|
||||
}
|
||||
150
devicetypes/smartthings/zll-rgbw-bulb.src/zll-rgbw-bulb.groovy
Normal file
150
devicetypes/smartthings/zll-rgbw-bulb.src/zll-rgbw-bulb.groovy
Normal file
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* Copyright 2016 SmartThings
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
metadata {
|
||||
definition (name: "ZLL RGBW Bulb", namespace: "smartthings", author: "SmartThings") {
|
||||
|
||||
capability "Actuator"
|
||||
capability "Color Control"
|
||||
capability "Color Temperature"
|
||||
capability "Configuration"
|
||||
capability "Polling"
|
||||
capability "Refresh"
|
||||
capability "Switch"
|
||||
capability "Switch Level"
|
||||
|
||||
fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300", outClusters: "0019"
|
||||
fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 1000", outClusters: "0019"
|
||||
fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 1000", outClusters: "0019", "manufacturer":"OSRAM", "model":"Classic A60 RGBW", deviceJoinName: "OSRAM LIGHTIFY LED Classic A60 RGBW"
|
||||
}
|
||||
|
||||
// UI tile definitions
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){
|
||||
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
|
||||
attributeState "on", label:'${name}', action:"switch.off", icon:"st.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"
|
||||
}
|
||||
tileAttribute ("device.color", key: "COLOR_CONTROL") {
|
||||
attributeState "color", action:"color control.setColor"
|
||||
}
|
||||
}
|
||||
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", width: 4, height: 2, inactiveLabel: false, range:"(2700..6500)") {
|
||||
state "colorTemperature", action:"color temperature.setColorTemperature"
|
||||
}
|
||||
valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "colorTemperature", label: '${currentValue} K'
|
||||
}
|
||||
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
main(["switch"])
|
||||
details(["switch", "colorTempSliderControl", "colorTemp", "refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
//Globals
|
||||
private getATTRIBUTE_HUE() { 0x0000 }
|
||||
private getATTRIBUTE_SATURATION() { 0x0001 }
|
||||
private getHUE_COMMAND() { 0x00 }
|
||||
private getSATURATION_COMMAND() { 0x03 }
|
||||
private getCOLOR_CONTROL_CLUSTER() { 0x0300 }
|
||||
private getATTRIBUTE_COLOR_TEMPERATURE() { 0x0007 }
|
||||
|
||||
// Parse incoming device messages to generate events
|
||||
def parse(String description) {
|
||||
log.debug "description is $description"
|
||||
|
||||
def finalResult = zigbee.getEvent(description)
|
||||
if (finalResult) {
|
||||
log.debug finalResult
|
||||
sendEvent(finalResult)
|
||||
}
|
||||
else {
|
||||
def zigbeeMap = zigbee.parseDescriptionAsMap(description)
|
||||
log.trace "zigbeeMap : $zigbeeMap"
|
||||
|
||||
if (zigbeeMap?.clusterInt == COLOR_CONTROL_CLUSTER) {
|
||||
if(zigbeeMap.attrInt == ATTRIBUTE_HUE){ //Hue Attribute
|
||||
def hueValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 255 * 360)
|
||||
sendEvent(name: "hue", value: hueValue, displayed:false)
|
||||
}
|
||||
else if(zigbeeMap.attrInt == ATTRIBUTE_SATURATION){ //Saturation Attribute
|
||||
def saturationValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 255 * 100)
|
||||
sendEvent(name: "saturation", value: saturationValue, displayed:false)
|
||||
}
|
||||
}
|
||||
else {
|
||||
log.info "DID NOT PARSE MESSAGE for description : $description"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def on() {
|
||||
zigbee.on() + ["delay 1500"] + zigbee.onOffRefresh()
|
||||
}
|
||||
|
||||
def off() {
|
||||
zigbee.off() + ["delay 1500"] + zigbee.onOffRefresh()
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
refreshAttributes() + configureAttributes()
|
||||
}
|
||||
|
||||
def poll() {
|
||||
refreshAttributes()
|
||||
}
|
||||
|
||||
def configure() {
|
||||
log.debug "Configuring Reporting and Bindings."
|
||||
configureAttributes() + refreshAttributes()
|
||||
}
|
||||
|
||||
def configureAttributes() {
|
||||
zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.colorTemperatureConfig() + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE, 0x20, 1, 3600, 0x01) + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION, 0x20, 1, 3600, 0x01)
|
||||
}
|
||||
|
||||
def refreshAttributes() {
|
||||
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh() + zigbee.readAttribute(0x0300, 0x00) + zigbee.readAttribute(0x0300, ATTRIBUTE_HUE) + zigbee.readAttribute(0x0300, ATTRIBUTE_SATURATION)
|
||||
}
|
||||
|
||||
def setColorTemperature(value) {
|
||||
zigbee.setColorTemperature(value) + ["delay 1500"] + zigbee.colorTemperatureRefresh()
|
||||
}
|
||||
|
||||
def setLevel(value) {
|
||||
zigbee.setLevel(value) + ["delay 1500"] + zigbee.levelRefresh() //adding refresh because of ZLL bulb not conforming to send-me-a-report
|
||||
}
|
||||
|
||||
def setColor(value){
|
||||
log.trace "setColor($value)"
|
||||
zigbee.on() + setHue(value.hue) + ["delay 300"] + setSaturation(value.saturation) + ["delay 2000"] + refreshAttributes()
|
||||
}
|
||||
|
||||
def setHue(value) {
|
||||
def scaledHueValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2)
|
||||
zigbee.command(COLOR_CONTROL_CLUSTER, HUE_COMMAND, scaledHueValue, "00", "0500") //payload-> hue value, direction (00-> shortest distance), transition time (1/10th second) (0500 in U16 reads 5)
|
||||
}
|
||||
|
||||
def setSaturation(value) {
|
||||
def scaledSatValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2)
|
||||
zigbee.command(COLOR_CONTROL_CLUSTER, SATURATION_COMMAND, scaledSatValue, "0500") //payload-> sat value, transition time
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* Copyright 2016 SmartThings
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
metadata {
|
||||
definition (name: "ZLL White Color Temperature Bulb", namespace: "smartthings", author: "SmartThings") {
|
||||
|
||||
capability "Actuator"
|
||||
capability "Color Temperature"
|
||||
capability "Configuration"
|
||||
capability "Polling"
|
||||
capability "Refresh"
|
||||
capability "Switch"
|
||||
capability "Switch Level"
|
||||
|
||||
attribute "colorName", "string"
|
||||
command "setGenericName"
|
||||
|
||||
fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 1000, 0B04, FC0F", outClusters: "0019", "manufacturer":"OSRAM", "model":"Classic A60 TW", deviceJoinName: "OSRAM LIGHTIFY LED Classic A60 Tunable White"
|
||||
fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 1000, FC0F", outClusters: "0019", "manufacturer":"OSRAM", "model":"PAR16 50 TW", deviceJoinName: "OSRAM LIGHTIFY LED PAR16 50 Tunable White"
|
||||
}
|
||||
|
||||
// UI tile definitions
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){
|
||||
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
|
||||
attributeState "on", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
attributeState "off", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
}
|
||||
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
|
||||
attributeState "level", action:"switch level.setLevel"
|
||||
}
|
||||
tileAttribute ("colorName", key: "SECONDARY_CONTROL") {
|
||||
attributeState "colorName", label:'${currentValue}'
|
||||
}
|
||||
}
|
||||
|
||||
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", width: 4, height: 2, inactiveLabel: false, range:"(2700..6500)") {
|
||||
state "colorTemperature", action:"color temperature.setColorTemperature"
|
||||
}
|
||||
valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "colorTemperature", label: '${currentValue} K'
|
||||
}
|
||||
|
||||
main(["switch"])
|
||||
details(["switch", "colorTempSliderControl", "colorTemp", "refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
// Parse incoming device messages to generate events
|
||||
def parse(String description) {
|
||||
log.debug "description is $description"
|
||||
def event = zigbee.getEvent(description)
|
||||
if (event) {
|
||||
sendEvent(event)
|
||||
}
|
||||
else {
|
||||
log.warn "DID NOT PARSE MESSAGE for description : $description"
|
||||
log.debug zigbee.parseDescriptionAsMap(description)
|
||||
}
|
||||
}
|
||||
|
||||
def off() {
|
||||
zigbee.off() + ["delay 1500"] + zigbee.onOffRefresh()
|
||||
}
|
||||
|
||||
def on() {
|
||||
zigbee.on() + ["delay 1500"] + zigbee.onOffRefresh()
|
||||
}
|
||||
|
||||
def setLevel(value) {
|
||||
zigbee.setLevel(value) + ["delay 1500"] + zigbee.levelRefresh()
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh() + zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.colorTemperatureConfig()
|
||||
}
|
||||
|
||||
def poll() {
|
||||
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh()
|
||||
}
|
||||
|
||||
def configure() {
|
||||
log.debug "Configuring Reporting and Bindings."
|
||||
zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.colorTemperatureConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh()
|
||||
}
|
||||
|
||||
def setColorTemperature(value) {
|
||||
setGenericName(value)
|
||||
zigbee.setColorTemperature(value) + ["delay 1500"] + zigbee.colorTemperatureRefresh()
|
||||
}
|
||||
|
||||
//Naming based on the wiki article here: http://en.wikipedia.org/wiki/Color_temperature
|
||||
def setGenericName(value){
|
||||
if (value != null) {
|
||||
def genericName = ""
|
||||
if (value < 3300) {
|
||||
genericName = "Soft White"
|
||||
} else if (value < 4150) {
|
||||
genericName = "Moonlight"
|
||||
} else if (value <= 5000) {
|
||||
genericName = "Cool White"
|
||||
} else {
|
||||
genericName = "Daylight"
|
||||
}
|
||||
sendEvent(name: "colorName", value: genericName)
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,10 @@ metadata {
|
||||
|
||||
fingerprint deviceId: "0x2001", inClusters: "0x30,0x80,0x84,0x85,0x86,0x72"
|
||||
fingerprint deviceId: "0x07", inClusters: "0x30"
|
||||
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
|
||||
@@ -240,17 +243,25 @@ def batteryGetCommand() {
|
||||
def retypeBasedOnMSR() {
|
||||
switch (state.MSR) {
|
||||
case "0086-0002-002D":
|
||||
log.debug("Changing device type to Z-Wave Water Sensor")
|
||||
log.debug "Changing device type to Z-Wave Water Sensor"
|
||||
setDeviceType("Z-Wave Water Sensor")
|
||||
break
|
||||
case "011F-0001-0001": // Schlage motion
|
||||
case "014A-0001-0001": // Ecolink motion
|
||||
case "014A-0004-0001": // Ecolink motion +
|
||||
case "0060-0001-0002": // Everspring SP814
|
||||
case "0060-0001-0003": // Everspring HSP02
|
||||
case "011A-0601-0901": // Enerwave ZWN-BPC
|
||||
log.debug("Changing device type to Z-Wave Motion Sensor")
|
||||
log.debug "Changing device type to Z-Wave Motion Sensor"
|
||||
setDeviceType("Z-Wave Motion Sensor")
|
||||
break
|
||||
|
||||
case "013C-0002-000D": // Philio multi +
|
||||
log.debug "Changing device type to 3-in-1 Multisensor Plus (SG)"
|
||||
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)")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ def parse(String description) {
|
||||
return result
|
||||
}
|
||||
|
||||
def sensorValueEvent(Short value) {
|
||||
def sensorValueEvent(value) {
|
||||
if (value) {
|
||||
createEvent(name: "motion", value: "active", descriptionText: "$device.displayName detected motion")
|
||||
} else {
|
||||
@@ -94,24 +94,24 @@ def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cm
|
||||
{
|
||||
def result = []
|
||||
if (cmd.notificationType == 0x07) {
|
||||
if (cmd.event == 0x01 || cmd.event == 0x02) {
|
||||
if (cmd.v1AlarmType == 0x07) { // special case for nonstandard messages from Monoprice ensors
|
||||
result << sensorValueEvent(cmd.v1AlarmLevel)
|
||||
} else if (cmd.event == 0x01 || cmd.event == 0x02 || cmd.event == 0x07 || cmd.event == 0x08) {
|
||||
result << sensorValueEvent(1)
|
||||
} else if (cmd.event == 0x00) {
|
||||
result << sensorValueEvent(0)
|
||||
} else if (cmd.event == 0x03) {
|
||||
result << createEvent(descriptionText: "$device.displayName covering was removed", isStateChange: true)
|
||||
result << response(zwave.wakeUpV1.wakeUpIntervalSet(seconds:4*3600, nodeid:zwaveHubNodeId))
|
||||
if(!state.MSR) result << response(zwave.manufacturerSpecificV2.manufacturerSpecificGet())
|
||||
result << createEvent(name: "tamper", value: "detected", descriptionText: "$device.displayName covering was removed", isStateChange: true)
|
||||
result << response(zwave.batteryV1.batteryGet())
|
||||
} else if (cmd.event == 0x05 || cmd.event == 0x06) {
|
||||
result << createEvent(descriptionText: "$device.displayName detected glass breakage", isStateChange: true)
|
||||
} else if (cmd.event == 0x07) {
|
||||
if(!state.MSR) result << response(zwave.manufacturerSpecificV2.manufacturerSpecificGet())
|
||||
result << sensorValueEvent(1)
|
||||
}
|
||||
} else if (cmd.notificationType) {
|
||||
def text = "Notification $cmd.notificationType: event ${([cmd.event] + cmd.eventParameter).join(", ")}"
|
||||
result << createEvent(name: "notification$cmd.notificationType", value: "$cmd.event", descriptionText: text, displayed: false)
|
||||
result << createEvent(name: "notification$cmd.notificationType", value: "$cmd.event", descriptionText: text, isStateChange: true, displayed: false)
|
||||
} else {
|
||||
def value = cmd.v1AlarmLevel == 255 ? "active" : cmd.v1AlarmLevel ?: "inactive"
|
||||
result << createEvent(name: "alarm $cmd.v1AlarmType", value: value, displayed: false)
|
||||
result << createEvent(name: "alarm $cmd.v1AlarmType", value: value, isStateChange: true, displayed: false)
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
@@ -61,37 +61,44 @@ def parse(String description) {
|
||||
zwaveEvent(cmd, results)
|
||||
}
|
||||
}
|
||||
// log.debug "\"$description\" parsed to ${results.inspect()}"
|
||||
log.debug "'$description' parsed to ${results.inspect()}"
|
||||
return results
|
||||
}
|
||||
|
||||
|
||||
def createSmokeOrCOEvents(name, results) {
|
||||
def text = null
|
||||
if (name == "smoke") {
|
||||
text = "$device.displayName smoke was detected!"
|
||||
// these are displayed:false because the composite event is the one we want to see in the app
|
||||
results << createEvent(name: "smoke", value: "detected", descriptionText: text, displayed: false)
|
||||
} else if (name == "carbonMonoxide") {
|
||||
text = "$device.displayName carbon monoxide was detected!"
|
||||
results << createEvent(name: "carbonMonoxide", value: "detected", descriptionText: text, displayed: false)
|
||||
} else if (name == "tested") {
|
||||
text = "$device.displayName was tested"
|
||||
results << createEvent(name: "smoke", value: "tested", descriptionText: text, displayed: false)
|
||||
results << createEvent(name: "carbonMonoxide", value: "tested", descriptionText: text, displayed: false)
|
||||
} else if (name == "smokeClear") {
|
||||
text = "$device.displayName smoke is clear"
|
||||
results << createEvent(name: "smoke", value: "clear", descriptionText: text, displayed: false)
|
||||
name = "clear"
|
||||
} else if (name == "carbonMonoxideClear") {
|
||||
text = "$device.displayName carbon monoxide is clear"
|
||||
results << createEvent(name: "carbonMonoxide", value: "clear", descriptionText: text, displayed: false)
|
||||
name = "clear"
|
||||
} else if (name == "testClear") {
|
||||
text = "$device.displayName smoke is clear"
|
||||
results << createEvent(name: "smoke", value: "clear", descriptionText: text, displayed: false)
|
||||
results << createEvent(name: "carbonMonoxide", value: "clear", displayed: false)
|
||||
name = "clear"
|
||||
switch (name) {
|
||||
case "smoke":
|
||||
text = "$device.displayName smoke was detected!"
|
||||
// these are displayed:false because the composite event is the one we want to see in the app
|
||||
results << createEvent(name: "smoke", value: "detected", descriptionText: text, displayed: false)
|
||||
break
|
||||
case "carbonMonoxide":
|
||||
text = "$device.displayName carbon monoxide was detected!"
|
||||
results << createEvent(name: "carbonMonoxide", value: "detected", descriptionText: text, displayed: false)
|
||||
break
|
||||
case "tested":
|
||||
text = "$device.displayName was tested"
|
||||
results << createEvent(name: "smoke", value: "tested", descriptionText: text, displayed: false)
|
||||
results << createEvent(name: "carbonMonoxide", value: "tested", descriptionText: text, displayed: false)
|
||||
break
|
||||
case "smokeClear":
|
||||
text = "$device.displayName smoke is clear"
|
||||
results << createEvent(name: "smoke", value: "clear", descriptionText: text, displayed: false)
|
||||
name = "clear"
|
||||
break
|
||||
case "carbonMonoxideClear":
|
||||
text = "$device.displayName carbon monoxide is clear"
|
||||
results << createEvent(name: "carbonMonoxide", value: "clear", descriptionText: text, displayed: false)
|
||||
name = "clear"
|
||||
break
|
||||
case "testClear":
|
||||
text = "$device.displayName test cleared"
|
||||
results << createEvent(name: "smoke", value: "clear", descriptionText: text, displayed: false)
|
||||
results << createEvent(name: "carbonMonoxide", value: "clear", displayed: false)
|
||||
name = "clear"
|
||||
break
|
||||
}
|
||||
// This composite event is used for updating the tile
|
||||
results << createEvent(name: "alarmState", value: name, descriptionText: text)
|
||||
@@ -117,8 +124,10 @@ def zwaveEvent(physicalgraph.zwave.commands.alarmv2.AlarmReport cmd, results) {
|
||||
createSmokeOrCOEvents(cmd.alarmLevel ? "tested" : "testClear", results)
|
||||
break
|
||||
case 13: // sent every hour -- not sure what this means, just a wake up notification?
|
||||
if (cmd.alarmLevel != 255) {
|
||||
results << createEvent(descriptionText: "$device.displayName code 13 is $cmd.alarmLevel", displayed: true)
|
||||
if (cmd.alarmLevel == 255) {
|
||||
results << createEvent(descriptionText: "$device.displayName checked in", isStateChange: false)
|
||||
} else {
|
||||
results << createEvent(descriptionText: "$device.displayName code 13 is $cmd.alarmLevel", isStateChange:true, displayed:false)
|
||||
}
|
||||
|
||||
// Clear smoke in case they pulled batteries and we missed the clear msg
|
||||
@@ -127,9 +136,8 @@ def zwaveEvent(physicalgraph.zwave.commands.alarmv2.AlarmReport cmd, results) {
|
||||
}
|
||||
|
||||
// Check battery if we don't have a recent battery event
|
||||
def prevBattery = device.currentState("battery")
|
||||
if (!prevBattery || (new Date().time - prevBattery.date.time)/60000 >= 60 * 53) {
|
||||
results << new physicalgraph.device.HubAction(zwave.batteryV1.batteryGet().format())
|
||||
if (!state.lastbatt || (now() - state.lastbatt) >= 48*60*60*1000) {
|
||||
results << response(zwave.batteryV1.batteryGet())
|
||||
}
|
||||
break
|
||||
default:
|
||||
@@ -158,12 +166,17 @@ def zwaveEvent(physicalgraph.zwave.commands.sensoralarmv1.SensorAlarmReport cmd,
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd, results) {
|
||||
results << new physicalgraph.device.HubAction(zwave.wakeUpV1.wakeUpNoMoreInformation().format())
|
||||
results << createEvent(descriptionText: "$device.displayName woke up", isStateChange: false)
|
||||
if (!state.lastbatt || (now() - state.lastbatt) >= 56*60*60*1000) {
|
||||
results << response(zwave.batteryV1.batteryGet(), "delay 2000", zwave.wakeUpV1.wakeUpNoMoreInformation())
|
||||
} else {
|
||||
results << response(zwave.wakeUpV1.wakeUpNoMoreInformation())
|
||||
}
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd, results) {
|
||||
def map = [ name: "battery", unit: "%" ]
|
||||
def map = [ name: "battery", unit: "%", isStateChange: true ]
|
||||
state.lastbatt = now()
|
||||
if (cmd.batteryLevel == 0xFF) {
|
||||
map.value = 1
|
||||
map.descriptionText = "$device.displayName battery is low!"
|
||||
|
||||
@@ -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
|
||||
[:]
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* BeaconThing Manager
|
||||
*
|
||||
* Copyright 2015 obycode
|
||||
*
|
||||
* 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: "BeaconThings Manager",
|
||||
namespace: "com.obycode",
|
||||
author: "obycode",
|
||||
description: "SmartApp to interact with the BeaconThings iOS app. Use this app to integrate iBeacons into your smart home.",
|
||||
category: "Convenience",
|
||||
iconUrl: "http://beaconthingsapp.com/images/Icon-60.png",
|
||||
iconX2Url: "http://beaconthingsapp.com/images/Icon-60@2x.png",
|
||||
iconX3Url: "http://beaconthingsapp.com/images/Icon-60@3x.png",
|
||||
oauth: true)
|
||||
|
||||
|
||||
preferences {
|
||||
section("Allow BeaconThings to talk to your home") {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
|
||||
initialize()
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
}
|
||||
|
||||
def uninstalled() {
|
||||
removeChildDevices(getChildDevices())
|
||||
}
|
||||
|
||||
mappings {
|
||||
path("/beacons") {
|
||||
action: [
|
||||
DELETE: "clearBeacons",
|
||||
POST: "addBeacon"
|
||||
]
|
||||
}
|
||||
|
||||
path("/beacons/:id") {
|
||||
action: [
|
||||
PUT: "updateBeacon",
|
||||
DELETE: "deleteBeacon"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
void clearBeacons() {
|
||||
removeChildDevices(getChildDevices())
|
||||
}
|
||||
|
||||
void addBeacon() {
|
||||
def beacon = request.JSON?.beacon
|
||||
if (beacon) {
|
||||
def beaconId = "BeaconThings"
|
||||
if (beacon.major) {
|
||||
beaconId = "$beaconId-${beacon.major}"
|
||||
if (beacon.minor) {
|
||||
beaconId = "$beaconId-${beacon.minor}"
|
||||
}
|
||||
}
|
||||
log.debug "adding beacon $beaconId"
|
||||
def d = addChildDevice("com.obycode", "BeaconThing", beaconId, null, [label:beacon.name, name:"BeaconThing", completedSetup: true])
|
||||
log.debug "addChildDevice returned $d"
|
||||
|
||||
if (beacon.present) {
|
||||
d.arrive(beacon.present)
|
||||
}
|
||||
else if (beacon.presence) {
|
||||
d.setPresence(beacon.presence)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void updateBeacon() {
|
||||
log.debug "updating beacon ${params.id}"
|
||||
def beaconDevice = getChildDevice(params.id)
|
||||
// def children = getChildDevices()
|
||||
// def beaconDevice = children.find{ d -> d.deviceNetworkId == "${params.id}" }
|
||||
if (!beaconDevice) {
|
||||
log.debug "Beacon not found directly"
|
||||
def children = getChildDevices()
|
||||
beaconDevice = children.find{ d -> d.deviceNetworkId == "${params.id}" }
|
||||
if (!beaconDevice) {
|
||||
log.debug "Beacon not found in list either"
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// This could be just updating the presence
|
||||
def presence = request.JSON?.presence
|
||||
if (presence) {
|
||||
log.debug "Setting ${beaconDevice.label} to $presence"
|
||||
beaconDevice.setPresence(presence)
|
||||
}
|
||||
|
||||
// It could be someone arriving
|
||||
def arrived = request.JSON?.arrived
|
||||
if (arrived) {
|
||||
log.debug "$arrived arrived at ${beaconDevice.label}"
|
||||
beaconDevice.arrived(arrived)
|
||||
}
|
||||
|
||||
// It could be someone left
|
||||
def left = request.JSON?.left
|
||||
if (left) {
|
||||
log.debug "$left left ${beaconDevice.label}"
|
||||
beaconDevice.left(left)
|
||||
}
|
||||
|
||||
// or it could be updating the name
|
||||
def beacon = request.JSON?.beacon
|
||||
if (beacon) {
|
||||
beaconDevice.label = beacon.name
|
||||
}
|
||||
}
|
||||
|
||||
void deleteBeacon() {
|
||||
log.debug "deleting beacon ${params.id}"
|
||||
deleteChildDevice(params.id)
|
||||
// def children = getChildDevices()
|
||||
// def beaconDevice = children.find{ d -> d.deviceNetworkId == "${params.id}" }
|
||||
// if (beaconDevice) {
|
||||
// deleteChildDevice(beaconDevice.deviceNetworkId)
|
||||
// }
|
||||
}
|
||||
|
||||
private removeChildDevices(delete) {
|
||||
delete.each {
|
||||
deleteChildDevice(it.deviceNetworkId)
|
||||
}
|
||||
}
|
||||
@@ -67,11 +67,11 @@ mappings {
|
||||
def listAllDevices() {
|
||||
def resp = []
|
||||
switches.each {
|
||||
resp << [name: it.name, label: it.label, value: it.currentValue("switch"), type: "switch", id: it.id, hub: it.hub.name]
|
||||
resp << [name: it.name, label: it.label, value: it.currentValue("switch"), type: "switch", id: it.id, hub: it.hub?.name]
|
||||
}
|
||||
|
||||
locks.each {
|
||||
resp << [name: it.name, label: it.label, value: it.currentValue("lock"), type: "lock", id: it.id, hub: it.hub.name]
|
||||
resp << [name: it.name, label: it.label, value: it.currentValue("lock"), type: "lock", id: it.id, hub: it.hub?.name]
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
]
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Door Jammed Notification
|
||||
*
|
||||
* Copyright 2015 John Rucker
|
||||
*
|
||||
* 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: "Door Jammed Notification",
|
||||
namespace: "JohnRucker",
|
||||
author: "John.Rucker@Solar-current.com",
|
||||
description: "Sends a SmartThings notification and text messages when your CoopBoss detects a door jam.",
|
||||
category: "My Apps",
|
||||
iconUrl: "http://coopboss.com/images/SmartThingsIcons/coopbossLogo.png",
|
||||
iconX2Url: "http://coopboss.com/images/SmartThingsIcons/coopbossLogo2x.png",
|
||||
iconX3Url: "http://coopboss.com/images/SmartThingsIcons/coopbossLogo3x.png")
|
||||
|
||||
preferences {
|
||||
section("When the door state changes") {
|
||||
paragraph "Send a SmartThings notification when the coop's door jammed and did not close."
|
||||
input "doorSensor", "capability.doorControl", title: "Select CoopBoss", required: true, multiple: false
|
||||
input("recipients", "contact", title: "Recipients", description: "Send notifications to") {
|
||||
input "phone", "phone", title: "Phone number?", required: true}
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
initialize()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
unsubscribe()
|
||||
initialize()
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
subscribe(doorSensor, "doorState", coopDoorStateHandler)
|
||||
}
|
||||
|
||||
def coopDoorStateHandler(evt) {
|
||||
if (evt.value == "jammed"){
|
||||
def msg = "WARNING ${doorSensor.displayName} door is jammed and did not close!"
|
||||
log.debug "WARNING ${doorSensor.displayName} door is jammed and did not close, texting $phone"
|
||||
|
||||
if (location.contactBookEnabled) {
|
||||
sendNotificationToContacts(msg, recipients)
|
||||
}
|
||||
else {
|
||||
sendPush(msg)
|
||||
if (phone) {
|
||||
sendSms(phone, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* CoopBoss Door Status to color
|
||||
*
|
||||
* Copyright 2015 John Rucker
|
||||
*
|
||||
* 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: "Door State to Color Light (Hue Bulb)",
|
||||
namespace: "JohnRucker",
|
||||
author: "John Rucker",
|
||||
description: "Change the color of your Hue bulbs based on your coop's door status.",
|
||||
category: "My Apps",
|
||||
iconUrl: "http://coopboss.com/images/SmartThingsIcons/coopbossLogo.png",
|
||||
iconX2Url: "http://coopboss.com/images/SmartThingsIcons/coopbossLogo2x.png",
|
||||
iconX3Url: "http://coopboss.com/images/SmartThingsIcons/coopbossLogo3x.png")
|
||||
|
||||
|
||||
preferences {
|
||||
section("When the door opens/closese...") {
|
||||
paragraph "Sets a Hue bulb or bulbs to a color based on your coop's door status:\r unknown = white\r open = blue\r opening = purple\r closed = green\r closing = pink\r jammed = red\r forced close = orange."
|
||||
input "doorSensor", "capability.doorControl", title: "Select CoopBoss", required: true, multiple: false
|
||||
input "bulbs", "capability.colorControl", title: "pick a bulb", required: true, multiple: true
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
initialize()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
unsubscribe()
|
||||
initialize()
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
subscribe(doorSensor, "doorState", coopDoorStateHandler)
|
||||
}
|
||||
|
||||
def coopDoorStateHandler(evt) {
|
||||
log.debug "${evt.descriptionText}, $evt.value"
|
||||
def color = "White"
|
||||
def hueColor = 100
|
||||
def saturation = 100
|
||||
Map hClr = [:]
|
||||
hClr.hex = "#FFFFFF"
|
||||
|
||||
switch(evt.value) {
|
||||
case "open":
|
||||
color = "Blue"
|
||||
break;
|
||||
case "opening":
|
||||
color = "Purple"
|
||||
break;
|
||||
case "closed":
|
||||
color = "Green"
|
||||
break;
|
||||
case "closing":
|
||||
color = "Pink"
|
||||
break;
|
||||
case "jammed":
|
||||
color = "Red"
|
||||
break;
|
||||
case "forced close":
|
||||
color = "Orange"
|
||||
break;
|
||||
case "unknown":
|
||||
color = "White"
|
||||
break;
|
||||
}
|
||||
|
||||
switch(color) {
|
||||
case "White":
|
||||
hueColor = 52
|
||||
saturation = 19
|
||||
break;
|
||||
case "Daylight":
|
||||
hueColor = 53
|
||||
saturation = 91
|
||||
break;
|
||||
case "Soft White":
|
||||
hueColor = 23
|
||||
saturation = 56
|
||||
break;
|
||||
case "Warm White":
|
||||
hueColor = 20
|
||||
saturation = 80 //83
|
||||
break;
|
||||
case "Blue":
|
||||
hueColor = 70
|
||||
hClr.hex = "#0000FF"
|
||||
break;
|
||||
case "Green":
|
||||
hueColor = 39
|
||||
hClr.hex = "#00FF00"
|
||||
break;
|
||||
case "Yellow":
|
||||
hueColor = 25
|
||||
hClr.hex = "#FFFF00"
|
||||
break;
|
||||
case "Orange":
|
||||
hueColor = 10
|
||||
hClr.hex = "#FF6000"
|
||||
break;
|
||||
case "Purple":
|
||||
hueColor = 75
|
||||
hClr.hex = "#BF7FBF"
|
||||
break;
|
||||
case "Pink":
|
||||
hueColor = 83
|
||||
hClr.hex = "#FF5F5F"
|
||||
break;
|
||||
case "Red":
|
||||
hueColor = 100
|
||||
hClr.hex = "#FF0000"
|
||||
break;
|
||||
}
|
||||
|
||||
//bulbs*.on()
|
||||
bulbs*.setHue(hueColor)
|
||||
bulbs*.setSaturation(saturation)
|
||||
bulbs*.setColor(hClr)
|
||||
|
||||
//bulbs.each{
|
||||
//it.on() // Turn the bulb on when open (this method does not come directly from the colorControl capability)
|
||||
//it.setLevel(100) // Make sure the light brightness is 100%
|
||||
//it.setHue(hueColor)
|
||||
//it.setSaturation(saturation)
|
||||
//}
|
||||
}
|
||||
@@ -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 = [
|
||||
|
||||
345
smartapps/prempoint-com/prempoint.src/prempoint.groovy
Normal file
345
smartapps/prempoint-com/prempoint.src/prempoint.groovy
Normal file
@@ -0,0 +1,345 @@
|
||||
/**
|
||||
* SmartThings service for Prempoint
|
||||
*
|
||||
* Author: Prempoint Inc. (c) 2016
|
||||
*
|
||||
*/
|
||||
definition(
|
||||
name: "Prempoint",
|
||||
namespace: "prempoint.com",
|
||||
author: "Prempoint Inc.",
|
||||
description: "SmartThings service for Prempoint",
|
||||
category: "Connections",
|
||||
iconUrl: "http://www.prempoint.com/images/social_app_emblem_50x50.png",
|
||||
iconX2Url: "http://www.prempoint.com/images/social_app_emblem_100x100.png",
|
||||
iconX3Url: "http://www.prempoint.com/images/social_app_emblem_150x150.png",
|
||||
oauth: [displayName: "Prempoint", displayLink: "http://www.prempoint.com/"])
|
||||
|
||||
preferences {
|
||||
section("Allow Prempoint to Control & Access 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 "garagedoors", "capability.garageDoorControl", title: "Which Garage Doors?", multiple: true, required: false
|
||||
//input "doors", "capability.doorControl", title: "Which Doors?", multiple: true, required: false
|
||||
input "cameras", "capability.imageCapture", title: "Which Cameras?", multiple: true, required: false
|
||||
}
|
||||
}
|
||||
|
||||
mappings {
|
||||
path("/list") {
|
||||
action: [
|
||||
GET: "listDevices"
|
||||
]
|
||||
}
|
||||
path("/switches") {
|
||||
action: [
|
||||
GET: "listSwitches"
|
||||
]
|
||||
}
|
||||
path("/switches/:id") {
|
||||
action: [
|
||||
GET: "showSwitch"
|
||||
]
|
||||
}
|
||||
path("/switches/:id/:command") {
|
||||
action: [
|
||||
GET: "updateSwitch"
|
||||
]
|
||||
}
|
||||
path("/switches/:id/:command/:level") {
|
||||
action: [
|
||||
GET: "updateSwitch"
|
||||
]
|
||||
}
|
||||
path("/locks") {
|
||||
action: [
|
||||
GET: "listLocks"
|
||||
]
|
||||
}
|
||||
path("/locks/:id") {
|
||||
action: [
|
||||
GET: "showLock"
|
||||
]
|
||||
}
|
||||
path("/locks/:id/:command") {
|
||||
action: [
|
||||
GET: "updateLock"
|
||||
]
|
||||
}
|
||||
path("/doors/:id") {
|
||||
action: [
|
||||
GET: "showDoor"
|
||||
]
|
||||
}
|
||||
path("/doors/:id/:command") {
|
||||
action: [
|
||||
GET: "updateDoor"
|
||||
]
|
||||
}
|
||||
path("/garagedoors/:id") {
|
||||
action: [
|
||||
GET: "showGarageDoor"
|
||||
]
|
||||
}
|
||||
path("/garagedoors/:id/:command") {
|
||||
action: [
|
||||
GET: "updateGarageDoor"
|
||||
]
|
||||
}
|
||||
path("/cameras/:id") {
|
||||
action: [
|
||||
GET: "showCamera"
|
||||
]
|
||||
}
|
||||
path("/cameras/:id/:command") {
|
||||
action: [
|
||||
GET: "updateCamera"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {}
|
||||
|
||||
def updated() {}
|
||||
|
||||
def listDevices() {
|
||||
log.debug "entering listDevices"
|
||||
//return listSwitches() + listLocks() + listGarageDoors() + listDoors() + listCameras()
|
||||
return listSwitches() + listLocks() + listGarageDoors() + listCameras()
|
||||
}
|
||||
|
||||
//switches
|
||||
def listSwitches() {
|
||||
log.debug "entering listSwitches"
|
||||
switches.collect{showDevice(it,"switch")}
|
||||
}
|
||||
|
||||
def showSwitch() {
|
||||
log.debug "entering showSwitches"
|
||||
show(switches, "switch")
|
||||
}
|
||||
|
||||
def updateSwitch() {
|
||||
log.debug "entering updateSwitches"
|
||||
update(switches, "switch")
|
||||
}
|
||||
|
||||
//locks
|
||||
def listLocks() {
|
||||
log.debug "entering listLocks"
|
||||
locks.collect{showDevice(it,"lock")}
|
||||
}
|
||||
|
||||
def showLock() {
|
||||
log.debug "entering showLock"
|
||||
show(locks, "lock")
|
||||
}
|
||||
|
||||
def updateLock() {
|
||||
log.debug "entering updateLock"
|
||||
update(locks, "lock")
|
||||
}
|
||||
|
||||
//doors
|
||||
def listDoors() {
|
||||
log.debug "entering listDoors"
|
||||
locks.collect{showDevice(it,"door")}
|
||||
}
|
||||
|
||||
def showDoor() {
|
||||
log.debug "entering showDoors"
|
||||
show(doors, "door")
|
||||
}
|
||||
|
||||
def updateDoor() {
|
||||
log.debug "entering updateDoor"
|
||||
update(doors, "door")
|
||||
}
|
||||
|
||||
//garagedoors
|
||||
def listGarageDoors() {
|
||||
log.debug "entering listGarageDoors"
|
||||
locks.collect{showDevice(it,"garagedoor")}
|
||||
}
|
||||
|
||||
def showGarageDoor() {
|
||||
log.debug "entering showGarageDoors"
|
||||
show(garagedoors, "garagedoor")
|
||||
}
|
||||
|
||||
def updateGarageDoor() {
|
||||
log.debug "entering updateGarageDoor"
|
||||
update(gargedoors, "garagedoor")
|
||||
}
|
||||
|
||||
//cameras
|
||||
def listCameras() {
|
||||
log.debug "entering listCameras"
|
||||
cameras.collect{showDevice(it,"image")}
|
||||
}
|
||||
|
||||
def showCamera() {
|
||||
log.debug "entering showCameras"
|
||||
show(cameras, "camera")
|
||||
}
|
||||
|
||||
def updateCamera() {
|
||||
log.debug "entering updateCamera"
|
||||
update(cameras, "camera")
|
||||
}
|
||||
|
||||
def deviceHandler(evt) {}
|
||||
|
||||
private update(devices, type) {
|
||||
def rc = null
|
||||
|
||||
//def command = request.JSON?.command
|
||||
def command = params.command
|
||||
|
||||
log.debug "update, request: params: ${params}, devices: $devices.id type=$type command=$command"
|
||||
|
||||
// Process the command.
|
||||
if (command)
|
||||
{
|
||||
def dev = devices.find { it.id == params.id }
|
||||
if (!dev) {
|
||||
httpError(404, "Device not found: $params.id")
|
||||
} else if (type == "switch") {
|
||||
switch(command) {
|
||||
case "on":
|
||||
rc = dev.on()
|
||||
break
|
||||
case "off":
|
||||
rc = dev.off()
|
||||
break
|
||||
default:
|
||||
httpError(400, "Device command=$command is not a valid for device=$it.id $dev")
|
||||
}
|
||||
} else if (type == "lock") {
|
||||
switch(command) {
|
||||
case "lock":
|
||||
rc = dev.lock()
|
||||
break
|
||||
case "unlock":
|
||||
rc = dev.unlock()
|
||||
break
|
||||
default:
|
||||
httpError(400, "Device command=$command is not a valid for device:=$it.id $dev")
|
||||
}
|
||||
} else if (type == "door") {
|
||||
switch(command) {
|
||||
case "open":
|
||||
rc = dev.open()
|
||||
break
|
||||
case "close":
|
||||
rc = dev.close()
|
||||
break
|
||||
default:
|
||||
httpError(400, "Device command=$command is not a valid for device=$it.id $dev")
|
||||
}
|
||||
} else if (type == "garagedoor") {
|
||||
switch(command) {
|
||||
case "open":
|
||||
rc = dev.open()
|
||||
break
|
||||
case "close":
|
||||
rc = dev.close()
|
||||
break
|
||||
default:
|
||||
httpError(400, "Device command=$command is not a valid for device=$it.id $dev")
|
||||
}
|
||||
} else if (type == "camera") {
|
||||
switch(command) {
|
||||
case "take":
|
||||
rc = dev.take()
|
||||
log.debug "Device command=$command device=$it.id $dev current image=$it.currentImage"
|
||||
break
|
||||
default:
|
||||
httpError(400, "Device command=$command is not a valid for device=$it.id $dev")
|
||||
}
|
||||
}
|
||||
|
||||
log.debug "executed device=$it.id $dev command=$command rc=$rc"
|
||||
|
||||
// Check that the device is a switch that is currently on, supports 'setLevel"
|
||||
// and that a level was specified.
|
||||
int level = params.level ? params.level as int : -1;
|
||||
if ((type == "switch") && (dev.currentValue('switch') == "on") && hasLevel(dev) && (level != -1)) {
|
||||
log.debug "device about to setLevel=$level"
|
||||
dev.setLevel(level);
|
||||
}
|
||||
|
||||
// Show the device info if necessary.
|
||||
if (rc == null) {
|
||||
rc = showDevice(dev, type)
|
||||
}
|
||||
}
|
||||
|
||||
return rc
|
||||
}
|
||||
|
||||
private show(devices, type) {
|
||||
def dev = devices.find { it.id == params.id }
|
||||
if (!dev) {
|
||||
httpError(404, "Device not found")
|
||||
} else {
|
||||
// Show the device info.
|
||||
showDevice(dev, type)
|
||||
}
|
||||
}
|
||||
|
||||
private showDevice(it, type) {
|
||||
def props = null
|
||||
|
||||
// Get the current state for the device type.
|
||||
def state = [it.currentState(type)]
|
||||
|
||||
// Check that whether the a switch device with level support is located and update the returned device type.
|
||||
def devType = type
|
||||
|
||||
if (type == "switch" && hasLevel(it)) {
|
||||
// Assign "switchWithLevel" to device type.
|
||||
devType = "switchWithLevel"
|
||||
// Add the level state.
|
||||
def levelState = it.currentState("level")
|
||||
if (levelState) {
|
||||
state.add(levelState)
|
||||
}
|
||||
}
|
||||
|
||||
log.debug "device label=$it.label type=$devType"
|
||||
|
||||
// Assign the device item properties if appropriate.
|
||||
if (it) {
|
||||
props = [id: it.id, label: it.label, type: devType, state: state]
|
||||
// Add the hub information to the device properties
|
||||
// if appropriate.
|
||||
if (it.hub) {
|
||||
props.put("location", it.hub.hub.location)
|
||||
}
|
||||
if (it.currentImage) {
|
||||
props.put("currentImage", it.currentImage)
|
||||
}
|
||||
}
|
||||
|
||||
return props
|
||||
}
|
||||
|
||||
private hasLevel(device) {
|
||||
// Default return value.
|
||||
def rc = false;
|
||||
|
||||
// Get the device supported commands.
|
||||
def supportedCommands = device.supportedCommands
|
||||
|
||||
// Check to see if the "setLevel" was found and assign
|
||||
// the appropriate return value.
|
||||
if (supportedCommands) {
|
||||
// Find the "setLevel" command.
|
||||
rc = supportedCommands.toString().indexOf("setLevel") != -1
|
||||
}
|
||||
|
||||
log.debug "hasLevel device label=$device.label supportedCommands=$supportedCommands rc=$rc"
|
||||
|
||||
return rc
|
||||
}
|
||||
@@ -0,0 +1,383 @@
|
||||
/**
|
||||
* Simple Sync Connect
|
||||
*
|
||||
* Copyright 2015 Roomie Remote, Inc.
|
||||
*
|
||||
* Date: 2015-09-22
|
||||
*/
|
||||
|
||||
definition(
|
||||
name: "Simple Sync Connect",
|
||||
namespace: "roomieremote-raconnect",
|
||||
author: "Roomie Remote, Inc.",
|
||||
description: "Integrate SmartThings with your Simple Control activities via Simple Sync.",
|
||||
category: "My Apps",
|
||||
iconUrl: "https://s3.amazonaws.com/roomieuser/remotes/simplesync-60.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/roomieuser/remotes/simplesync-120.png",
|
||||
iconX3Url: "https://s3.amazonaws.com/roomieuser/remotes/simplesync-120.png")
|
||||
|
||||
preferences()
|
||||
{
|
||||
page(name: "mainPage", title: "Simple Sync Setup", content: "mainPage", refreshTimeout: 5)
|
||||
page(name:"agentDiscovery", title:"Simple Sync Discovery", content:"agentDiscovery", refreshTimeout:5)
|
||||
page(name:"manualAgentEntry")
|
||||
page(name:"verifyManualEntry")
|
||||
}
|
||||
|
||||
def mainPage()
|
||||
{
|
||||
if (canInstallLabs())
|
||||
{
|
||||
return agentDiscovery()
|
||||
}
|
||||
else
|
||||
{
|
||||
def upgradeNeeded = """To use SmartThings Labs, your Hub should be completely up to date.
|
||||
|
||||
To update your Hub, access Location Settings in the Main Menu (tap the gear next to your location name), select your Hub, and choose "Update Hub"."""
|
||||
|
||||
return dynamicPage(name:"mainPage", title:"Upgrade needed!", nextPage:"", install:false, uninstall: true) {
|
||||
section("Upgrade")
|
||||
{
|
||||
paragraph "$upgradeNeeded"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def agentDiscovery(params=[:])
|
||||
{
|
||||
int refreshCount = !state.refreshCount ? 0 : state.refreshCount as int
|
||||
state.refreshCount = refreshCount + 1
|
||||
def refreshInterval = refreshCount == 0 ? 2 : 5
|
||||
|
||||
if (!state.subscribe)
|
||||
{
|
||||
subscribe(location, null, locationHandler, [filterEvents:false])
|
||||
state.subscribe = true
|
||||
}
|
||||
|
||||
//ssdp request every fifth refresh
|
||||
if ((refreshCount % 5) == 0)
|
||||
{
|
||||
discoverAgents()
|
||||
}
|
||||
|
||||
def agentsDiscovered = agentsDiscovered()
|
||||
|
||||
return dynamicPage(name:"agentDiscovery", title:"Pair with Simple Sync", nextPage:"", refreshInterval: refreshInterval, install:true, uninstall: true) {
|
||||
section("Pair with Simple Sync")
|
||||
{
|
||||
input "selectedAgent", "enum", required:true, title:"Select Simple Sync\n(${agentsDiscovered.size() ?: 0} found)", multiple:false, options:agentsDiscovered
|
||||
href(name:"manualAgentEntry",
|
||||
title:"Manually Configure Simple Sync",
|
||||
required:false,
|
||||
page:"manualAgentEntry")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def manualAgentEntry()
|
||||
{
|
||||
dynamicPage(name:"manualAgentEntry", title:"Manually Configure Simple Sync", nextPage:"verifyManualEntry", install:false, uninstall:true) {
|
||||
section("Manually Configure Simple Sync")
|
||||
{
|
||||
paragraph "In the event that Simple Sync cannot be automatically discovered by your SmartThings hub, you may enter Simple Sync's IP address here."
|
||||
input(name: "manualIPAddress", type: "text", title: "IP Address", required: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def verifyManualEntry()
|
||||
{
|
||||
def hexIP = convertIPToHexString(manualIPAddress)
|
||||
def hexPort = convertToHexString(47147)
|
||||
def uuid = "593C03D2-1DA9-4CDB-A335-6C6DC98E56C3"
|
||||
def hubId = ""
|
||||
|
||||
for (hub in location.hubs)
|
||||
{
|
||||
if (hub.localIP != null)
|
||||
{
|
||||
hubId = hub.id
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
def manualAgent = [deviceType: "04",
|
||||
mac: "unknown",
|
||||
ip: hexIP,
|
||||
port: hexPort,
|
||||
ssdpPath: "/upnp/Roomie.xml",
|
||||
ssdpUSN: "uuid:$uuid::urn:roomieremote-com:device:roomie:1",
|
||||
hub: hubId,
|
||||
verified: true,
|
||||
name: "Simple Sync $manualIPAddress"]
|
||||
|
||||
state.agents[uuid] = manualAgent
|
||||
|
||||
addOrUpdateAgent(state.agents[uuid])
|
||||
|
||||
dynamicPage(name: "verifyManualEntry", title: "Manual Configuration Complete", nextPage: "", install:true, uninstall:true) {
|
||||
section("")
|
||||
{
|
||||
paragraph("Tap Done to complete the installation process.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def discoverAgents()
|
||||
{
|
||||
def urn = getURN()
|
||||
|
||||
sendHubCommand(new physicalgraph.device.HubAction("lan discovery $urn", physicalgraph.device.Protocol.LAN))
|
||||
}
|
||||
|
||||
def agentsDiscovered()
|
||||
{
|
||||
def gAgents = getAgents()
|
||||
def agents = gAgents.findAll { it?.value?.verified == true }
|
||||
def map = [:]
|
||||
agents.each
|
||||
{
|
||||
map["${it.value.uuid}"] = it.value.name
|
||||
}
|
||||
map
|
||||
}
|
||||
|
||||
def getAgents()
|
||||
{
|
||||
if (!state.agents)
|
||||
{
|
||||
state.agents = [:]
|
||||
}
|
||||
|
||||
state.agents
|
||||
}
|
||||
|
||||
def installed()
|
||||
{
|
||||
initialize()
|
||||
}
|
||||
|
||||
def updated()
|
||||
{
|
||||
initialize()
|
||||
}
|
||||
|
||||
def initialize()
|
||||
{
|
||||
if (state.subscribe)
|
||||
{
|
||||
unsubscribe()
|
||||
state.subscribe = false
|
||||
}
|
||||
|
||||
if (selectedAgent)
|
||||
{
|
||||
addOrUpdateAgent(state.agents[selectedAgent])
|
||||
}
|
||||
}
|
||||
|
||||
def addOrUpdateAgent(agent)
|
||||
{
|
||||
def children = getChildDevices()
|
||||
def dni = agent.ip + ":" + agent.port
|
||||
def found = false
|
||||
|
||||
children.each
|
||||
{
|
||||
if ((it.getDeviceDataByName("mac") == agent.mac))
|
||||
{
|
||||
found = true
|
||||
|
||||
if (it.getDeviceNetworkId() != dni)
|
||||
{
|
||||
it.setDeviceNetworkId(dni)
|
||||
}
|
||||
}
|
||||
else if (it.getDeviceNetworkId() == dni)
|
||||
{
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
if (!found)
|
||||
{
|
||||
addChildDevice("roomieremote-agent", "Simple Sync", dni, agent.hub, [label: "Simple Sync"])
|
||||
}
|
||||
}
|
||||
|
||||
def locationHandler(evt)
|
||||
{
|
||||
def description = evt?.description
|
||||
def urn = getURN()
|
||||
def hub = evt?.hubId
|
||||
def parsedEvent = parseEventMessage(description)
|
||||
|
||||
parsedEvent?.putAt("hub", hub)
|
||||
|
||||
//SSDP DISCOVERY EVENTS
|
||||
if (parsedEvent?.ssdpTerm?.contains(urn))
|
||||
{
|
||||
def agent = parsedEvent
|
||||
def ip = convertHexToIP(agent.ip)
|
||||
def agents = getAgents()
|
||||
|
||||
agent.verified = true
|
||||
agent.name = "Simple Sync $ip"
|
||||
|
||||
if (!agents[agent.uuid])
|
||||
{
|
||||
state.agents[agent.uuid] = agent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def parseEventMessage(String description)
|
||||
{
|
||||
def event = [:]
|
||||
def parts = description.split(',')
|
||||
|
||||
parts.each
|
||||
{ part ->
|
||||
part = part.trim()
|
||||
if (part.startsWith('devicetype:'))
|
||||
{
|
||||
def valueString = part.split(":")[1].trim()
|
||||
event.devicetype = valueString
|
||||
}
|
||||
else if (part.startsWith('mac:'))
|
||||
{
|
||||
def valueString = part.split(":")[1].trim()
|
||||
if (valueString)
|
||||
{
|
||||
event.mac = valueString
|
||||
}
|
||||
}
|
||||
else if (part.startsWith('networkAddress:'))
|
||||
{
|
||||
def valueString = part.split(":")[1].trim()
|
||||
if (valueString)
|
||||
{
|
||||
event.ip = valueString
|
||||
}
|
||||
}
|
||||
else if (part.startsWith('deviceAddress:'))
|
||||
{
|
||||
def valueString = part.split(":")[1].trim()
|
||||
if (valueString)
|
||||
{
|
||||
event.port = valueString
|
||||
}
|
||||
}
|
||||
else if (part.startsWith('ssdpPath:'))
|
||||
{
|
||||
def valueString = part.split(":")[1].trim()
|
||||
if (valueString)
|
||||
{
|
||||
event.ssdpPath = valueString
|
||||
}
|
||||
}
|
||||
else if (part.startsWith('ssdpUSN:'))
|
||||
{
|
||||
part -= "ssdpUSN:"
|
||||
def valueString = part.trim()
|
||||
if (valueString)
|
||||
{
|
||||
event.ssdpUSN = valueString
|
||||
|
||||
def uuid = getUUIDFromUSN(valueString)
|
||||
|
||||
if (uuid)
|
||||
{
|
||||
event.uuid = uuid
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (part.startsWith('ssdpTerm:'))
|
||||
{
|
||||
part -= "ssdpTerm:"
|
||||
def valueString = part.trim()
|
||||
if (valueString)
|
||||
{
|
||||
event.ssdpTerm = valueString
|
||||
}
|
||||
}
|
||||
else if (part.startsWith('headers'))
|
||||
{
|
||||
part -= "headers:"
|
||||
def valueString = part.trim()
|
||||
if (valueString)
|
||||
{
|
||||
event.headers = valueString
|
||||
}
|
||||
}
|
||||
else if (part.startsWith('body'))
|
||||
{
|
||||
part -= "body:"
|
||||
def valueString = part.trim()
|
||||
if (valueString)
|
||||
{
|
||||
event.body = valueString
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
event
|
||||
}
|
||||
|
||||
def getURN()
|
||||
{
|
||||
return "urn:roomieremote-com:device:roomie:1"
|
||||
}
|
||||
|
||||
def getUUIDFromUSN(usn)
|
||||
{
|
||||
def parts = usn.split(":")
|
||||
|
||||
for (int i = 0; i < parts.size(); ++i)
|
||||
{
|
||||
if (parts[i] == "uuid")
|
||||
{
|
||||
return parts[i + 1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def String convertHexToIP(hex)
|
||||
{
|
||||
[convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
|
||||
}
|
||||
|
||||
def Integer convertHexToInt(hex)
|
||||
{
|
||||
Integer.parseInt(hex,16)
|
||||
}
|
||||
|
||||
def String convertToHexString(n)
|
||||
{
|
||||
String hex = String.format("%X", n.toInteger())
|
||||
}
|
||||
|
||||
def String convertIPToHexString(ipString)
|
||||
{
|
||||
String hex = ipString.tokenize(".").collect {
|
||||
String.format("%02X", it.toInteger())
|
||||
}.join()
|
||||
}
|
||||
|
||||
def Boolean canInstallLabs()
|
||||
{
|
||||
return hasAllHubsOver("000.011.00603")
|
||||
}
|
||||
|
||||
def Boolean hasAllHubsOver(String desiredFirmware)
|
||||
{
|
||||
return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware }
|
||||
}
|
||||
|
||||
def List getRealHubFirmwareVersions()
|
||||
{
|
||||
return location.hubs*.firmwareVersionString.findAll { it }
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
/**
|
||||
* Simple Sync Trigger
|
||||
*
|
||||
* Copyright 2015 Roomie Remote, Inc.
|
||||
*
|
||||
* Date: 2015-09-22
|
||||
*/
|
||||
definition(
|
||||
name: "Simple Sync Trigger",
|
||||
namespace: "roomieremote-ratrigger",
|
||||
author: "Roomie Remote, Inc.",
|
||||
description: "Trigger Simple Control activities when certain actions take place in your home.",
|
||||
category: "My Apps",
|
||||
iconUrl: "https://s3.amazonaws.com/roomieuser/remotes/simplesync-60.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/roomieuser/remotes/simplesync-120.png",
|
||||
iconX3Url: "https://s3.amazonaws.com/roomieuser/remotes/simplesync-120.png")
|
||||
|
||||
|
||||
preferences {
|
||||
page(name: "agentSelection", title: "Select your Simple Sync")
|
||||
page(name: "refreshActivities", title: "Updating list of Simple Sync activities")
|
||||
page(name: "control", title: "Run a Simple Control activity when something happens")
|
||||
page(name: "timeIntervalInput", title: "Only during a certain time", install: true, uninstall: true) {
|
||||
section {
|
||||
input "starting", "time", title: "Starting", required: false
|
||||
input "ending", "time", title: "Ending", required: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def agentSelection()
|
||||
{
|
||||
if (agent)
|
||||
{
|
||||
state.refreshCount = 0
|
||||
}
|
||||
|
||||
dynamicPage(name: "agentSelection", title: "Select your Simple Sync", nextPage: "control", install: false, uninstall: true) {
|
||||
section {
|
||||
input "agent", "capability.mediaController", title: "Simple Sync", required: true, multiple: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def control()
|
||||
{
|
||||
def activities = agent.latestValue('activities')
|
||||
|
||||
if (!activities || !state.refreshCount)
|
||||
{
|
||||
int refreshCount = !state.refreshCount ? 0 : state.refreshCount as int
|
||||
state.refreshCount = refreshCount + 1
|
||||
def refreshInterval = refreshCount == 0 ? 2 : 4
|
||||
|
||||
// Request activities every 5th attempt
|
||||
if((refreshCount % 5) == 0)
|
||||
{
|
||||
agent.getAllActivities()
|
||||
}
|
||||
|
||||
dynamicPage(name: "control", title: "Updating list of Simple Control activities", nextPage: "", refreshInterval: refreshInterval, install: false, uninstall: true) {
|
||||
section("") {
|
||||
paragraph "Retrieving activities from Simple Sync"
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
dynamicPage(name: "control", title: "Run a Simple Control activity when something happens", nextPage: "timeIntervalInput", install: false, uninstall: true) {
|
||||
def anythingSet = anythingSet()
|
||||
if (anythingSet) {
|
||||
section("When..."){
|
||||
ifSet "motion", "capability.motionSensor", title: "Motion Detected", required: false, multiple: true
|
||||
ifSet "motionInactive", "capability.motionSensor", title: "Motion Stops", required: false, multiple: true
|
||||
ifSet "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true
|
||||
ifSet "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true
|
||||
ifSet "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true
|
||||
ifSet "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true
|
||||
ifSet "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true
|
||||
ifSet "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true
|
||||
ifSet "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true
|
||||
ifSet "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production
|
||||
ifSet "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true
|
||||
ifSet "timeOfDay", "time", title: "At a Scheduled Time", required: false
|
||||
}
|
||||
}
|
||||
section(anythingSet ? "Select additional triggers" : "When...", hideable: anythingSet, hidden: true){
|
||||
ifUnset "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true
|
||||
ifUnset "motionInactive", "capability.motionSensor", title: "Motion Stops", required: false, multiple: true
|
||||
ifUnset "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true
|
||||
ifUnset "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true
|
||||
ifUnset "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true
|
||||
ifUnset "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true
|
||||
ifUnset "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true
|
||||
ifUnset "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true
|
||||
ifUnset "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true
|
||||
ifUnset "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production
|
||||
ifUnset "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true
|
||||
ifUnset "timeOfDay", "time", title: "At a Scheduled Time", required: false
|
||||
}
|
||||
section("Run this activity"){
|
||||
input "activity", "enum", title: "Activity?", required: true, options: new groovy.json.JsonSlurper().parseText(activities ?: "[]").activities?.collect { ["${it.uuid}": it.name] }
|
||||
}
|
||||
|
||||
section("More options", hideable: true, hidden: true) {
|
||||
input "frequency", "decimal", title: "Minimum time between actions (defaults to every event)", description: "Minutes", required: false
|
||||
href "timeIntervalInput", title: "Only during a certain time", description: timeLabel ?: "Tap to set", state: timeLabel ? "complete" : "incomplete"
|
||||
input "days", "enum", title: "Only on certain days of the week", multiple: true, required: false,
|
||||
options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
|
||||
input "modes", "mode", title: "Only when mode is", multiple: true, required: false
|
||||
input "oncePerDay", "bool", title: "Only once per day", required: false, defaultValue: false
|
||||
}
|
||||
section([mobileOnly:true]) {
|
||||
label title: "Assign a name", required: false
|
||||
mode title: "Set for specific mode(s)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private anythingSet() {
|
||||
for (name in ["motion","motionInactive","contact","contactClosed","acceleration","mySwitch","mySwitchOff","arrivalPresence","departurePresence","button1","triggerModes","timeOfDay"]) {
|
||||
if (settings[name]) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private ifUnset(Map options, String name, String capability) {
|
||||
if (!settings[name]) {
|
||||
input(options, name, capability)
|
||||
}
|
||||
}
|
||||
|
||||
private ifSet(Map options, String name, String capability) {
|
||||
if (settings[name]) {
|
||||
input(options, name, capability)
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
subscribeToEvents()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
unsubscribe()
|
||||
unschedule()
|
||||
subscribeToEvents()
|
||||
}
|
||||
|
||||
def subscribeToEvents() {
|
||||
log.trace "subscribeToEvents()"
|
||||
subscribe(app, appTouchHandler)
|
||||
subscribe(contact, "contact.open", eventHandler)
|
||||
subscribe(contactClosed, "contact.closed", eventHandler)
|
||||
subscribe(acceleration, "acceleration.active", eventHandler)
|
||||
subscribe(motion, "motion.active", eventHandler)
|
||||
subscribe(motionInactive, "motion.inactive", eventHandler)
|
||||
subscribe(mySwitch, "switch.on", eventHandler)
|
||||
subscribe(mySwitchOff, "switch.off", eventHandler)
|
||||
subscribe(arrivalPresence, "presence.present", eventHandler)
|
||||
subscribe(departurePresence, "presence.not present", eventHandler)
|
||||
subscribe(button1, "button.pushed", eventHandler)
|
||||
|
||||
if (triggerModes) {
|
||||
subscribe(location, modeChangeHandler)
|
||||
}
|
||||
|
||||
if (timeOfDay) {
|
||||
schedule(timeOfDay, scheduledTimeHandler)
|
||||
}
|
||||
}
|
||||
|
||||
def eventHandler(evt) {
|
||||
if (allOk) {
|
||||
def lastTime = state[frequencyKey(evt)]
|
||||
if (oncePerDayOk(lastTime)) {
|
||||
if (frequency) {
|
||||
if (lastTime == null || now() - lastTime >= frequency * 60000) {
|
||||
startActivity(evt)
|
||||
}
|
||||
else {
|
||||
log.debug "Not taking action because $frequency minutes have not elapsed since last action"
|
||||
}
|
||||
}
|
||||
else {
|
||||
startActivity(evt)
|
||||
}
|
||||
}
|
||||
else {
|
||||
log.debug "Not taking action because it was already taken today"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def modeChangeHandler(evt) {
|
||||
log.trace "modeChangeHandler $evt.name: $evt.value ($triggerModes)"
|
||||
if (evt.value in triggerModes) {
|
||||
eventHandler(evt)
|
||||
}
|
||||
}
|
||||
|
||||
def scheduledTimeHandler() {
|
||||
eventHandler(null)
|
||||
}
|
||||
|
||||
def appTouchHandler(evt) {
|
||||
startActivity(evt)
|
||||
}
|
||||
|
||||
private startActivity(evt) {
|
||||
agent.startActivity(activity)
|
||||
|
||||
if (frequency) {
|
||||
state.lastActionTimeStamp = now()
|
||||
}
|
||||
}
|
||||
|
||||
private frequencyKey(evt) {
|
||||
//evt.deviceId ?: evt.value
|
||||
"lastActionTimeStamp"
|
||||
}
|
||||
|
||||
private dayString(Date date) {
|
||||
def df = new java.text.SimpleDateFormat("yyyy-MM-dd")
|
||||
if (location.timeZone) {
|
||||
df.setTimeZone(location.timeZone)
|
||||
}
|
||||
else {
|
||||
df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
|
||||
}
|
||||
df.format(date)
|
||||
}
|
||||
|
||||
private oncePerDayOk(Long lastTime) {
|
||||
def result = true
|
||||
if (oncePerDay) {
|
||||
result = lastTime ? dayString(new Date()) != dayString(new Date(lastTime)) : true
|
||||
log.trace "oncePerDayOk = $result"
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
// TODO - centralize somehow
|
||||
private getAllOk() {
|
||||
modeOk && daysOk && timeOk
|
||||
}
|
||||
|
||||
private getModeOk() {
|
||||
def result = !modes || modes.contains(location.mode)
|
||||
log.trace "modeOk = $result"
|
||||
result
|
||||
}
|
||||
|
||||
private getDaysOk() {
|
||||
def result = true
|
||||
if (days) {
|
||||
def df = new java.text.SimpleDateFormat("EEEE")
|
||||
if (location.timeZone) {
|
||||
df.setTimeZone(location.timeZone)
|
||||
}
|
||||
else {
|
||||
df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
|
||||
}
|
||||
def day = df.format(new Date())
|
||||
result = days.contains(day)
|
||||
}
|
||||
log.trace "daysOk = $result"
|
||||
result
|
||||
}
|
||||
|
||||
private getTimeOk() {
|
||||
def result = true
|
||||
if (starting && ending) {
|
||||
def currTime = now()
|
||||
def start = timeToday(starting).time
|
||||
def stop = timeToday(ending).time
|
||||
result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start
|
||||
}
|
||||
log.trace "timeOk = $result"
|
||||
result
|
||||
}
|
||||
|
||||
private hhmm(time, fmt = "h:mm a")
|
||||
{
|
||||
def t = timeToday(time, location.timeZone)
|
||||
def f = new java.text.SimpleDateFormat(fmt)
|
||||
f.setTimeZone(location.timeZone ?: timeZone(time))
|
||||
f.format(t)
|
||||
}
|
||||
|
||||
private timeIntervalLabel()
|
||||
{
|
||||
(starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z") : ""
|
||||
}
|
||||
@@ -0,0 +1,774 @@
|
||||
/**
|
||||
* Simple Control
|
||||
*
|
||||
* Copyright 2015 Roomie Remote, Inc.
|
||||
*
|
||||
* Date: 2015-09-22
|
||||
*/
|
||||
|
||||
definition(
|
||||
name: "Simple Control",
|
||||
namespace: "roomieremote-roomieconnect",
|
||||
author: "Roomie Remote, Inc.",
|
||||
description: "Integrate SmartThings with your Simple Control activities.",
|
||||
category: "My Apps",
|
||||
iconUrl: "https://s3.amazonaws.com/roomieuser/remotes/simplesync-60.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/roomieuser/remotes/simplesync-120.png",
|
||||
iconX3Url: "https://s3.amazonaws.com/roomieuser/remotes/simplesync-120.png")
|
||||
|
||||
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)
|
||||
page(name:"agentDiscovery", title:"Simple Sync Discovery", content:"agentDiscovery", refreshTimeout:5)
|
||||
page(name:"manualAgentEntry")
|
||||
page(name:"verifyManualEntry")
|
||||
}
|
||||
|
||||
mappings {
|
||||
path("/devices") {
|
||||
action: [
|
||||
GET: "getDevices"
|
||||
]
|
||||
}
|
||||
path("/:deviceType/devices") {
|
||||
action: [
|
||||
GET: "getDevices",
|
||||
POST: "handleDevicesWithIDs"
|
||||
]
|
||||
}
|
||||
path("/device/:deviceType/:id") {
|
||||
action: [
|
||||
GET: "getDevice",
|
||||
POST: "updateDevice"
|
||||
]
|
||||
}
|
||||
path("/subscriptions") {
|
||||
action: [
|
||||
GET: "listSubscriptions",
|
||||
POST: "addSubscription", // {"deviceId":"xxx", "attributeName":"xxx","callbackUrl":"http://..."}
|
||||
DELETE: "removeAllSubscriptions"
|
||||
]
|
||||
}
|
||||
path("/subscriptions/:id") {
|
||||
action: [
|
||||
DELETE: "removeSubscription"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
private getAllDevices()
|
||||
{
|
||||
//log.debug("getAllDevices()")
|
||||
([] + switches + locks + thermostats + imageCaptures + relaySwitches + doorControls + colorControls + musicPlayers + speechSynthesizers + switchLevels + indicators + mediaControllers + tones + tvs + alarms + valves + motionSensors + presenceSensors + beacons + pushButtons + smokeDetectors + coDetectors + contactSensors + accelerationSensors + energyMeters + powerMeters + lightSensors + humiditySensors + temperatureSensors + speechRecognizers + stepSensors + touchSensors)?.findAll()?.unique { it.id }
|
||||
}
|
||||
|
||||
def getDevices()
|
||||
{
|
||||
//log.debug("getDevices, params: ${params}")
|
||||
allDevices.collect {
|
||||
//log.debug("device: ${it}")
|
||||
deviceItem(it)
|
||||
}
|
||||
}
|
||||
|
||||
def getDevice()
|
||||
{
|
||||
//log.debug("getDevice, params: ${params}")
|
||||
def device = allDevices.find { it.id == params.id }
|
||||
if (!device)
|
||||
{
|
||||
render status: 404, data: '{"msg": "Device not found"}'
|
||||
}
|
||||
else
|
||||
{
|
||||
deviceItem(device)
|
||||
}
|
||||
}
|
||||
|
||||
def handleDevicesWithIDs()
|
||||
{
|
||||
//log.debug("handleDevicesWithIDs, params: ${params}")
|
||||
def data = request.JSON
|
||||
def ids = data?.ids?.findAll()?.unique()
|
||||
//log.debug("ids: ${ids}")
|
||||
def command = data?.command
|
||||
def arguments = data?.arguments
|
||||
def type = params?.deviceType
|
||||
//log.debug("device type: ${type}")
|
||||
if (command)
|
||||
{
|
||||
def statusCode = 404
|
||||
//log.debug("command ${command}, arguments ${arguments}")
|
||||
for (devId in ids)
|
||||
{
|
||||
def device = allDevices.find { it.id == devId }
|
||||
//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"()
|
||||
}
|
||||
statusCode = 200
|
||||
} else {
|
||||
statusCode = 403
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
else
|
||||
{
|
||||
ids.collect {
|
||||
def currentId = it
|
||||
def device = allDevices.find { it.id == currentId }
|
||||
if (device)
|
||||
{
|
||||
deviceItem(device)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private deviceItem(device) {
|
||||
[
|
||||
id: device.id,
|
||||
label: device.displayName,
|
||||
currentState: device.currentStates,
|
||||
capabilities: device.capabilities?.collect {[
|
||||
name: it.name
|
||||
]},
|
||||
attributes: device.supportedAttributes?.collect {[
|
||||
name: it.name,
|
||||
dataType: it.dataType,
|
||||
values: it.values
|
||||
]},
|
||||
commands: device.supportedCommands?.collect {[
|
||||
name: it.name,
|
||||
arguments: it.arguments
|
||||
]},
|
||||
type: [
|
||||
name: device.typeName,
|
||||
author: device.typeAuthor
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
def updateDevice()
|
||||
{
|
||||
//log.debug("updateDevice, params: ${params}")
|
||||
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) {
|
||||
// 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 {
|
||||
statusCode = 403
|
||||
}
|
||||
}
|
||||
|
||||
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()"
|
||||
app.subscriptions?.findAll { it.deviceId }?.collect {
|
||||
def deviceInfo = state[it.deviceId]
|
||||
def response = [
|
||||
id: it.id,
|
||||
deviceId: it.deviceId,
|
||||
attributeName: it.data,
|
||||
handler: it.handler
|
||||
]
|
||||
//if (!selectedAgent) {
|
||||
response.callbackUrl = deviceInfo?.callbackUrl
|
||||
//}
|
||||
response
|
||||
} ?: []
|
||||
}
|
||||
|
||||
def addSubscription() {
|
||||
def data = request.JSON
|
||||
def attribute = data.attributeName
|
||||
def callbackUrl = data.callbackUrl
|
||||
|
||||
//log.debug "addSubscription, params: ${params}, request: ${data}"
|
||||
if (!attribute) {
|
||||
render status: 400, data: '{"msg": "attributeName is required"}'
|
||||
} else {
|
||||
def device = allDevices.find { it.id == data.deviceId }
|
||||
if (device) {
|
||||
//if (!selectedAgent) {
|
||||
//log.debug "Adding callbackUrl: $callbackUrl"
|
||||
state[device.id] = [callbackUrl: callbackUrl]
|
||||
//}
|
||||
//log.debug "Adding subscription"
|
||||
def subscription = subscribe(device, attribute, deviceHandler)
|
||||
if (!subscription || !subscription.eventSubscription) {
|
||||
//log.debug("subscriptions: ${app.subscriptions}")
|
||||
//for (sub in app.subscriptions)
|
||||
//{
|
||||
//log.debug("subscription.id ${sub.id} subscription.handler ${sub.handler} subscription.deviceId ${sub.deviceId}")
|
||||
//log.debug(sub.properties.collect{it}.join('\n'))
|
||||
//}
|
||||
subscription = app.subscriptions?.find { it.device.id == data.deviceId && it.data == attribute && it.handler == 'deviceHandler' }
|
||||
}
|
||||
|
||||
def response = [
|
||||
id: subscription.id,
|
||||
deviceId: subscription.device?.id,
|
||||
attributeName: subscription.data,
|
||||
handler: subscription.handler
|
||||
]
|
||||
//if (!selectedAgent) {
|
||||
response.callbackUrl = callbackUrl
|
||||
//}
|
||||
response
|
||||
} else {
|
||||
render status: 400, data: '{"msg": "Device not found"}'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def removeSubscription()
|
||||
{
|
||||
def subscription = app.subscriptions?.find { it.id == params.id }
|
||||
def device = subscription?.device
|
||||
|
||||
//log.debug "removeSubscription, params: ${params}, subscription: ${subscription}, device: ${device}"
|
||||
if (device) {
|
||||
//log.debug "Removing subscription for device: ${device.id}"
|
||||
state.remove(device.id)
|
||||
unsubscribe(device)
|
||||
}
|
||||
render status: 204, data: "{}"
|
||||
}
|
||||
|
||||
def removeAllSubscriptions()
|
||||
{
|
||||
for (sub in app.subscriptions)
|
||||
{
|
||||
//log.debug("Subscription: ${sub}")
|
||||
//log.debug(sub.properties.collect{it}.join('\n'))
|
||||
def handler = sub.handler
|
||||
def device = sub.device
|
||||
|
||||
if (device && handler == 'deviceHandler')
|
||||
{
|
||||
//log.debug(device.properties.collect{it}.join('\n'))
|
||||
//log.debug("Removing subscription for device: ${device}")
|
||||
state.remove(device.id)
|
||||
unsubscribe(device)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def deviceHandler(evt) {
|
||||
def deviceInfo = state[evt.deviceId]
|
||||
//if (selectedAgent) {
|
||||
// sendToRoomie(evt, agentCallbackUrl)
|
||||
//} else if (deviceInfo) {
|
||||
if (deviceInfo)
|
||||
{
|
||||
if (deviceInfo.callbackUrl) {
|
||||
sendToRoomie(evt, deviceInfo.callbackUrl)
|
||||
} else {
|
||||
log.warn "No callbackUrl set for device: ${evt.deviceId}"
|
||||
}
|
||||
} else {
|
||||
log.warn "No subscribed device found for device: ${evt.deviceId}"
|
||||
}
|
||||
}
|
||||
|
||||
def sendToRoomie(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 mainPage()
|
||||
{
|
||||
if (canInstallLabs())
|
||||
{
|
||||
return agentDiscovery()
|
||||
}
|
||||
else
|
||||
{
|
||||
def upgradeNeeded = """To use SmartThings Labs, your Hub should be completely up to date.
|
||||
|
||||
To update your Hub, access Location Settings in the Main Menu (tap the gear next to your location name), select your Hub, and choose "Update Hub"."""
|
||||
|
||||
return dynamicPage(name:"mainPage", title:"Upgrade needed!", nextPage:"", install:false, uninstall: true) {
|
||||
section("Upgrade")
|
||||
{
|
||||
paragraph "$upgradeNeeded"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def agentDiscovery(params=[:])
|
||||
{
|
||||
int refreshCount = !state.refreshCount ? 0 : state.refreshCount as int
|
||||
state.refreshCount = refreshCount + 1
|
||||
def refreshInterval = refreshCount == 0 ? 2 : 5
|
||||
|
||||
if (!state.subscribe)
|
||||
{
|
||||
subscribe(location, null, locationHandler, [filterEvents:false])
|
||||
state.subscribe = true
|
||||
}
|
||||
|
||||
//ssdp request every fifth refresh
|
||||
if ((refreshCount % 5) == 0)
|
||||
{
|
||||
discoverAgents()
|
||||
}
|
||||
|
||||
def agentsDiscovered = agentsDiscovered()
|
||||
|
||||
return dynamicPage(name:"agentDiscovery", title:"Pair with Simple Sync", nextPage:"", refreshInterval: refreshInterval, install:true, uninstall: true) {
|
||||
section("Pair with Simple Sync")
|
||||
{
|
||||
input "selectedAgent", "enum", required:false, title:"Select Simple Sync\n(${agentsDiscovered.size() ?: 0} found)", multiple:false, options:agentsDiscovered
|
||||
href(name:"manualAgentEntry",
|
||||
title:"Manually Configure Simple Sync",
|
||||
required:false,
|
||||
page:"manualAgentEntry")
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def manualAgentEntry()
|
||||
{
|
||||
dynamicPage(name:"manualAgentEntry", title:"Manually Configure Simple Sync", nextPage:"verifyManualEntry", install:false, uninstall:true) {
|
||||
section("Manually Configure Simple Sync")
|
||||
{
|
||||
paragraph "In the event that Simple Sync cannot be automatically discovered by your SmartThings hub, you may enter Simple Sync's IP address here."
|
||||
input(name: "manualIPAddress", type: "text", title: "IP Address", required: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def verifyManualEntry()
|
||||
{
|
||||
def hexIP = convertIPToHexString(manualIPAddress)
|
||||
def hexPort = convertToHexString(47147)
|
||||
def uuid = "593C03D2-1DA9-4CDB-A335-6C6DC98E56C3"
|
||||
def hubId = ""
|
||||
|
||||
for (hub in location.hubs)
|
||||
{
|
||||
if (hub.localIP != null)
|
||||
{
|
||||
hubId = hub.id
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
def manualAgent = [deviceType: "04",
|
||||
mac: "unknown",
|
||||
ip: hexIP,
|
||||
port: hexPort,
|
||||
ssdpPath: "/upnp/Roomie.xml",
|
||||
ssdpUSN: "uuid:$uuid::urn:roomieremote-com:device:roomie:1",
|
||||
hub: hubId,
|
||||
verified: true,
|
||||
name: "Simple Sync $manualIPAddress"]
|
||||
|
||||
state.agents[uuid] = manualAgent
|
||||
|
||||
addOrUpdateAgent(state.agents[uuid])
|
||||
|
||||
dynamicPage(name: "verifyManualEntry", title: "Manual Configuration Complete", nextPage: "", install:true, uninstall:true) {
|
||||
section("")
|
||||
{
|
||||
paragraph("Tap Done to complete the installation process.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def discoverAgents()
|
||||
{
|
||||
def urn = getURN()
|
||||
|
||||
sendHubCommand(new physicalgraph.device.HubAction("lan discovery $urn", physicalgraph.device.Protocol.LAN))
|
||||
}
|
||||
|
||||
def agentsDiscovered()
|
||||
{
|
||||
def gAgents = getAgents()
|
||||
def agents = gAgents.findAll { it?.value?.verified == true }
|
||||
def map = [:]
|
||||
agents.each
|
||||
{
|
||||
map["${it.value.uuid}"] = it.value.name
|
||||
}
|
||||
map
|
||||
}
|
||||
|
||||
def getAgents()
|
||||
{
|
||||
if (!state.agents)
|
||||
{
|
||||
state.agents = [:]
|
||||
}
|
||||
|
||||
state.agents
|
||||
}
|
||||
|
||||
def installed()
|
||||
{
|
||||
initialize()
|
||||
}
|
||||
|
||||
def updated()
|
||||
{
|
||||
initialize()
|
||||
}
|
||||
|
||||
def initialize()
|
||||
{
|
||||
if (state.subscribe)
|
||||
{
|
||||
unsubscribe()
|
||||
state.subscribe = false
|
||||
}
|
||||
|
||||
if (selectedAgent)
|
||||
{
|
||||
addOrUpdateAgent(state.agents[selectedAgent])
|
||||
}
|
||||
}
|
||||
|
||||
def addOrUpdateAgent(agent)
|
||||
{
|
||||
def children = getChildDevices()
|
||||
def dni = agent.ip + ":" + agent.port
|
||||
def found = false
|
||||
|
||||
children.each
|
||||
{
|
||||
if ((it.getDeviceDataByName("mac") == agent.mac))
|
||||
{
|
||||
found = true
|
||||
|
||||
if (it.getDeviceNetworkId() != dni)
|
||||
{
|
||||
it.setDeviceNetworkId(dni)
|
||||
}
|
||||
}
|
||||
else if (it.getDeviceNetworkId() == dni)
|
||||
{
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
if (!found)
|
||||
{
|
||||
addChildDevice("roomieremote-agent", "Simple Sync", dni, agent.hub, [label: "Simple Sync"])
|
||||
}
|
||||
}
|
||||
|
||||
def locationHandler(evt)
|
||||
{
|
||||
def description = evt?.description
|
||||
def urn = getURN()
|
||||
def hub = evt?.hubId
|
||||
def parsedEvent = parseEventMessage(description)
|
||||
|
||||
parsedEvent?.putAt("hub", hub)
|
||||
|
||||
//SSDP DISCOVERY EVENTS
|
||||
if (parsedEvent?.ssdpTerm?.contains(urn))
|
||||
{
|
||||
def agent = parsedEvent
|
||||
def ip = convertHexToIP(agent.ip)
|
||||
def agents = getAgents()
|
||||
|
||||
agent.verified = true
|
||||
agent.name = "Simple Sync $ip"
|
||||
|
||||
if (!agents[agent.uuid])
|
||||
{
|
||||
state.agents[agent.uuid] = agent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def parseEventMessage(String description)
|
||||
{
|
||||
def event = [:]
|
||||
def parts = description.split(',')
|
||||
|
||||
parts.each
|
||||
{ part ->
|
||||
part = part.trim()
|
||||
if (part.startsWith('devicetype:'))
|
||||
{
|
||||
def valueString = part.split(":")[1].trim()
|
||||
event.devicetype = valueString
|
||||
}
|
||||
else if (part.startsWith('mac:'))
|
||||
{
|
||||
def valueString = part.split(":")[1].trim()
|
||||
if (valueString)
|
||||
{
|
||||
event.mac = valueString
|
||||
}
|
||||
}
|
||||
else if (part.startsWith('networkAddress:'))
|
||||
{
|
||||
def valueString = part.split(":")[1].trim()
|
||||
if (valueString)
|
||||
{
|
||||
event.ip = valueString
|
||||
}
|
||||
}
|
||||
else if (part.startsWith('deviceAddress:'))
|
||||
{
|
||||
def valueString = part.split(":")[1].trim()
|
||||
if (valueString)
|
||||
{
|
||||
event.port = valueString
|
||||
}
|
||||
}
|
||||
else if (part.startsWith('ssdpPath:'))
|
||||
{
|
||||
def valueString = part.split(":")[1].trim()
|
||||
if (valueString)
|
||||
{
|
||||
event.ssdpPath = valueString
|
||||
}
|
||||
}
|
||||
else if (part.startsWith('ssdpUSN:'))
|
||||
{
|
||||
part -= "ssdpUSN:"
|
||||
def valueString = part.trim()
|
||||
if (valueString)
|
||||
{
|
||||
event.ssdpUSN = valueString
|
||||
|
||||
def uuid = getUUIDFromUSN(valueString)
|
||||
|
||||
if (uuid)
|
||||
{
|
||||
event.uuid = uuid
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (part.startsWith('ssdpTerm:'))
|
||||
{
|
||||
part -= "ssdpTerm:"
|
||||
def valueString = part.trim()
|
||||
if (valueString)
|
||||
{
|
||||
event.ssdpTerm = valueString
|
||||
}
|
||||
}
|
||||
else if (part.startsWith('headers'))
|
||||
{
|
||||
part -= "headers:"
|
||||
def valueString = part.trim()
|
||||
if (valueString)
|
||||
{
|
||||
event.headers = valueString
|
||||
}
|
||||
}
|
||||
else if (part.startsWith('body'))
|
||||
{
|
||||
part -= "body:"
|
||||
def valueString = part.trim()
|
||||
if (valueString)
|
||||
{
|
||||
event.body = valueString
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
event
|
||||
}
|
||||
|
||||
def getURN()
|
||||
{
|
||||
return "urn:roomieremote-com:device:roomie:1"
|
||||
}
|
||||
|
||||
def getUUIDFromUSN(usn)
|
||||
{
|
||||
def parts = usn.split(":")
|
||||
|
||||
for (int i = 0; i < parts.size(); ++i)
|
||||
{
|
||||
if (parts[i] == "uuid")
|
||||
{
|
||||
return parts[i + 1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def String convertHexToIP(hex)
|
||||
{
|
||||
[convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
|
||||
}
|
||||
|
||||
def Integer convertHexToInt(hex)
|
||||
{
|
||||
Integer.parseInt(hex,16)
|
||||
}
|
||||
|
||||
def String convertToHexString(n)
|
||||
{
|
||||
String hex = String.format("%X", n.toInteger())
|
||||
}
|
||||
|
||||
def String convertIPToHexString(ipString)
|
||||
{
|
||||
String hex = ipString.tokenize(".").collect {
|
||||
String.format("%02X", it.toInteger())
|
||||
}.join()
|
||||
}
|
||||
|
||||
def Boolean canInstallLabs()
|
||||
{
|
||||
return hasAllHubsOver("000.011.00603")
|
||||
}
|
||||
|
||||
def Boolean hasAllHubsOver(String desiredFirmware)
|
||||
{
|
||||
return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware }
|
||||
}
|
||||
|
||||
def List getRealHubFirmwareVersions()
|
||||
{
|
||||
return location.hubs*.firmwareVersionString.findAll { it }
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -463,29 +463,32 @@ def pollChildren(child = null) {
|
||||
}
|
||||
|
||||
// Poll Child is invoked from the Child Device itself as part of the Poll Capability
|
||||
def pollChild(child){
|
||||
def pollChild(){
|
||||
|
||||
if (pollChildren(child)){
|
||||
if (!child.device.deviceNetworkId.startsWith("ecobee_sensor")){
|
||||
if(atomicState.thermostats[child.device.deviceNetworkId] != null) {
|
||||
def tData = atomicState.thermostats[child.device.deviceNetworkId]
|
||||
log.info "pollChild(child)>> data for ${child.device.deviceNetworkId} : ${tData.data}"
|
||||
child.generateEvent(tData.data) //parse received message from parent
|
||||
} else if(atomicState.thermostats[child.device.deviceNetworkId] == null) {
|
||||
log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId}"
|
||||
return null
|
||||
def devices = getChildDevices()
|
||||
|
||||
if (pollChildren()){
|
||||
devices.each { child ->
|
||||
if (!child.device.deviceNetworkId.startsWith("ecobee_sensor")){
|
||||
if(atomicState.thermostats[child.device.deviceNetworkId] != null) {
|
||||
def tData = atomicState.thermostats[child.device.deviceNetworkId]
|
||||
log.info "pollChild(child)>> data for ${child.device.deviceNetworkId} : ${tData.data}"
|
||||
child.generateEvent(tData.data) //parse received message from parent
|
||||
} else if(atomicState.thermostats[child.device.deviceNetworkId] == null) {
|
||||
log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId}"
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.info "ERROR: pollChildren(child) for ${child.device.deviceNetworkId} after polling"
|
||||
log.info "ERROR: pollChildren()"
|
||||
return null
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void poll() {
|
||||
def devices = getChildDevices()
|
||||
devices.each {pollChild(it)}
|
||||
pollChild()
|
||||
}
|
||||
|
||||
def availableModes(child) {
|
||||
|
||||
@@ -24,7 +24,7 @@ definition(
|
||||
category: "SmartThings Labs",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/hue.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/hue@2x.png",
|
||||
//singleInstance: true
|
||||
singleInstance: true
|
||||
)
|
||||
|
||||
preferences {
|
||||
@@ -60,7 +60,7 @@ def bridgeDiscovery(params=[:])
|
||||
app.updateSetting("selectedHue", "")
|
||||
}
|
||||
|
||||
subscribe(location, null, locationHandler, [filterEvents:false])
|
||||
ssdpSubscribe()
|
||||
|
||||
//bridge discovery request every 15 //25 seconds
|
||||
if((bridgeRefreshCount % 5) == 0) {
|
||||
@@ -68,7 +68,7 @@ def bridgeDiscovery(params=[:])
|
||||
}
|
||||
|
||||
//setup.xml request every 3 seconds except on discoveries
|
||||
if(((bridgeRefreshCount % 1) == 0) && ((bridgeRefreshCount % 5) != 0)) {
|
||||
if(((bridgeRefreshCount % 3) == 0) && ((bridgeRefreshCount % 5) != 0)) {
|
||||
verifyHueBridges()
|
||||
}
|
||||
|
||||
@@ -152,6 +152,10 @@ private discoverBridges() {
|
||||
sendHubCommand(new physicalgraph.device.HubAction("lan discovery urn:schemas-upnp-org:device:basic:1", physicalgraph.device.Protocol.LAN))
|
||||
}
|
||||
|
||||
void ssdpSubscribe() {
|
||||
subscribe(location, "ssdpTerm.urn:schemas-upnp-org:device:basic:1", ssdpBridgeHandler)
|
||||
}
|
||||
|
||||
private sendDeveloperReq() {
|
||||
def token = app.id
|
||||
def host = getBridgeIP()
|
||||
@@ -161,7 +165,7 @@ private sendDeveloperReq() {
|
||||
headers: [
|
||||
HOST: host
|
||||
],
|
||||
body: [devicetype: "$token-0", username: "$token-0"]], "${selectedHue}"))
|
||||
body: [devicetype: "$token-0"]], "${selectedHue}", [callback: "usernameHandler"]))
|
||||
}
|
||||
|
||||
private discoverHueBulbs() {
|
||||
@@ -171,16 +175,17 @@ private discoverHueBulbs() {
|
||||
path: "/api/${state.username}/lights",
|
||||
headers: [
|
||||
HOST: host
|
||||
]], "${selectedHue}"))
|
||||
]], "${selectedHue}", [callback: "lightsHandler"]))
|
||||
}
|
||||
|
||||
private verifyHueBridge(String deviceNetworkId, String host) {
|
||||
log.trace "Verify Hue Bridge $deviceNetworkId"
|
||||
sendHubCommand(new physicalgraph.device.HubAction([
|
||||
method: "GET",
|
||||
path: "/description.xml",
|
||||
headers: [
|
||||
HOST: host
|
||||
]], deviceNetworkId))
|
||||
]], deviceNetworkId, [callback: "bridgeDescriptionHandler"]))
|
||||
}
|
||||
|
||||
private verifyHueBridges() {
|
||||
@@ -288,17 +293,53 @@ def bulbListHandler(hub, data = "") {
|
||||
def object = new groovy.json.JsonSlurper().parseText(data)
|
||||
object.each { k,v ->
|
||||
if (v instanceof Map)
|
||||
bulbs[k] = [id: k, name: v.name, type: v.type, hub:hub]
|
||||
bulbs[k] = [id: k, name: v.name, type: v.type, modelid: v.modelid, hub:hub]
|
||||
}
|
||||
}
|
||||
def bridge = null
|
||||
if (selectedHue)
|
||||
bridge = getChildDevice(selectedHue)
|
||||
if (selectedHue) {
|
||||
bridge = getChildDevice(selectedHue)
|
||||
}
|
||||
bridge.sendEvent(name: "bulbList", value: hub, data: bulbs, isStateChange: true, displayed: false)
|
||||
msg = "${bulbs.size()} bulbs found. ${bulbs}"
|
||||
return msg
|
||||
}
|
||||
|
||||
private upgradeDeviceType(device, newHueType) {
|
||||
def deviceType = getDeviceType(newHueType)
|
||||
|
||||
// Automatically change users Hue bulbs to correct device types
|
||||
if (deviceType && !(device?.typeName?.equalsIgnoreCase(deviceType))) {
|
||||
log.debug "Update device type: \"$device.label\" ${device?.typeName}->$deviceType"
|
||||
device.setDeviceType(deviceType)
|
||||
}
|
||||
}
|
||||
|
||||
private getDeviceType(hueType) {
|
||||
// Determine ST device type based on Hue classification of light
|
||||
if (hueType?.equalsIgnoreCase("Dimmable light"))
|
||||
return "Hue Lux Bulb"
|
||||
else if (hueType?.equalsIgnoreCase("Extended Color Light"))
|
||||
return "Hue Bulb"
|
||||
else if (hueType?.equalsIgnoreCase("Color Light"))
|
||||
return "Hue Bloom"
|
||||
else if (hueType?.equalsIgnoreCase("Color Temperature Light"))
|
||||
return "Hue White Ambiance Bulb"
|
||||
else
|
||||
return null
|
||||
}
|
||||
|
||||
private addChildBulb(dni, hueType, name, hub, update=false, device = null) {
|
||||
def deviceType = getDeviceType(hueType)
|
||||
|
||||
if (deviceType) {
|
||||
return addChildDevice("smartthings", deviceType, dni, hub, ["label": name])
|
||||
} else {
|
||||
log.warn "Device type $hueType not supported"
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
def addBulbs() {
|
||||
def bulbs = getHueBulbs()
|
||||
selectedBulbs?.each { dni ->
|
||||
@@ -308,29 +349,26 @@ def addBulbs() {
|
||||
if (bulbs instanceof java.util.Map) {
|
||||
newHueBulb = bulbs.find { (app.id + "/" + it.value.id) == dni }
|
||||
if (newHueBulb != null) {
|
||||
if (newHueBulb?.value?.type?.equalsIgnoreCase("Dimmable light") ) {
|
||||
d = addChildDevice("smartthings", "Hue Lux Bulb", dni, newHueBulb?.value.hub, ["label":newHueBulb?.value.name])
|
||||
} else {
|
||||
d = addChildDevice("smartthings", "Hue Bulb", dni, newHueBulb?.value.hub, ["label":newHueBulb?.value.name])
|
||||
}
|
||||
log.debug "created ${d.displayName} with id $dni"
|
||||
d.refresh()
|
||||
d = addChildBulb(dni, newHueBulb?.value?.type, newHueBulb?.value?.name, newHueBulb?.value?.hub)
|
||||
if (d) {
|
||||
log.debug "created ${d.displayName} with id $dni"
|
||||
d.refresh()
|
||||
}
|
||||
} else {
|
||||
log.debug "$dni in not longer paired to the Hue Bridge or ID changed"
|
||||
}
|
||||
} else {
|
||||
//backwards compatable
|
||||
newHueBulb = bulbs.find { (app.id + "/" + it.id) == dni }
|
||||
d = addChildDevice("smartthings", "Hue Bulb", dni, newHueBulb?.hub, ["label":newHueBulb?.name])
|
||||
d.refresh()
|
||||
d = addChildBulb(dni, "Extended Color Light", newHueBulb?.value?.name, newHueBulb?.value?.hub)
|
||||
d?.refresh()
|
||||
}
|
||||
} else {
|
||||
log.debug "found ${d.displayName} with id $dni already exists, type: '$d.typeName'"
|
||||
if (bulbs instanceof java.util.Map) {
|
||||
// Update device type if incorrect
|
||||
def newHueBulb = bulbs.find { (app.id + "/" + it.value.id) == dni }
|
||||
if (newHueBulb?.value?.type?.equalsIgnoreCase("Dimmable light") && d.typeName == "Hue Bulb") {
|
||||
d.setDeviceType("Hue Lux Bulb")
|
||||
}
|
||||
upgradeDeviceType(d, newHueBulb?.value?.type)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -352,8 +390,9 @@ def addBridge() {
|
||||
def oldDNI = it.deviceNetworkId
|
||||
log.debug "updating dni for device ${it} with $newDNI - previous DNI = ${it.deviceNetworkId}"
|
||||
it.setDeviceNetworkId("${newDNI}")
|
||||
if (oldDNI == selectedHue)
|
||||
app.updateSetting("selectedHue", newDNI)
|
||||
if (oldDNI == selectedHue) {
|
||||
app.updateSetting("selectedHue", newDNI)
|
||||
}
|
||||
newbridge = false
|
||||
}
|
||||
}
|
||||
@@ -382,6 +421,111 @@ def addBridge() {
|
||||
}
|
||||
}
|
||||
|
||||
def ssdpBridgeHandler(evt) {
|
||||
def description = evt.description
|
||||
log.trace "Location: $description"
|
||||
|
||||
def hub = evt?.hubId
|
||||
def parsedEvent = parseLanMessage(description)
|
||||
parsedEvent << ["hub":hub]
|
||||
|
||||
def bridges = getHueBridges()
|
||||
log.trace bridges.toString()
|
||||
if (!(bridges."${parsedEvent.ssdpUSN.toString()}")) {
|
||||
//bridge does not exist
|
||||
log.trace "Adding bridge ${parsedEvent.ssdpUSN}"
|
||||
bridges << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent]
|
||||
} else {
|
||||
// update the values
|
||||
def ip = convertHexToIP(parsedEvent.networkAddress)
|
||||
def port = convertHexToInt(parsedEvent.deviceAddress)
|
||||
def host = ip + ":" + port
|
||||
log.debug "Device ($parsedEvent.mac) was already found in state with ip = $host."
|
||||
def dstate = bridges."${parsedEvent.ssdpUSN.toString()}"
|
||||
def dni = "${parsedEvent.mac}"
|
||||
def d = getChildDevice(dni)
|
||||
def networkAddress = null
|
||||
if (!d) {
|
||||
childDevices.each {
|
||||
if (it.getDeviceDataByName("mac")) {
|
||||
def newDNI = "${it.getDeviceDataByName("mac")}"
|
||||
if (newDNI != it.deviceNetworkId) {
|
||||
def oldDNI = it.deviceNetworkId
|
||||
log.debug "updating dni for device ${it} with $newDNI - previous DNI = ${it.deviceNetworkId}"
|
||||
it.setDeviceNetworkId("${newDNI}")
|
||||
if (oldDNI == selectedHue) {
|
||||
app.updateSetting("selectedHue", newDNI)
|
||||
}
|
||||
doDeviceSync()
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (d.getDeviceDataByName("networkAddress")) {
|
||||
networkAddress = d.getDeviceDataByName("networkAddress")
|
||||
} else {
|
||||
networkAddress = d.latestState('networkAddress').stringValue
|
||||
}
|
||||
log.trace "Host: $host - $networkAddress"
|
||||
if (host != networkAddress) {
|
||||
log.debug "Device's port or ip changed for device $d..."
|
||||
dstate.ip = ip
|
||||
dstate.port = port
|
||||
dstate.name = "Philips hue ($ip)"
|
||||
d.sendEvent(name:"networkAddress", value: host)
|
||||
d.updateDataValue("networkAddress", host)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void bridgeDescriptionHandler(physicalgraph.device.HubResponse hubResponse) {
|
||||
log.trace "description.xml response (application/xml)"
|
||||
def body = hubResponse.xml
|
||||
if (body?.device?.modelName?.text().startsWith("Philips hue bridge")) {
|
||||
def bridges = getHueBridges()
|
||||
def bridge = bridges.find {it?.key?.contains(body?.device?.UDN?.text())}
|
||||
if (bridge) {
|
||||
bridge.value << [name:body?.device?.friendlyName?.text(), serialNumber:body?.device?.serialNumber?.text(), verified: true]
|
||||
} else {
|
||||
log.error "/description.xml returned a bridge that didn't exist"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void lightsHandler(physicalgraph.device.HubResponse hubResponse) {
|
||||
if (isValidSource(hubResponse.mac)) {
|
||||
def body = hubResponse.json
|
||||
if (!body?.state?.on) { //check if first time poll made it here by mistake
|
||||
def bulbs = getHueBulbs()
|
||||
log.debug "Adding bulbs to state!"
|
||||
body.each { k, v ->
|
||||
bulbs[k] = [id: k, name: v.name, type: v.type, modelid: v.modelid, hub: hubResponse.hubId]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void usernameHandler(physicalgraph.device.HubResponse hubResponse) {
|
||||
if (isValidSource(hubResponse.mac)) {
|
||||
def body = hubResponse.json
|
||||
if (body.success != null) {
|
||||
if (body.success[0] != null) {
|
||||
if (body.success[0].username)
|
||||
state.username = body.success[0].username
|
||||
}
|
||||
} else if (body.error != null) {
|
||||
//TODO: handle retries...
|
||||
log.error "ERROR: application/json ${body.error}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated This has been replaced by the combination of {@link #ssdpBridgeHandler()}, {@link #bridgeDescriptionHandler()},
|
||||
* {@link #lightsHandler()}, and {@link #usernameHandler()}. After a pending event subscription migration, it can be removed.
|
||||
*/
|
||||
@Deprecated
|
||||
def locationHandler(evt) {
|
||||
def description = evt.description
|
||||
log.trace "Location: $description"
|
||||
@@ -417,17 +561,19 @@ def locationHandler(evt) {
|
||||
def oldDNI = it.deviceNetworkId
|
||||
log.debug "updating dni for device ${it} with $newDNI - previous DNI = ${it.deviceNetworkId}"
|
||||
it.setDeviceNetworkId("${newDNI}")
|
||||
if (oldDNI == selectedHue)
|
||||
app.updateSetting("selectedHue", newDNI)
|
||||
if (oldDNI == selectedHue) {
|
||||
app.updateSetting("selectedHue", newDNI)
|
||||
}
|
||||
doDeviceSync()
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (d.getDeviceDataByName("networkAddress"))
|
||||
networkAddress = d.getDeviceDataByName("networkAddress")
|
||||
else
|
||||
networkAddress = d.latestState('networkAddress').stringValue
|
||||
if (d.getDeviceDataByName("networkAddress")) {
|
||||
networkAddress = d.getDeviceDataByName("networkAddress")
|
||||
} else {
|
||||
networkAddress = d.latestState('networkAddress').stringValue
|
||||
}
|
||||
log.trace "Host: $host - $networkAddress"
|
||||
if(host != networkAddress) {
|
||||
log.debug "Device's port or ip changed for device $d..."
|
||||
@@ -460,8 +606,9 @@ def locationHandler(evt) {
|
||||
def body = new groovy.json.JsonSlurper().parseText(parsedEvent.body)
|
||||
if (body.success != null) {
|
||||
if (body.success[0] != null) {
|
||||
if (body.success[0].username)
|
||||
if (body.success[0].username) {
|
||||
state.username = body.success[0].username
|
||||
}
|
||||
}
|
||||
} else if (body.error != null) {
|
||||
//TODO: handle retries...
|
||||
@@ -472,7 +619,7 @@ def locationHandler(evt) {
|
||||
def bulbs = getHueBulbs()
|
||||
log.debug "Adding bulbs to state!"
|
||||
body.each { k,v ->
|
||||
bulbs[k] = [id: k, name: v.name, type: v.type, hub:parsedEvent.hub]
|
||||
bulbs[k] = [id: k, name: v.name, type: v.type, modelid: v.modelid, hub:parsedEvent.hub]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -486,11 +633,7 @@ def doDeviceSync(){
|
||||
log.trace "Doing Hue Device Sync!"
|
||||
convertBulbListToMap()
|
||||
poll()
|
||||
try {
|
||||
subscribe(location, null, locationHandler, [filterEvents:false])
|
||||
} catch (all) {
|
||||
log.trace "Subscription already exist"
|
||||
}
|
||||
ssdpSubscribe()
|
||||
discoverBridges()
|
||||
}
|
||||
|
||||
@@ -602,6 +745,20 @@ def parse(childDevice, description) {
|
||||
}
|
||||
}
|
||||
|
||||
def hubVerification(bodytext) {
|
||||
log.trace "Bridge sent back description.xml for verification"
|
||||
def body = new XmlSlurper().parseText(bodytext)
|
||||
if (body?.device?.modelName?.text().startsWith("Philips hue bridge")) {
|
||||
def bridges = getHueBridges()
|
||||
def bridge = bridges.find {it?.key?.contains(body?.device?.UDN?.text())}
|
||||
if (bridge) {
|
||||
bridge.value << [name:body?.device?.friendlyName?.text(), serialNumber:body?.device?.serialNumber?.text(), verified: true]
|
||||
} else {
|
||||
log.error "/description.xml returned a bridge that didn't exist"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def on(childDevice) {
|
||||
log.debug "Executing 'on'"
|
||||
put("lights/${getId(childDevice)}/state", [on: true])
|
||||
@@ -642,35 +799,53 @@ def setColorTemperature(childDevice, huesettings) {
|
||||
}
|
||||
|
||||
def setColor(childDevice, huesettings) {
|
||||
log.debug "Executing 'setColor($huesettings)'"
|
||||
def hue = Math.min(Math.round(huesettings.hue * 65535 / 100), 65535)
|
||||
def sat = Math.min(Math.round(huesettings.saturation * 255 / 100), 255)
|
||||
def alert = huesettings.alert ? huesettings.alert : "none"
|
||||
def transition = huesettings.transition ? huesettings.transition : 4
|
||||
log.debug "Executing 'setColor($huesettings)'"
|
||||
|
||||
def value = [:]
|
||||
def hue = null
|
||||
def sat = null
|
||||
def xy = null
|
||||
|
||||
if (huesettings.hex != null) {
|
||||
value.xy = getHextoXY(huesettings.hex)
|
||||
} else {
|
||||
if (huesettings.hue != null)
|
||||
value.hue = Math.min(Math.round(huesettings.hue * 65535 / 100), 65535)
|
||||
if (huesettings.saturation != null)
|
||||
value.sat = Math.min(Math.round(huesettings.saturation * 255 / 100), 255)
|
||||
}
|
||||
|
||||
// Default behavior is to turn light on
|
||||
value.on = true
|
||||
|
||||
def value = [sat: sat, hue: hue, alert: alert, transitiontime: transition]
|
||||
if (huesettings.level != null) {
|
||||
if (huesettings.level == 1) value.bri = 1 else value.bri = Math.min(Math.round(huesettings.level * 255 / 100), 255)
|
||||
value.on = value.bri > 0
|
||||
}
|
||||
if (huesettings.level != null) {
|
||||
if (huesettings.level <= 0)
|
||||
value.on = false
|
||||
else if (huesettings.level == 1)
|
||||
value.bri = 1
|
||||
else
|
||||
value.bri = Math.min(Math.round(huesettings.level * 255 / 100), 255)
|
||||
}
|
||||
value.alert = huesettings.alert ? huesettings.alert : "none"
|
||||
value.transitiontime = huesettings.transitiontime ? huesettings.transitiontime : 4
|
||||
|
||||
if (huesettings.switch) {
|
||||
value.on = huesettings.switch == "on"
|
||||
}
|
||||
// Make sure to turn off light if requested
|
||||
if (huesettings.switch == "off")
|
||||
value.on = false
|
||||
|
||||
log.debug "sending command $value"
|
||||
put("lights/${getId(childDevice)}/state", value)
|
||||
log.debug "sending command $value"
|
||||
put("lights/${getId(childDevice)}/state", value)
|
||||
return "Color set to $value"
|
||||
}
|
||||
|
||||
def nextLevel(childDevice) {
|
||||
def level = device.latestValue("level") as Integer ?: 0
|
||||
if (level < 100) {
|
||||
level = Math.min(25 * (Math.round(level / 25) + 1), 100) as Integer
|
||||
}
|
||||
else {
|
||||
level = 25
|
||||
}
|
||||
setLevel(childDevice,level)
|
||||
def level = device.latestValue("level") as Integer ?: 0
|
||||
if (level < 100) {
|
||||
level = Math.min(25 * (Math.round(level / 25) + 1), 100) as Integer
|
||||
} else {
|
||||
level = 25
|
||||
}
|
||||
setLevel(childDevice,level)
|
||||
}
|
||||
|
||||
private getId(childDevice) {
|
||||
@@ -685,15 +860,11 @@ private getId(childDevice) {
|
||||
private poll() {
|
||||
def host = getBridgeIP()
|
||||
def uri = "/api/${state.username}/lights/"
|
||||
try {
|
||||
sendHubCommand(new physicalgraph.device.HubAction("""GET ${uri} HTTP/1.1
|
||||
log.debug "GET: $host$uri"
|
||||
sendHubCommand(new physicalgraph.device.HubAction("""GET ${uri} HTTP/1.1
|
||||
HOST: ${host}
|
||||
|
||||
""", physicalgraph.device.Protocol.LAN, selectedHue))
|
||||
} catch (all) {
|
||||
log.warn "Parsing Body failed - trying again..."
|
||||
doDeviceSync()
|
||||
}
|
||||
}
|
||||
|
||||
private put(path, body) {
|
||||
@@ -743,6 +914,57 @@ private getBridgeIP() {
|
||||
return host
|
||||
}
|
||||
|
||||
private getHextoXY(String colorStr) {
|
||||
// For the hue bulb the corners of the triangle are:
|
||||
// -Red: 0.675, 0.322
|
||||
// -Green: 0.4091, 0.518
|
||||
// -Blue: 0.167, 0.04
|
||||
|
||||
def cred = Integer.valueOf( colorStr.substring( 1, 3 ), 16 )
|
||||
def cgreen = Integer.valueOf( colorStr.substring( 3, 5 ), 16 )
|
||||
def cblue = Integer.valueOf( colorStr.substring( 5, 7 ), 16 )
|
||||
|
||||
double[] normalizedToOne = new double[3];
|
||||
normalizedToOne[0] = (cred / 255);
|
||||
normalizedToOne[1] = (cgreen / 255);
|
||||
normalizedToOne[2] = (cblue / 255);
|
||||
float red, green, blue;
|
||||
|
||||
// Make red more vivid
|
||||
if (normalizedToOne[0] > 0.04045) {
|
||||
red = (float) Math.pow(
|
||||
(normalizedToOne[0] + 0.055) / (1.0 + 0.055), 2.4);
|
||||
} else {
|
||||
red = (float) (normalizedToOne[0] / 12.92);
|
||||
}
|
||||
|
||||
// Make green more vivid
|
||||
if (normalizedToOne[1] > 0.04045) {
|
||||
green = (float) Math.pow((normalizedToOne[1] + 0.055) / (1.0 + 0.055), 2.4);
|
||||
} else {
|
||||
green = (float) (normalizedToOne[1] / 12.92);
|
||||
}
|
||||
|
||||
// Make blue more vivid
|
||||
if (normalizedToOne[2] > 0.04045) {
|
||||
blue = (float) Math.pow((normalizedToOne[2] + 0.055) / (1.0 + 0.055), 2.4);
|
||||
} else {
|
||||
blue = (float) (normalizedToOne[2] / 12.92);
|
||||
}
|
||||
|
||||
float X = (float) (red * 0.649926 + green * 0.103455 + blue * 0.197109);
|
||||
float Y = (float) (red * 0.234327 + green * 0.743075 + blue * 0.022598);
|
||||
float Z = (float) (red * 0.0000000 + green * 0.053077 + blue * 1.035763);
|
||||
|
||||
float x = (X != 0 ? X / (X + Y + Z) : 0);
|
||||
float y = (Y != 0 ? Y / (X + Y + Z) : 0);
|
||||
|
||||
double[] xy = new double[2];
|
||||
xy[0] = x;
|
||||
xy[1] = y;
|
||||
return xy;
|
||||
}
|
||||
|
||||
private Integer convertHexToInt(hex) {
|
||||
Integer.parseInt(hex,16)
|
||||
}
|
||||
@@ -752,7 +974,7 @@ def convertBulbListToMap() {
|
||||
if (state.bulbs instanceof java.util.List) {
|
||||
def map = [:]
|
||||
state.bulbs.unique {it.id}.each { bulb ->
|
||||
map << ["${bulb.id}":["id":bulb.id, "name":bulb.name, "hub":bulb.hub]]
|
||||
map << ["${bulb.id}":["id":bulb.id, "name":bulb.name, "type": bulb.type, "modelid": bulb.modelid, "hub":bulb.hub]]
|
||||
}
|
||||
state.bulbs = map
|
||||
}
|
||||
|
||||
@@ -131,19 +131,69 @@ def update() {
|
||||
def type = params.deviceType
|
||||
def data = request.JSON
|
||||
def devices = settings[type]
|
||||
def device = settings[type]?.find { it.id == params.id }
|
||||
def command = data.command
|
||||
|
||||
log.debug "[PROD] update, params: ${params}, request: ${data}, devices: ${devices*.id}"
|
||||
if (command) {
|
||||
def device = devices?.find { it.id == params.id }
|
||||
if (!device) {
|
||||
httpError(404, "Device not found")
|
||||
} else {
|
||||
device."$command"()
|
||||
}
|
||||
|
||||
if (!device) {
|
||||
httpError(404, "Device not found")
|
||||
}
|
||||
|
||||
if (validateCommand(device, type, command)) {
|
||||
device."$command"()
|
||||
} else {
|
||||
httpError(403, "Access denied. This command is not supported by current capability.")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 "alarms":
|
||||
return "Alarm"
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
def show() {
|
||||
def type = params.deviceType
|
||||
def devices = settings[type]
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
'''Acceleration Detected'''.ko=가속화 감지됨
|
||||
'''Arrival Of'''.ko=도착
|
||||
'''Both Push and SMS?'''.ko=푸시 메시지와 SMS를 모두 사용하시겠습니까?
|
||||
'''Button Pushed'''.ko=버튼이 눌렸습니다
|
||||
'''Contact Closes'''.ko=접점 닫힘
|
||||
'''Contact Opens'''.ko=접점 열림
|
||||
'''Departure Of'''.ko=출발
|
||||
'''Acceleration Detected'''.ko=가속이 감지되었을 때
|
||||
'''Arrival Of'''.ko=도착했을 때
|
||||
'''Both Push and SMS?'''.ko=푸시 알람과 SMS 모두 사용
|
||||
'''Button Pushed'''.ko=버튼이 눌렸을 때
|
||||
'''Contact Closes'''.ko=닫힘이 감지되었을 때
|
||||
'''Contact Opens'''.ko=열림이 감지되었을 때
|
||||
'''Departure Of'''.ko=출발할 때
|
||||
'''Message Text'''.ko=문자 메시지
|
||||
'''Minutes'''.ko=분
|
||||
'''Motion Here'''.ko=동작
|
||||
'''Phone Number (for SMS, optional)'''.ko=휴대전화 번호(문자 메시지 - 옵션)
|
||||
'''Minutes'''.ko=메시지 전송 간격(분)
|
||||
'''Motion Here'''.ko=움직임이 감지되었을 때
|
||||
'''Phone Number (for SMS, optional)'''.ko=전화번호 (옵션)
|
||||
'''Receive notifications when anything happens in your home.'''.ko=집 안에 무슨 일이 일어나면 알림이 전송됩니다.
|
||||
'''Smoke Detected'''.ko=연기가 감지되었습니다
|
||||
'''Switch Turned Off'''.ko=스위치 꺼짐
|
||||
'''Switch Turned On'''.ko=스위치 꺼짐
|
||||
'''Choose one or more, when...'''.ko=다음의 경우 하나 이상 선택
|
||||
'''Smoke Detected'''.ko=연기가 감지되었을 때
|
||||
'''Switch Turned Off'''.ko=스위치가 꺼졌을 때
|
||||
'''Switch Turned On'''.ko=스위치가 켜졌을 때
|
||||
'''Choose one or more, when...'''.ko=다음 상황 중 하나 이상 선택
|
||||
'''Yes'''.ko=예
|
||||
'''No'''.ko=아니요
|
||||
'''Send this message (optional, sends standard status message if not specified)'''.ko=이 메시지 전송(선택적, 지정되지 않은 경우 표준 상태 메시지를 보냅니다)
|
||||
'''Via a push notification and/or an SMS message'''.ko=푸시 알림 및/또는 문자 메시지를 통해
|
||||
'''Set for specific mode(s)'''.ko=특정 모드 설정
|
||||
'''Send this message (optional, sends standard status message if not specified)'''.ko=메시지 작성 (작성하지 않을 경우 디폴트 메시지 전송)
|
||||
'''Via a push notification and/or an SMS message'''.ko=푸시 알람 및 SMS 설정
|
||||
'''Set for specific mode(s)'''.ko=특정 상태 설정
|
||||
'''Tap to set'''.ko=눌러서 설정
|
||||
'''Minimum time between messages (optional, defaults to every message)'''.ko=메시지작 간 최소 시간(선택 사항, 모든 메시지의 기본 설정)
|
||||
'''If outside the US please make sure to enter the proper country code'''.ko=미국 이외 거주자는 적절한 국가 코드를 입력했는지 확인하십시오
|
||||
'''Water Sensor Wet'''.ko=Water Sensor에서 물이 감지되었습니다
|
||||
'''{{ triggerEvent.linkText }} has arrived at the {{ location.name }}'''.ko={{ triggerEvent.linkText }}님이 {{ location.name }}에 도착했습니다
|
||||
'''{{ triggerEvent.linkText }} has arrived at {{ location.name }}'''.ko={{ triggerEvent.linkText }}님이 {{ location.name }}에 도착했습니다
|
||||
'''{{ triggerEvent.linkText }} has left the {{ location.name }}'''.ko={{ triggerEvent.linkText }}님이 {{ location.name }}을(를) 떠났습니다
|
||||
'''{{ triggerEvent.linkText }} has left {{ location.name }}'''.ko={{ triggerEvent.linkText }}님이 {{ location.name }}을(를) 떠났습니다
|
||||
'''Assign a name'''.ko=이름 배정
|
||||
'''Choose Modes'''.ko=모드 선택
|
||||
'''Minimum time between messages (optional, defaults to every message)'''.ko=메시지 전송 간격 설정
|
||||
'''If outside the US please make sure to enter the proper country code'''.ko=미국 이외 국가에 거주한다면 국가 코드와 함께 입력하여 주세요.
|
||||
'''Water Sensor Wet'''.ko=누수가 감지되었을 때
|
||||
'''{{ triggerEvent.linkText }} has arrived at the {{ location.name }}'''.ko={{ location.name }}에 {{ triggerEvent.linkText }} 귀가
|
||||
'''{{ triggerEvent.linkText }} has arrived at {{ location.name }}'''.ko={{ location.name }}에 {{ triggerEvent.linkText }} 귀가
|
||||
'''{{ triggerEvent.linkText }} has left the {{ location.name }}'''.ko={{ location.name }}에서 {{ triggerEvent.linkText }} 외출
|
||||
'''{{ triggerEvent.linkText }} has left {{ location.name }}'''.ko={{ location.name }}에서 {{ triggerEvent.linkText }} 외출
|
||||
'''Assign a name'''.ko=이름 설정
|
||||
'''Choose Modes'''.ko=상태 선택
|
||||
|
||||
@@ -76,7 +76,7 @@ def mainPage() {
|
||||
}
|
||||
section{
|
||||
input "actionType", "enum", title: "Action?", required: true, defaultValue: "Bell 1", options: [
|
||||
//"Custom Message",
|
||||
"Custom Message",
|
||||
"Bell 1",
|
||||
"Bell 2",
|
||||
"Dogs Barking",
|
||||
@@ -89,7 +89,7 @@ def mainPage() {
|
||||
"Someone is arriving",
|
||||
"Piano",
|
||||
"Lightsaber"]
|
||||
//input "message","text",title:"Play this message", required:false, multiple: false
|
||||
input "message","text",title:"Play this message", required:false, multiple: false
|
||||
}
|
||||
section {
|
||||
input "sonos", "capability.musicPlayer", title: "On this Speaker player", required: true
|
||||
@@ -408,13 +408,15 @@ private loadText() {
|
||||
case "Lightsaber":
|
||||
state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/lightsaber.mp3", duration: "10"]
|
||||
break;
|
||||
default:
|
||||
/*if (message) {
|
||||
case "Custom Message":
|
||||
if (message) {
|
||||
state.sound = textToSpeech(message instanceof List ? message[0] : message) // not sure why this is (sometimes) needed)
|
||||
}
|
||||
else {
|
||||
state.sound = textToSpeech("You selected the custom message option but did not enter a message in the $app.label Smart App")
|
||||
}*/
|
||||
}
|
||||
break;
|
||||
default:
|
||||
state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/bell1.mp3", duration: "10"]
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* Device Tile Controller
|
||||
*
|
||||
* Copyright 2016 SmartThings
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
definition(
|
||||
name: "Device Tile Controller",
|
||||
namespace: "smartthings/tile-ux",
|
||||
author: "SmartThings",
|
||||
description: "A controller SmartApp to install virtual devices into your location in order to simulate various native Device Tiles.",
|
||||
category: "SmartThings Internal",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
|
||||
iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
|
||||
singleInstance: true)
|
||||
|
||||
|
||||
preferences {
|
||||
// landing page
|
||||
page(name: "defaultPage")
|
||||
}
|
||||
|
||||
def defaultPage() {
|
||||
dynamicPage(name: "defaultPage", install: true, uninstall: true) {
|
||||
section {
|
||||
paragraph "Select on Unselect the devices that you want to install"
|
||||
}
|
||||
section(title: "Multi Attribute Tile Types") {
|
||||
input(type: "bool", name: "genericDeviceTile", title: "generic", description: "A device that showcases the various use of generic multi-attribute-tiles.", defaultValue: "false")
|
||||
input(type: "bool", name: "lightingDeviceTile", title: "lighting", description: "A device that showcases the various use of lighting multi-attribute-tiles.", defaultValue: "false")
|
||||
input(type: "bool", name: "thermostatDeviceTile", title: "thermostat", description: "A device that showcases the various use of thermostat multi-attribute-tiles.", defaultValue: "true")
|
||||
input(type: "bool", name: "mediaPlayerDeviceTile", title: "media player", description: "A device that showcases the various use of mediaPlayer multi-attribute-tiles.", defaultValue: "false")
|
||||
input(type: "bool", name: "videoPlayerDeviceTile", title: "video player", description: "A device that showcases the various use of videoPlayer multi-attribute-tiles.", defaultValue: "false")
|
||||
}
|
||||
section(title: "Device Tile Types") {
|
||||
input(type: "bool", name: "standardDeviceTile", title: "standard device tiles", description: "A device that showcases the various use of standard device tiles.", defaultValue: "false")
|
||||
input(type: "bool", name: "valueDeviceTile", title: "value device tiles", description: "A device that showcases the various use of value device tiles.", defaultValue: "false")
|
||||
input(type: "bool", name: "presenceDeviceTile", title: "presence device tiles", description: "A device that showcases the various use of color control device tile.", defaultValue: "false")
|
||||
}
|
||||
section(title: "Other Tile Types") {
|
||||
input(type: "bool", name: "carouselDeviceTile", title: "image carousel", description: "A device that showcases the various use of carousel device tile.", defaultValue: "false")
|
||||
input(type: "bool", name: "sliderDeviceTile", title: "slider", description: "A device that showcases the various use of slider device tile.", defaultValue: "false")
|
||||
input(type: "bool", name: "colorWheelDeviceTile", title: "color wheel", description: "A device that showcases the various use of color wheel device tile.", defaultValue: "false")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
}
|
||||
|
||||
def uninstalled() {
|
||||
getChildDevices().each {
|
||||
deleteChildDevice(it.deviceNetworkId)
|
||||
}
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
unsubscribe()
|
||||
initializeDevices()
|
||||
}
|
||||
|
||||
def initializeDevices() {
|
||||
settings.each { key, value ->
|
||||
log.debug "$key : $value"
|
||||
def existingDevice = getChildDevices().find { it.name == key }
|
||||
log.debug "$existingDevice"
|
||||
if (existingDevice && !value) {
|
||||
deleteChildDevice(existingDevice.deviceNetworkId)
|
||||
} else if (!existingDevice && value) {
|
||||
String dni = UUID.randomUUID()
|
||||
log.debug "$dni"
|
||||
addChildDevice(app.namespace, key, dni, null, [
|
||||
label: labelMap()[key] ?: key,
|
||||
completedSetup: true
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Map the name of the Device to a proper Label
|
||||
def labelMap() {
|
||||
[
|
||||
genericDeviceTile: "Tile Multiattribute Generic",
|
||||
lightingDeviceTile: "Tile Multiattribute Lighting",
|
||||
thermostatDeviceTile: "Tile Multiattribute Thermostat",
|
||||
mediaPlayerDeviceTile: "Tile Multiattribute Media Player",
|
||||
videoPlayerDeviceTile: "Tile Multiattribute Video Player",
|
||||
standardDeviceTile: "Tile Device Standard",
|
||||
valueDeviceTile: "Tile Device Value",
|
||||
presenceDeviceTile: "Tile Device Presence",
|
||||
carouselDeviceTile: "Tile Device Carousel",
|
||||
sliderDeviceTile: "Tile Device Slider",
|
||||
colorWheelDeviceTile: "Tile Device Color Wheel"
|
||||
]
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -234,25 +234,24 @@ def addSwitches() {
|
||||
def d
|
||||
if (selectedSwitch) {
|
||||
d = getChildDevices()?.find {
|
||||
it.dni == selectedSwitch.value.mac || it.device.getDataValue("mac") == selectedSwitch.value.mac
|
||||
it.deviceNetworkId == selectedSwitch.value.mac || it.device.getDataValue("mac") == selectedSwitch.value.mac
|
||||
}
|
||||
if (!d) {
|
||||
log.debug "Creating WeMo Switch with dni: ${selectedSwitch.value.mac}"
|
||||
d = addChildDevice("smartthings", "Wemo Switch", selectedSwitch.value.mac, selectedSwitch?.value.hub, [
|
||||
"label": selectedSwitch?.value?.name ?: "Wemo Switch",
|
||||
"data": [
|
||||
"mac": selectedSwitch.value.mac,
|
||||
"ip": selectedSwitch.value.ip,
|
||||
"port": selectedSwitch.value.port
|
||||
]
|
||||
])
|
||||
def ipvalue = convertHexToIP(selectedSwitch.value.ip)
|
||||
d.sendEvent(name: "currentIP", value: ipvalue, descriptionText: "IP is ${ipvalue}")
|
||||
log.debug "Created ${d.displayName} with id: ${d.id}, dni: ${d.deviceNetworkId}"
|
||||
} else {
|
||||
log.debug "found ${d.displayName} with id $dni already exists"
|
||||
}
|
||||
}
|
||||
|
||||
if (!d) {
|
||||
log.debug "Creating WeMo Switch with dni: ${selectedSwitch.value.mac}"
|
||||
d = addChildDevice("smartthings", "Wemo Switch", selectedSwitch.value.mac, selectedSwitch?.value.hub, [
|
||||
"label": selectedSwitch?.value?.name ?: "Wemo Switch",
|
||||
"data": [
|
||||
"mac": selectedSwitch.value.mac,
|
||||
"ip": selectedSwitch.value.ip,
|
||||
"port": selectedSwitch.value.port
|
||||
]
|
||||
])
|
||||
def ipvalue = convertHexToIP(selectedSwitch.value.ip)
|
||||
d.sendEvent(name: "currentIP", value: ipvalue, descriptionText: "IP is ${ipvalue}")
|
||||
log.debug "Created ${d.displayName} with id: ${d.id}, dni: ${d.deviceNetworkId}"
|
||||
} else {
|
||||
log.debug "found ${d.displayName} with id $dni already exists"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -265,25 +264,24 @@ def addMotions() {
|
||||
def d
|
||||
if (selectedMotion) {
|
||||
d = getChildDevices()?.find {
|
||||
it.dni == selectedMotion.value.mac || it.device.getDataValue("mac") == selectedMotion.value.mac
|
||||
it.deviceNetworkId == selectedMotion.value.mac || it.device.getDataValue("mac") == selectedMotion.value.mac
|
||||
}
|
||||
if (!d) {
|
||||
log.debug "Creating WeMo Motion with dni: ${selectedMotion.value.mac}"
|
||||
d = addChildDevice("smartthings", "Wemo Motion", selectedMotion.value.mac, selectedMotion?.value.hub, [
|
||||
"label": selectedMotion?.value?.name ?: "Wemo Motion",
|
||||
"data": [
|
||||
"mac": selectedMotion.value.mac,
|
||||
"ip": selectedMotion.value.ip,
|
||||
"port": selectedMotion.value.port
|
||||
]
|
||||
])
|
||||
def ipvalue = convertHexToIP(selectedMotion.value.ip)
|
||||
d.sendEvent(name: "currentIP", value: ipvalue, descriptionText: "IP is ${ipvalue}")
|
||||
log.debug "Created ${d.displayName} with id: ${d.id}, dni: ${d.deviceNetworkId}"
|
||||
} else {
|
||||
log.debug "found ${d.displayName} with id $dni already exists"
|
||||
}
|
||||
}
|
||||
|
||||
if (!d) {
|
||||
log.debug "Creating WeMo Motion with dni: ${selectedMotion.value.mac}"
|
||||
d = addChildDevice("smartthings", "Wemo Motion", selectedMotion.value.mac, selectedMotion?.value.hub, [
|
||||
"label": selectedMotion?.value?.name ?: "Wemo Motion",
|
||||
"data": [
|
||||
"mac": selectedMotion.value.mac,
|
||||
"ip": selectedMotion.value.ip,
|
||||
"port": selectedMotion.value.port
|
||||
]
|
||||
])
|
||||
def ipvalue = convertHexToIP(selectedMotion.value.ip)
|
||||
d.sendEvent(name: "currentIP", value: ipvalue, descriptionText: "IP is ${ipvalue}")
|
||||
log.debug "Created ${d.displayName} with id: ${d.id}, dni: ${d.deviceNetworkId}"
|
||||
} else {
|
||||
log.debug "found ${d.displayName} with id $dni already exists"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -296,25 +294,24 @@ def addLightSwitches() {
|
||||
def d
|
||||
if (selectedLightSwitch) {
|
||||
d = getChildDevices()?.find {
|
||||
it.dni == selectedLightSwitch.value.mac || it.device.getDataValue("mac") == selectedLightSwitch.value.mac
|
||||
it.deviceNetworkId == selectedLightSwitch.value.mac || it.device.getDataValue("mac") == selectedLightSwitch.value.mac
|
||||
}
|
||||
if (!d) {
|
||||
log.debug "Creating WeMo Light Switch with dni: ${selectedLightSwitch.value.mac}"
|
||||
d = addChildDevice("smartthings", "Wemo Light Switch", selectedLightSwitch.value.mac, selectedLightSwitch?.value.hub, [
|
||||
"label": selectedLightSwitch?.value?.name ?: "Wemo Light Switch",
|
||||
"data": [
|
||||
"mac": selectedLightSwitch.value.mac,
|
||||
"ip": selectedLightSwitch.value.ip,
|
||||
"port": selectedLightSwitch.value.port
|
||||
]
|
||||
])
|
||||
def ipvalue = convertHexToIP(selectedLightSwitch.value.ip)
|
||||
d.sendEvent(name: "currentIP", value: ipvalue, descriptionText: "IP is ${ipvalue}")
|
||||
log.debug "created ${d.displayName} with id $dni"
|
||||
} else {
|
||||
log.debug "found ${d.displayName} with id $dni already exists"
|
||||
}
|
||||
}
|
||||
|
||||
if (!d) {
|
||||
log.debug "Creating WeMo Light Switch with dni: ${selectedLightSwitch.value.mac}"
|
||||
d = addChildDevice("smartthings", "Wemo Light Switch", selectedLightSwitch.value.mac, selectedLightSwitch?.value.hub, [
|
||||
"label": selectedLightSwitch?.value?.name ?: "Wemo Light Switch",
|
||||
"data": [
|
||||
"mac": selectedLightSwitch.value.mac,
|
||||
"ip": selectedLightSwitch.value.ip,
|
||||
"port": selectedLightSwitch.value.port
|
||||
]
|
||||
])
|
||||
def ipvalue = convertHexToIP(selectedLightSwitch.value.ip)
|
||||
d.sendEvent(name: "currentIP", value: ipvalue, descriptionText: "IP is ${ipvalue}")
|
||||
log.debug "created ${d.displayName} with id $dni"
|
||||
} else {
|
||||
log.debug "found ${d.displayName} with id $dni already exists"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
/**
|
||||
* Vacation Lighting Director
|
||||
*
|
||||
* Version 2.4 - Added information paragraphs
|
||||
* Version 2.5 - Moved scheduling over to Cron and added time as a trigger.
|
||||
* Cleaned up formatting and some typos.
|
||||
* Updated license.
|
||||
* Made people option optional
|
||||
* Added sttement to unschedule on mode change if people option is not selected
|
||||
*
|
||||
* Version 2.4 - Added information paragraphs
|
||||
*
|
||||
* Source code can be found here: https://github.com/tslagle13/SmartThings/blob/master/smartapps/tslagle13/vacation-lighting-director.groovy
|
||||
*
|
||||
* Copyright 2015 Tim Slagle
|
||||
* Copyright 2016 Tim Slagle
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
@@ -34,6 +40,7 @@ preferences {
|
||||
page name:"pageSetup"
|
||||
page name:"Setup"
|
||||
page name:"Settings"
|
||||
page name: "timeIntervalInput"
|
||||
|
||||
}
|
||||
|
||||
@@ -51,8 +58,7 @@ def pageSetup() {
|
||||
return dynamicPage(pageProperties) {
|
||||
section(""){
|
||||
paragraph "This app can be used to make your home seem occupied anytime you are away from your home. " +
|
||||
"Please use each othe the sections below to setup the different preferences to your liking. " +
|
||||
"I recommend this app be used with at least two away modes. An example would be 'Away Day' 'and Away Night'. "
|
||||
"Please use each of the the sections below to setup the different preferences to your liking. "
|
||||
}
|
||||
section("Setup Menu") {
|
||||
href "Setup", title: "Setup", description: "", state:greyedOut()
|
||||
@@ -70,7 +76,7 @@ def Setup() {
|
||||
def newMode = [
|
||||
name: "newMode",
|
||||
type: "mode",
|
||||
title: "Which?",
|
||||
title: "Modes",
|
||||
multiple: true,
|
||||
required: true
|
||||
]
|
||||
@@ -96,14 +102,6 @@ def Setup() {
|
||||
required: true,
|
||||
]
|
||||
|
||||
def people = [
|
||||
name: "people",
|
||||
type: "capability.presenceSensor",
|
||||
title: "If these people are home do not change light status",
|
||||
required: true,
|
||||
multiple: true
|
||||
]
|
||||
|
||||
def pageName = "Setup"
|
||||
|
||||
def pageProperties = [
|
||||
@@ -116,10 +114,11 @@ def Setup() {
|
||||
|
||||
section(""){
|
||||
paragraph "In this section you need to setup the deatils of how you want your lighting to be affected while " +
|
||||
paragraph "you are away. All of these settings are required in order for the simulator to run correctly."
|
||||
"you are away. All of these settings are required in order for the simulator to run correctly."
|
||||
}
|
||||
section("Which mode change triggers the simulator? (This app will only run in selected mode(s))") {
|
||||
input newMode
|
||||
section("Simulator Triggers") {
|
||||
input newMode
|
||||
href "timeIntervalInput", title: "Times", description: timeIntervalLabel(), refreshAfterSelection:true
|
||||
}
|
||||
section("Light switches to turn on/off") {
|
||||
input switches
|
||||
@@ -130,9 +129,6 @@ def Setup() {
|
||||
section("Number of active lights at any given time") {
|
||||
input number_of_active_lights
|
||||
}
|
||||
section("People") {
|
||||
input people
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -162,30 +158,58 @@ def Settings() {
|
||||
title: "Settings",
|
||||
nextPage: "pageSetup"
|
||||
]
|
||||
|
||||
def people = [
|
||||
name: "people",
|
||||
type: "capability.presenceSensor",
|
||||
title: "If these people are home do not change light status",
|
||||
required: false,
|
||||
multiple: true
|
||||
]
|
||||
|
||||
return dynamicPage(pageProperties) {
|
||||
|
||||
section(""){
|
||||
paragraph "In this section you can restrict how your simulator runs. For instance you can restrict on which days it will run " +
|
||||
paragraph "as well as a delay for the simulator to start after it is in the correct mode. Delaying the simulator helps with false starts based on a incorrect mode change."
|
||||
"as well as a delay for the simulator to start after it is in the correct mode. Delaying the simulator helps with false starts based on a incorrect mode change."
|
||||
}
|
||||
section("Delay to start simulator") {
|
||||
input falseAlarmThreshold
|
||||
}
|
||||
section("People") {
|
||||
paragraph "Not using this setting may cause some lights to remain on when you arrive home"
|
||||
input people
|
||||
}
|
||||
section("More options") {
|
||||
href "timeIntervalInput", title: "Only during a certain time", description: getTimeLabel(starting, ending), state: greyedOutTime(starting, ending), refreshAfterSelection:true
|
||||
input days
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
page(name: "timeIntervalInput", title: "Only during a certain time", refreshAfterSelection:true) {
|
||||
def timeIntervalInput() {
|
||||
dynamicPage(name: "timeIntervalInput") {
|
||||
section {
|
||||
input "starting", "time", title: "Starting", required: false
|
||||
input "ending", "time", title: "Ending", required: false
|
||||
input "startTimeType", "enum", title: "Starting at", options: [["time": "A specific time"], ["sunrise": "Sunrise"], ["sunset": "Sunset"]], defaultValue: "time", submitOnChange: true
|
||||
if (startTimeType in ["sunrise","sunset"]) {
|
||||
input "startTimeOffset", "number", title: "Offset in minutes (+/-)", range: "*..*", required: false
|
||||
}
|
||||
else {
|
||||
input "starting", "time", title: "Start time", required: false
|
||||
}
|
||||
}
|
||||
section {
|
||||
input "endTimeType", "enum", title: "Ending at", options: [["time": "A specific time"], ["sunrise": "Sunrise"], ["sunset": "Sunset"]], defaultValue: "time", submitOnChange: true
|
||||
if (endTimeType in ["sunrise","sunset"]) {
|
||||
input "endTimeOffset", "number", title: "Offset in minutes (+/-)", range: "*..*", required: false
|
||||
}
|
||||
else {
|
||||
input "ending", "time", title: "End time", required: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def installed() {
|
||||
initialize()
|
||||
}
|
||||
@@ -201,10 +225,13 @@ def initialize(){
|
||||
if (newMode != null) {
|
||||
subscribe(location, modeChangeHandler)
|
||||
}
|
||||
if (starting != null) {
|
||||
schedule(starting, modeChangeHandler)
|
||||
}
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
}
|
||||
|
||||
def modeChangeHandler(evt) {
|
||||
log.debug "Mode change to: ${evt.value}"
|
||||
def delay = (falseAlarmThreshold != null && falseAlarmThreshold != "") ? falseAlarmThreshold * 60 : 2 * 60
|
||||
runIn(delay, scheduleCheck)
|
||||
}
|
||||
@@ -212,48 +239,54 @@ def modeChangeHandler(evt) {
|
||||
|
||||
//Main logic to pick a random set of lights from the large set of lights to turn on and then turn the rest off
|
||||
def scheduleCheck(evt) {
|
||||
if(allOk){
|
||||
log.debug("Running")
|
||||
// turn off all the switches
|
||||
switches.off()
|
||||
|
||||
// grab a random switch
|
||||
def random = new Random()
|
||||
def inactive_switches = switches
|
||||
for (int i = 0 ; i < number_of_active_lights ; i++) {
|
||||
// if there are no inactive switches to turn on then let's break
|
||||
if (inactive_switches.size() == 0){
|
||||
break
|
||||
if(allOk){
|
||||
log.debug("Running")
|
||||
// turn off all the switches
|
||||
switches.off()
|
||||
|
||||
// grab a random switch
|
||||
def random = new Random()
|
||||
def inactive_switches = switches
|
||||
for (int i = 0 ; i < number_of_active_lights ; i++) {
|
||||
// if there are no inactive switches to turn on then let's break
|
||||
if (inactive_switches.size() == 0){
|
||||
break
|
||||
}
|
||||
|
||||
// grab a random switch and turn it on
|
||||
def random_int = random.nextInt(inactive_switches.size())
|
||||
inactive_switches[random_int].on()
|
||||
|
||||
// then remove that switch from the pool off switches that can be turned on
|
||||
inactive_switches.remove(random_int)
|
||||
}
|
||||
|
||||
// re-run again when the frequency demands it
|
||||
schedule("0 0/${frequency_minutes} * 1/1 * ? *", scheduleCheck)
|
||||
}
|
||||
|
||||
// grab a random switch and turn it on
|
||||
def random_int = random.nextInt(inactive_switches.size())
|
||||
inactive_switches[random_int].on()
|
||||
|
||||
// then remove that switch from the pool off switches that can be turned on
|
||||
inactive_switches.remove(random_int)
|
||||
}
|
||||
|
||||
// re-run again when the frequency demands it
|
||||
runIn(frequency_minutes * 60, scheduleCheck)
|
||||
}
|
||||
//Check to see if mode is ok but not time/day. If mode is still ok, check again after frequency period.
|
||||
else if (modeOk) {
|
||||
log.debug("mode OK. Running again")
|
||||
runIn(frequency_minutes * 60, scheduleCheck)
|
||||
switches.off()
|
||||
}
|
||||
//if none is ok turn off frequency check and turn off lights.
|
||||
else if(people){
|
||||
//don't turn off lights if anyone is home
|
||||
if(someoneIsHome()){
|
||||
log.debug("Stopping Check for Light")
|
||||
//Check to see if mode is ok but not time/day. If mode is still ok, check again after frequency period.
|
||||
else if (modeOk) {
|
||||
log.debug("mode OK. Running again")
|
||||
switches.off()
|
||||
}
|
||||
//if none is ok turn off frequency check and turn off lights.
|
||||
else {
|
||||
if(people){
|
||||
//don't turn off lights if anyone is home
|
||||
if(someoneIsHome){
|
||||
log.debug("Stopping Check for Light")
|
||||
unschedule()
|
||||
}
|
||||
else{
|
||||
log.debug("Stopping Check for Light and turning off all lights")
|
||||
switches.off()
|
||||
unschedule()
|
||||
}
|
||||
}
|
||||
else{
|
||||
log.debug("Stopping Check for Light and turning off all lights")
|
||||
switches.off()
|
||||
else if (!modeOk) {
|
||||
unschedule()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -286,26 +319,6 @@ private getDaysOk() {
|
||||
result
|
||||
}
|
||||
|
||||
private getTimeOk() {
|
||||
def result = true
|
||||
if (starting && ending) {
|
||||
def currTime = now()
|
||||
def start = timeToday(starting).time
|
||||
def stop = timeToday(ending).time
|
||||
result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start
|
||||
}
|
||||
|
||||
else if (starting){
|
||||
result = currTime >= start
|
||||
}
|
||||
else if (ending){
|
||||
result = currTime <= stop
|
||||
}
|
||||
|
||||
log.trace "timeOk = $result"
|
||||
result
|
||||
}
|
||||
|
||||
private getHomeIsEmpty() {
|
||||
def result = true
|
||||
|
||||
@@ -330,25 +343,59 @@ private getSomeoneIsHome() {
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
//gets the label for time restriction. Label phrasing changes depending on if there is both start and stop times or just one start/stop time.
|
||||
def getTimeLabel(starting, ending){
|
||||
|
||||
def timeLabel = "Tap to set"
|
||||
|
||||
if(starting && ending){
|
||||
timeLabel = "Between" + " " + hhmm(starting) + " " + "and" + " " + hhmm(ending)
|
||||
}
|
||||
else if (starting) {
|
||||
timeLabel = "Start at" + " " + hhmm(starting)
|
||||
}
|
||||
else if(ending){
|
||||
timeLabel = "End at" + hhmm(ending)
|
||||
}
|
||||
timeLabel
|
||||
private getTimeOk() {
|
||||
def result = true
|
||||
def start = timeWindowStart()
|
||||
def stop = timeWindowStop()
|
||||
if (start && stop && location.timeZone) {
|
||||
result = timeOfDayIsBetween(start, stop, new Date(), location.timeZone)
|
||||
}
|
||||
log.trace "timeOk = $result"
|
||||
result
|
||||
}
|
||||
|
||||
private timeWindowStart() {
|
||||
def result = null
|
||||
if (startTimeType == "sunrise") {
|
||||
result = location.currentState("sunriseTime")?.dateValue
|
||||
if (result && startTimeOffset) {
|
||||
result = new Date(result.time + Math.round(startTimeOffset * 60000))
|
||||
}
|
||||
}
|
||||
else if (startTimeType == "sunset") {
|
||||
result = location.currentState("sunsetTime")?.dateValue
|
||||
if (result && startTimeOffset) {
|
||||
result = new Date(result.time + Math.round(startTimeOffset * 60000))
|
||||
}
|
||||
}
|
||||
else if (starting && location.timeZone) {
|
||||
result = timeToday(starting, location.timeZone)
|
||||
}
|
||||
log.trace "timeWindowStart = ${result}"
|
||||
result
|
||||
}
|
||||
|
||||
private timeWindowStop() {
|
||||
def result = null
|
||||
if (endTimeType == "sunrise") {
|
||||
result = location.currentState("sunriseTime")?.dateValue
|
||||
if (result && endTimeOffset) {
|
||||
result = new Date(result.time + Math.round(endTimeOffset * 60000))
|
||||
}
|
||||
}
|
||||
else if (endTimeType == "sunset") {
|
||||
result = location.currentState("sunsetTime")?.dateValue
|
||||
if (result && endTimeOffset) {
|
||||
result = new Date(result.time + Math.round(endTimeOffset * 60000))
|
||||
}
|
||||
}
|
||||
else if (ending && location.timeZone) {
|
||||
result = timeToday(ending, location.timeZone)
|
||||
}
|
||||
log.trace "timeWindowStop = ${result}"
|
||||
result
|
||||
}
|
||||
|
||||
//fomrats time to readable format for time label
|
||||
private hhmm(time, fmt = "h:mm a")
|
||||
{
|
||||
def t = timeToday(time, location.timeZone)
|
||||
@@ -357,6 +404,41 @@ private hhmm(time, fmt = "h:mm a")
|
||||
f.format(t)
|
||||
}
|
||||
|
||||
private timeIntervalLabel() {
|
||||
def start = ""
|
||||
switch (startTimeType) {
|
||||
case "time":
|
||||
if (ending) {
|
||||
start += hhmm(starting)
|
||||
}
|
||||
break
|
||||
case "sunrise":
|
||||
case "sunset":
|
||||
start += startTimeType[0].toUpperCase() + startTimeType[1..-1]
|
||||
if (startTimeOffset) {
|
||||
start += startTimeOffset > 0 ? "+${startTimeOffset} min" : "${startTimeOffset} min"
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
def finish = ""
|
||||
switch (endTimeType) {
|
||||
case "time":
|
||||
if (ending) {
|
||||
finish += hhmm(ending)
|
||||
}
|
||||
break
|
||||
case "sunrise":
|
||||
case "sunset":
|
||||
finish += endTimeType[0].toUpperCase() + endTimeType[1..-1]
|
||||
if (endTimeOffset) {
|
||||
finish += endTimeOffset > 0 ? "+${endTimeOffset} min" : "${endTimeOffset} min"
|
||||
}
|
||||
break
|
||||
}
|
||||
start && finish ? "${start} to ${finish}" : ""
|
||||
}
|
||||
|
||||
//sets complete/not complete for the setup section on the main dynamic page
|
||||
def greyedOut(){
|
||||
def result = ""
|
||||
@@ -369,16 +451,7 @@ def greyedOut(){
|
||||
//sets complete/not complete for the settings section on the main dynamic page
|
||||
def greyedOutSettings(){
|
||||
def result = ""
|
||||
if (starting || ending || days || falseAlarmThreshold) {
|
||||
result = "complete"
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
//sets complete/not complete for time restriction section in settings
|
||||
def greyedOutTime(starting, ending){
|
||||
def result = ""
|
||||
if (starting || ending) {
|
||||
if (people || days || falseAlarmThreshold ) {
|
||||
result = "complete"
|
||||
}
|
||||
result
|
||||
|
||||
129
smartapps/vlaminck/-.src/-.groovy
Normal file
129
smartapps/vlaminck/-.src/-.groovy
Normal file
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* Copyright 2015 SmartThings
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
* Alfred Workflow
|
||||
*
|
||||
* Author: SmartThings
|
||||
*/
|
||||
|
||||
definition(
|
||||
name: "김신동",
|
||||
namespace: "vlaminck",
|
||||
author: "SmartThings",
|
||||
description: "This SmartApp allows you to interact with the things in your physical graph through Alfred.",
|
||||
category: "Convenience",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/alfred-app.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/alfred-app@2x.png",
|
||||
oauth: [displayName: "SmartThings Alfred Workflow", displayLink: ""]
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("Allow Alfred to 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
|
||||
}
|
||||
}
|
||||
|
||||
mappings {
|
||||
path("/switches") {
|
||||
action: [
|
||||
GET: "listSwitches",
|
||||
PUT: "updateSwitches"
|
||||
]
|
||||
}
|
||||
path("/switches/:id") {
|
||||
action: [
|
||||
GET: "showSwitch",
|
||||
PUT: "updateSwitch"
|
||||
]
|
||||
}
|
||||
path("/locks") {
|
||||
action: [
|
||||
GET: "listLocks",
|
||||
PUT: "updateLocks"
|
||||
]
|
||||
}
|
||||
path("/locks/:id") {
|
||||
action: [
|
||||
GET: "showLock",
|
||||
PUT: "updateLock"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {}
|
||||
|
||||
def updated() {}
|
||||
|
||||
def listSwitches() {
|
||||
switches.collect { device(it,"switch") }
|
||||
}
|
||||
void updateSwitches() {
|
||||
updateAll(switches)
|
||||
}
|
||||
def showSwitch() {
|
||||
show(switches, "switch")
|
||||
}
|
||||
void updateSwitch() {
|
||||
update(switches)
|
||||
}
|
||||
|
||||
def listLocks() {
|
||||
locks.collect { device(it, "lock") }
|
||||
}
|
||||
void updateLocks() {
|
||||
updateAll(locks)
|
||||
}
|
||||
def showLock() {
|
||||
show(locks, "lock")
|
||||
}
|
||||
void updateLock() {
|
||||
update(locks)
|
||||
}
|
||||
|
||||
private void updateAll(devices) {
|
||||
def command = request.JSON?.command
|
||||
if (command) {
|
||||
devices."$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"()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private show(devices, name) {
|
||||
def device = devices.find { it.id == params.id }
|
||||
if (!device) {
|
||||
httpError(404, "Device not found")
|
||||
}
|
||||
else {
|
||||
def s = device.currentState(name)
|
||||
[id: device.id, label: device.displayName, name: device.displayName, state: s]
|
||||
}
|
||||
}
|
||||
|
||||
private device(it, name) {
|
||||
if (it) {
|
||||
def s = it.currentState(name)
|
||||
[id: it.id, label: it.displayName, name: it.displayName, state: s]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user