mirror of
https://github.com/mtan93/SmartThingsPublic.git
synced 2026-03-15 13:10:51 +00:00
Compare commits
113 Commits
master_old
...
revert-490
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe2b36cd30 | ||
|
|
55c17383e1 | ||
|
|
bd9a1d1dc5 | ||
|
|
a6bec43f2a | ||
|
|
5ec82217ac | ||
|
|
3b89368d45 | ||
|
|
12f6039de5 | ||
|
|
65c9da32e7 | ||
|
|
7147770e2d | ||
|
|
5eb33eca19 | ||
|
|
ec321ce85d | ||
|
|
7669bec0bc | ||
|
|
a20a58bd48 | ||
|
|
efabd07dea | ||
|
|
1e55f62048 | ||
|
|
12b18eae08 | ||
|
|
aad82e10ef | ||
|
|
b11b362c60 | ||
|
|
e1853b8e50 | ||
|
|
968834e33e | ||
|
|
e51a38eb28 | ||
|
|
553b45a3f3 | ||
|
|
d9ab3bca00 | ||
|
|
56cfe9e936 | ||
|
|
3a7abd6169 | ||
|
|
d69abb64bd | ||
|
|
7429ecc83b | ||
|
|
112a35f5db | ||
|
|
0d214b742e | ||
|
|
c297564665 | ||
|
|
26ab32565b | ||
|
|
c20026b376 | ||
|
|
9733947fea | ||
|
|
6abf8c7f20 | ||
|
|
fe505ddc9f | ||
|
|
f4034f5ccf | ||
|
|
c1c2431299 | ||
|
|
39f0c49ea6 | ||
|
|
ed5a409c63 | ||
|
|
8453292038 | ||
|
|
e98a04a1b4 | ||
|
|
41e95b9248 | ||
|
|
63f20c912d | ||
|
|
837d2d0cfd | ||
|
|
629c4cc231 | ||
|
|
f12684565c | ||
|
|
51e727b91a | ||
|
|
49a858eb5c | ||
|
|
112a4087b0 | ||
|
|
132d8fc9d8 | ||
|
|
7def620f04 | ||
|
|
5b0b239caa | ||
|
|
3ea70fecad | ||
|
|
358cf261e8 | ||
|
|
f420907043 | ||
|
|
bf915b49dc | ||
|
|
1c2a65e313 | ||
|
|
075fdf0974 | ||
|
|
21041570db | ||
|
|
8c3daf5f34 | ||
|
|
f3138501e8 | ||
|
|
2dfb43f841 | ||
|
|
f9b5e2ba79 | ||
|
|
1f86001418 | ||
|
|
e4b010eb46 | ||
|
|
c921121645 | ||
|
|
9be808a3eb | ||
|
|
6778f8d58a | ||
|
|
961ee321ac | ||
|
|
22fb4e36c6 | ||
|
|
18bfa87948 | ||
|
|
7fc4aaa341 | ||
|
|
d1504e9a3c | ||
|
|
91fefe536d | ||
|
|
fe4a2ed3d0 | ||
|
|
e529624d36 | ||
|
|
6ba37caa03 | ||
|
|
8d0fa7f561 | ||
|
|
6ede225715 | ||
|
|
30844676b6 | ||
|
|
94aa114ccb | ||
|
|
a9e68d086c | ||
|
|
d427ab8709 | ||
|
|
a205a94d78 | ||
|
|
e775752496 | ||
|
|
9318e9a311 | ||
|
|
e1c52454c6 | ||
|
|
145fce2062 | ||
|
|
e9996b9fd7 | ||
|
|
8be585e544 | ||
|
|
e114fafd56 | ||
|
|
e6367a7832 | ||
|
|
c5da3fe4a0 | ||
|
|
1b9d2fe9ce | ||
|
|
90dee51255 | ||
|
|
7b7fdd43cd | ||
|
|
9059718818 | ||
|
|
9d7c66c7af | ||
|
|
8e81967227 | ||
|
|
290e8e4129 | ||
|
|
8c6c68f102 | ||
|
|
661f8b3bc0 | ||
|
|
96f2c5ed8b | ||
|
|
a62d825f69 | ||
|
|
1890147221 | ||
|
|
2fb5f8c78c | ||
|
|
b44356248c | ||
|
|
c30af84d70 | ||
|
|
5cf72c644c | ||
|
|
8ae9b06022 | ||
|
|
71fc8e7f5f | ||
|
|
30dedde0df | ||
|
|
dcfc16cf12 |
@@ -0,0 +1,104 @@
|
|||||||
|
/**
|
||||||
|
* EnerTalk Energy Meter
|
||||||
|
*
|
||||||
|
* Copyright 2015 hyeon seok yang
|
||||||
|
*
|
||||||
|
* 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: "EnerTalk Energy Meter", namespace: "Encored Technologies", author: "hyeon seok yang") {
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
// TODO: define status and reply messages here
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles(scale:2) {
|
||||||
|
valueTile("view", "device.view", decoration: "flat") {
|
||||||
|
state "view", label:' ${currentValue} kWh'
|
||||||
|
}
|
||||||
|
valueTile("month", "device.month", width: 6, height : 3, decoration: "flat") {
|
||||||
|
state "month", label:' ${currentValue}'
|
||||||
|
}
|
||||||
|
valueTile("real", "device.real", width: 2, height : 2, decoration: "flat") {
|
||||||
|
state "real", label:' ${currentValue}'
|
||||||
|
}
|
||||||
|
valueTile("tier", "device.tier", width: 2, height : 2, decoration: "flat") {
|
||||||
|
state "tier", label:' ${currentValue}'
|
||||||
|
}
|
||||||
|
valueTile("plan", "device.plan", width: 2, height : 2, decoration: "flat") {
|
||||||
|
state "plan", label:' ${currentValue}'
|
||||||
|
}
|
||||||
|
|
||||||
|
htmlTile(name:"deepLink", action:"linkApp", whitelist:["code.jquery.com",
|
||||||
|
"ajax.googleapis.com",
|
||||||
|
"fonts.googleapis.com",
|
||||||
|
"code.highcharts.com",
|
||||||
|
"enertalk-card.encoredtech.com",
|
||||||
|
"s3-ap-northeast-1.amazonaws.com",
|
||||||
|
"s3.amazonaws.com",
|
||||||
|
"ui-hub.encoredtech.com",
|
||||||
|
"enertalk-auth.encoredtech.com",
|
||||||
|
"api.encoredtech.com",
|
||||||
|
"cdnjs.cloudflare.com",
|
||||||
|
"encoredtech.com",
|
||||||
|
"itunes.apple.com"], width:2, height:2){}
|
||||||
|
|
||||||
|
main (["view"])
|
||||||
|
details (["month", "real", "tier", "plan", "deepLink"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mappings {
|
||||||
|
|
||||||
|
path("/linkApp") {action: [ GET: "getLinkedApp" ]}
|
||||||
|
}
|
||||||
|
|
||||||
|
def getLinkedApp() {
|
||||||
|
def lang = clientLocale?.language
|
||||||
|
if ("${lang}" == "ko") {
|
||||||
|
lang = "<p style=\'margin-left:15vw; color: #aeaeb0;\'>기기 설정</p>"
|
||||||
|
} else {
|
||||||
|
lang = "<p style=\'margin-left:5vw; color: #aeaeb0;\'>Setup Device</p>"
|
||||||
|
}
|
||||||
|
renderHTML() {
|
||||||
|
head {
|
||||||
|
"""
|
||||||
|
<meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width, height=device-height">
|
||||||
|
<style>
|
||||||
|
#레이어_1 { margin-left : 17vw; width : 50vw; height : 50vw;}
|
||||||
|
.st0{fill:#B5B6BB;}
|
||||||
|
</style>
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
"""
|
||||||
|
<div id="container">
|
||||||
|
<a id="st-deep-link" href="#">
|
||||||
|
<svg version="1.1" id="레이어_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 40 40" style="enable-background:new 0 0 40 40;" xml:space="preserve"><path class="st0" d="M20,0C9,0,0,9,0,20C0,30.5,8,39,18.2,40l3.8-4.8l-3.9-4.8c-4.9-0.9-8.6-5.2-8.6-10.4c0-5.8,4.7-10.5,10.5-10.5
|
||||||
|
S30.5,14.2,30.5,20c0,5.1-3.7,9.4-8.5,10.3l3.7,4.5L21.8,40C32,39.1,40,30.5,40,20C40,9,31,0,20,0z"/></svg>
|
||||||
|
</a>
|
||||||
|
${lang}
|
||||||
|
</div>
|
||||||
|
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
|
||||||
|
<script>
|
||||||
|
var ua = navigator.userAgent.toLowerCase();
|
||||||
|
var isAndroid = ua.indexOf("android") > -1;
|
||||||
|
if(!isAndroid) {
|
||||||
|
\$("#st-deep-link").attr("href", "https://itunes.apple.com/kr/app/enertalk-for-home/id1024660780?mt=8");
|
||||||
|
} else {
|
||||||
|
\$("#st-deep-link").attr("href", "market://details?id=com.ionicframework.enertalkhome874425");
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
163
devicetypes/osotech/plantlink.src/plantlink.groovy
Normal file
163
devicetypes/osotech/plantlink.src/plantlink.groovy
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
/**
|
||||||
|
* PlantLink
|
||||||
|
*
|
||||||
|
* This device type takes sensor data and converts it into a json packet to send to myplantlink.com
|
||||||
|
* where its values will be computed for soil and plant type to show user readable values of how your
|
||||||
|
* specific plant is doing.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* Copyright 2015 Oso Technologies
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
import groovy.json.JsonBuilder
|
||||||
|
|
||||||
|
metadata {
|
||||||
|
definition (name: "PlantLink", namespace: "OsoTech", author: "Oso Technologies") {
|
||||||
|
capability "Sensor"
|
||||||
|
|
||||||
|
command "setStatusIcon"
|
||||||
|
command "setPlantFuelLevel"
|
||||||
|
command "setBatteryLevel"
|
||||||
|
command "setInstallSmartApp"
|
||||||
|
|
||||||
|
attribute "plantStatus","string"
|
||||||
|
attribute "plantFuelLevel","number"
|
||||||
|
attribute "linkBatteryLevel","string"
|
||||||
|
attribute "installSmartApp","string"
|
||||||
|
|
||||||
|
fingerprint profileId: "0104", inClusters: "0000,0001,0B04"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
status "battery": "read attr - raw: C0720100010A000021340A, dni: C072, endpoint: 01, cluster: 0001, size: 0A, attrId: 0000, encoding: 21, value: 0a34"
|
||||||
|
status "moisture": "read attr - raw: C072010B040A0001290000, dni: C072, endpoint: 01, cluster: 0B04, size: 0A, attrId: 0100, encoding: 29, value: 0000"
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
standardTile("Title", "device.label") {
|
||||||
|
state("label", label:'PlantLink ${device.label}')
|
||||||
|
}
|
||||||
|
|
||||||
|
valueTile("plantMoistureTile", "device.plantFuelLevel", width: 1, height: 1) {
|
||||||
|
state("plantMoisture", label: '${currentValue}% Moisture')
|
||||||
|
}
|
||||||
|
|
||||||
|
valueTile("plantStatusTextTile", "device.plantStatus", decoration: "flat", width: 2, height: 2) {
|
||||||
|
state("plantStatusTextTile", label:'${currentValue}')
|
||||||
|
}
|
||||||
|
|
||||||
|
valueTile("battery", "device.linkBatteryLevel" ) {
|
||||||
|
state("battery", label:'${currentValue}% battery')
|
||||||
|
}
|
||||||
|
|
||||||
|
valueTile("installSmartApp","device.installSmartApp", decoration: "flat", width: 3, height: 1) {
|
||||||
|
state "needSmartApp", label:'Please install SmartApp "Required PlantLink Connector"', defaultState:true
|
||||||
|
state "connectedToSmartApp", label:'Connected to myplantlink.com'
|
||||||
|
}
|
||||||
|
|
||||||
|
main "plantStatusTextTile"
|
||||||
|
details(['plantStatusTextTile', "plantMoistureTile", "battery", "installSmartApp"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def setStatusIcon(value){
|
||||||
|
def status = ''
|
||||||
|
switch (value) {
|
||||||
|
case '0':
|
||||||
|
status = 'Needs Water'
|
||||||
|
break
|
||||||
|
case '1':
|
||||||
|
status = 'Dry'
|
||||||
|
break
|
||||||
|
case '2':
|
||||||
|
case '3':
|
||||||
|
status = 'Good'
|
||||||
|
break
|
||||||
|
case '4':
|
||||||
|
status = 'Too Wet'
|
||||||
|
break
|
||||||
|
case 'No Soil':
|
||||||
|
status = 'Too Dry'
|
||||||
|
setPlantFuelLevel(0)
|
||||||
|
break
|
||||||
|
case 'Recently Watered':
|
||||||
|
status = 'Watered'
|
||||||
|
setPlantFuelLevel(100)
|
||||||
|
break
|
||||||
|
case 'Low Battery':
|
||||||
|
status = 'Low Battery'
|
||||||
|
break
|
||||||
|
case 'Waiting on First Measurement':
|
||||||
|
status = 'Calibrating'
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
status = "?"
|
||||||
|
break
|
||||||
|
}
|
||||||
|
sendEvent("name":"plantStatus", "value":status, "description":statusText, displayed: true, isStateChange: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
def setPlantFuelLevel(value){
|
||||||
|
sendEvent("name":"plantFuelLevel", "value":value, "description":statusText, displayed: true, isStateChange: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
def setBatteryLevel(value){
|
||||||
|
sendEvent("name":"linkBatteryLevel", "value":value, "description":statusText, displayed: true, isStateChange: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
def setInstallSmartApp(value){
|
||||||
|
sendEvent("name":"installSmartApp", "value":value)
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(String description) {
|
||||||
|
|
||||||
|
def description_map = parseDescriptionAsMap(description)
|
||||||
|
def event_name = ""
|
||||||
|
def measurement_map = [
|
||||||
|
type: "link",
|
||||||
|
signal: "0x00",
|
||||||
|
zigbeedeviceid: device.zigbeeId,
|
||||||
|
created: new Date().time /1000 as int
|
||||||
|
]
|
||||||
|
if (description_map.cluster == "0000"){
|
||||||
|
/* version number, not used */
|
||||||
|
|
||||||
|
} else if (description_map.cluster == "0001"){
|
||||||
|
/* battery voltage in mV (device needs minimium 2.1v to run) */
|
||||||
|
log.debug "PlantLink - id ${device.zigbeeId} battery ${description_map.value}"
|
||||||
|
event_name = "battery_status"
|
||||||
|
measurement_map["battery"] = "0x${description_map.value}"
|
||||||
|
|
||||||
|
} else if (description_map.cluster == "0B04"){
|
||||||
|
/* raw moisture reading (needs to be sent to plantlink for soil/plant type conversion) */
|
||||||
|
log.debug "PlantLink - id ${device.zigbeeId} raw moisture ${description_map.value}"
|
||||||
|
measurement_map["moisture"] = "0x${description_map.value}"
|
||||||
|
event_name = "moisture_status"
|
||||||
|
|
||||||
|
} else{
|
||||||
|
log.debug "PlantLink - id ${device.zigbeeId} Unknown '${description}'"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
def json_builder = new JsonBuilder(measurement_map)
|
||||||
|
def result = createEvent(name: event_name, value: json_builder.toString())
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def parseDescriptionAsMap(description) {
|
||||||
|
(description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||||
|
def nameAndValue = param.split(":")
|
||||||
|
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
/**
|
||||||
|
* 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: "Arrival Sensor HA", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Tone"
|
||||||
|
capability "Actuator"
|
||||||
|
capability "Presence Sensor"
|
||||||
|
capability "Sensor"
|
||||||
|
capability "Battery"
|
||||||
|
capability "Configuration"
|
||||||
|
|
||||||
|
fingerprint inClusters: "0000,0001,0003,000F,0020", outClusters: "0003,0019",
|
||||||
|
manufacturer: "SmartThings", model: "tagv4", deviceJoinName: "Arrival Sensor"
|
||||||
|
}
|
||||||
|
|
||||||
|
preferences {
|
||||||
|
section {
|
||||||
|
image(name: 'educationalcontent', multiple: true, images: [
|
||||||
|
"http://cdn.device-gse.smartthings.com/Arrival/Arrival1.png",
|
||||||
|
"http://cdn.device-gse.smartthings.com/Arrival/Arrival2.png"
|
||||||
|
])
|
||||||
|
}
|
||||||
|
section {
|
||||||
|
input "checkInterval", "enum", title: "Presence timeout (minutes)",
|
||||||
|
defaultValue:"2", options: ["2", "3", "5"], displayDuringSetup: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
standardTile("beep", "device.beep", decoration: "flat") {
|
||||||
|
state "beep", label:'', action:"tone.beep", icon:"st.secondary.beep", backgroundColor:"#ffffff"
|
||||||
|
}
|
||||||
|
valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false) {
|
||||||
|
state "battery", label:'${currentValue}% battery', unit:""
|
||||||
|
}
|
||||||
|
|
||||||
|
main "presence"
|
||||||
|
details(["presence", "beep", "battery"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def updated() {
|
||||||
|
startTimer()
|
||||||
|
}
|
||||||
|
|
||||||
|
def configure() {
|
||||||
|
def cmds = zigbee.configureReporting(0x0001, 0x0020, 0x20, 20, 20, 0x01)
|
||||||
|
log.debug "configure -- cmds: ${cmds}"
|
||||||
|
return cmds
|
||||||
|
}
|
||||||
|
|
||||||
|
def beep() {
|
||||||
|
log.debug "Sending Identify command to beep the sensor for 5 seconds"
|
||||||
|
return zigbee.command(0x0003, 0x00, "0500")
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(String description) {
|
||||||
|
state.lastCheckin = now()
|
||||||
|
handlePresenceEvent(true)
|
||||||
|
|
||||||
|
if (description?.startsWith('read attr -')) {
|
||||||
|
handleReportAttributeMessage(description)
|
||||||
|
}
|
||||||
|
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleReportAttributeMessage(String description) {
|
||||||
|
def descMap = zigbee.parseDescriptionAsMap(description)
|
||||||
|
|
||||||
|
if (descMap.clusterInt == 0x0001 && descMap.attrInt == 0x0020) {
|
||||||
|
handleBatteryEvent(Integer.parseInt(descMap.value, 16))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create battery event from reported battery voltage.
|
||||||
|
*
|
||||||
|
* @param volts Battery voltage in .1V increments
|
||||||
|
*/
|
||||||
|
private handleBatteryEvent(volts) {
|
||||||
|
if (volts == 0 || volts == 255) {
|
||||||
|
log.debug "Ignoring invalid value for voltage (${volts/10}V)"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
def batteryMap = [28:100, 27:100, 26:100, 25:90, 24:90, 23:70,
|
||||||
|
22:70, 21:50, 20:50, 19:30, 18:30, 17:15, 16:1, 15:0]
|
||||||
|
def minVolts = 15
|
||||||
|
def maxVolts = 28
|
||||||
|
|
||||||
|
if (volts < minVolts)
|
||||||
|
volts = minVolts
|
||||||
|
else if (volts > maxVolts)
|
||||||
|
volts = maxVolts
|
||||||
|
def pct = batteryMap[volts]
|
||||||
|
if (pct != null) {
|
||||||
|
def linkText = getLinkText(device)
|
||||||
|
def eventMap = [
|
||||||
|
name: 'battery',
|
||||||
|
value: pct,
|
||||||
|
descriptionText: "${linkText} battery was ${pct}%"
|
||||||
|
]
|
||||||
|
log.debug "Creating battery event for voltage=${volts/10}V: ${eventMap}"
|
||||||
|
sendEvent(eventMap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handlePresenceEvent(present) {
|
||||||
|
def wasPresent = device.currentState("presence")?.value == "present"
|
||||||
|
if (!wasPresent && present) {
|
||||||
|
log.debug "Sensor is present"
|
||||||
|
startTimer()
|
||||||
|
} else if (!present) {
|
||||||
|
log.debug "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'}",
|
||||||
|
]
|
||||||
|
log.debug "Creating presence event: ${eventMap}"
|
||||||
|
sendEvent(eventMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
private startTimer() {
|
||||||
|
log.debug "Scheduling periodic timer"
|
||||||
|
schedule("0 * * * * ?", checkPresenceCallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
private stopTimer() {
|
||||||
|
log.debug "Stopping periodic timer"
|
||||||
|
unschedule()
|
||||||
|
}
|
||||||
|
|
||||||
|
def checkPresenceCallback() {
|
||||||
|
def timeSinceLastCheckin = (now() - state.lastCheckin) / 1000
|
||||||
|
def theCheckInterval = (checkInterval ? checkInterval as int : 2) * 60
|
||||||
|
log.debug "Sensor checked in ${timeSinceLastCheckin} seconds ago"
|
||||||
|
if (timeSinceLastCheckin >= theCheckInterval) {
|
||||||
|
handlePresenceEvent(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
* Author: SmartThings
|
* Author: SmartThings
|
||||||
* Date: 2013-12-04
|
* Date: 2013-12-04
|
||||||
*/
|
*/
|
||||||
|
//DEPRECATED - Using the generic DTH for this device. Users need to be moved before deleting this DTH
|
||||||
metadata {
|
metadata {
|
||||||
definition (name: "CentraLite Dimmer", namespace: "smartthings", author: "SmartThings") {
|
definition (name: "CentraLite Dimmer", namespace: "smartthings", author: "SmartThings") {
|
||||||
capability "Switch Level"
|
capability "Switch Level"
|
||||||
@@ -25,7 +26,6 @@ metadata {
|
|||||||
capability "Refresh"
|
capability "Refresh"
|
||||||
capability "Sensor"
|
capability "Sensor"
|
||||||
|
|
||||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0B04,0B05", outClusters: "0019"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// simulator metadata
|
// simulator metadata
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
/**
|
||||||
|
* Ecobee Sensor
|
||||||
|
*
|
||||||
|
* Copyright 2015 Juan Risso
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
metadata {
|
||||||
|
definition (name: "Ecobee Sensor", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Sensor"
|
||||||
|
capability "Temperature Measurement"
|
||||||
|
capability "Motion Sensor"
|
||||||
|
capability "Refresh"
|
||||||
|
capability "Polling"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
// TODO: define status and reply messages here
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
valueTile("temperature", "device.temperature", width: 2, height: 2) {
|
||||||
|
state("temperature", label:'${currentValue}°', unit:"F",
|
||||||
|
backgroundColors:[
|
||||||
|
[value: 31, color: "#153591"],
|
||||||
|
[value: 44, color: "#1e9cbb"],
|
||||||
|
[value: 59, color: "#90d2a7"],
|
||||||
|
[value: 74, color: "#44b621"],
|
||||||
|
[value: 84, color: "#f1d801"],
|
||||||
|
[value: 95, color: "#d04e00"],
|
||||||
|
[value: 96, color: "#bc2323"]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
standardTile("motion", "device.motion") {
|
||||||
|
state("active", label:'motion', icon:"st.motion.motion.active", backgroundColor:"#53a7c0")
|
||||||
|
state("inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff")
|
||||||
|
}
|
||||||
|
|
||||||
|
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
|
||||||
|
main (["temperature","motion"])
|
||||||
|
details(["temperature","motion","refresh"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def refresh() {
|
||||||
|
log.debug "refresh..."
|
||||||
|
poll()
|
||||||
|
}
|
||||||
|
|
||||||
|
void poll() {
|
||||||
|
log.debug "Executing 'poll' using parent SmartApp"
|
||||||
|
parent.pollChildren(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
//generate custom mobile activity feeds event
|
||||||
|
def generateActivityFeedsEvent(notificationMessage) {
|
||||||
|
sendEvent(name: "notificationMessage", value: "$device.displayName $notificationMessage", descriptionText: "$device.displayName $notificationMessage", displayed: true)
|
||||||
|
}
|
||||||
@@ -22,31 +22,31 @@ metadata {
|
|||||||
capability "Polling"
|
capability "Polling"
|
||||||
capability "Sensor"
|
capability "Sensor"
|
||||||
capability "Refresh"
|
capability "Refresh"
|
||||||
|
|
||||||
command "generateEvent"
|
command "generateEvent"
|
||||||
command "raiseSetpoint"
|
command "raiseSetpoint"
|
||||||
command "lowerSetpoint"
|
command "lowerSetpoint"
|
||||||
command "resumeProgram"
|
command "resumeProgram"
|
||||||
command "switchMode"
|
command "switchMode"
|
||||||
|
|
||||||
attribute "thermostatSetpoint","number"
|
attribute "thermostatSetpoint","number"
|
||||||
attribute "thermostatStatus","string"
|
attribute "thermostatStatus","string"
|
||||||
}
|
}
|
||||||
|
|
||||||
simulator { }
|
simulator { }
|
||||||
|
|
||||||
tiles {
|
tiles {
|
||||||
valueTile("temperature", "device.temperature", width: 2, height: 2) {
|
valueTile("temperature", "device.temperature", width: 2, height: 2) {
|
||||||
state("temperature", label:'${currentValue}°', unit:"F",
|
state("temperature", label:'${currentValue}°', unit:"F",
|
||||||
backgroundColors:[
|
backgroundColors:[
|
||||||
[value: 31, color: "#153591"],
|
[value: 31, color: "#153591"],
|
||||||
[value: 44, color: "#1e9cbb"],
|
[value: 44, color: "#1e9cbb"],
|
||||||
[value: 59, color: "#90d2a7"],
|
[value: 59, color: "#90d2a7"],
|
||||||
[value: 74, color: "#44b621"],
|
[value: 74, color: "#44b621"],
|
||||||
[value: 84, color: "#f1d801"],
|
[value: 84, color: "#f1d801"],
|
||||||
[value: 95, color: "#d04e00"],
|
[value: 95, color: "#d04e00"],
|
||||||
[value: 96, color: "#bc2323"]
|
[value: 96, color: "#bc2323"]
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
standardTile("mode", "device.thermostatMode", inactiveLabel: false, decoration: "flat") {
|
standardTile("mode", "device.thermostatMode", inactiveLabel: false, decoration: "flat") {
|
||||||
@@ -54,27 +54,27 @@ metadata {
|
|||||||
state "heat", action:"switchMode", nextState: "updating", icon: "st.thermostat.heat"
|
state "heat", action:"switchMode", nextState: "updating", icon: "st.thermostat.heat"
|
||||||
state "cool", action:"switchMode", nextState: "updating", icon: "st.thermostat.cool"
|
state "cool", action:"switchMode", nextState: "updating", icon: "st.thermostat.cool"
|
||||||
state "auto", action:"switchMode", nextState: "updating", icon: "st.thermostat.auto"
|
state "auto", action:"switchMode", nextState: "updating", icon: "st.thermostat.auto"
|
||||||
state "auxHeatOnly", action:"switchMode", icon: "st.thermostat.emergency-heat"
|
state "auxHeatOnly", action:"switchMode", icon: "st.thermostat.emergency-heat"
|
||||||
state "updating", label:"Working", icon: "st.secondary.secondary"
|
state "updating", label:"Working", icon: "st.secondary.secondary"
|
||||||
}
|
}
|
||||||
standardTile("fanMode", "device.thermostatFanMode", inactiveLabel: false, decoration: "flat") {
|
standardTile("fanMode", "device.thermostatFanMode", inactiveLabel: false, decoration: "flat") {
|
||||||
state "auto", label:'Fan: ${currentValue}', action:"switchFanMode", nextState: "on"
|
state "auto", label:'Fan: ${currentValue}', action:"switchFanMode", nextState: "on"
|
||||||
state "on", label:'Fan: ${currentValue}', action:"switchFanMode", nextState: "off"
|
state "on", label:'Fan: ${currentValue}', action:"switchFanMode", nextState: "off"
|
||||||
state "off", label:'Fan: ${currentValue}', action:"switchFanMode", nextState: "circulate"
|
state "off", label:'Fan: ${currentValue}', action:"switchFanMode", nextState: "circulate"
|
||||||
state "circulate", label:'Fan: ${currentValue}', action:"switchFanMode", nextState: "auto"
|
state "circulate", label:'Fan: ${currentValue}', action:"switchFanMode", nextState: "auto"
|
||||||
}
|
}
|
||||||
standardTile("upButtonControl", "device.thermostatSetpoint", inactiveLabel: false, decoration: "flat") {
|
standardTile("upButtonControl", "device.thermostatSetpoint", inactiveLabel: false, decoration: "flat") {
|
||||||
state "setpoint", action:"raiseSetpoint", backgroundColor:"#d04e00", icon:"st.thermostat.thermostat-up"
|
state "setpoint", action:"raiseSetpoint", icon:"st.thermostat.thermostat-up"
|
||||||
}
|
}
|
||||||
valueTile("thermostatSetpoint", "device.thermostatSetpoint", width: 1, height: 1, decoration: "flat") {
|
valueTile("thermostatSetpoint", "device.thermostatSetpoint", width: 1, height: 1, decoration: "flat") {
|
||||||
state "thermostatSetpoint", label:'${currentValue}'
|
state "thermostatSetpoint", label:'${currentValue}'
|
||||||
}
|
}
|
||||||
valueTile("currentStatus", "device.thermostatStatus", height: 1, width: 2, decoration: "flat") {
|
valueTile("currentStatus", "device.thermostatStatus", height: 1, width: 2, decoration: "flat") {
|
||||||
state "thermostatStatus", label:'${currentValue}', backgroundColor:"#ffffff"
|
state "thermostatStatus", label:'${currentValue}', backgroundColor:"#ffffff"
|
||||||
}
|
}
|
||||||
standardTile("downButtonControl", "device.thermostatSetpoint", inactiveLabel: false, decoration: "flat") {
|
standardTile("downButtonControl", "device.thermostatSetpoint", inactiveLabel: false, decoration: "flat") {
|
||||||
state "setpoint", action:"lowerSetpoint", backgroundColor:"#d04e00", icon:"st.thermostat.thermostat-down"
|
state "setpoint", action:"lowerSetpoint", icon:"st.thermostat.thermostat-down"
|
||||||
}
|
}
|
||||||
controlTile("heatSliderControl", "device.heatingSetpoint", "slider", height: 1, width: 2, inactiveLabel: false) {
|
controlTile("heatSliderControl", "device.heatingSetpoint", "slider", height: 1, width: 2, inactiveLabel: false) {
|
||||||
state "setHeatingSetpoint", action:"thermostat.setHeatingSetpoint", backgroundColor:"#d04e00"
|
state "setHeatingSetpoint", action:"thermostat.setHeatingSetpoint", backgroundColor:"#d04e00"
|
||||||
}
|
}
|
||||||
@@ -91,218 +91,196 @@ metadata {
|
|||||||
state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
|
state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
}
|
}
|
||||||
standardTile("resumeProgram", "device.resumeProgram", inactiveLabel: false, decoration: "flat") {
|
standardTile("resumeProgram", "device.resumeProgram", inactiveLabel: false, decoration: "flat") {
|
||||||
state "resume", label:'Resume Program', action:"device.resumeProgram", icon:"st.sonos.play-icon"
|
state "resume", action:"resumeProgram", nextState: "updating", label:'Resume Schedule', icon:"st.samsung.da.oven_ic_send"
|
||||||
|
state "updating", label:"Working", icon: "st.secondary.secondary"
|
||||||
}
|
}
|
||||||
main "temperature"
|
main "temperature"
|
||||||
details(["temperature", "upButtonControl", "thermostatSetpoint", "currentStatus", "downButtonControl", "mode", "resumeProgram", "refresh"])
|
details(["temperature", "upButtonControl", "thermostatSetpoint", "currentStatus", "downButtonControl", "mode", "resumeProgram", "refresh"])
|
||||||
|
}
|
||||||
|
|
||||||
|
preferences {
|
||||||
|
input "holdType", "enum", title: "Hold Type", description: "When changing temperature, use Temporary (Until next transition) or Permanent hold (default)", required: false, options:["Temporary", "Permanent"]
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
preferences {
|
|
||||||
input "highTemperature", "number", title: "Auto Mode High Temperature:", defaultValue: 80
|
|
||||||
input "lowTemperature", "number", title: "Auto Mode Low Temperature:", defaultValue: 70
|
|
||||||
input name: "holdType", type: "enum", title: "Hold Type", description: "When changing temperature, use Temporary or Permanent hold", required: true, options:["Temporary", "Permanent"]
|
|
||||||
}
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
// parse events into attributes
|
// parse events into attributes
|
||||||
def parse(String description) {
|
def parse(String description) {
|
||||||
log.debug "Parsing '${description}'"
|
log.debug "Parsing '${description}'"
|
||||||
// TODO: handle '' attribute
|
// TODO: handle '' attribute
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def refresh()
|
def refresh() {
|
||||||
{
|
log.debug "refresh called"
|
||||||
log.debug "refresh called"
|
poll()
|
||||||
poll()
|
log.debug "refresh ended"
|
||||||
log.debug "refresh ended"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def go()
|
|
||||||
{
|
|
||||||
log.debug "before:go tile tapped"
|
|
||||||
poll()
|
|
||||||
log.debug "after"
|
|
||||||
}
|
|
||||||
|
|
||||||
void poll() {
|
void poll() {
|
||||||
log.debug "Executing 'poll' using parent SmartApp"
|
log.debug "Executing 'poll' using parent SmartApp"
|
||||||
|
|
||||||
def results = parent.pollChild(this)
|
def results = parent.pollChild(this)
|
||||||
parseEventData(results)
|
generateEvent(results) //parse received message from parent
|
||||||
generateStatusEvent()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def parseEventData(Map results)
|
def generateEvent(Map results) {
|
||||||
{
|
|
||||||
log.debug "parsing data $results"
|
log.debug "parsing data $results"
|
||||||
if(results)
|
if(results) {
|
||||||
{
|
results.each { name, value ->
|
||||||
results.each { name, value ->
|
|
||||||
|
|
||||||
def linkText = getLinkText(device)
|
def linkText = getLinkText(device)
|
||||||
def isChange = false
|
def isChange = false
|
||||||
def isDisplayed = true
|
def isDisplayed = true
|
||||||
|
def event = [name: name, linkText: linkText, descriptionText: getThermostatDescriptionText(name, value, linkText),
|
||||||
if (name=="temperature" || name=="heatingSetpoint" || name=="coolingSetpoint") {
|
handlerName: name]
|
||||||
isChange = isTemperatureStateChange(device, name, value.toString())
|
|
||||||
isDisplayed = isChange
|
|
||||||
|
|
||||||
sendEvent(
|
|
||||||
name: name,
|
|
||||||
value: value,
|
|
||||||
unit: "F",
|
|
||||||
linkText: linkText,
|
|
||||||
descriptionText: getThermostatDescriptionText(name, value, linkText),
|
|
||||||
handlerName: name,
|
|
||||||
isStateChange: isChange,
|
|
||||||
displayed: isDisplayed)
|
|
||||||
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
isChange = isStateChange(device, name, value.toString())
|
|
||||||
isDisplayed = isChange
|
|
||||||
|
|
||||||
sendEvent(
|
|
||||||
name: name,
|
|
||||||
value: value.toString(),
|
|
||||||
linkText: linkText,
|
|
||||||
descriptionText: getThermostatDescriptionText(name, value, linkText),
|
|
||||||
handlerName: name,
|
|
||||||
isStateChange: isChange,
|
|
||||||
displayed: isDisplayed)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
generateSetpointEvent ()
|
|
||||||
generateStatusEvent ()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void generateEvent(Map results)
|
if (name=="temperature" || name=="heatingSetpoint" || name=="coolingSetpoint") {
|
||||||
{
|
def sendValue = value? convertTemperatureIfNeeded(value.toDouble(), "F", 1): value //API return temperature value in F
|
||||||
log.debug "parsing data $results"
|
|
||||||
if(results)
|
|
||||||
{
|
|
||||||
results.each { name, value ->
|
|
||||||
|
|
||||||
def linkText = getLinkText(device)
|
|
||||||
def isChange = false
|
|
||||||
def isDisplayed = true
|
|
||||||
|
|
||||||
if (name=="temperature" || name=="heatingSetpoint" || name=="coolingSetpoint") {
|
|
||||||
isChange = isTemperatureStateChange(device, name, value.toString())
|
isChange = isTemperatureStateChange(device, name, value.toString())
|
||||||
isDisplayed = isChange
|
isDisplayed = isChange
|
||||||
|
event << [value: sendValue, isStateChange: isChange, displayed: isDisplayed]
|
||||||
sendEvent(
|
} else if (name=="heatMode" || name=="coolMode" || name=="autoMode" || name=="auxHeatMode"){
|
||||||
name: name,
|
isChange = isStateChange(device, name, value.toString())
|
||||||
value: value,
|
event << [value: value.toString(), isStateChange: isChange, displayed: false]
|
||||||
unit: "F",
|
} else {
|
||||||
linkText: linkText,
|
isChange = isStateChange(device, name, value.toString())
|
||||||
descriptionText: getThermostatDescriptionText(name, value, linkText),
|
isDisplayed = isChange
|
||||||
handlerName: name,
|
event << [value: value.toString(), isStateChange: isChange, displayed: isDisplayed]
|
||||||
isStateChange: isChange,
|
}
|
||||||
displayed: isDisplayed)
|
sendEvent(event)
|
||||||
}
|
|
||||||
else {
|
|
||||||
isChange = isStateChange(device, name, value.toString())
|
|
||||||
isDisplayed = isChange
|
|
||||||
|
|
||||||
sendEvent(
|
|
||||||
name: name,
|
|
||||||
value: value.toString(),
|
|
||||||
linkText: linkText,
|
|
||||||
descriptionText: getThermostatDescriptionText(name, value, linkText),
|
|
||||||
handlerName: name,
|
|
||||||
isStateChange: isChange,
|
|
||||||
displayed: isDisplayed)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
generateSetpointEvent ()
|
generateSetpointEvent ()
|
||||||
generateStatusEvent()
|
generateStatusEvent ()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getThermostatDescriptionText(name, value, linkText)
|
//return descriptionText to be shown on mobile activity feed
|
||||||
{
|
private getThermostatDescriptionText(name, value, linkText) {
|
||||||
if(name == "temperature")
|
if(name == "temperature") {
|
||||||
{
|
return "$linkText temperature is $value°F"
|
||||||
return "$linkText was $value°F"
|
|
||||||
|
} else if(name == "heatingSetpoint") {
|
||||||
|
return "heating setpoint is $value°F"
|
||||||
|
|
||||||
|
} else if(name == "coolingSetpoint"){
|
||||||
|
return "cooling setpoint is $value°F"
|
||||||
|
|
||||||
|
} else if (name == "thermostatMode") {
|
||||||
|
return "thermostat mode is ${value}"
|
||||||
|
|
||||||
|
} else if (name == "thermostatFanMode") {
|
||||||
|
return "thermostat fan mode is ${value}"
|
||||||
|
|
||||||
|
} else {
|
||||||
|
return "${name} = ${value}"
|
||||||
}
|
}
|
||||||
else if(name == "heatingSetpoint")
|
}
|
||||||
{
|
|
||||||
return "latest heating setpoint was $value°F"
|
void setHeatingSetpoint(setpoint) {
|
||||||
|
setHeatingSetpoint(setpoint.toDouble())
|
||||||
|
}
|
||||||
|
|
||||||
|
void setHeatingSetpoint(Double setpoint) {
|
||||||
|
// def mode = device.currentValue("thermostatMode")
|
||||||
|
def heatingSetpoint = setpoint
|
||||||
|
def coolingSetpoint = device.currentValue("coolingSetpoint").toDouble()
|
||||||
|
def deviceId = device.deviceNetworkId.split(/\./).last()
|
||||||
|
|
||||||
|
//enforce limits of heatingSetpoint
|
||||||
|
if (heatingSetpoint > 79) {
|
||||||
|
heatingSetpoint = 79
|
||||||
|
} else if (heatingSetpoint < 45) {
|
||||||
|
heatingSetpoint = 45
|
||||||
}
|
}
|
||||||
else if(name == "coolingSetpoint")
|
|
||||||
{
|
//enforce limits of heatingSetpoint vs coolingSetpoint
|
||||||
return "latest cooling setpoint was $value°F"
|
if (heatingSetpoint >= coolingSetpoint) {
|
||||||
|
coolingSetpoint = heatingSetpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug "Sending setHeatingSetpoint> coolingSetpoint: ${coolingSetpoint}, heatingSetpoint: ${heatingSetpoint}"
|
||||||
|
|
||||||
|
def sendHoldType = holdType ? (holdType=="Temporary")? "nextTransition" : (holdType=="Permanent")? "indefinite" : "indefinite" : "indefinite"
|
||||||
|
if (parent.setHold (this, heatingSetpoint, coolingSetpoint, deviceId, sendHoldType)) {
|
||||||
|
sendEvent("name":"heatingSetpoint", "value":heatingSetpoint)
|
||||||
|
sendEvent("name":"coolingSetpoint", "value":coolingSetpoint)
|
||||||
|
log.debug "Done setHeatingSetpoint> coolingSetpoint: ${coolingSetpoint}, heatingSetpoint: ${heatingSetpoint}"
|
||||||
|
generateSetpointEvent()
|
||||||
|
generateStatusEvent()
|
||||||
|
} else {
|
||||||
|
log.error "Error setHeatingSetpoint(setpoint)" //This error is handled by the connect app
|
||||||
}
|
}
|
||||||
else if (name == "thermostatMode")
|
|
||||||
{
|
|
||||||
return "thermostat mode is ${value}"
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return "${name} = ${value}"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setCoolingSetpoint(setpoint) {
|
||||||
void setHeatingSetpoint(degreesF) {
|
setCoolingSetpoint(setpoint.toDouble())
|
||||||
setHeatingSetpoint(degreesF.toDouble())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void setHeatingSetpoint(Double degreesF) {
|
void setCoolingSetpoint(Double setpoint) {
|
||||||
log.debug "setHeatingSetpoint({$degreesF})"
|
// def mode = device.currentValue("thermostatMode")
|
||||||
sendEvent("name":"heatingSetpoint", "value":degreesF)
|
def heatingSetpoint = device.currentValue("heatingSetpoint").toDouble()
|
||||||
Double coolingSetpoint = device.currentValue("coolingSetpoint")
|
def coolingSetpoint = setpoint
|
||||||
log.debug "coolingSetpoint: $coolingSetpoint"
|
def deviceId = device.deviceNetworkId.split(/\./).last()
|
||||||
parent.setHold(this, degreesF, coolingSetpoint)
|
|
||||||
|
if (coolingSetpoint > 92) {
|
||||||
|
coolingSetpoint = 92
|
||||||
|
} else if (coolingSetpoint < 65) {
|
||||||
|
coolingSetpoint = 65
|
||||||
|
}
|
||||||
|
|
||||||
|
//enforce limits of heatingSetpoint vs coolingSetpoint
|
||||||
|
if (heatingSetpoint >= coolingSetpoint) {
|
||||||
|
heatingSetpoint = coolingSetpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug "Sending setCoolingSetpoint> coolingSetpoint: ${coolingSetpoint}, heatingSetpoint: ${heatingSetpoint}"
|
||||||
|
|
||||||
|
def sendHoldType = holdType ? (holdType=="Temporary")? "nextTransition" : (holdType=="Permanent")? "indefinite" : "indefinite" : "indefinite"
|
||||||
|
if (parent.setHold (this, heatingSetpoint, coolingSetpoint, deviceId, sendHoldType)) {
|
||||||
|
sendEvent("name":"heatingSetpoint", "value":heatingSetpoint)
|
||||||
|
sendEvent("name":"coolingSetpoint", "value":coolingSetpoint)
|
||||||
|
log.debug "Done setCoolingSetpoint>> coolingSetpoint = ${coolingSetpoint}, heatingSetpoint = ${heatingSetpoint}"
|
||||||
|
generateSetpointEvent()
|
||||||
|
generateStatusEvent()
|
||||||
|
} else {
|
||||||
|
log.error "Error setCoolingSetpoint(setpoint)" //This error is handled by the connect app
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void setCoolingSetpoint(degreesF) {
|
void resumeProgram() {
|
||||||
setCoolingSetpoint(degreesF.toDouble())
|
|
||||||
}
|
|
||||||
|
|
||||||
void setCoolingSetpoint(Double degreesF) {
|
log.debug "resumeProgram() is called"
|
||||||
log.debug "setCoolingSetpoint({$degreesF})"
|
sendEvent("name":"thermostatStatus", "value":"resuming schedule", "description":statusText, displayed: false)
|
||||||
sendEvent("name":"coolingSetpoint", "value":degreesF)
|
def deviceId = device.deviceNetworkId.split(/\./).last()
|
||||||
Double heatingSetpoint = device.currentValue("heatingSetpoint")
|
if (parent.resumeProgram(this, deviceId)) {
|
||||||
parent.setHold(this, heatingSetpoint, degreesF)
|
sendEvent("name":"thermostatStatus", "value":"setpoint is updating", "description":statusText, displayed: false)
|
||||||
}
|
runIn(5, "poll")
|
||||||
|
log.debug "resumeProgram() is done"
|
||||||
|
sendEvent("name":"resumeProgram", "value":"resume", descriptionText: "resumeProgram is done", displayed: false, isStateChange: true)
|
||||||
|
} else {
|
||||||
|
sendEvent("name":"thermostatStatus", "value":"failed resume click refresh", "description":statusText, displayed: false)
|
||||||
|
log.error "Error resumeProgram() check parent.resumeProgram(this, deviceId)"
|
||||||
|
}
|
||||||
|
|
||||||
def configure() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
def resumeProgram() {
|
|
||||||
parent.resumeProgram(this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def modes() {
|
def modes() {
|
||||||
if (state.modes) {
|
if (state.modes) {
|
||||||
log.debug "Modes = ${state.modes}"
|
log.debug "Modes = ${state.modes}"
|
||||||
return state.modes
|
return state.modes
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
state.modes = parent.availableModes(this)
|
state.modes = parent.availableModes(this)
|
||||||
log.debug "Modes = ${state.modes}"
|
log.debug "Modes = ${state.modes}"
|
||||||
return state.modes
|
return state.modes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def fanModes() {
|
def fanModes() {
|
||||||
["off", "on", "auto", "circulate"]
|
["off", "on", "auto", "circulate"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def switchMode() {
|
def switchMode() {
|
||||||
log.debug "in switchMode"
|
log.debug "in switchMode"
|
||||||
def currentMode = device.currentState("thermostatMode")?.value
|
def currentMode = device.currentState("thermostatMode")?.value
|
||||||
@@ -314,7 +292,7 @@ def switchMode() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def switchToMode(nextMode) {
|
def switchToMode(nextMode) {
|
||||||
log.debug "In switchToMode = ${nextMode}"
|
log.debug "In switchToMode = ${nextMode}"
|
||||||
if (nextMode in modes()) {
|
if (nextMode in modes()) {
|
||||||
state.lastTriedMode = nextMode
|
state.lastTriedMode = nextMode
|
||||||
"$nextMode"()
|
"$nextMode"()
|
||||||
@@ -376,300 +354,326 @@ def getDataByName(String name) {
|
|||||||
|
|
||||||
def setThermostatMode(String value) {
|
def setThermostatMode(String value) {
|
||||||
log.debug "setThermostatMode({$value})"
|
log.debug "setThermostatMode({$value})"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def setThermostatFanMode(String value) {
|
def setThermostatFanMode(String value) {
|
||||||
|
|
||||||
log.debug "setThermostatFanMode({$value})"
|
log.debug "setThermostatFanMode({$value})"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def generateModeEvent(mode) {
|
def generateModeEvent(mode) {
|
||||||
|
sendEvent(name: "thermostatMode", value: mode, descriptionText: "$device.displayName is in ${mode} mode", displayed: true)
|
||||||
sendEvent(name: "thermostatMode", value: mode, descriptionText: "$device.displayName is in ${mode} mode", displayed: true, isStateChange: true)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def generateFanModeEvent(fanMode) {
|
def generateFanModeEvent(fanMode) {
|
||||||
|
sendEvent(name: "thermostatFanMode", value: fanMode, descriptionText: "$device.displayName fan is in ${mode} mode", displayed: true)
|
||||||
sendEvent(name: "thermostatFanMode", value: fanMode, descriptionText: "$device.displayName fan is in ${mode} mode", displayed: true, isStateChange: true)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def generateOperatingStateEvent(operatingState) {
|
def generateOperatingStateEvent(operatingState) {
|
||||||
|
sendEvent(name: "thermostatOperatingState", value: operatingState, descriptionText: "$device.displayName is ${operatingState}", displayed: true)
|
||||||
sendEvent(name: "thermostatOperatingState", value: operatingState, descriptionText: "$device.displayName is ${operatingState}", displayed: true, isStateChange: true)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def off() {
|
def off() {
|
||||||
log.debug "off"
|
log.debug "off"
|
||||||
generateModeEvent("off")
|
def deviceId = device.deviceNetworkId.split(/\./).last()
|
||||||
if (parent.setMode (this,"off"))
|
if (parent.setMode (this,"off", deviceId))
|
||||||
generateModeEvent("off")
|
generateModeEvent("off")
|
||||||
else {
|
else {
|
||||||
log.debug "Error setting new mode."
|
log.debug "Error setting new mode."
|
||||||
def currentMode = device.currentState("thermostatMode")?.value
|
def currentMode = device.currentState("thermostatMode")?.value
|
||||||
generateModeEvent(currentMode) // reset the tile back
|
generateModeEvent(currentMode) // reset the tile back
|
||||||
}
|
}
|
||||||
generateSetpointEvent()
|
generateSetpointEvent()
|
||||||
generateStatusEvent()
|
generateStatusEvent()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def heat() {
|
def heat() {
|
||||||
log.debug "heat"
|
log.debug "heat"
|
||||||
generateModeEvent("heat")
|
def deviceId = device.deviceNetworkId.split(/\./).last()
|
||||||
if (parent.setMode (this,"heat"))
|
if (parent.setMode (this,"heat", deviceId))
|
||||||
generateModeEvent("heat")
|
generateModeEvent("heat")
|
||||||
else {
|
else {
|
||||||
log.debug "Error setting new mode."
|
log.debug "Error setting new mode."
|
||||||
def currentMode = device.currentState("thermostatMode")?.value
|
def currentMode = device.currentState("thermostatMode")?.value
|
||||||
generateModeEvent(currentMode) // reset the tile back
|
generateModeEvent(currentMode) // reset the tile back
|
||||||
}
|
}
|
||||||
generateSetpointEvent()
|
generateSetpointEvent()
|
||||||
generateStatusEvent()
|
generateStatusEvent()
|
||||||
}
|
}
|
||||||
|
|
||||||
def auxHeatOnly() {
|
def auxHeatOnly() {
|
||||||
log.debug "auxHeatOnly"
|
log.debug "auxHeatOnly"
|
||||||
generateModeEvent("auxHeatOnly")
|
def deviceId = device.deviceNetworkId.split(/\./).last()
|
||||||
if (parent.setMode (this,"auxHeatOnly"))
|
if (parent.setMode (this,"auxHeatOnly", deviceId))
|
||||||
generateModeEvent("auxHeatOnly")
|
generateModeEvent("auxHeatOnly")
|
||||||
else {
|
else {
|
||||||
log.debug "Error setting new mode."
|
log.debug "Error setting new mode."
|
||||||
def currentMode = device.currentState("thermostatMode")?.value
|
def currentMode = device.currentState("thermostatMode")?.value
|
||||||
generateModeEvent(currentMode) // reset the tile back
|
generateModeEvent(currentMode) // reset the tile back
|
||||||
}
|
}
|
||||||
generateSetpointEvent()
|
generateSetpointEvent()
|
||||||
generateStatusEvent()
|
generateStatusEvent()
|
||||||
}
|
}
|
||||||
|
|
||||||
def cool() {
|
def cool() {
|
||||||
log.debug "cool"
|
log.debug "cool"
|
||||||
generateModeEvent("cool")
|
def deviceId = device.deviceNetworkId.split(/\./).last()
|
||||||
if (parent.setMode (this,"cool"))
|
if (parent.setMode (this,"cool", deviceId))
|
||||||
generateModeEvent("cool")
|
generateModeEvent("cool")
|
||||||
else {
|
else {
|
||||||
log.debug "Error setting new mode."
|
log.debug "Error setting new mode."
|
||||||
def currentMode = device.currentState("thermostatMode")?.value
|
def currentMode = device.currentState("thermostatMode")?.value
|
||||||
generateModeEvent(currentMode) // reset the tile back
|
generateModeEvent(currentMode) // reset the tile back
|
||||||
}
|
}
|
||||||
generateSetpointEvent()
|
generateSetpointEvent()
|
||||||
generateStatusEvent()
|
generateStatusEvent()
|
||||||
}
|
}
|
||||||
|
|
||||||
def auto() {
|
def auto() {
|
||||||
log.debug "auto"
|
log.debug "auto"
|
||||||
generateModeEvent("auto")
|
def deviceId = device.deviceNetworkId.split(/\./).last()
|
||||||
if (parent.setMode (this,"auto"))
|
if (parent.setMode (this,"auto", deviceId))
|
||||||
generateModeEvent("auto")
|
generateModeEvent("auto")
|
||||||
else {
|
else {
|
||||||
log.debug "Error setting new mode."
|
log.debug "Error setting new mode."
|
||||||
def currentMode = device.currentState("thermostatMode")?.value
|
def currentMode = device.currentState("thermostatMode")?.value
|
||||||
generateModeEvent(currentMode) // reset the tile back
|
generateModeEvent(currentMode) // reset the tile back
|
||||||
}
|
}
|
||||||
generateSetpointEvent()
|
generateSetpointEvent()
|
||||||
generateStatusEvent()
|
generateStatusEvent()
|
||||||
}
|
}
|
||||||
|
|
||||||
def fanOn() {
|
def fanOn() {
|
||||||
log.debug "fanOn"
|
log.debug "fanOn"
|
||||||
parent.setFanMode (this,"on")
|
// parent.setFanMode (this,"on")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def fanAuto() {
|
def fanAuto() {
|
||||||
log.debug "fanAuto"
|
log.debug "fanAuto"
|
||||||
parent.setFanMode (this,"auto")
|
// parent.setFanMode (this,"auto")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def fanCirculate() {
|
def fanCirculate() {
|
||||||
log.debug "fanCirculate"
|
log.debug "fanCirculate"
|
||||||
parent.setFanMode (this,"circulate")
|
// parent.setFanMode (this,"circulate")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def fanOff() {
|
def fanOff() {
|
||||||
log.debug "fanOff"
|
log.debug "fanOff"
|
||||||
parent.setFanMode (this,"off")
|
// parent.setFanMode (this,"off")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def generateSetpointEvent() {
|
def generateSetpointEvent() {
|
||||||
|
|
||||||
log.debug "Generate SetPoint Event"
|
log.debug "Generate SetPoint Event"
|
||||||
|
|
||||||
def mode = device.currentValue("thermostatMode")
|
def mode = device.currentValue("thermostatMode")
|
||||||
log.debug "Current Mode = ${mode}"
|
log.debug "Current Mode = ${mode}"
|
||||||
|
|
||||||
def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger()
|
def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger()
|
||||||
log.debug "Heating Setpoint = ${heatingSetpoint}"
|
log.debug "Heating Setpoint = ${heatingSetpoint}"
|
||||||
|
|
||||||
def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger()
|
def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger()
|
||||||
log.debug "Cooling Setpoint = ${coolingSetpoint}"
|
log.debug "Cooling Setpoint = ${coolingSetpoint}"
|
||||||
|
|
||||||
if (mode == "heat") {
|
|
||||||
|
|
||||||
sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint.toString()+"°")
|
|
||||||
|
|
||||||
}
|
|
||||||
else if (mode == "cool") {
|
|
||||||
|
|
||||||
sendEvent("name":"thermostatSetpoint", "value":coolingSetpoint.toString()+"°")
|
|
||||||
|
|
||||||
} else if (mode == "auto") {
|
if (mode == "heat") {
|
||||||
|
|
||||||
sendEvent("name":"thermostatSetpoint", "value":"Auto")
|
|
||||||
|
|
||||||
} else if (mode == "off") {
|
sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint.toString())
|
||||||
|
|
||||||
sendEvent("name":"thermostatSetpoint", "value":"Off")
|
|
||||||
|
|
||||||
} else if (mode == "emergencyHeat") {
|
}
|
||||||
|
else if (mode == "cool") {
|
||||||
sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint.toString()+"°")
|
|
||||||
|
|
||||||
}
|
sendEvent("name":"thermostatSetpoint", "value":coolingSetpoint.toString())
|
||||||
|
|
||||||
|
} else if (mode == "auto") {
|
||||||
|
|
||||||
|
sendEvent("name":"thermostatSetpoint", "value":"Auto")
|
||||||
|
|
||||||
|
} else if (mode == "off") {
|
||||||
|
|
||||||
|
sendEvent("name":"thermostatSetpoint", "value":"Off")
|
||||||
|
|
||||||
|
} else if (mode == "emergencyHeat") {
|
||||||
|
|
||||||
|
sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint.toString())
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void raiseSetpoint() {
|
void raiseSetpoint() {
|
||||||
|
|
||||||
log.debug "Raise SetPoint"
|
|
||||||
|
|
||||||
def mode = device.currentValue("thermostatMode")
|
def mode = device.currentValue("thermostatMode")
|
||||||
def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger()
|
def targetvalue
|
||||||
def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger()
|
|
||||||
|
|
||||||
log.debug "Current Mode = ${mode}"
|
|
||||||
|
|
||||||
if (mode == "heat") {
|
|
||||||
|
|
||||||
heatingSetpoint++
|
|
||||||
|
|
||||||
if (heatingSetpoint > 99)
|
|
||||||
heatingSetpoint = 99
|
|
||||||
|
|
||||||
sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint.toString()+"°")
|
|
||||||
sendEvent("name":"heatingSetpoint", "value":heatingSetpoint)
|
|
||||||
|
|
||||||
parent.setHold (this, heatingSetpoint, coolingSetpoint)
|
|
||||||
|
|
||||||
log.debug "New Heating Setpoint = ${heatingSetpoint}"
|
|
||||||
|
|
||||||
}
|
|
||||||
else if (mode == "cool") {
|
|
||||||
|
|
||||||
coolingSetpoint++
|
|
||||||
|
|
||||||
if (coolingSetpoint > 99)
|
|
||||||
coolingSetpoint = 99
|
|
||||||
|
|
||||||
sendEvent("name":"thermostatSetpoint", "value":coolingSetpoint.toString()+"°")
|
|
||||||
sendEvent("name":"coolingSetpoint", "value":coolingSetpoint)
|
|
||||||
|
|
||||||
parent.setHold (this, heatingSetpoint, coolingSetpoint)
|
|
||||||
|
|
||||||
log.debug "New Cooling Setpoint = ${coolingSetpoint}"
|
|
||||||
|
|
||||||
}
|
|
||||||
generateStatusEvent()
|
|
||||||
|
|
||||||
|
if (mode == "off" || mode == "auto") {
|
||||||
|
log.warn "this mode: $mode does not allow raiseSetpoint"
|
||||||
|
} else {
|
||||||
|
def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger()
|
||||||
|
def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger()
|
||||||
|
def thermostatSetpoint = device.currentValue("thermostatSetpoint").toInteger()
|
||||||
|
log.debug "raiseSetpoint() mode = ${mode}, heatingSetpoint: ${heatingSetpoint}, coolingSetpoint:${coolingSetpoint}, thermostatSetpoint:${thermostatSetpoint}"
|
||||||
|
|
||||||
|
if (device.latestState('thermostatSetpoint')) {
|
||||||
|
targetvalue = device.latestState('thermostatSetpoint').value as Integer
|
||||||
|
} else {
|
||||||
|
targetvalue = 0
|
||||||
|
}
|
||||||
|
targetvalue = targetvalue + 1
|
||||||
|
|
||||||
|
if (mode == "heat" && targetvalue > 79) {
|
||||||
|
targetvalue = 79
|
||||||
|
} else if (mode == "cool" && targetvalue > 92) {
|
||||||
|
targetvalue = 92
|
||||||
|
}
|
||||||
|
|
||||||
|
sendEvent("name":"thermostatSetpoint", "value":targetvalue, displayed: false)
|
||||||
|
log.info "In mode $mode raiseSetpoint() to $targetvalue"
|
||||||
|
|
||||||
|
runIn(3, "alterSetpoint", [data: [value:targetvalue], overwrite: true]) //when user click button this runIn will be overwrite
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//called by tile when user hit raise temperature button on UI
|
||||||
void lowerSetpoint() {
|
void lowerSetpoint() {
|
||||||
log.debug "Lower SetPoint"
|
|
||||||
|
|
||||||
def mode = device.currentValue("thermostatMode")
|
def mode = device.currentValue("thermostatMode")
|
||||||
def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger()
|
def targetvalue
|
||||||
def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger()
|
|
||||||
|
|
||||||
log.debug "Current Mode = ${mode}, Current Heating Setpoint = ${heatingSetpoint}, Current Cooling Setpoint = ${coolingSetpoint}"
|
|
||||||
|
|
||||||
if (mode == "heat" || mode == "emergencyHeat") {
|
|
||||||
|
|
||||||
heatingSetpoint--
|
|
||||||
|
|
||||||
if (heatingSetpoint < 32)
|
|
||||||
heatingSetpoint = 32
|
|
||||||
|
|
||||||
sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint.toString()+"°")
|
if (mode == "off" || mode == "auto") {
|
||||||
sendEvent("name":"heatingSetpoint", "value":heatingSetpoint)
|
log.warn "this mode: $mode does not allow lowerSetpoint"
|
||||||
|
} else {
|
||||||
parent.setHold (this, heatingSetpoint, coolingSetpoint)
|
def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger()
|
||||||
|
def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger()
|
||||||
log.debug "New Heating Setpoint = ${heatingSetpoint}"
|
def thermostatSetpoint = device.currentValue("thermostatSetpoint").toInteger()
|
||||||
|
log.debug "lowerSetpoint() mode = ${mode}, heatingSetpoint: ${heatingSetpoint}, coolingSetpoint:${coolingSetpoint}, thermostatSetpoint:${thermostatSetpoint}"
|
||||||
}
|
if (device.latestState('thermostatSetpoint')) {
|
||||||
else if (mode == "cool") {
|
targetvalue = device.latestState('thermostatSetpoint').value as Integer
|
||||||
|
} else {
|
||||||
coolingSetpoint--
|
targetvalue = 0
|
||||||
|
}
|
||||||
if (coolingSetpoint < 32)
|
targetvalue = targetvalue - 1
|
||||||
coolingSetpoint = 32
|
|
||||||
|
if (mode == "heat" && targetvalue.toInteger() < 45) {
|
||||||
sendEvent("name":"thermostatSetpoint", "value":coolingSetpoint.toString()+"°")
|
targetvalue = 45
|
||||||
sendEvent("name":"coolingSetpoint", "value":coolingSetpoint)
|
} else if (mode == "cool" && targetvalue.toInteger() < 65) {
|
||||||
|
targetvalue = 65
|
||||||
parent.setHold (this, heatingSetpoint, coolingSetpoint)
|
}
|
||||||
|
|
||||||
log.debug "New Cooling Setpoint = ${coolingSetpoint}"
|
sendEvent("name":"thermostatSetpoint", "value":targetvalue, displayed: false)
|
||||||
|
log.info "In mode $mode lowerSetpoint() to $targetvalue"
|
||||||
}
|
|
||||||
generateStatusEvent()
|
runIn(3, "alterSetpoint", [data: [value:targetvalue], overwrite: true]) //when user click button this runIn will be overwrite
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//called by raiseSetpoint() and lowerSetpoint()
|
||||||
|
void alterSetpoint(temp) {
|
||||||
|
|
||||||
|
def mode = device.currentValue("thermostatMode")
|
||||||
|
def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger()
|
||||||
|
def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger()
|
||||||
|
def deviceId = device.deviceNetworkId.split(/\./).last()
|
||||||
|
|
||||||
|
def targetHeatingSetpoint
|
||||||
|
def targetCoolingSetpoint
|
||||||
|
|
||||||
|
//step1: check thermostatMode, enforce limits before sending request to cloud
|
||||||
|
if (mode == "heat"){
|
||||||
|
if (temp.value > coolingSetpoint){
|
||||||
|
targetHeatingSetpoint = temp.value
|
||||||
|
targetCoolingSetpoint = temp.value
|
||||||
|
} else {
|
||||||
|
targetHeatingSetpoint = temp.value
|
||||||
|
targetCoolingSetpoint = coolingSetpoint
|
||||||
|
}
|
||||||
|
} else if (mode == "cool") {
|
||||||
|
//enforce limits before sending request to cloud
|
||||||
|
if (temp.value < heatingSetpoint){
|
||||||
|
targetHeatingSetpoint = temp.value
|
||||||
|
targetCoolingSetpoint = temp.value
|
||||||
|
} else {
|
||||||
|
targetHeatingSetpoint = heatingSetpoint
|
||||||
|
targetCoolingSetpoint = temp.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug "alterSetpoint >> in mode ${mode} trying to change heatingSetpoint to ${targetHeatingSetpoint} " +
|
||||||
|
"coolingSetpoint to ${targetCoolingSetpoint} with holdType : ${holdType}"
|
||||||
|
|
||||||
|
def sendHoldType = holdType ? (holdType=="Temporary")? "nextTransition" : (holdType=="Permanent")? "indefinite" : "indefinite" : "indefinite"
|
||||||
|
//step2: call parent.setHold to send http request to 3rd party cloud
|
||||||
|
if (parent.setHold(this, targetHeatingSetpoint, targetCoolingSetpoint, deviceId, sendHoldType)) {
|
||||||
|
sendEvent("name": "thermostatSetpoint", "value": temp.value.toString(), displayed: false)
|
||||||
|
sendEvent("name": "heatingSetpoint", "value": targetHeatingSetpoint)
|
||||||
|
sendEvent("name": "coolingSetpoint", "value": targetCoolingSetpoint)
|
||||||
|
log.debug "alterSetpoint in mode $mode succeed change setpoint to= ${temp.value}"
|
||||||
|
} else {
|
||||||
|
log.error "Error alterSetpoint()"
|
||||||
|
if (mode == "heat"){
|
||||||
|
sendEvent("name": "thermostatSetpoint", "value": heatingSetpoint.toString(), displayed: false)
|
||||||
|
} else if (mode == "cool") {
|
||||||
|
sendEvent("name": "thermostatSetpoint", "value": coolingSetpoint.toString(), displayed: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
generateStatusEvent()
|
||||||
}
|
}
|
||||||
|
|
||||||
def generateStatusEvent() {
|
def generateStatusEvent() {
|
||||||
|
|
||||||
def mode = device.currentValue("thermostatMode")
|
def mode = device.currentValue("thermostatMode")
|
||||||
def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger()
|
def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger()
|
||||||
def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger()
|
def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger()
|
||||||
def temperature = device.currentValue("temperature").toInteger()
|
def temperature = device.currentValue("temperature").toInteger()
|
||||||
|
|
||||||
def statusText
|
def statusText
|
||||||
|
|
||||||
log.debug "Generate Status Event for Mode = ${mode}"
|
log.debug "Generate Status Event for Mode = ${mode}"
|
||||||
log.debug "Temperature = ${temperature}"
|
log.debug "Temperature = ${temperature}"
|
||||||
log.debug "Heating set point = ${heatingSetpoint}"
|
log.debug "Heating set point = ${heatingSetpoint}"
|
||||||
log.debug "Cooling set point = ${coolingSetpoint}"
|
log.debug "Cooling set point = ${coolingSetpoint}"
|
||||||
log.debug "HVAC Mode = ${mode}"
|
log.debug "HVAC Mode = ${mode}"
|
||||||
|
|
||||||
if (mode == "heat") {
|
if (mode == "heat") {
|
||||||
|
|
||||||
if (temperature >= heatingSetpoint)
|
if (temperature >= heatingSetpoint)
|
||||||
statusText = "Right Now: Idle"
|
statusText = "Right Now: Idle"
|
||||||
else
|
else
|
||||||
statusText = "Heating to ${heatingSetpoint}° F"
|
statusText = "Heating to ${heatingSetpoint}° F"
|
||||||
|
|
||||||
} else if (mode == "cool") {
|
} else if (mode == "cool") {
|
||||||
|
|
||||||
if (temperature <= coolingSetpoint)
|
if (temperature <= coolingSetpoint)
|
||||||
statusText = "Right Now: Idle"
|
statusText = "Right Now: Idle"
|
||||||
else
|
else
|
||||||
statusText = "Cooling to ${coolingSetpoint}° F"
|
statusText = "Cooling to ${coolingSetpoint}° F"
|
||||||
|
|
||||||
} else if (mode == "auto") {
|
} else if (mode == "auto") {
|
||||||
|
|
||||||
statusText = "Right Now: Auto"
|
statusText = "Right Now: Auto"
|
||||||
|
|
||||||
} else if (mode == "off") {
|
} else if (mode == "off") {
|
||||||
|
|
||||||
statusText = "Right Now: Off"
|
statusText = "Right Now: Off"
|
||||||
|
|
||||||
} else if (mode == "emergencyHeat") {
|
} else if (mode == "emergencyHeat") {
|
||||||
|
|
||||||
statusText = "Emergency Heat"
|
statusText = "Emergency Heat"
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
statusText = "?"
|
statusText = "?"
|
||||||
|
|
||||||
}
|
}
|
||||||
log.debug "Generate Status Event = ${statusText}"
|
log.debug "Generate Status Event = ${statusText}"
|
||||||
sendEvent("name":"thermostatStatus", "value":statusText, "description":statusText, displayed: true, isStateChange: true)
|
sendEvent("name":"thermostatStatus", "value":statusText, "description":statusText, displayed: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//generate custom mobile activity feeds event
|
||||||
|
def generateActivityFeedsEvent(notificationMessage) {
|
||||||
|
sendEvent(name: "notificationMessage", value: "$device.displayName $notificationMessage", descriptionText: "$device.displayName $notificationMessage", displayed: true)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
/**
|
||||||
|
* 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: "Gentle Wake Up Controller", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
capability "Switch"
|
||||||
|
capability "Timed Session"
|
||||||
|
|
||||||
|
attribute "percentComplete", "number"
|
||||||
|
|
||||||
|
command "setPercentComplete", ["number"]
|
||||||
|
}
|
||||||
|
|
||||||
|
simulator {
|
||||||
|
// TODO: define status and reply messages here
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles(scale: 2) {
|
||||||
|
|
||||||
|
multiAttributeTile(name: "richTile", type:"generic", width:6, height:4) {
|
||||||
|
tileAttribute("sessionStatus", key: "PRIMARY_CONTROL") {
|
||||||
|
attributeState "cancelled", action: "timed session.start", icon: "http://f.cl.ly/items/322n181j2K3f281r2s0A/playbutton.png", backgroundColor: "#ffffff", nextState: "running"
|
||||||
|
attributeState "stopped", action: "timed session.start", icon: "http://f.cl.ly/items/322n181j2K3f281r2s0A/playbutton.png", backgroundColor: "#ffffff", nextState: "cancelled"
|
||||||
|
attributeState "running", action: "timed session.stop", icon: "http://f.cl.ly/items/0B3y3p2V3X2l3P3y3W09/stopbutton.png", backgroundColor: "#79b821", nextState: "cancelled"
|
||||||
|
}
|
||||||
|
tileAttribute("timeRemaining", key: "SECONDARY_CONTROL") {
|
||||||
|
attributeState "timeRemaining", label:'${currentValue} remaining'
|
||||||
|
}
|
||||||
|
tileAttribute("percentComplete", key: "SLIDER_CONTROL") {
|
||||||
|
attributeState "percentComplete", action: "timed session.setTimeRemaining"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// start/stop
|
||||||
|
standardTile("sessionStatusTile", "sessionStatus", width: 1, height: 1, canChangeIcon: true) {
|
||||||
|
state "cancelled", label: "Stopped", action: "timed session.start", backgroundColor: "#ffffff", icon: "http://f.cl.ly/items/1J1g0H2P0S1G1f2O1s1s/icon.png"
|
||||||
|
state "stopped", label: "Stopped", action: "timed session.start", backgroundColor: "#ffffff", icon: "http://f.cl.ly/items/1J1g0H2P0S1G1f2O1s1s/icon.png"
|
||||||
|
state "running", label: "Running", action: "timed session.stop", backgroundColor: "#79b821", icon: "http://f.cl.ly/items/1J1g0H2P0S1G1f2O1s1s/icon.png"
|
||||||
|
}
|
||||||
|
|
||||||
|
// duration
|
||||||
|
valueTile("timeRemainingTile", "timeRemaining", decoration: "flat", width: 2) {
|
||||||
|
state "timeRemaining", label:'${currentValue} left'
|
||||||
|
}
|
||||||
|
controlTile("percentCompleteTile", "percentComplete", "slider", height: 1, width: 3) {
|
||||||
|
state "percentComplete", action: "timed session.setTimeRemaining"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "sessionStatusTile"
|
||||||
|
details "richTile"
|
||||||
|
// details(["richTile", "sessionStatusTile", "timeRemainingTile", "percentCompleteTile"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse events into attributes
|
||||||
|
def parse(description) {
|
||||||
|
log.debug "Parsing '${description}'"
|
||||||
|
// TODO: handle 'switch' attribute
|
||||||
|
// TODO: handle 'level' attribute
|
||||||
|
// TODO: handle 'sessionStatus' attribute
|
||||||
|
// TODO: handle 'timeRemaining' attribute
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle commands
|
||||||
|
def on() {
|
||||||
|
log.debug "Executing 'on'"
|
||||||
|
startDimming()
|
||||||
|
}
|
||||||
|
|
||||||
|
def off() {
|
||||||
|
log.debug "Executing 'off'"
|
||||||
|
stopDimming()
|
||||||
|
}
|
||||||
|
|
||||||
|
def setTimeRemaining(percentComplete) {
|
||||||
|
log.debug "Executing 'setTimeRemaining' to ${percentComplete}% complete"
|
||||||
|
parent.jumpTo(percentComplete)
|
||||||
|
}
|
||||||
|
|
||||||
|
def start() {
|
||||||
|
log.debug "Executing 'start'"
|
||||||
|
startDimming()
|
||||||
|
}
|
||||||
|
|
||||||
|
def stop() {
|
||||||
|
log.debug "Executing 'stop'"
|
||||||
|
stopDimming()
|
||||||
|
}
|
||||||
|
|
||||||
|
def pause() {
|
||||||
|
log.debug "Executing 'pause'"
|
||||||
|
// TODO: handle 'pause' command
|
||||||
|
}
|
||||||
|
|
||||||
|
def cancel() {
|
||||||
|
log.debug "Executing 'cancel'"
|
||||||
|
stopDimming()
|
||||||
|
}
|
||||||
|
|
||||||
|
def startDimming() {
|
||||||
|
log.trace "startDimming"
|
||||||
|
log.debug "parent: ${parent}"
|
||||||
|
parent.start("controller")
|
||||||
|
}
|
||||||
|
|
||||||
|
def stopDimming() {
|
||||||
|
log.trace "stopDimming"
|
||||||
|
log.debug "parent: ${parent}"
|
||||||
|
parent.stop("controller")
|
||||||
|
}
|
||||||
|
|
||||||
|
def controllerEvent(eventData) {
|
||||||
|
log.trace "controllerEvent"
|
||||||
|
sendEvent(eventData)
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
metadata {
|
metadata {
|
||||||
definition (name: "Logitech Harmony Activity", namespace: "smartthings", author: "Juan Risso") {
|
definition (name: "Harmony Activity", namespace: "smartthings", author: "Juan Risso") {
|
||||||
capability "Switch"
|
capability "Switch"
|
||||||
capability "Actuator"
|
capability "Actuator"
|
||||||
capability "Refresh"
|
capability "Refresh"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
Osram bulbs have a firmware issue causing it to forget its dimming level when turned off (via commands). Handling
|
Osram bulbs have a firmware issue causing it to forget its dimming level when turned off (via commands). Handling
|
||||||
that issue by using state variables
|
that issue by using state variables
|
||||||
*/
|
*/
|
||||||
|
//DEPRECATED - Using the generic DTH for this device. Users need to be moved before deleting this DTH
|
||||||
|
|
||||||
metadata {
|
metadata {
|
||||||
definition (name: "OSRAM LIGHTIFY LED Flexible Strip RGBW", namespace: "smartthings", author: "SmartThings") {
|
definition (name: "OSRAM LIGHTIFY LED Flexible Strip RGBW", namespace: "smartthings", author: "SmartThings") {
|
||||||
@@ -23,8 +24,8 @@ metadata {
|
|||||||
command "setAdjustedColor"
|
command "setAdjustedColor"
|
||||||
|
|
||||||
|
|
||||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY Flex RGBW"
|
//fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY Flex RGBW"
|
||||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Flex RGBW"
|
//fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Flex RGBW"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ metadata {
|
|||||||
capability "Switch"
|
capability "Switch"
|
||||||
capability "Sensor"
|
capability "Sensor"
|
||||||
|
|
||||||
fingerprint profileId: "0104", inClusters: "0000,0003,0006", outClusters: "0019"
|
fingerprint profileId: "0104", inClusters: "0006, 0004, 0003, 0000, 0005", outClusters: "0019", manufacturer: "Compacta International, Ltd", model: "ZBMPlug15", deviceJoinName: "SmartPower Outlet V1"
|
||||||
}
|
}
|
||||||
|
|
||||||
// simulator metadata
|
// simulator metadata
|
||||||
|
|||||||
@@ -79,8 +79,8 @@ metadata {
|
|||||||
}
|
}
|
||||||
|
|
||||||
preferences {
|
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 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: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
|
input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -88,8 +88,8 @@ metadata {
|
|||||||
}
|
}
|
||||||
|
|
||||||
preferences {
|
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 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: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
|
input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,18 +20,18 @@ metadata {
|
|||||||
capability "Refresh"
|
capability "Refresh"
|
||||||
capability "Temperature Measurement"
|
capability "Temperature Measurement"
|
||||||
capability "Water Sensor"
|
capability "Water Sensor"
|
||||||
|
|
||||||
command "enrollResponse"
|
command "enrollResponse"
|
||||||
|
|
||||||
|
|
||||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3315-S", deviceJoinName: "Water Leak Sensor"
|
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3315-S", deviceJoinName: "Water Leak Sensor"
|
||||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3315"
|
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3315"
|
||||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3315-Seu", deviceJoinName: "Water Leak Sensor"
|
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3315-Seu", deviceJoinName: "Water Leak Sensor"
|
||||||
|
fingerprint inClusters: "0000,0001,0003,000F,0020,0402,0500", outClusters: "0019", manufacturer: "SmartThings", model: "moisturev4", deviceJoinName: "Water Leak Sensor"
|
||||||
}
|
}
|
||||||
|
|
||||||
simulator {
|
simulator {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
preferences {
|
preferences {
|
||||||
@@ -43,11 +43,11 @@ metadata {
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
section {
|
section {
|
||||||
input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
|
input 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: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
|
input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tiles(scale: 2) {
|
tiles(scale: 2) {
|
||||||
multiAttributeTile(name:"water", type: "generic", width: 6, height: 4){
|
multiAttributeTile(name:"water", type: "generic", width: 6, height: 4){
|
||||||
tileAttribute ("device.water", key: "PRIMARY_CONTROL") {
|
tileAttribute ("device.water", key: "PRIMARY_CONTROL") {
|
||||||
@@ -78,7 +78,7 @@ metadata {
|
|||||||
details(["water", "temperature", "battery", "refresh"])
|
details(["water", "temperature", "battery", "refresh"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def parse(String description) {
|
def parse(String description) {
|
||||||
log.debug "description: $description"
|
log.debug "description: $description"
|
||||||
|
|
||||||
@@ -92,59 +92,59 @@ def parse(String description) {
|
|||||||
else if (description?.startsWith('temperature: ')) {
|
else if (description?.startsWith('temperature: ')) {
|
||||||
map = parseCustomMessage(description)
|
map = parseCustomMessage(description)
|
||||||
}
|
}
|
||||||
else if (description?.startsWith('zone status')) {
|
else if (description?.startsWith('zone status')) {
|
||||||
map = parseIasMessage(description)
|
map = parseIasMessage(description)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.debug "Parse returned $map"
|
log.debug "Parse returned $map"
|
||||||
def result = map ? createEvent(map) : null
|
def result = map ? createEvent(map) : null
|
||||||
|
|
||||||
if (description?.startsWith('enroll request')) {
|
if (description?.startsWith('enroll request')) {
|
||||||
List cmds = enrollResponse()
|
List cmds = enrollResponse()
|
||||||
log.debug "enroll response: ${cmds}"
|
log.debug "enroll response: ${cmds}"
|
||||||
result = cmds?.collect { new physicalgraph.device.HubAction(it) }
|
result = cmds?.collect { new physicalgraph.device.HubAction(it) }
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map parseCatchAllMessage(String description) {
|
private Map parseCatchAllMessage(String description) {
|
||||||
Map resultMap = [:]
|
Map resultMap = [:]
|
||||||
def cluster = zigbee.parse(description)
|
def cluster = zigbee.parse(description)
|
||||||
if (shouldProcessMessage(cluster)) {
|
if (shouldProcessMessage(cluster)) {
|
||||||
switch(cluster.clusterId) {
|
switch(cluster.clusterId) {
|
||||||
case 0x0001:
|
case 0x0001:
|
||||||
resultMap = getBatteryResult(cluster.data.last())
|
resultMap = getBatteryResult(cluster.data.last())
|
||||||
break
|
break
|
||||||
|
|
||||||
case 0x0402:
|
case 0x0402:
|
||||||
// temp is last 2 data values. reverse to swap endian
|
// temp is last 2 data values. reverse to swap endian
|
||||||
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
|
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
|
||||||
def value = getTemperature(temp)
|
def value = getTemperature(temp)
|
||||||
resultMap = getTemperatureResult(value)
|
resultMap = getTemperatureResult(value)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return resultMap
|
return resultMap
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean shouldProcessMessage(cluster) {
|
private boolean shouldProcessMessage(cluster) {
|
||||||
// 0x0B is default response indicating message got through
|
// 0x0B is default response indicating message got through
|
||||||
// 0x07 is bind message
|
// 0x07 is bind message
|
||||||
boolean ignoredMessage = cluster.profileId != 0x0104 ||
|
boolean ignoredMessage = cluster.profileId != 0x0104 ||
|
||||||
cluster.command == 0x0B ||
|
cluster.command == 0x0B ||
|
||||||
cluster.command == 0x07 ||
|
cluster.command == 0x07 ||
|
||||||
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)
|
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)
|
||||||
return !ignoredMessage
|
return !ignoredMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map parseReportAttributeMessage(String description) {
|
private Map parseReportAttributeMessage(String description) {
|
||||||
Map descMap = (description - "read attr - ").split(",").inject([:]) { map, param ->
|
Map descMap = (description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||||
def nameAndValue = param.split(":")
|
def nameAndValue = param.split(":")
|
||||||
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
|
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
|
||||||
}
|
}
|
||||||
log.debug "Desc Map: $descMap"
|
log.debug "Desc Map: $descMap"
|
||||||
|
|
||||||
Map resultMap = [:]
|
Map resultMap = [:]
|
||||||
if (descMap.cluster == "0402" && descMap.attrId == "0000") {
|
if (descMap.cluster == "0402" && descMap.attrId == "0000") {
|
||||||
def value = getTemperature(descMap.value)
|
def value = getTemperature(descMap.value)
|
||||||
@@ -153,10 +153,10 @@ private Map parseReportAttributeMessage(String description) {
|
|||||||
else if (descMap.cluster == "0001" && descMap.attrId == "0020") {
|
else if (descMap.cluster == "0001" && descMap.attrId == "0020") {
|
||||||
resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16))
|
resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16))
|
||||||
}
|
}
|
||||||
|
|
||||||
return resultMap
|
return resultMap
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map parseCustomMessage(String description) {
|
private Map parseCustomMessage(String description) {
|
||||||
Map resultMap = [:]
|
Map resultMap = [:]
|
||||||
if (description?.startsWith('temperature: ')) {
|
if (description?.startsWith('temperature: ')) {
|
||||||
@@ -167,42 +167,42 @@ private Map parseCustomMessage(String description) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Map parseIasMessage(String description) {
|
private Map parseIasMessage(String description) {
|
||||||
List parsedMsg = description.split(' ')
|
List parsedMsg = description.split(' ')
|
||||||
String msgCode = parsedMsg[2]
|
String msgCode = parsedMsg[2]
|
||||||
|
|
||||||
Map resultMap = [:]
|
|
||||||
switch(msgCode) {
|
|
||||||
case '0x0020': // Closed/No Motion/Dry
|
|
||||||
resultMap = getMoistureResult('dry')
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0021': // Open/Motion/Wet
|
Map resultMap = [:]
|
||||||
resultMap = getMoistureResult('wet')
|
switch(msgCode) {
|
||||||
break
|
case '0x0020': // Closed/No Motion/Dry
|
||||||
|
resultMap = getMoistureResult('dry')
|
||||||
|
break
|
||||||
|
|
||||||
case '0x0022': // Tamper Alarm
|
case '0x0021': // Open/Motion/Wet
|
||||||
break
|
resultMap = getMoistureResult('wet')
|
||||||
|
break
|
||||||
|
|
||||||
case '0x0023': // Battery Alarm
|
case '0x0022': // Tamper Alarm
|
||||||
break
|
break
|
||||||
|
|
||||||
case '0x0024': // Supervision Report
|
case '0x0023': // Battery Alarm
|
||||||
log.debug 'dry with tamper alarm'
|
break
|
||||||
resultMap = getMoistureResult('dry')
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0025': // Restore Report
|
case '0x0024': // Supervision Report
|
||||||
log.debug 'water with tamper alarm'
|
log.debug 'dry with tamper alarm'
|
||||||
resultMap = getMoistureResult('wet')
|
resultMap = getMoistureResult('dry')
|
||||||
break
|
break
|
||||||
|
|
||||||
case '0x0026': // Trouble/Failure
|
case '0x0025': // Restore Report
|
||||||
break
|
log.debug 'water with tamper alarm'
|
||||||
|
resultMap = getMoistureResult('wet')
|
||||||
|
break
|
||||||
|
|
||||||
case '0x0028': // Test Mode
|
case '0x0026': // Trouble/Failure
|
||||||
break
|
break
|
||||||
}
|
|
||||||
return resultMap
|
case '0x0028': // Test Mode
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return resultMap
|
||||||
}
|
}
|
||||||
|
|
||||||
def getTemperature(value) {
|
def getTemperature(value) {
|
||||||
@@ -215,24 +215,47 @@ def getTemperature(value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Map getBatteryResult(rawValue) {
|
private Map getBatteryResult(rawValue) {
|
||||||
log.debug 'Battery'
|
log.debug "Battery rawValue = ${rawValue}"
|
||||||
def linkText = getLinkText(device)
|
def linkText = getLinkText(device)
|
||||||
|
|
||||||
def result = [
|
def result = [
|
||||||
name: 'battery'
|
name: 'battery',
|
||||||
]
|
value: '--'
|
||||||
|
]
|
||||||
|
|
||||||
def volts = rawValue / 10
|
def volts = rawValue / 10
|
||||||
def descriptionText
|
|
||||||
if (volts > 3.5) {
|
if (rawValue == 0 || rawValue == 255) {}
|
||||||
result.descriptionText = "${linkText} battery has too much power (${volts} volts)."
|
|
||||||
}
|
|
||||||
else {
|
else {
|
||||||
def minVolts = 2.1
|
if (volts > 3.5) {
|
||||||
def maxVolts = 3.0
|
result.descriptionText = "${linkText} battery has too much power (${volts} volts)."
|
||||||
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
}
|
||||||
result.value = Math.min(100, (int) pct * 100)
|
else {
|
||||||
result.descriptionText = "${linkText} battery was ${result.value}%"
|
if (device.getDataValue("manufacturer") == "SmartThings") {
|
||||||
|
volts = rawValue // For the batteryMap to work the key needs to be an int
|
||||||
|
def batteryMap = [28:100, 27:100, 26:100, 25:90, 24:90, 23:70,
|
||||||
|
22:70, 21:50, 20:50, 19:30, 18:30, 17:15, 16:1, 15:0]
|
||||||
|
def minVolts = 15
|
||||||
|
def maxVolts = 28
|
||||||
|
|
||||||
|
if (volts < minVolts)
|
||||||
|
volts = minVolts
|
||||||
|
else if (volts > maxVolts)
|
||||||
|
volts = maxVolts
|
||||||
|
def pct = batteryMap[volts]
|
||||||
|
if (pct != null) {
|
||||||
|
result.value = pct
|
||||||
|
result.descriptionText = "${linkText} battery was ${result.value}%"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
def minVolts = 2.1
|
||||||
|
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}%"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
@@ -267,7 +290,7 @@ private Map getMoistureResult(value) {
|
|||||||
def refresh() {
|
def refresh() {
|
||||||
log.debug "Refreshing Temperature and Battery"
|
log.debug "Refreshing Temperature and Battery"
|
||||||
def refreshCmds = [
|
def refreshCmds = [
|
||||||
"st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200",
|
"st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200",
|
||||||
"st rattr 0x${device.deviceNetworkId} 1 1 0x20", "delay 200"
|
"st rattr 0x${device.deviceNetworkId} 1 1 0x20", "delay 200"
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -277,32 +300,32 @@ def refresh() {
|
|||||||
def configure() {
|
def configure() {
|
||||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||||
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
||||||
def configCmds = [
|
def configCmds = [
|
||||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||||
|
|
||||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 500",
|
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 500",
|
||||||
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
|
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
|
||||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||||
|
|
||||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 500",
|
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 500",
|
||||||
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
|
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
|
||||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500"
|
"send 0x${device.deviceNetworkId} 1 1", "delay 500"
|
||||||
]
|
]
|
||||||
return configCmds + refresh() // send refresh cmds as part of config
|
return configCmds + refresh() // send refresh cmds as part of config
|
||||||
}
|
}
|
||||||
|
|
||||||
def enrollResponse() {
|
def enrollResponse() {
|
||||||
log.debug "Sending enroll response"
|
log.debug "Sending enroll response"
|
||||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||||
[
|
[
|
||||||
//Resending the CIE in case the enroll request is sent before CIE is written
|
//Resending the CIE in case the enroll request is sent before CIE is written
|
||||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||||
//Enroll Response
|
//Enroll Response
|
||||||
"raw 0x500 {01 23 00 00 00}",
|
"raw 0x500 {01 23 00 00 00}",
|
||||||
"send 0x${device.deviceNetworkId} 1 1", "delay 200"
|
"send 0x${device.deviceNetworkId} 1 1", "delay 200"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
private getEndpointId() {
|
private getEndpointId() {
|
||||||
@@ -314,19 +337,19 @@ private hex(value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private String swapEndianHex(String hex) {
|
private String swapEndianHex(String hex) {
|
||||||
reverseArray(hex.decodeHex()).encodeHex()
|
reverseArray(hex.decodeHex()).encodeHex()
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] reverseArray(byte[] array) {
|
private byte[] reverseArray(byte[] array) {
|
||||||
int i = 0;
|
int i = 0;
|
||||||
int j = array.length - 1;
|
int j = array.length - 1;
|
||||||
byte tmp;
|
byte tmp;
|
||||||
while (j > i) {
|
while (j > i) {
|
||||||
tmp = array[j];
|
tmp = array[j];
|
||||||
array[j] = array[i];
|
array[j] = array[i];
|
||||||
array[i] = tmp;
|
array[i] = tmp;
|
||||||
j--;
|
j--;
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
return array
|
return array
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,16 +19,17 @@ metadata {
|
|||||||
capability "Motion Sensor"
|
capability "Motion Sensor"
|
||||||
capability "Configuration"
|
capability "Configuration"
|
||||||
capability "Battery"
|
capability "Battery"
|
||||||
capability "Temperature Measurement"
|
capability "Temperature Measurement"
|
||||||
capability "Refresh"
|
capability "Refresh"
|
||||||
|
|
||||||
command "enrollResponse"
|
command "enrollResponse"
|
||||||
|
|
||||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3305-S"
|
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3305-S"
|
||||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3325-S", deviceJoinName: "Motion Sensor"
|
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3325-S", deviceJoinName: "Motion Sensor"
|
||||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3305"
|
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3305"
|
||||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3325"
|
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3325"
|
||||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3326"
|
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3326"
|
||||||
|
fingerprint inClusters: "0000,0001,0003,000F,0020,0402,0500", outClusters: "0019", manufacturer: "SmartThings", model: "motionv4", deviceJoinName: "Motion Sensor"
|
||||||
}
|
}
|
||||||
|
|
||||||
simulator {
|
simulator {
|
||||||
@@ -45,8 +46,8 @@ metadata {
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
section {
|
section {
|
||||||
input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
|
input 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: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
|
input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,7 +85,7 @@ metadata {
|
|||||||
|
|
||||||
def parse(String description) {
|
def parse(String description) {
|
||||||
log.debug "description: $description"
|
log.debug "description: $description"
|
||||||
|
|
||||||
Map map = [:]
|
Map map = [:]
|
||||||
if (description?.startsWith('catchall:')) {
|
if (description?.startsWith('catchall:')) {
|
||||||
map = parseCatchAllMessage(description)
|
map = parseCatchAllMessage(description)
|
||||||
@@ -95,55 +96,55 @@ def parse(String description) {
|
|||||||
else if (description?.startsWith('temperature: ')) {
|
else if (description?.startsWith('temperature: ')) {
|
||||||
map = parseCustomMessage(description)
|
map = parseCustomMessage(description)
|
||||||
}
|
}
|
||||||
else if (description?.startsWith('zone status')) {
|
else if (description?.startsWith('zone status')) {
|
||||||
map = parseIasMessage(description)
|
map = parseIasMessage(description)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.debug "Parse returned $map"
|
log.debug "Parse returned $map"
|
||||||
def result = map ? createEvent(map) : null
|
def result = map ? createEvent(map) : null
|
||||||
|
|
||||||
if (description?.startsWith('enroll request')) {
|
if (description?.startsWith('enroll request')) {
|
||||||
List cmds = enrollResponse()
|
List cmds = enrollResponse()
|
||||||
log.debug "enroll response: ${cmds}"
|
log.debug "enroll response: ${cmds}"
|
||||||
result = cmds?.collect { new physicalgraph.device.HubAction(it) }
|
result = cmds?.collect { new physicalgraph.device.HubAction(it) }
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map parseCatchAllMessage(String description) {
|
private Map parseCatchAllMessage(String description) {
|
||||||
Map resultMap = [:]
|
Map resultMap = [:]
|
||||||
def cluster = zigbee.parse(description)
|
def cluster = zigbee.parse(description)
|
||||||
if (shouldProcessMessage(cluster)) {
|
if (shouldProcessMessage(cluster)) {
|
||||||
switch(cluster.clusterId) {
|
switch(cluster.clusterId) {
|
||||||
case 0x0001:
|
case 0x0001:
|
||||||
resultMap = getBatteryResult(cluster.data.last())
|
resultMap = getBatteryResult(cluster.data.last())
|
||||||
break
|
break
|
||||||
|
|
||||||
case 0x0402:
|
case 0x0402:
|
||||||
// temp is last 2 data values. reverse to swap endian
|
// temp is last 2 data values. reverse to swap endian
|
||||||
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
|
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
|
||||||
def value = getTemperature(temp)
|
def value = getTemperature(temp)
|
||||||
resultMap = getTemperatureResult(value)
|
resultMap = getTemperatureResult(value)
|
||||||
break
|
break
|
||||||
|
|
||||||
case 0x0406:
|
case 0x0406:
|
||||||
log.debug 'motion'
|
log.debug 'motion'
|
||||||
resultMap.name = 'motion'
|
resultMap.name = 'motion'
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return resultMap
|
return resultMap
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean shouldProcessMessage(cluster) {
|
private boolean shouldProcessMessage(cluster) {
|
||||||
// 0x0B is default response indicating message got through
|
// 0x0B is default response indicating message got through
|
||||||
// 0x07 is bind message
|
// 0x07 is bind message
|
||||||
boolean ignoredMessage = cluster.profileId != 0x0104 ||
|
boolean ignoredMessage = cluster.profileId != 0x0104 ||
|
||||||
cluster.command == 0x0B ||
|
cluster.command == 0x0B ||
|
||||||
cluster.command == 0x07 ||
|
cluster.command == 0x07 ||
|
||||||
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)
|
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)
|
||||||
return !ignoredMessage
|
return !ignoredMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map parseReportAttributeMessage(String description) {
|
private Map parseReportAttributeMessage(String description) {
|
||||||
@@ -152,7 +153,7 @@ private Map parseReportAttributeMessage(String description) {
|
|||||||
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
|
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
|
||||||
}
|
}
|
||||||
log.debug "Desc Map: $descMap"
|
log.debug "Desc Map: $descMap"
|
||||||
|
|
||||||
Map resultMap = [:]
|
Map resultMap = [:]
|
||||||
if (descMap.cluster == "0402" && descMap.attrId == "0000") {
|
if (descMap.cluster == "0402" && descMap.attrId == "0000") {
|
||||||
def value = getTemperature(descMap.value)
|
def value = getTemperature(descMap.value)
|
||||||
@@ -161,14 +162,14 @@ private Map parseReportAttributeMessage(String description) {
|
|||||||
else if (descMap.cluster == "0001" && descMap.attrId == "0020") {
|
else if (descMap.cluster == "0001" && descMap.attrId == "0020") {
|
||||||
resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16))
|
resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16))
|
||||||
}
|
}
|
||||||
else if (descMap.cluster == "0406" && descMap.attrId == "0000") {
|
else if (descMap.cluster == "0406" && descMap.attrId == "0000") {
|
||||||
def value = descMap.value.endsWith("01") ? "active" : "inactive"
|
def value = descMap.value.endsWith("01") ? "active" : "inactive"
|
||||||
resultMap = getMotionResult(value)
|
resultMap = getMotionResult(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
return resultMap
|
return resultMap
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map parseCustomMessage(String description) {
|
private Map parseCustomMessage(String description) {
|
||||||
Map resultMap = [:]
|
Map resultMap = [:]
|
||||||
if (description?.startsWith('temperature: ')) {
|
if (description?.startsWith('temperature: ')) {
|
||||||
@@ -179,44 +180,44 @@ private Map parseCustomMessage(String description) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Map parseIasMessage(String description) {
|
private Map parseIasMessage(String description) {
|
||||||
List parsedMsg = description.split(' ')
|
List parsedMsg = description.split(' ')
|
||||||
String msgCode = parsedMsg[2]
|
String msgCode = parsedMsg[2]
|
||||||
|
|
||||||
Map resultMap = [:]
|
|
||||||
switch(msgCode) {
|
|
||||||
case '0x0020': // Closed/No Motion/Dry
|
|
||||||
resultMap = getMotionResult('inactive')
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0021': // Open/Motion/Wet
|
Map resultMap = [:]
|
||||||
resultMap = getMotionResult('active')
|
switch(msgCode) {
|
||||||
break
|
case '0x0020': // Closed/No Motion/Dry
|
||||||
|
resultMap = getMotionResult('inactive')
|
||||||
|
break
|
||||||
|
|
||||||
case '0x0022': // Tamper Alarm
|
case '0x0021': // Open/Motion/Wet
|
||||||
log.debug 'motion with tamper alarm'
|
resultMap = getMotionResult('active')
|
||||||
resultMap = getMotionResult('active')
|
break
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0023': // Battery Alarm
|
case '0x0022': // Tamper Alarm
|
||||||
break
|
log.debug 'motion with tamper alarm'
|
||||||
|
resultMap = getMotionResult('active')
|
||||||
|
break
|
||||||
|
|
||||||
case '0x0024': // Supervision Report
|
case '0x0023': // Battery Alarm
|
||||||
log.debug 'no motion with tamper alarm'
|
break
|
||||||
resultMap = getMotionResult('inactive')
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0025': // Restore Report
|
case '0x0024': // Supervision Report
|
||||||
break
|
log.debug 'no motion with tamper alarm'
|
||||||
|
resultMap = getMotionResult('inactive')
|
||||||
|
break
|
||||||
|
|
||||||
case '0x0026': // Trouble/Failure
|
case '0x0025': // Restore Report
|
||||||
log.debug 'motion with failure alarm'
|
break
|
||||||
resultMap = getMotionResult('active')
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0028': // Test Mode
|
case '0x0026': // Trouble/Failure
|
||||||
break
|
log.debug 'motion with failure alarm'
|
||||||
}
|
resultMap = getMotionResult('active')
|
||||||
return resultMap
|
break
|
||||||
|
|
||||||
|
case '0x0028': // Test Mode
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return resultMap
|
||||||
}
|
}
|
||||||
|
|
||||||
def getTemperature(value) {
|
def getTemperature(value) {
|
||||||
@@ -229,30 +230,46 @@ def getTemperature(value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Map getBatteryResult(rawValue) {
|
private Map getBatteryResult(rawValue) {
|
||||||
log.debug 'Battery'
|
log.debug "Battery rawValue = ${rawValue}"
|
||||||
def linkText = getLinkText(device)
|
def linkText = getLinkText(device)
|
||||||
|
|
||||||
log.debug rawValue
|
|
||||||
|
|
||||||
def result = [
|
def result = [
|
||||||
name: 'battery',
|
name: 'battery',
|
||||||
value: '--'
|
value: '--'
|
||||||
]
|
]
|
||||||
|
|
||||||
def volts = rawValue / 10
|
def volts = rawValue / 10
|
||||||
def descriptionText
|
|
||||||
|
|
||||||
if (rawValue == 0) {}
|
if (rawValue == 0 || rawValue == 255) {}
|
||||||
else {
|
else {
|
||||||
if (volts > 3.5) {
|
if (volts > 3.5) {
|
||||||
result.descriptionText = "${linkText} battery has too much power (${volts} volts)."
|
result.descriptionText = "${linkText} battery has too much power (${volts} volts)."
|
||||||
}
|
}
|
||||||
else if (volts > 0){
|
else {
|
||||||
def minVolts = 2.1
|
if (device.getDataValue("manufacturer") == "SmartThings") {
|
||||||
def maxVolts = 3.0
|
volts = rawValue // For the batteryMap to work the key needs to be an int
|
||||||
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
def batteryMap = [28:100, 27:100, 26:100, 25:90, 24:90, 23:70,
|
||||||
result.value = Math.min(100, (int) pct * 100)
|
22:70, 21:50, 20:50, 19:30, 18:30, 17:15, 16:1, 15:0]
|
||||||
result.descriptionText = "${linkText} battery was ${result.value}%"
|
def minVolts = 15
|
||||||
|
def maxVolts = 28
|
||||||
|
|
||||||
|
if (volts < minVolts)
|
||||||
|
volts = minVolts
|
||||||
|
else if (volts > maxVolts)
|
||||||
|
volts = maxVolts
|
||||||
|
def pct = batteryMap[volts]
|
||||||
|
if (pct != null) {
|
||||||
|
result.value = pct
|
||||||
|
result.descriptionText = "${linkText} battery was ${result.value}%"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
def minVolts = 2.1
|
||||||
|
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}%"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,19 +354,19 @@ private hex(value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private String swapEndianHex(String hex) {
|
private String swapEndianHex(String hex) {
|
||||||
reverseArray(hex.decodeHex()).encodeHex()
|
reverseArray(hex.decodeHex()).encodeHex()
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] reverseArray(byte[] array) {
|
private byte[] reverseArray(byte[] array) {
|
||||||
int i = 0;
|
int i = 0;
|
||||||
int j = array.length - 1;
|
int j = array.length - 1;
|
||||||
byte tmp;
|
byte tmp;
|
||||||
while (j > i) {
|
while (j > i) {
|
||||||
tmp = array[j];
|
tmp = array[j];
|
||||||
array[j] = array[i];
|
array[j] = array[i];
|
||||||
array[i] = tmp;
|
array[i] = tmp;
|
||||||
j--;
|
j--;
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
return array
|
return array
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ metadata {
|
|||||||
}
|
}
|
||||||
|
|
||||||
preferences {
|
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 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: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
|
input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
|
||||||
}
|
}
|
||||||
|
|
||||||
tiles(scale: 2) {
|
tiles(scale: 2) {
|
||||||
|
|||||||
@@ -14,27 +14,28 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
metadata {
|
metadata {
|
||||||
definition (name: "SmartSense Multi Sensor", namespace: "smartthings", author: "SmartThings") {
|
definition (name: "SmartSense Multi Sensor", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
|
||||||
capability "Three Axis"
|
capability "Three Axis"
|
||||||
capability "Battery"
|
capability "Battery"
|
||||||
capability "Configuration"
|
capability "Configuration"
|
||||||
capability "Sensor"
|
capability "Sensor"
|
||||||
capability "Contact Sensor"
|
capability "Contact Sensor"
|
||||||
capability "Acceleration Sensor"
|
capability "Acceleration Sensor"
|
||||||
capability "Refresh"
|
capability "Refresh"
|
||||||
capability "Temperature Measurement"
|
capability "Temperature Measurement"
|
||||||
|
|
||||||
command "enrollResponse"
|
command "enrollResponse"
|
||||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3320"
|
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3320"
|
||||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3321"
|
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3321"
|
||||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3321-S", deviceJoinName: "Multipurpose Sensor"
|
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3321-S", deviceJoinName: "Multipurpose Sensor"
|
||||||
|
fingerprint inClusters: "0000,0001,0003,000F,0020,0402,0500,FC02", outClusters: "0019", manufacturer: "SmartThings", model: "multiv4", deviceJoinName: "Multipurpose Sensor"
|
||||||
|
|
||||||
attribute "status", "string"
|
attribute "status", "string"
|
||||||
}
|
}
|
||||||
|
|
||||||
simulator {
|
simulator {
|
||||||
status "open": "zone report :: type: 19 value: 0031"
|
status "open": "zone report :: type: 19 value: 0031"
|
||||||
status "closed": "zone report :: type: 19 value: 0030"
|
status "closed": "zone report :: type: 19 value: 0030"
|
||||||
|
|
||||||
@@ -51,7 +52,7 @@
|
|||||||
status "x,y,z: 0,1000,0": "x: 0, y: 1000, z: 0"
|
status "x,y,z: 0,1000,0": "x: 0, y: 1000, z: 0"
|
||||||
status "x,y,z: 0,0,1000": "x: 0, y: 0, z: 1000"
|
status "x,y,z: 0,0,1000": "x: 0, y: 0, z: 1000"
|
||||||
}
|
}
|
||||||
preferences {
|
preferences {
|
||||||
section {
|
section {
|
||||||
image(name: 'educationalcontent', multiple: true, images: [
|
image(name: 'educationalcontent', multiple: true, images: [
|
||||||
"http://cdn.device-gse.smartthings.com/Multi/Multi1.jpg",
|
"http://cdn.device-gse.smartthings.com/Multi/Multi1.jpg",
|
||||||
@@ -61,13 +62,13 @@
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
section {
|
section {
|
||||||
input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
|
input 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: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
|
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)
|
|
||||||
}
|
}
|
||||||
}
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tiles(scale: 2) {
|
tiles(scale: 2) {
|
||||||
multiAttributeTile(name:"status", type: "generic", width: 6, height: 4){
|
multiAttributeTile(name:"status", type: "generic", width: 6, height: 4){
|
||||||
@@ -105,9 +106,9 @@
|
|||||||
valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) {
|
valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) {
|
||||||
state "battery", label:'${currentValue}% battery', unit:""
|
state "battery", label:'${currentValue}% battery', unit:""
|
||||||
}
|
}
|
||||||
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||||
state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
|
state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
main(["status", "acceleration", "temperature"])
|
main(["status", "acceleration", "temperature"])
|
||||||
@@ -115,91 +116,104 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def parse(String description) {
|
def parse(String description) {
|
||||||
|
Map map = [:]
|
||||||
Map map = [:]
|
if (description?.startsWith('catchall:')) {
|
||||||
if (description?.startsWith('catchall:')) {
|
map = parseCatchAllMessage(description)
|
||||||
map = parseCatchAllMessage(description)
|
}
|
||||||
}
|
else if (description?.startsWith('temperature: ')) {
|
||||||
else if (description?.startsWith('read attr -')) {
|
map = parseCustomMessage(description)
|
||||||
map = parseReportAttributeMessage(description)
|
}
|
||||||
}
|
else if (description?.startsWith('zone status')) {
|
||||||
else if (description?.startsWith('temperature: ')) {
|
map = parseIasMessage(description)
|
||||||
map = parseCustomMessage(description)
|
}
|
||||||
}
|
|
||||||
else if (description?.startsWith('zone status')) {
|
|
||||||
map = parseIasMessage(description)
|
|
||||||
}
|
|
||||||
|
|
||||||
def result = map ? createEvent(map) : null
|
def result = map ? createEvent(map) : null
|
||||||
|
|
||||||
if (description?.startsWith('enroll request')) {
|
if (description?.startsWith('enroll request')) {
|
||||||
List cmds = enrollResponse()
|
List cmds = enrollResponse()
|
||||||
log.debug "enroll response: ${cmds}"
|
log.debug "enroll response: ${cmds}"
|
||||||
result = cmds?.collect { new physicalgraph.device.HubAction(it) }
|
result = cmds?.collect { new physicalgraph.device.HubAction(it) }
|
||||||
}
|
}
|
||||||
return result
|
else if (description?.startsWith('read attr -')) {
|
||||||
}
|
result = parseReportAttributeMessage(description).each { createEvent(it) }
|
||||||
|
}
|
||||||
private Map parseCatchAllMessage(String description) {
|
return result
|
||||||
Map resultMap = [:]
|
|
||||||
def cluster = zigbee.parse(description)
|
|
||||||
log.debug cluster
|
|
||||||
if (shouldProcessMessage(cluster)) {
|
|
||||||
switch(cluster.clusterId) {
|
|
||||||
case 0x0001:
|
|
||||||
resultMap = getBatteryResult(cluster.data.last())
|
|
||||||
break
|
|
||||||
|
|
||||||
case 0xFC02:
|
|
||||||
log.debug 'ACCELERATION'
|
|
||||||
break
|
|
||||||
|
|
||||||
case 0x0402:
|
|
||||||
log.debug 'TEMP'
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean shouldProcessMessage(cluster) {
|
|
||||||
// 0x0B is default response indicating message got through
|
|
||||||
// 0x07 is bind message
|
|
||||||
boolean ignoredMessage = cluster.profileId != 0x0104 ||
|
|
||||||
cluster.command == 0x0B ||
|
|
||||||
cluster.command == 0x07 ||
|
|
||||||
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)
|
|
||||||
return !ignoredMessage
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map parseReportAttributeMessage(String description) {
|
private Map parseCatchAllMessage(String description) {
|
||||||
|
Map resultMap = [:]
|
||||||
|
def cluster = zigbee.parse(description)
|
||||||
|
log.debug cluster
|
||||||
|
if (shouldProcessMessage(cluster)) {
|
||||||
|
switch(cluster.clusterId) {
|
||||||
|
case 0x0001:
|
||||||
|
resultMap = getBatteryResult(cluster.data.last())
|
||||||
|
break
|
||||||
|
|
||||||
|
case 0xFC02:
|
||||||
|
log.debug 'ACCELERATION'
|
||||||
|
break
|
||||||
|
|
||||||
|
case 0x0402:
|
||||||
|
log.debug 'TEMP'
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldProcessMessage(cluster) {
|
||||||
|
// 0x0B is default response indicating message got through
|
||||||
|
// 0x07 is bind message
|
||||||
|
boolean ignoredMessage = cluster.profileId != 0x0104 ||
|
||||||
|
cluster.command == 0x0B ||
|
||||||
|
cluster.command == 0x07 ||
|
||||||
|
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)
|
||||||
|
return !ignoredMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
private List parseReportAttributeMessage(String description) {
|
||||||
Map descMap = (description - "read attr - ").split(",").inject([:]) { map, param ->
|
Map descMap = (description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||||
def nameAndValue = param.split(":")
|
def nameAndValue = param.split(":")
|
||||||
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
|
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
|
||||||
}
|
}
|
||||||
|
|
||||||
Map resultMap = [:]
|
List result = []
|
||||||
if (descMap.cluster == "0402" && descMap.attrId == "0000") {
|
if (descMap.cluster == "0402" && descMap.attrId == "0000") {
|
||||||
def value = getTemperature(descMap.value)
|
def value = getTemperature(descMap.value)
|
||||||
resultMap = getTemperatureResult(value)
|
result << getTemperatureResult(value)
|
||||||
}
|
}
|
||||||
else if (descMap.cluster == "FC02" && descMap.attrId == "0010") {
|
else if (descMap.cluster == "FC02" && descMap.attrId == "0010") {
|
||||||
resultMap = getAccelerationResult(descMap.value)
|
if (descMap.value.size() == 32) {
|
||||||
|
// value will look like 00ae29001403e2290013001629001201
|
||||||
|
// breaking this apart and swapping byte order where appropriate, this breaks down to:
|
||||||
|
// X (0x0012) = 0x0016
|
||||||
|
// Y (0x0013) = 0x03E2
|
||||||
|
// Z (0x0014) = 0x00AE
|
||||||
|
// note that there is a known bug in that the x,y,z attributes are interpreted in the wrong order
|
||||||
|
// this will be fixed in a future update
|
||||||
|
def threeAxisAttributes = descMap.value[0..-9]
|
||||||
|
result << parseAxis(threeAxisAttributes)
|
||||||
|
descMap.value = descMap.value[-2..-1]
|
||||||
|
}
|
||||||
|
result << getAccelerationResult(descMap.value)
|
||||||
}
|
}
|
||||||
else if (descMap.cluster == "FC02" && descMap.attrId == "0012") {
|
else if (descMap.cluster == "FC02" && descMap.attrId == "0012" && descMap.value.size() == 24) {
|
||||||
resultMap = parseAxis(descMap.value)
|
// The size is checked to ensure the attribute report contains X, Y and Z values
|
||||||
|
// If all three axis are not included then the attribute report is ignored
|
||||||
|
result << parseAxis(descMap.value)
|
||||||
}
|
}
|
||||||
else if (descMap.cluster == "0001" && descMap.attrId == "0020") {
|
else if (descMap.cluster == "0001" && descMap.attrId == "0020") {
|
||||||
resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16))
|
result << getBatteryResult(Integer.parseInt(descMap.value, 16))
|
||||||
}
|
}
|
||||||
|
|
||||||
return resultMap
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map parseCustomMessage(String description) {
|
private Map parseCustomMessage(String description) {
|
||||||
@@ -217,43 +231,43 @@ private Map parseIasMessage(String description) {
|
|||||||
|
|
||||||
Map resultMap = [:]
|
Map resultMap = [:]
|
||||||
switch(msgCode) {
|
switch(msgCode) {
|
||||||
case '0x0020': // Closed/No Motion/Dry
|
case '0x0020': // Closed/No Motion/Dry
|
||||||
if (garageSensor != "Yes"){
|
if (garageSensor != "Yes"){
|
||||||
resultMap = getContactResult('closed')
|
resultMap = getContactResult('closed')
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
|
||||||
case '0x0021': // Open/Motion/Wet
|
case '0x0021': // Open/Motion/Wet
|
||||||
if (garageSensor != "Yes"){
|
if (garageSensor != "Yes"){
|
||||||
resultMap = getContactResult('open')
|
resultMap = getContactResult('open')
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
|
||||||
case '0x0022': // Tamper Alarm
|
case '0x0022': // Tamper Alarm
|
||||||
break
|
break
|
||||||
|
|
||||||
case '0x0023': // Battery Alarm
|
case '0x0023': // Battery Alarm
|
||||||
break
|
break
|
||||||
|
|
||||||
case '0x0024': // Supervision Report
|
case '0x0024': // Supervision Report
|
||||||
if (garageSensor != "Yes"){
|
if (garageSensor != "Yes"){
|
||||||
resultMap = getContactResult('closed')
|
resultMap = getContactResult('closed')
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
|
||||||
case '0x0025': // Restore Report
|
case '0x0025': // Restore Report
|
||||||
if (garageSensor != "Yes"){
|
if (garageSensor != "Yes"){
|
||||||
resultMap = getContactResult('open')
|
resultMap = getContactResult('open')
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
|
||||||
case '0x0026': // Trouble/Failure
|
case '0x0026': // Trouble/Failure
|
||||||
break
|
break
|
||||||
|
|
||||||
case '0x0028': // Test Mode
|
case '0x0028': // Test Mode
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
return resultMap
|
return resultMap
|
||||||
}
|
}
|
||||||
|
|
||||||
def updated() {
|
def updated() {
|
||||||
@@ -288,146 +302,171 @@ def getTemperature(value) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map getBatteryResult(rawValue) {
|
private Map getBatteryResult(rawValue) {
|
||||||
log.debug "Battery"
|
log.debug "Battery rawValue = ${rawValue}"
|
||||||
log.debug rawValue
|
def linkText = getLinkText(device)
|
||||||
def linkText = getLinkText(device)
|
|
||||||
|
|
||||||
def result = [
|
def result = [
|
||||||
name: 'battery',
|
name: 'battery',
|
||||||
value: '--'
|
value: '--'
|
||||||
]
|
]
|
||||||
|
|
||||||
def volts = rawValue / 10
|
def volts = rawValue / 10
|
||||||
def descriptionText
|
|
||||||
|
if (rawValue == 0 || rawValue == 255) {}
|
||||||
if (rawValue == 255) {}
|
else {
|
||||||
else {
|
if (volts > 3.5) {
|
||||||
|
|
||||||
if (volts > 3.5) {
|
|
||||||
result.descriptionText = "${linkText} battery has too much power (${volts} volts)."
|
result.descriptionText = "${linkText} battery has too much power (${volts} volts)."
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
def minVolts = 2.1
|
if (device.getDataValue("manufacturer") == "SmartThings") {
|
||||||
def maxVolts = 3.0
|
volts = rawValue // For the batteryMap to work the key needs to be an int
|
||||||
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
def batteryMap = [28:100, 27:100, 26:100, 25:90, 24:90, 23:70,
|
||||||
result.value = Math.min(100, (int) pct * 100)
|
22:70, 21:50, 20:50, 19:30, 18:30, 17:15, 16:1, 15:0]
|
||||||
result.descriptionText = "${linkText} battery was ${result.value}%"
|
def minVolts = 15
|
||||||
}}
|
def maxVolts = 28
|
||||||
|
|
||||||
return result
|
if (volts < minVolts)
|
||||||
}
|
volts = minVolts
|
||||||
|
else if (volts > maxVolts)
|
||||||
private Map getTemperatureResult(value) {
|
volts = maxVolts
|
||||||
log.debug "Temperature"
|
def pct = batteryMap[volts]
|
||||||
def linkText = getLinkText(device)
|
if (pct != null) {
|
||||||
if (tempOffset) {
|
result.value = pct
|
||||||
def offset = tempOffset as int
|
result.descriptionText = "${linkText} battery was ${result.value}%"
|
||||||
def v = value as int
|
}
|
||||||
value = v + offset
|
}
|
||||||
|
else {
|
||||||
|
def minVolts = 2.1
|
||||||
|
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}%"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
def descriptionText = "${linkText} was ${value}°${temperatureScale}"
|
}
|
||||||
return [
|
|
||||||
name: 'temperature',
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
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}"
|
||||||
|
return [
|
||||||
|
name: 'temperature',
|
||||||
|
value: value,
|
||||||
|
descriptionText: descriptionText
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 isStateChange = isStateChange(device, name, value)
|
||||||
|
[
|
||||||
|
name: name,
|
||||||
value: value,
|
value: value,
|
||||||
descriptionText: descriptionText
|
descriptionText: descriptionText,
|
||||||
|
isStateChange: isStateChange
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def refresh() {
|
||||||
|
log.debug "Refreshing Values "
|
||||||
|
|
||||||
|
def refreshCmds = []
|
||||||
|
|
||||||
|
if (device.getDataValue("manufacturer") == "SmartThings") {
|
||||||
|
log.debug "Refreshing Values for manufacturer: SmartThings "
|
||||||
|
refreshCmds = refreshCmds + [
|
||||||
|
/* These values of Motion Threshold Multiplier(01) and Motion Threshold (7602)
|
||||||
|
seem to be giving pretty accurate results for the XYZ co-ordinates for this manufacturer.
|
||||||
|
Separating these out in a separate if-else because I do not want to touch Centralite part
|
||||||
|
as of now.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"zcl mfg-code ${manufacturerCode}", "delay 200",
|
||||||
|
"zcl global write 0xFC02 0 0x20 {01}", "delay 200",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 1", "delay 400",
|
||||||
|
|
||||||
|
"zcl mfg-code ${manufacturerCode}", "delay 200",
|
||||||
|
"zcl global write 0xFC02 2 0x21 {7602}", "delay 200",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 1", "delay 400",
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
refreshCmds = refreshCmds + [
|
||||||
|
/* sensitivity - default value (8) */
|
||||||
|
"zcl mfg-code ${manufacturerCode}", "delay 200",
|
||||||
|
"zcl global write 0xFC02 0 0x20 {02}", "delay 200",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 1", "delay 400",
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map getContactResult(value) {
|
//Common refresh commands
|
||||||
log.debug "Contact"
|
refreshCmds = refreshCmds + [
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 isStateChange = isStateChange(device, name, value)
|
|
||||||
[
|
|
||||||
name: name,
|
|
||||||
value: value,
|
|
||||||
descriptionText: descriptionText,
|
|
||||||
isStateChange: isStateChange
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
def refresh() {
|
|
||||||
log.debug "Refreshing Values "
|
|
||||||
def refreshCmds = [
|
|
||||||
|
|
||||||
/* sensitivity - default value (8) */
|
|
||||||
|
|
||||||
"zcl mfg-code 0x104E", "delay 200",
|
|
||||||
"zcl global write 0xFC02 0 0x20 {02}", "delay 200",
|
|
||||||
"send 0x${device.deviceNetworkId} 1 1", "delay 400",
|
|
||||||
|
|
||||||
"st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200",
|
"st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200",
|
||||||
"st rattr 0x${device.deviceNetworkId} 1 1 0x20", "delay 200",
|
"st rattr 0x${device.deviceNetworkId} 1 1 0x20", "delay 200",
|
||||||
|
|
||||||
"zcl mfg-code 0x104E", "delay 200",
|
"zcl mfg-code ${manufacturerCode}", "delay 200",
|
||||||
"zcl global read 0xFC02 0x0010",
|
"zcl global read 0xFC02 0x0010",
|
||||||
"send 0x${device.deviceNetworkId} 1 1","delay 400",
|
"send 0x${device.deviceNetworkId} 1 1","delay 400"
|
||||||
|
]
|
||||||
"zcl mfg-code 0x104E", "delay 200",
|
|
||||||
"zcl global read 0xFC02 0x0012",
|
|
||||||
"send 0x${device.deviceNetworkId} 1 1","delay 400",
|
|
||||||
|
|
||||||
"zcl mfg-code 0x104E", "delay 200",
|
|
||||||
"zcl global read 0xFC02 0x0013",
|
|
||||||
"send 0x${device.deviceNetworkId} 1 1","delay 400",
|
|
||||||
|
|
||||||
"zcl mfg-code 0x104E", "delay 200",
|
|
||||||
"zcl global read 0xFC02 0x0014",
|
|
||||||
"send 0x${device.deviceNetworkId} 1 1", "delay 400"
|
|
||||||
]
|
|
||||||
|
|
||||||
return refreshCmds + enrollResponse()
|
return refreshCmds + enrollResponse()
|
||||||
}
|
}
|
||||||
|
|
||||||
def configure() {
|
def configure() {
|
||||||
|
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
log.debug "Configuring Reporting"
|
||||||
log.debug "Configuring Reporting"
|
|
||||||
|
|
||||||
def configCmds = [
|
|
||||||
|
|
||||||
|
def configCmds = [
|
||||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||||
|
|
||||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 200",
|
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 200",
|
||||||
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
|
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", "delay 200", //checkin time 6 hrs
|
||||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||||
|
|
||||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 200",
|
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 200",
|
||||||
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
|
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}", "delay 200",
|
||||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||||
|
|
||||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0xFC02 {${device.zigbeeId}} {}", "delay 200",
|
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0xFC02 {${device.zigbeeId}} {}", "delay 200",
|
||||||
"zcl mfg-code 0x104E",
|
"zcl mfg-code ${manufacturerCode}", "delay 200",
|
||||||
"zcl global send-me-a-report 0xFC02 0x0010 0x18 10 3600 {01}",
|
"zcl global send-me-a-report 0xFC02 0x0010 0x18 10 3600 {01}", "delay 200",
|
||||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||||
|
|
||||||
"zcl mfg-code 0x104E",
|
"zcl mfg-code ${manufacturerCode}", "delay 200",
|
||||||
"zcl global send-me-a-report 0xFC02 0x0012 0x29 1 3600 {01}",
|
"zcl global send-me-a-report 0xFC02 0x0012 0x29 1 3600 {01}", "delay 200",
|
||||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||||
|
|
||||||
"zcl mfg-code 0x104E",
|
"zcl mfg-code ${manufacturerCode}", "delay 200",
|
||||||
"zcl global send-me-a-report 0xFC02 0x0013 0x29 1 3600 {01}",
|
"zcl global send-me-a-report 0xFC02 0x0013 0x29 1 3600 {01}", "delay 200",
|
||||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||||
|
|
||||||
"zcl mfg-code 0x104E",
|
"zcl mfg-code ${manufacturerCode}", "delay 200",
|
||||||
"zcl global send-me-a-report 0xFC02 0x0014 0x29 1 3600 {01}",
|
"zcl global send-me-a-report 0xFC02 0x0014 0x29 1 3600 {01}", "delay 200",
|
||||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500"
|
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500"
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
return configCmds + refresh()
|
return configCmds + refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
private getEndpointId() {
|
private getEndpointId() {
|
||||||
@@ -442,40 +481,39 @@ def enrollResponse() {
|
|||||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||||
//Enroll Response
|
//Enroll Response
|
||||||
"raw 0x500 {01 23 00 00 00}",
|
"raw 0x500 {01 23 00 00 00}", "delay 200",
|
||||||
"send 0x${device.deviceNetworkId} 1 1", "delay 200"
|
"send 0x${device.deviceNetworkId} 1 1", "delay 200"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private Map parseAxis(String description) {
|
private Map parseAxis(String description) {
|
||||||
log.debug "parseAxis"
|
def hexToSignedInt = { hexVal ->
|
||||||
def xyzResults = [x: 0, y: 0, z: 0]
|
def unsignedVal = hexToInt(hexVal)
|
||||||
def parts = description.split("2900")
|
unsignedVal > 32767 ? unsignedVal - 65536 : unsignedVal
|
||||||
parts[0] = "12" + parts[0]
|
}
|
||||||
parts.each { part ->
|
|
||||||
part = part.trim()
|
def z = hexToSignedInt(description[0..3])
|
||||||
if (part.startsWith("12")) {
|
def y = hexToSignedInt(description[10..13])
|
||||||
def unsignedX = hexToInt(part.split("12")[1].trim())
|
def x = hexToSignedInt(description[20..23])
|
||||||
def signedX = unsignedX > 32767 ? unsignedX - 65536 : unsignedX
|
def xyzResults = [x: x, y: y, z: z]
|
||||||
xyzResults.x = signedX
|
|
||||||
log.debug "X Part: ${signedX}"
|
if (device.getDataValue("manufacturer") == "SmartThings") {
|
||||||
}
|
// This mapping matches the current behavior of the Device Handler for the Centralite sensors
|
||||||
else if (part.startsWith("13")) {
|
xyzResults.x = z
|
||||||
def unsignedY = hexToInt(part.split("13")[1].trim())
|
xyzResults.y = y
|
||||||
def signedY = unsignedY > 32767 ? unsignedY - 65536 : unsignedY
|
xyzResults.z = -x
|
||||||
xyzResults.y = signedY
|
} else {
|
||||||
log.debug "Y Part: ${signedY}"
|
// The axises reported by the Device Handler differ from the axises reported by the sensor
|
||||||
}
|
// This may change in the future
|
||||||
else if (part.startsWith("14")) {
|
xyzResults.x = z
|
||||||
def unsignedZ = hexToInt(part.split("14")[1].trim())
|
xyzResults.y = x
|
||||||
def signedZ = unsignedZ > 32767 ? unsignedZ - 65536 : unsignedZ
|
xyzResults.z = y
|
||||||
xyzResults.z = signedZ
|
}
|
||||||
log.debug "Z Part: ${signedZ}"
|
|
||||||
if (garageSensor == "Yes")
|
log.debug "parseAxis -- ${xyzResults}"
|
||||||
garageEvent(signedZ)
|
|
||||||
}
|
if (garageSensor == "Yes")
|
||||||
}
|
garageEvent(xyzResults.z)
|
||||||
|
|
||||||
getXyzResult(xyzResults, description)
|
getXyzResult(xyzResults, description)
|
||||||
}
|
}
|
||||||
@@ -519,6 +557,14 @@ private Map getXyzResult(results, description) {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getManufacturerCode() {
|
||||||
|
if (device.getDataValue("manufacturer") == "SmartThings") {
|
||||||
|
return "0x110A"
|
||||||
|
} else {
|
||||||
|
return "0x104E"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private hexToInt(value) {
|
private hexToInt(value) {
|
||||||
new BigInteger(value, 16)
|
new BigInteger(value, 16)
|
||||||
}
|
}
|
||||||
@@ -544,4 +590,3 @@ private byte[] reverseArray(byte[] array) {
|
|||||||
}
|
}
|
||||||
return array
|
return array
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,8 +43,8 @@ metadata {
|
|||||||
}
|
}
|
||||||
|
|
||||||
preferences {
|
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 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: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
|
input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
|
||||||
}
|
}
|
||||||
|
|
||||||
tiles(scale: 2) {
|
tiles(scale: 2) {
|
||||||
|
|||||||
@@ -32,8 +32,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
preferences {
|
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 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: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
|
input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
|
||||||
}
|
}
|
||||||
|
|
||||||
tiles(scale: 2) {
|
tiles(scale: 2) {
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ metadata {
|
|||||||
}
|
}
|
||||||
|
|
||||||
preferences {
|
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 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: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
|
input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
|
||||||
}
|
}
|
||||||
|
|
||||||
tiles(scale: 2) {
|
tiles(scale: 2) {
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ metadata {
|
|||||||
}
|
}
|
||||||
|
|
||||||
preferences {
|
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 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: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
|
input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
|
||||||
}
|
}
|
||||||
|
|
||||||
tiles(scale: 2) {
|
tiles(scale: 2) {
|
||||||
|
|||||||
@@ -45,8 +45,8 @@ metadata {
|
|||||||
}
|
}
|
||||||
|
|
||||||
preferences {
|
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 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: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
|
input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
|
||||||
}
|
}
|
||||||
|
|
||||||
tiles {
|
tiles {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright 2014 SmartThings
|
* Copyright 2015 SmartThings
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
* 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:
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
@@ -15,6 +15,7 @@ metadata {
|
|||||||
// Automatically generated. Make future change here.
|
// Automatically generated. Make future change here.
|
||||||
definition (name: "Simulated Thermostat", namespace: "smartthings/testing", author: "SmartThings") {
|
definition (name: "Simulated Thermostat", namespace: "smartthings/testing", author: "SmartThings") {
|
||||||
capability "Thermostat"
|
capability "Thermostat"
|
||||||
|
capability "Relative Humidity Measurement"
|
||||||
|
|
||||||
command "tempUp"
|
command "tempUp"
|
||||||
command "tempDown"
|
command "tempDown"
|
||||||
@@ -22,11 +23,40 @@ metadata {
|
|||||||
command "heatDown"
|
command "heatDown"
|
||||||
command "coolUp"
|
command "coolUp"
|
||||||
command "coolDown"
|
command "coolDown"
|
||||||
command "setTemperature", ["number"]
|
command "setTemperature", ["number"]
|
||||||
}
|
}
|
||||||
|
|
||||||
tiles {
|
tiles(scale: 2) {
|
||||||
valueTile("temperature", "device.temperature", width: 1, height: 1) {
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
valueTile("temperature", "device.temperature", width: 2, height: 2) {
|
||||||
state("temperature", label:'${currentValue}', unit:"dF",
|
state("temperature", label:'${currentValue}', unit:"dF",
|
||||||
backgroundColors:[
|
backgroundColors:[
|
||||||
[value: 31, color: "#153591"],
|
[value: 31, color: "#153591"],
|
||||||
@@ -39,51 +69,51 @@ metadata {
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
standardTile("tempDown", "device.temperature", inactiveLabel: false, decoration: "flat") {
|
standardTile("tempDown", "device.temperature", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||||
state "default", label:'down', action:"tempDown"
|
state "default", label:'down', action:"tempDown"
|
||||||
}
|
}
|
||||||
standardTile("tempUp", "device.temperature", inactiveLabel: false, decoration: "flat") {
|
standardTile("tempUp", "device.temperature", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||||
state "default", label:'up', action:"tempUp"
|
state "default", label:'up', action:"tempUp"
|
||||||
}
|
}
|
||||||
|
|
||||||
valueTile("heatingSetpoint", "device.heatingSetpoint", inactiveLabel: false, decoration: "flat") {
|
valueTile("heatingSetpoint", "device.heatingSetpoint", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||||
state "heat", label:'${currentValue} heat', unit: "F", backgroundColor:"#ffffff"
|
state "heat", label:'${currentValue} heat', unit: "F", backgroundColor:"#ffffff"
|
||||||
}
|
}
|
||||||
standardTile("heatDown", "device.temperature", inactiveLabel: false, decoration: "flat") {
|
standardTile("heatDown", "device.temperature", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||||
state "default", label:'down', action:"heatDown"
|
state "default", label:'down', action:"heatDown"
|
||||||
}
|
}
|
||||||
standardTile("heatUp", "device.temperature", inactiveLabel: false, decoration: "flat") {
|
standardTile("heatUp", "device.temperature", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||||
state "default", label:'up', action:"heatUp"
|
state "default", label:'up', action:"heatUp"
|
||||||
}
|
}
|
||||||
|
|
||||||
valueTile("coolingSetpoint", "device.coolingSetpoint", inactiveLabel: false, decoration: "flat") {
|
valueTile("coolingSetpoint", "device.coolingSetpoint", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||||
state "cool", label:'${currentValue} cool', unit:"F", backgroundColor:"#ffffff"
|
state "cool", label:'${currentValue} cool', unit:"F", backgroundColor:"#ffffff"
|
||||||
}
|
}
|
||||||
standardTile("coolDown", "device.temperature", inactiveLabel: false, decoration: "flat") {
|
standardTile("coolDown", "device.temperature", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||||
state "default", label:'down', action:"coolDown"
|
state "default", label:'down', action:"coolDown"
|
||||||
}
|
}
|
||||||
standardTile("coolUp", "device.temperature", inactiveLabel: false, decoration: "flat") {
|
standardTile("coolUp", "device.temperature", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||||
state "default", label:'up', action:"coolUp"
|
state "default", label:'up', action:"coolUp"
|
||||||
}
|
}
|
||||||
|
|
||||||
standardTile("mode", "device.thermostatMode", inactiveLabel: false, decoration: "flat") {
|
standardTile("mode", "device.thermostatMode", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||||
state "off", label:'${name}', action:"thermostat.heat", backgroundColor:"#ffffff"
|
state "off", label:'${name}', action:"thermostat.heat", backgroundColor:"#ffffff"
|
||||||
state "heat", label:'${name}', action:"thermostat.cool", backgroundColor:"#ffa81e"
|
state "heat", label:'${name}', action:"thermostat.cool", backgroundColor:"#ffa81e"
|
||||||
state "cool", label:'${name}', action:"thermostat.auto", backgroundColor:"#269bd2"
|
state "cool", label:'${name}', action:"thermostat.auto", backgroundColor:"#269bd2"
|
||||||
state "auto", label:'${name}', action:"thermostat.off", backgroundColor:"#79b821"
|
state "auto", label:'${name}', action:"thermostat.off", backgroundColor:"#79b821"
|
||||||
}
|
}
|
||||||
standardTile("fanMode", "device.thermostatFanMode", inactiveLabel: false, decoration: "flat") {
|
standardTile("fanMode", "device.thermostatFanMode", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||||
state "fanAuto", label:'${name}', action:"thermostat.fanOn", backgroundColor:"#ffffff"
|
state "fanAuto", label:'${name}', action:"thermostat.fanOn", backgroundColor:"#ffffff"
|
||||||
state "fanOn", label:'${name}', action:"thermostat.fanCirculate", backgroundColor:"#ffffff"
|
state "fanOn", label:'${name}', action:"thermostat.fanCirculate", backgroundColor:"#ffffff"
|
||||||
state "fanCirculate", label:'${name}', action:"thermostat.fanAuto", backgroundColor:"#ffffff"
|
state "fanCirculate", label:'${name}', action:"thermostat.fanAuto", backgroundColor:"#ffffff"
|
||||||
}
|
}
|
||||||
standardTile("operatingState", "device.thermostatOperatingState") {
|
standardTile("operatingState", "device.thermostatOperatingState", width: 2, height: 2) {
|
||||||
state "idle", label:'${name}', backgroundColor:"#ffffff"
|
state "idle", label:'${name}', backgroundColor:"#ffffff"
|
||||||
state "heating", label:'${name}', backgroundColor:"#ffa81e"
|
state "heating", label:'${name}', backgroundColor:"#ffa81e"
|
||||||
state "cooling", label:'${name}', backgroundColor:"#269bd2"
|
state "cooling", label:'${name}', backgroundColor:"#269bd2"
|
||||||
}
|
}
|
||||||
|
|
||||||
main("temperature","operatingState")
|
main("thermostatMulti")
|
||||||
details([
|
details([
|
||||||
"temperature","tempDown","tempUp",
|
"temperature","tempDown","tempUp",
|
||||||
"mode", "fanMode", "operatingState",
|
"mode", "fanMode", "operatingState",
|
||||||
@@ -101,6 +131,7 @@ def installed() {
|
|||||||
sendEvent(name: "thermostatMode", value: "off")
|
sendEvent(name: "thermostatMode", value: "off")
|
||||||
sendEvent(name: "thermostatFanMode", value: "fanAuto")
|
sendEvent(name: "thermostatFanMode", value: "fanAuto")
|
||||||
sendEvent(name: "thermostatOperatingState", value: "idle")
|
sendEvent(name: "thermostatOperatingState", value: "idle")
|
||||||
|
sendEvent(name: "humidity", value: 53, unit: "%")
|
||||||
}
|
}
|
||||||
|
|
||||||
def parse(String description) {
|
def parse(String description) {
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ metadata {
|
|||||||
}
|
}
|
||||||
|
|
||||||
preferences {
|
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 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: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
|
input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
|
||||||
}
|
}
|
||||||
|
|
||||||
tiles {
|
tiles {
|
||||||
|
|||||||
@@ -29,18 +29,18 @@ metadata {
|
|||||||
|
|
||||||
tiles {
|
tiles {
|
||||||
|
|
||||||
valueTile("power", "device.power") {
|
valueTile("power", "device.power", canChangeIcon: true) {
|
||||||
state "power", label: '${currentValue} W'
|
state "power", label: '${currentValue} W'
|
||||||
}
|
}
|
||||||
|
|
||||||
tile(name: "powerChart", attribute: "powerContent", type: "HTML", url: '${currentValue}', width: 3, height: 2) { }
|
htmlTile(name: "powerContent", attribute: "powerContent", type: "HTML", whitelist: "www.wattvision.com" , url: '${currentValue}', width: 3, height: 2)
|
||||||
|
|
||||||
standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat") {
|
standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat") {
|
||||||
state "default", label: '', action: "refresh.refresh", icon: "st.secondary.refresh"
|
state "default", label: '', action: "refresh.refresh", icon: "st.secondary.refresh"
|
||||||
}
|
}
|
||||||
|
|
||||||
main "power"
|
main "power"
|
||||||
details(["powerChart", "power", "refresh"])
|
details(["powerContent", "power", "refresh"])
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -74,10 +74,10 @@ public addWattvisionData(json) {
|
|||||||
|
|
||||||
log.trace "Adding data from Wattvision"
|
log.trace "Adding data from Wattvision"
|
||||||
|
|
||||||
def data = json.data
|
def data = parseJson(json.data.toString())
|
||||||
def units = json.units ?: "watts"
|
def units = json.units ?: "watts"
|
||||||
|
|
||||||
if (data) {
|
if (data.size() > 0) {
|
||||||
def latestData = data[-1]
|
def latestData = data[-1]
|
||||||
data.each {
|
data.each {
|
||||||
sendPowerEvent(it.t, it.v, units, (latestData == it))
|
sendPowerEvent(it.t, it.v, units, (latestData == it))
|
||||||
@@ -103,3 +103,7 @@ private sendPowerEvent(time, value, units, isLatest = false) {
|
|||||||
sendEvent(eventData)
|
sendEvent(eventData)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def parseJson(String s) {
|
||||||
|
new groovy.json.JsonSlurper().parseText(s)
|
||||||
|
}
|
||||||
|
|||||||
@@ -273,7 +273,7 @@ User-Agent: CyberGarage-HTTP/1.0
|
|||||||
def poll() {
|
def poll() {
|
||||||
log.debug "Executing 'poll'"
|
log.debug "Executing 'poll'"
|
||||||
if (device.currentValue("currentIP") != "Offline")
|
if (device.currentValue("currentIP") != "Offline")
|
||||||
runIn(10, setOffline)
|
runIn(30, setOffline)
|
||||||
new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1
|
new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1
|
||||||
SOAPACTION: "urn:Belkin:service:basicevent:1#GetBinaryState"
|
SOAPACTION: "urn:Belkin:service:basicevent:1#GetBinaryState"
|
||||||
Content-Length: 277
|
Content-Length: 277
|
||||||
|
|||||||
@@ -77,9 +77,8 @@ def parse(String description) {
|
|||||||
def result = []
|
def result = []
|
||||||
def bodyString = msg.body
|
def bodyString = msg.body
|
||||||
if (bodyString) {
|
if (bodyString) {
|
||||||
unschedule("setOffline")
|
unschedule("setOffline")
|
||||||
def body = new XmlSlurper().parseText(bodyString)
|
def body = new XmlSlurper().parseText(bodyString)
|
||||||
|
|
||||||
if (body?.property?.TimeSyncRequest?.text()) {
|
if (body?.property?.TimeSyncRequest?.text()) {
|
||||||
log.trace "Got TimeSyncRequest"
|
log.trace "Got TimeSyncRequest"
|
||||||
result << timeSyncResponse()
|
result << timeSyncResponse()
|
||||||
@@ -134,7 +133,7 @@ def refresh() {
|
|||||||
def getStatus() {
|
def getStatus() {
|
||||||
log.debug "Executing WeMo Motion 'getStatus'"
|
log.debug "Executing WeMo Motion 'getStatus'"
|
||||||
if (device.currentValue("currentIP") != "Offline")
|
if (device.currentValue("currentIP") != "Offline")
|
||||||
runIn(10, setOffline)
|
runIn(30, setOffline)
|
||||||
new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1
|
new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1
|
||||||
SOAPACTION: "urn:Belkin:service:basicevent:1#GetBinaryState"
|
SOAPACTION: "urn:Belkin:service:basicevent:1#GetBinaryState"
|
||||||
Content-Length: 277
|
Content-Length: 277
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
command "subscribe"
|
command "subscribe"
|
||||||
command "resubscribe"
|
command "resubscribe"
|
||||||
command "unsubscribe"
|
command "unsubscribe"
|
||||||
|
command "setOffline"
|
||||||
}
|
}
|
||||||
|
|
||||||
// simulator metadata
|
// simulator metadata
|
||||||
@@ -207,7 +208,7 @@ def subscribe(ip, port) {
|
|||||||
def existingIp = getDataValue("ip")
|
def existingIp = getDataValue("ip")
|
||||||
def existingPort = getDataValue("port")
|
def existingPort = getDataValue("port")
|
||||||
if (ip && ip != existingIp) {
|
if (ip && ip != existingIp) {
|
||||||
log.debug "Updating ip from $existingIp to $ip"
|
log.debug "Updating ip from $existingIp to $ip"
|
||||||
updateDataValue("ip", ip)
|
updateDataValue("ip", ip)
|
||||||
def ipvalue = convertHexToIP(getDataValue("ip"))
|
def ipvalue = convertHexToIP(getDataValue("ip"))
|
||||||
sendEvent(name: "currentIP", value: ipvalue, descriptionText: "IP changed to ${ipvalue}")
|
sendEvent(name: "currentIP", value: ipvalue, descriptionText: "IP changed to ${ipvalue}")
|
||||||
@@ -275,7 +276,7 @@ def setOffline() {
|
|||||||
def poll() {
|
def poll() {
|
||||||
log.debug "Executing 'poll'"
|
log.debug "Executing 'poll'"
|
||||||
if (device.currentValue("currentIP") != "Offline")
|
if (device.currentValue("currentIP") != "Offline")
|
||||||
runIn(10, setOffline)
|
runIn(30, setOffline)
|
||||||
new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1
|
new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1
|
||||||
SOAPACTION: "urn:Belkin:service:basicevent:1#GetBinaryState"
|
SOAPACTION: "urn:Belkin:service:basicevent:1#GetBinaryState"
|
||||||
Content-Length: 277
|
Content-Length: 277
|
||||||
@@ -290,4 +291,4 @@ User-Agent: CyberGarage-HTTP/1.0
|
|||||||
</u:GetBinaryState>
|
</u:GetBinaryState>
|
||||||
</s:Body>
|
</s:Body>
|
||||||
</s:Envelope>""", physicalgraph.device.Protocol.LAN)
|
</s:Envelope>""", physicalgraph.device.Protocol.LAN)
|
||||||
}
|
}
|
||||||
@@ -25,8 +25,8 @@ metadata {
|
|||||||
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B04"
|
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B04"
|
||||||
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702"
|
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702"
|
||||||
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702, 0B05", outClusters: "0019", manufacturer: "sengled", model: "Z01-CIA19NAE26", deviceJoinName: "Sengled Element touch"
|
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702, 0B05", outClusters: "0019", manufacturer: "sengled", model: "Z01-CIA19NAE26", deviceJoinName: "Sengled Element touch"
|
||||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0B05,0702", outClusters: "000A,0019", manufacturer: "Jasco Products", model: "45852", deviceJoinName: "GE Zigbee Plug-In Dimmer"
|
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702, 0B05", outClusters: "000A, 0019", manufacturer: "Jasco Products", model: "45852", deviceJoinName: "GE Zigbee Plug-In Dimmer"
|
||||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0B05,0702", outClusters: "000A,0019", manufacturer: "Jasco Products", model: "45857", deviceJoinName: "GE Zigbee In-Wall Dimmer"
|
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702, 0B05", outClusters: "000A, 0019", manufacturer: "Jasco Products", model: "45857", deviceJoinName: "GE Zigbee In-Wall Dimmer"
|
||||||
}
|
}
|
||||||
|
|
||||||
tiles(scale: 2) {
|
tiles(scale: 2) {
|
||||||
|
|||||||
@@ -17,15 +17,14 @@ metadata {
|
|||||||
capability "Actuator"
|
capability "Actuator"
|
||||||
capability "Configuration"
|
capability "Configuration"
|
||||||
capability "Refresh"
|
capability "Refresh"
|
||||||
capability "Sensor"
|
|
||||||
capability "Switch"
|
capability "Switch"
|
||||||
capability "Switch Level"
|
capability "Switch Level"
|
||||||
|
|
||||||
|
|
||||||
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008"
|
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008"
|
||||||
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY A19 ON/OFF/DIM", deviceJoinName: "OSRAM LIGHTIFY LED Smart Connected Light"
|
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY A19 ON/OFF/DIM", deviceJoinName: "OSRAM LIGHTIFY LED Smart Connected Light"
|
||||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,FF00", outClusters: "0019", manufacturer: "MRVL", model: "MZ100", deviceJoinName: "Wemo Bulb"
|
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, FF00", outClusters: "0019", manufacturer: "MRVL", model: "MZ100", deviceJoinName: "Wemo Bulb"
|
||||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0B05", outClusters: "0019", manufacturer: "OSRAM SYLVANIA", model: "iQBR30", deviceJoinName: "Sylvania Ultra iQ"
|
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B05", outClusters: "0019", manufacturer: "OSRAM SYLVANIA", model: "iQBR30", deviceJoinName: "Sylvania Ultra iQ"
|
||||||
}
|
}
|
||||||
|
|
||||||
tiles(scale: 2) {
|
tiles(scale: 2) {
|
||||||
|
|||||||
@@ -136,15 +136,15 @@ def parse(String description) {
|
|||||||
|
|
||||||
// Lock capability commands
|
// Lock capability commands
|
||||||
def lock() {
|
def lock() {
|
||||||
def cmds = zigbee.zigbeeCommand("${CLUSTER_DOORLOCK}", "${DOORLOCK_CMD_LOCK_DOOR}", "{}")
|
//def cmds = zigbee.zigbeeCommand("${CLUSTER_DOORLOCK}", "${DOORLOCK_CMD_LOCK_DOOR}", "{}")
|
||||||
log.info "lock() -- cmds: $cmds"
|
//log.info "lock() -- cmds: $cmds"
|
||||||
//return cmds
|
//return cmds
|
||||||
"st cmd 0x${device.deviceNetworkId} 0x${device.endpointId} ${CLUSTER_DOORLOCK} ${DOORLOCK_CMD_LOCK_DOOR} {}"
|
"st cmd 0x${device.deviceNetworkId} 0x${device.endpointId} ${CLUSTER_DOORLOCK} ${DOORLOCK_CMD_LOCK_DOOR} {}"
|
||||||
}
|
}
|
||||||
|
|
||||||
def unlock() {
|
def unlock() {
|
||||||
def cmds = zigbee.zigbeeCommand("${CLUSTER_DOORLOCK}", "${DOORLOCK_CMD_UNLOCK_DOOR}", "{}")
|
//def cmds = zigbee.zigbeeCommand("${CLUSTER_DOORLOCK}", "${DOORLOCK_CMD_UNLOCK_DOOR}", "{}")
|
||||||
log.info "unlock() -- cmds: $cmds"
|
//log.info "unlock() -- cmds: $cmds"
|
||||||
//return cmds
|
//return cmds
|
||||||
"st cmd 0x${device.deviceNetworkId} 0x${device.endpointId} ${CLUSTER_DOORLOCK} ${DOORLOCK_CMD_UNLOCK_DOOR} {}"
|
"st cmd 0x${device.deviceNetworkId} 0x${device.endpointId} ${CLUSTER_DOORLOCK} ${DOORLOCK_CMD_UNLOCK_DOOR} {}"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* Author: SmartThings
|
||||||
|
* Date: 2016-01-19
|
||||||
|
*
|
||||||
|
* This DTH should serve as the generic DTH to handle RGBW ZigBee HA devices
|
||||||
|
*/
|
||||||
|
|
||||||
|
metadata {
|
||||||
|
definition (name: "ZigBee 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: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY Flex RGBW", deviceJoinName: "OSRAM LIGHTIFY LED FLEXIBLE STRIP RGBW"
|
||||||
|
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Flex RGBW", deviceJoinName: "OSRAM LIGHTIFY LED FLEXIBLE STRIP RGBW"
|
||||||
|
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY A19 RGBW", deviceJoinName: "OSRAM LIGHTIFY LED A19 RGBW"
|
||||||
|
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY BR RGBW", deviceJoinName: "OSRAM LIGHTIFY LED BR30 RGBW"
|
||||||
|
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY RT RGBW", deviceJoinName: "OSRAM LIGHTIFY LED RT 5/6 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.switch", 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
def off() {
|
||||||
|
zigbee.off()
|
||||||
|
}
|
||||||
|
|
||||||
|
def refresh() {
|
||||||
|
zigbee.readAttribute(0x0006, 0x00) + zigbee.readAttribute(0x0008, 0x00) + zigbee.readAttribute(0x0300, 0x00) + zigbee.readAttribute(0x0300, ATTRIBUTE_COLOR_TEMPERATURE) + zigbee.readAttribute(0x0300, ATTRIBUTE_HUE) + zigbee.readAttribute(0x0300, ATTRIBUTE_SATURATION) + 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 configure() {
|
||||||
|
log.debug "Configuring Reporting and Bindings."
|
||||||
|
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) + zigbee.readAttribute(0x0006, 0x00) + zigbee.readAttribute(0x0008, 0x00) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, 0x00) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_COLOR_TEMPERATURE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION)
|
||||||
|
}
|
||||||
|
|
||||||
|
def setColorTemperature(value) {
|
||||||
|
zigbee.setColorTemperature(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
def setLevel(value) {
|
||||||
|
zigbee.setLevel(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
def setColor(value){
|
||||||
|
log.trace "setColor($value)"
|
||||||
|
zigbee.on() + setHue(value.hue) + "delay 300" + setSaturation(value.saturation)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -23,8 +23,8 @@ metadata {
|
|||||||
|
|
||||||
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0B04"
|
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0B04"
|
||||||
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0702"
|
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0702"
|
||||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B05,0702", outClusters: "0003, 000A,0019", manufacturer: "Jasco Products", model: "45853", deviceJoinName: "GE ZigBee Plug-In Switch"
|
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0702, 0B05", outClusters: "0003, 000A, 0019", manufacturer: "Jasco Products", model: "45853", deviceJoinName: "GE ZigBee Plug-In Switch"
|
||||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B05,0702", outClusters: "000A,0019", manufacturer: "Jasco Products", model: "45856", deviceJoinName: "GE ZigBee In-Wall Switch"
|
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0702, 0B05", outClusters: "000A, 0019", manufacturer: "Jasco Products", model: "45856", deviceJoinName: "GE ZigBee In-Wall Switch"
|
||||||
}
|
}
|
||||||
|
|
||||||
tiles(scale: 2) {
|
tiles(scale: 2) {
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ metadata {
|
|||||||
capability "Actuator"
|
capability "Actuator"
|
||||||
capability "Configuration"
|
capability "Configuration"
|
||||||
capability "Refresh"
|
capability "Refresh"
|
||||||
capability "Sensor"
|
|
||||||
capability "Switch"
|
capability "Switch"
|
||||||
|
|
||||||
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006"
|
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006"
|
||||||
|
|||||||
@@ -23,18 +23,18 @@ metadata {
|
|||||||
capability "Color Temperature"
|
capability "Color Temperature"
|
||||||
capability "Configuration"
|
capability "Configuration"
|
||||||
capability "Refresh"
|
capability "Refresh"
|
||||||
capability "Sensor"
|
|
||||||
capability "Switch"
|
capability "Switch"
|
||||||
capability "Switch Level"
|
capability "Switch Level"
|
||||||
|
|
||||||
attribute "colorName", "string"
|
attribute "colorName", "string"
|
||||||
command "setGenericName"
|
command "setGenericName"
|
||||||
|
|
||||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04", outClusters: "0019"
|
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300", outClusters: "0019"
|
||||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY BR Tunable White", deviceJoinName: "OSRAM LIGHTIFY LED Flood BR30 Tunable White"
|
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B04", outClusters: "0019"
|
||||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY RT Tunable White", deviceJoinName: "OSRAM LIGHTIFY LED Recessed Kit RT 5/6 Tunable White"
|
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY BR Tunable White", deviceJoinName: "OSRAM LIGHTIFY LED Flood BR30 Tunable White"
|
||||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Classic A60 TW", deviceJoinName: "OSRAM LIGHTIFY LED Tunable White 60W"
|
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY RT Tunable White", deviceJoinName: "OSRAM LIGHTIFY LED Recessed Kit RT 5/6 Tunable White"
|
||||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY A19 Tunable White", deviceJoinName: "OSRAM LIGHTIFY LED Tunable White 60W"
|
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Classic A60 TW", deviceJoinName: "OSRAM LIGHTIFY LED Tunable White 60W"
|
||||||
|
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY A19 Tunable White", deviceJoinName: "OSRAM LIGHTIFY LED Tunable White 60W"
|
||||||
}
|
}
|
||||||
|
|
||||||
// UI tile definitions
|
// UI tile definitions
|
||||||
|
|||||||
@@ -66,9 +66,20 @@ metadata {
|
|||||||
import physicalgraph.zwave.commands.doorlockv1.*
|
import physicalgraph.zwave.commands.doorlockv1.*
|
||||||
import physicalgraph.zwave.commands.usercodev1.*
|
import physicalgraph.zwave.commands.usercodev1.*
|
||||||
|
|
||||||
|
def updated() {
|
||||||
|
try {
|
||||||
|
if (!state.init) {
|
||||||
|
state.init = true
|
||||||
|
response(secureSequence([zwave.doorLockV1.doorLockOperationGet(), zwave.batteryV1.batteryGet()]))
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log.warn "updated() threw $e"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
def parse(String description) {
|
def parse(String description) {
|
||||||
def result = null
|
def result = null
|
||||||
if (description.startsWith("Err")) {
|
if (description.startsWith("Err 106")) {
|
||||||
if (state.sec) {
|
if (state.sec) {
|
||||||
result = createEvent(descriptionText:description, displayed:false)
|
result = createEvent(descriptionText:description, displayed:false)
|
||||||
} else {
|
} else {
|
||||||
@@ -80,6 +91,8 @@ def parse(String description) {
|
|||||||
displayed: true,
|
displayed: true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
} else if (description == "updated") {
|
||||||
|
return null
|
||||||
} else {
|
} else {
|
||||||
def cmd = zwave.parse(description, [ 0x98: 1, 0x72: 2, 0x85: 2, 0x86: 1 ])
|
def cmd = zwave.parse(description, [ 0x98: 1, 0x72: 2, 0x85: 2, 0x86: 1 ])
|
||||||
if (cmd) {
|
if (cmd) {
|
||||||
@@ -286,7 +299,7 @@ def zwaveEvent(physicalgraph.zwave.commands.alarmv2.AlarmReport cmd) {
|
|||||||
}
|
}
|
||||||
break
|
break
|
||||||
case 167:
|
case 167:
|
||||||
if (!state.lastbatt || (new Date().time) - state.lastbatt > 12*60*60*1000) {
|
if (!state.lastbatt || now() - state.lastbatt > 12*60*60*1000) {
|
||||||
map = [ descriptionText: "$device.displayName: battery low", isStateChange: true ]
|
map = [ descriptionText: "$device.displayName: battery low", isStateChange: true ]
|
||||||
result << response(secure(zwave.batteryV1.batteryGet()))
|
result << response(secure(zwave.batteryV1.batteryGet()))
|
||||||
} else {
|
} else {
|
||||||
@@ -431,7 +444,7 @@ def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) {
|
|||||||
} else {
|
} else {
|
||||||
map.value = cmd.batteryLevel
|
map.value = cmd.batteryLevel
|
||||||
}
|
}
|
||||||
state.lastbatt = new Date().time
|
state.lastbatt = now()
|
||||||
createEvent(map)
|
createEvent(map)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -499,15 +512,14 @@ def refresh() {
|
|||||||
cmds << "delay 4200"
|
cmds << "delay 4200"
|
||||||
cmds << zwave.associationV1.associationGet(groupingIdentifier:2).format() // old Schlage locks use group 2 and don't secure the Association CC
|
cmds << zwave.associationV1.associationGet(groupingIdentifier:2).format() // old Schlage locks use group 2 and don't secure the Association CC
|
||||||
cmds << secure(zwave.associationV1.associationGet(groupingIdentifier:1))
|
cmds << secure(zwave.associationV1.associationGet(groupingIdentifier:1))
|
||||||
state.associationQuery = new Date().time
|
state.associationQuery = now()
|
||||||
} else if (new Date().time - state.associationQuery.toLong() > 9000) {
|
} else if (secondsPast(state.associationQuery, 9)) {
|
||||||
log.debug "setting association"
|
|
||||||
cmds << "delay 6000"
|
cmds << "delay 6000"
|
||||||
cmds << zwave.associationV1.associationSet(groupingIdentifier:2, nodeId:zwaveHubNodeId).format()
|
cmds << zwave.associationV1.associationSet(groupingIdentifier:2, nodeId:zwaveHubNodeId).format()
|
||||||
cmds << secure(zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId))
|
cmds << secure(zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId))
|
||||||
cmds << zwave.associationV1.associationGet(groupingIdentifier:2).format()
|
cmds << zwave.associationV1.associationGet(groupingIdentifier:2).format()
|
||||||
cmds << secure(zwave.associationV1.associationGet(groupingIdentifier:1))
|
cmds << secure(zwave.associationV1.associationGet(groupingIdentifier:1))
|
||||||
state.associationQuery = new Date().time
|
state.associationQuery = now()
|
||||||
}
|
}
|
||||||
log.debug "refresh sending ${cmds.inspect()}"
|
log.debug "refresh sending ${cmds.inspect()}"
|
||||||
cmds
|
cmds
|
||||||
@@ -515,55 +527,22 @@ def refresh() {
|
|||||||
|
|
||||||
def poll() {
|
def poll() {
|
||||||
def cmds = []
|
def cmds = []
|
||||||
if (state.assoc != zwaveHubNodeId && secondsPast(state.associationQuery, 19 * 60)) {
|
// Only check lock state if it changed recently or we haven't had an update in an hour
|
||||||
log.debug "setting association"
|
def latest = device.currentState("lock")?.date?.time
|
||||||
cmds << zwave.associationV1.associationSet(groupingIdentifier:2, nodeId:zwaveHubNodeId).format()
|
if (!latest || !secondsPast(latest, 6 * 60) || secondsPast(state.lastPoll, 55 * 60)) {
|
||||||
cmds << secure(zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId))
|
cmds << secure(zwave.doorLockV1.doorLockOperationGet())
|
||||||
cmds << zwave.associationV1.associationGet(groupingIdentifier:2).format()
|
state.lastPoll = now()
|
||||||
cmds << "delay 6000"
|
} else if (!state.lastbatt || now() - state.lastbatt > 53*60*60*1000) {
|
||||||
cmds << secure(zwave.associationV1.associationGet(groupingIdentifier:1))
|
cmds << secure(zwave.batteryV1.batteryGet())
|
||||||
cmds << "delay 6000"
|
state.lastbatt = now() //inside-214
|
||||||
state.associationQuery = new Date().time
|
|
||||||
} else {
|
|
||||||
// Only check lock state if it changed recently or we haven't had an update in an hour
|
|
||||||
def latest = device.currentState("lock")?.date?.time
|
|
||||||
if (!latest || !secondsPast(latest, 6 * 60) || secondsPast(state.lastPoll, 55 * 60)) {
|
|
||||||
cmds << secure(zwave.doorLockV1.doorLockOperationGet())
|
|
||||||
state.lastPoll = (new Date()).time
|
|
||||||
} else if (!state.MSR) {
|
|
||||||
cmds << zwave.manufacturerSpecificV1.manufacturerSpecificGet().format()
|
|
||||||
} else if (!state.fw) {
|
|
||||||
cmds << zwave.versionV1.versionGet().format()
|
|
||||||
} else if (!state.codes) {
|
|
||||||
state.pollCode = 1
|
|
||||||
cmds << secure(zwave.userCodeV1.usersNumberGet())
|
|
||||||
} else if (state.pollCode && state.pollCode <= state.codes) {
|
|
||||||
cmds << requestCode(state.pollCode)
|
|
||||||
} else if (!state.lastbatt || (new Date().time) - state.lastbatt > 53*60*60*1000) {
|
|
||||||
cmds << secure(zwave.batteryV1.batteryGet())
|
|
||||||
} else if (!state.enc) {
|
|
||||||
encryptCodes()
|
|
||||||
state.enc = 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
log.debug "poll is sending ${cmds.inspect()}"
|
if (cmds) {
|
||||||
device.activity()
|
log.debug "poll is sending ${cmds.inspect()}"
|
||||||
cmds ?: null
|
cmds
|
||||||
}
|
} else {
|
||||||
|
// workaround to keep polling from stopping due to lack of activity
|
||||||
private def encryptCodes() {
|
sendEvent(descriptionText: "skipping poll", isStateChange: true, displayed: false)
|
||||||
def keys = new ArrayList(state.keySet().findAll { it.startsWith("code") })
|
null
|
||||||
keys.each { key ->
|
|
||||||
def match = (key =~ /^code(\d+)$/)
|
|
||||||
if (match) try {
|
|
||||||
def keynum = match[0][1].toInteger()
|
|
||||||
if (keynum > 30 && !state[key]) {
|
|
||||||
state.remove(key)
|
|
||||||
} else if (state[key] && !state[key].startsWith("~")) {
|
|
||||||
log.debug "encrypting $key: ${state[key].inspect()}"
|
|
||||||
state[key] = encrypt(state[key])
|
|
||||||
}
|
|
||||||
} catch (java.lang.NumberFormatException e) { }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -672,7 +651,7 @@ private Boolean secondsPast(timestamp, seconds) {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (new Date().time - timestamp) > (seconds * 1000)
|
return (now() - timestamp) > (seconds * 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
private allCodesDeleted() {
|
private allCodesDeleted() {
|
||||||
|
|||||||
@@ -115,6 +115,10 @@ def strobe() {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def both() {
|
||||||
|
on()
|
||||||
|
}
|
||||||
|
|
||||||
def refresh() {
|
def refresh() {
|
||||||
log.debug "sending battery refresh command"
|
log.debug "sending battery refresh command"
|
||||||
zwave.batteryV1.batteryGet().format()
|
zwave.batteryV1.batteryGet().format()
|
||||||
|
|||||||
@@ -0,0 +1,243 @@
|
|||||||
|
metadata {
|
||||||
|
definition (name: "Timevalve Smart", namespace: "timevalve.gaslock.t-08", author: "ruinnel") {
|
||||||
|
capability "Valve"
|
||||||
|
capability "Refresh"
|
||||||
|
capability "Battery"
|
||||||
|
capability "Temperature Measurement"
|
||||||
|
|
||||||
|
command "setRemaining"
|
||||||
|
command "setTimeout"
|
||||||
|
command "setTimeout10"
|
||||||
|
command "setTimeout20"
|
||||||
|
command "setTimeout30"
|
||||||
|
command "setTimeout40"
|
||||||
|
|
||||||
|
command "remainingLevel"
|
||||||
|
|
||||||
|
attribute "remaining", "number"
|
||||||
|
attribute "remainingText", "String"
|
||||||
|
attribute "timeout", "number"
|
||||||
|
|
||||||
|
//raw desc : 0 0 0x1006 0 0 0 7 0x5E 0x86 0x72 0x5A 0x73 0x98 0x80
|
||||||
|
//fingerprint deviceId:"0x1006", inClusters:"0x5E, 0x86, 0x72, 0x5A, 0x73, 0x98, 0x80"
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles (scale: 2) {
|
||||||
|
multiAttributeTile(name:"statusTile", type:"generic", width:6, height:4) {
|
||||||
|
tileAttribute("device.contact", key: "PRIMARY_CONTROL") {
|
||||||
|
attributeState "open", label: '${name}', action: "close", icon:"st.contact.contact.open", backgroundColor:"#ffa81e"
|
||||||
|
attributeState "closed", label:'${name}', action: "", icon:"st.contact.contact.closed", backgroundColor:"#79b821"
|
||||||
|
}
|
||||||
|
tileAttribute("device.remainingText", key: "SECONDARY_CONTROL") {
|
||||||
|
attributeState "open", label: '${currentValue}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e"
|
||||||
|
attributeState "closed", label:'', icon:"st.contact.contact.closed", backgroundColor:"#79b821"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
standardTile("refreshTile", "command.refresh", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
|
||||||
|
controlTile("remainingSliderTile", "device.remaining", "slider", inactiveLabel: false, range:"(0..590)", height: 2, width: 4) {
|
||||||
|
state "level", action:"remainingLevel"
|
||||||
|
}
|
||||||
|
valueTile("setRemaining", "device.remainingText", inactiveLabel: false, decoration: "flat", height: 2, width: 2){
|
||||||
|
state "remainingText", label:'${currentValue}\nRemaining'//, action: "setRemaining"//, icon: "st.Office.office6"
|
||||||
|
}
|
||||||
|
|
||||||
|
standardTile("setTimeout10", "device.remaining", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:'10Min', action: "setTimeout10", icon:"st.Health & Wellness.health7", defaultState: true
|
||||||
|
state "10", label:'10Min', action: "setTimeout10", icon:"st.Office.office13"
|
||||||
|
}
|
||||||
|
standardTile("setTimeout20", "device.remaining", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:'20Min', action: "setTimeout20", icon:"st.Health & Wellness.health7", defaultState: true
|
||||||
|
state "20", label:'20Min', action: "setTimeout20", icon:"st.Office.office13"
|
||||||
|
}
|
||||||
|
standardTile("setTimeout30", "device.remaining", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:'30Min', action: "setTimeout30", icon:"st.Health & Wellness.health7", defaultState: true
|
||||||
|
state "30", label:'30Min', action: "setTimeout30", icon:"st.Office.office13"
|
||||||
|
}
|
||||||
|
standardTile("setTimeout40", "device.remaining", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:'40Min', action: "setTimeout40", icon:"st.Health & Wellness.health7", defaultState: true
|
||||||
|
state "40", label:'40Min', action: "setTimeout40", icon:"st.Office.office13"
|
||||||
|
}
|
||||||
|
|
||||||
|
valueTile("batteryTile", "device.battery", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "battery", label:'${currentValue}% battery', unit:""
|
||||||
|
}
|
||||||
|
|
||||||
|
main (["statusTile"])
|
||||||
|
// details (["statusTile", "remainingSliderTile", "setRemaining", "setTimeout10", "setTimeout20", "batteryTile", "refreshTile", "setTimeout30", "setTimeout40"])
|
||||||
|
// details (["statusTile", "batteryTile", "setRemaining", "refreshTile"])
|
||||||
|
details (["statusTile", "batteryTile", "refreshTile"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(description) {
|
||||||
|
// log.debug "parse - " + description
|
||||||
|
def result = null
|
||||||
|
if (description.startsWith("Err 106")) {
|
||||||
|
state.sec = 0
|
||||||
|
result = createEvent(descriptionText: description, isStateChange: true)
|
||||||
|
} else if (description != "updated") {
|
||||||
|
def cmd = zwave.parse(description, [0x20: 1, 0x25: 1, 0x70: 1, 0x71: 1, 0x98: 1])
|
||||||
|
if (cmd) {
|
||||||
|
log.debug "parsed cmd = " + cmd
|
||||||
|
result = zwaveEvent(cmd)
|
||||||
|
//log.debug("'$description' parsed to $result")
|
||||||
|
} else {
|
||||||
|
log.debug("Couldn't zwave.parse '$description'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// 복호화 후 zwaveEvent() 호출
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) {
|
||||||
|
//log.debug "SecurityMessageEncapsulation - " + cmd
|
||||||
|
def encapsulatedCommand = cmd.encapsulatedCommand([0x20: 1, 0x25: 1, 0x70: 1, 0x71: 1, 0x98: 1])
|
||||||
|
if (encapsulatedCommand) {
|
||||||
|
state.sec = 1
|
||||||
|
log.debug "encapsulatedCommand = " + encapsulatedCommand
|
||||||
|
zwaveEvent(encapsulatedCommand)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) {
|
||||||
|
//log.debug "switch status - " + cmd.value
|
||||||
|
createEvent(name:"contact", value: cmd.value ? "open" : "closed")
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) {
|
||||||
|
def map = [ name: "battery", unit: "%" ]
|
||||||
|
if (cmd.batteryLevel == 0xFF) { // Special value for low battery alert
|
||||||
|
map.value = 1
|
||||||
|
map.descriptionText = "${device.displayName} has a low battery"
|
||||||
|
map.isStateChange = true
|
||||||
|
} else {
|
||||||
|
map.value = cmd.batteryLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug "battery - ${map.value}${map.unit}"
|
||||||
|
// Store time of last battery update so we don't ask every wakeup, see WakeUpNotification handler
|
||||||
|
state.lastbatt = new Date().time
|
||||||
|
createEvent(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.Command cmd) {
|
||||||
|
//log.debug "zwaveEvent - ${device.displayName}: ${cmd}"
|
||||||
|
createEvent(descriptionText: "${device.displayName}: ${cmd}")
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.configurationv1.ConfigurationReport cmd) {
|
||||||
|
def result = []
|
||||||
|
log.info "zwave.configurationV1.configurationGet - " + cmd
|
||||||
|
def array = cmd.configurationValue
|
||||||
|
def value = ( (array[0] * 0x1000000) + (array[1] * 0x10000) + (array[2] * 0x100) + array[3] ).intdiv(60)
|
||||||
|
if (device.currentValue("contact") == "open") {
|
||||||
|
value = ( (array[0] * 0x1000000) + (array[1] * 0x10000) + (array[2] * 0x100) + array[3] ).intdiv(60)
|
||||||
|
} else {
|
||||||
|
value = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (device.currentValue('contact') == 'open') {
|
||||||
|
def hour = value.intdiv(60);
|
||||||
|
def min = (value % 60).toString().padLeft(2, '0');
|
||||||
|
def text = "${hour}:${min}M"
|
||||||
|
|
||||||
|
log.info "remain - " + text
|
||||||
|
result.add( createEvent(name: "remaining", value: value, displayed: false, isStateChange: true) )
|
||||||
|
result.add( createEvent(name: "remainingText", value: text, displayed: false, isStateChange: true) )
|
||||||
|
} else {
|
||||||
|
result.add( createEvent(name: "timeout", value: value, displayed: false, isStateChange: true) )
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) {
|
||||||
|
def type = cmd.notificationType
|
||||||
|
if (type == cmd.NOTIFICATION_TYPE_HEAT) {
|
||||||
|
log.info "NotificationReport - ${type}"
|
||||||
|
createEvent(name: "temperature", value: 999, unit: "C", descriptionText: "${device.displayName} is over heat!", displayed: true, isStateChange: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.alarmv1.AlarmReport cmd) {
|
||||||
|
def type = cmd.alarmType
|
||||||
|
def level = cmd.alarmLevel
|
||||||
|
|
||||||
|
log.info "AlarmReport - type : ${type}, level : ${level}"
|
||||||
|
def msg = "${device.displayName} is over heat!"
|
||||||
|
def result = createEvent(name: "temperature", value: 999, unit: "C", descriptionText: msg, displayed: true, isStateChange: true)
|
||||||
|
if (sendPushMessage) {
|
||||||
|
sendPushMessage(msg)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// remote open not allow
|
||||||
|
def open() {}
|
||||||
|
|
||||||
|
def close() {
|
||||||
|
// log.debug 'cmd - close()'
|
||||||
|
commands([
|
||||||
|
zwave.switchBinaryV1.switchBinarySet(switchValue: 0x00),
|
||||||
|
zwave.switchBinaryV1.switchBinaryGet()
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
def setTimeout10() { setTimeout(10) }
|
||||||
|
def setTimeout20() { setTimeout(20) }
|
||||||
|
def setTimeout30() { setTimeout(30) }
|
||||||
|
def setTimeout40() { setTimeout(40) }
|
||||||
|
|
||||||
|
|
||||||
|
def setTimeout(value) {
|
||||||
|
// log.debug "setDefaultTime($value)"
|
||||||
|
commands([
|
||||||
|
zwave.configurationV1.configurationSet(parameterNumber: 0x01, size: 4, scaledConfigurationValue: value * 60),
|
||||||
|
zwave.configurationV1.configurationGet(parameterNumber: 0x01)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
def remainingLevel(value) {
|
||||||
|
// log.debug "remainingLevel($value)"
|
||||||
|
def hour = value.intdiv(60);
|
||||||
|
def min = (value % 60).toString().padLeft(2, '0');
|
||||||
|
def text = "${hour}:${min}M"
|
||||||
|
sendEvent(name: "remaining", value: value, displayed: false, isStateChange: true)
|
||||||
|
sendEvent(name: "remainingText", value: text, displayed: false, isStateChange: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
def setRemaining() {
|
||||||
|
def remaining = device.currentValue("remaining")
|
||||||
|
// log.debug "setConfiguration() - remaining : $remaining"
|
||||||
|
commands([
|
||||||
|
zwave.configurationV1.configurationSet(parameterNumber: 0x03, size: 4, scaledConfigurationValue: remaining * 60),
|
||||||
|
zwave.configurationV1.configurationGet(parameterNumber: 0x03)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private command(physicalgraph.zwave.Command cmd) {
|
||||||
|
if (state.sec != 0 && !(cmd instanceof physicalgraph.zwave.commands.batteryv1.BatteryGet)) {
|
||||||
|
log.debug "cmd = " + cmd + ", encapsulation"
|
||||||
|
zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format()
|
||||||
|
} else {
|
||||||
|
log.debug "cmd = " + cmd + ", plain"
|
||||||
|
cmd.format()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private commands(commands, delay=200) {
|
||||||
|
delayBetween(commands.collect{ command(it) }, delay)
|
||||||
|
}
|
||||||
|
|
||||||
|
def refresh() {
|
||||||
|
// log.debug 'cmd - refresh()'
|
||||||
|
commands([
|
||||||
|
zwave.batteryV1.batteryGet(),
|
||||||
|
zwave.switchBinaryV1.switchBinaryGet(),
|
||||||
|
zwave.configurationV1.configurationGet(parameterNumber: 0x01),
|
||||||
|
zwave.configurationV1.configurationGet(parameterNumber: 0x03)
|
||||||
|
], 400)
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 7.3 KiB |
@@ -0,0 +1,189 @@
|
|||||||
|
/**
|
||||||
|
* Vinli Home Beta
|
||||||
|
*
|
||||||
|
* Copyright 2015 Daniel
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
definition(
|
||||||
|
name: "Vinli Home Connect",
|
||||||
|
namespace: "com.vinli.smartthings",
|
||||||
|
author: "Daniel",
|
||||||
|
description: "Allows Vinli users to connect their car to SmartThings",
|
||||||
|
category: "SmartThings Labs",
|
||||||
|
iconUrl: "https://d3azp77rte0gip.cloudfront.net/smartapps/baeb2e5d-ebd0-49fe-a4ec-e92417ae20bb/images/vinli_oauth_60.png",
|
||||||
|
iconX2Url: "https://d3azp77rte0gip.cloudfront.net/smartapps/baeb2e5d-ebd0-49fe-a4ec-e92417ae20bb/images/vinli_oauth_120.png",
|
||||||
|
iconX3Url: "https://d3azp77rte0gip.cloudfront.net/smartapps/baeb2e5d-ebd0-49fe-a4ec-e92417ae20bb/images/vinli_oauth_120.png",
|
||||||
|
oauth: true)
|
||||||
|
|
||||||
|
preferences {
|
||||||
|
section ("Allow external service to control these things...") {
|
||||||
|
input "switches", "capability.switch", multiple: true, required: true
|
||||||
|
input "locks", "capability.lock", multiple: true, required: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mappings {
|
||||||
|
|
||||||
|
path("/devices") {
|
||||||
|
action: [
|
||||||
|
GET: "listAllDevices"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
path("/switches") {
|
||||||
|
action: [
|
||||||
|
GET: "listSwitches"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
path("/switches/:command") {
|
||||||
|
action: [
|
||||||
|
PUT: "updateSwitches"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
path("/switches/:id/:command") {
|
||||||
|
action: [
|
||||||
|
PUT: "updateSwitch"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
path("/locks/:command") {
|
||||||
|
action: [
|
||||||
|
PUT: "updateLocks"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
path("/locks/:id/:command") {
|
||||||
|
action: [
|
||||||
|
PUT: "updateLock"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
path("/devices/:id/:command") {
|
||||||
|
action: [
|
||||||
|
PUT: "commandDevice"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns a list of all devices
|
||||||
|
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]
|
||||||
|
}
|
||||||
|
|
||||||
|
locks.each {
|
||||||
|
resp << [name: it.name, label: it.label, value: it.currentValue("lock"), type: "lock", id: it.id, hub: it.hub.name]
|
||||||
|
}
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns a list like
|
||||||
|
// [[name: "kitchen lamp", value: "off"], [name: "bathroom", value: "on"]]
|
||||||
|
def listSwitches() {
|
||||||
|
def resp = []
|
||||||
|
switches.each {
|
||||||
|
resp << [name: it.displayName, value: it.currentValue("switch"), type: "switch", id: it.id]
|
||||||
|
}
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateLocks() {
|
||||||
|
// use the built-in request object to get the command parameter
|
||||||
|
def command = params.command
|
||||||
|
|
||||||
|
if (command) {
|
||||||
|
|
||||||
|
// check that the switch supports the specified command
|
||||||
|
// If not, return an error using httpError, providing a HTTP status code.
|
||||||
|
locks.each {
|
||||||
|
if (!it.hasCommand(command)) {
|
||||||
|
httpError(501, "$command is not a valid command for all switches specified")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// all switches have the comand
|
||||||
|
// execute the command on all switches
|
||||||
|
// (note we can do this on the array - the command will be invoked on every element
|
||||||
|
locks."$command"()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateLock() {
|
||||||
|
def command = params.command
|
||||||
|
|
||||||
|
locks.each {
|
||||||
|
if (!it.hasCommand(command)) {
|
||||||
|
httpError(400, "$command is not a valid command for all lock specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (it.id == params.id) {
|
||||||
|
it."$command"()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateSwitch() {
|
||||||
|
def command = params.command
|
||||||
|
|
||||||
|
switches.each {
|
||||||
|
if (!it.hasCommand(command)) {
|
||||||
|
httpError(400, "$command is not a valid command for all switches specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (it.id == params.id) {
|
||||||
|
it."$command"()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void commandDevice() {
|
||||||
|
def command = params.command
|
||||||
|
def devices = []
|
||||||
|
|
||||||
|
switches.each {
|
||||||
|
devices << it
|
||||||
|
}
|
||||||
|
|
||||||
|
locks.each {
|
||||||
|
devices << it
|
||||||
|
}
|
||||||
|
|
||||||
|
devices.each {
|
||||||
|
if (it.id == params.id) {
|
||||||
|
if (!it.hasCommand(command)) {
|
||||||
|
httpError(400, "$command is not a valid command for specified device")
|
||||||
|
}
|
||||||
|
it."$command"()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateSwitches() {
|
||||||
|
// use the built-in request object to get the command parameter
|
||||||
|
def command = params.command
|
||||||
|
|
||||||
|
if (command) {
|
||||||
|
|
||||||
|
// check that the switch supports the specified command
|
||||||
|
// If not, return an error using httpError, providing a HTTP status code.
|
||||||
|
switches.each {
|
||||||
|
if (!it.hasCommand(command)) {
|
||||||
|
httpError(400, "$command is not a valid command for all switches specified")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// all switches have the comand
|
||||||
|
// execute the command on all switches
|
||||||
|
// (note we can do this on the array - the command will be invoked on every element
|
||||||
|
switches."$command"()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def installed() {
|
||||||
|
log.debug "Installed with settings: ${settings}"
|
||||||
|
}
|
||||||
|
|
||||||
|
def updated() {
|
||||||
|
log.debug "Updated with settings: ${settings}"
|
||||||
|
|
||||||
|
unsubscribe()
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Initial State Event Streamer
|
* Initial State Event Streamer
|
||||||
*
|
*
|
||||||
* Copyright 2015 David Sulpy
|
* Copyright 2016 David Sulpy
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
* 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:
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
@@ -11,9 +11,9 @@
|
|||||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
* 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
|
* 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.
|
* for the specific language governing permissions and limitations under the License.
|
||||||
*
|
*
|
||||||
* SmartThings data is sent from this SmartApp to Initial State. This is event data only for
|
* SmartThings data is sent from this SmartApp to Initial State. This is event data only for
|
||||||
* devices for which the user has authorized. Likewise, Initial State's services call this
|
* devices for which the user has authorized. Likewise, Initial State's services call this
|
||||||
* SmartApp on the user's behalf to configure Initial State specific parameters. The ToS and
|
* SmartApp on the user's behalf to configure Initial State specific parameters. The ToS and
|
||||||
* Privacy Policy for Initial State can be found here: https://www.initialstate.com/terms
|
* Privacy Policy for Initial State can be found here: https://www.initialstate.com/terms
|
||||||
*/
|
*/
|
||||||
@@ -77,6 +77,62 @@ mappings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def getAccessKey() {
|
||||||
|
log.trace "get access key"
|
||||||
|
if (atomicState.accessKey == null) {
|
||||||
|
httpError(404, "Access Key Not Found")
|
||||||
|
} else {
|
||||||
|
[
|
||||||
|
accessKey: atomicState.accessKey
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def getBucketKey() {
|
||||||
|
log.trace "get bucket key"
|
||||||
|
if (atomicState.bucketKey == null) {
|
||||||
|
httpError(404, "Bucket key Not Found")
|
||||||
|
} else {
|
||||||
|
[
|
||||||
|
bucketKey: atomicState.bucketKey,
|
||||||
|
bucketName: atomicState.bucketName
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def setBucketKey() {
|
||||||
|
log.trace "set bucket key"
|
||||||
|
def newBucketKey = request.JSON?.bucketKey
|
||||||
|
def newBucketName = request.JSON?.bucketName
|
||||||
|
|
||||||
|
log.debug "bucket name: $newBucketName"
|
||||||
|
log.debug "bucket key: $newBucketKey"
|
||||||
|
|
||||||
|
if (newBucketKey && (newBucketKey != atomicState.bucketKey || newBucketName != atomicState.bucketName)) {
|
||||||
|
atomicState.bucketKey = "$newBucketKey"
|
||||||
|
atomicState.bucketName = "$newBucketName"
|
||||||
|
atomicState.isBucketCreated = false
|
||||||
|
}
|
||||||
|
|
||||||
|
tryCreateBucket()
|
||||||
|
}
|
||||||
|
|
||||||
|
def setAccessKey() {
|
||||||
|
log.trace "set access key"
|
||||||
|
def newAccessKey = request.JSON?.accessKey
|
||||||
|
def newGrokerSubdomain = request.JSON?.grokerSubdomain
|
||||||
|
|
||||||
|
if (newGrokerSubdomain && newGrokerSubdomain != "" && newGrokerSubdomain != atomicState.grokerSubdomain) {
|
||||||
|
atomicState.grokerSubdomain = "$newGrokerSubdomain"
|
||||||
|
atomicState.isBucketCreated = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newAccessKey && newAccessKey != atomicState.accessKey) {
|
||||||
|
atomicState.accessKey = "$newAccessKey"
|
||||||
|
atomicState.isBucketCreated = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
def subscribeToEvents() {
|
def subscribeToEvents() {
|
||||||
if (accelerometers != null) {
|
if (accelerometers != null) {
|
||||||
subscribe(accelerometers, "acceleration", genericHandler)
|
subscribe(accelerometers, "acceleration", genericHandler)
|
||||||
@@ -90,7 +146,7 @@ def subscribeToEvents() {
|
|||||||
if (beacons != null) {
|
if (beacons != null) {
|
||||||
subscribe(beacons, "presence", genericHandler)
|
subscribe(beacons, "presence", genericHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cos != null) {
|
if (cos != null) {
|
||||||
subscribe(cos, "carbonMonoxide", genericHandler)
|
subscribe(cos, "carbonMonoxide", genericHandler)
|
||||||
}
|
}
|
||||||
@@ -169,85 +225,27 @@ def subscribeToEvents() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def getAccessKey() {
|
|
||||||
log.trace "get access key"
|
|
||||||
if (atomicState.accessKey == null) {
|
|
||||||
httpError(404, "Access Key Not Found")
|
|
||||||
} else {
|
|
||||||
[
|
|
||||||
accessKey: atomicState.accessKey
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def getBucketKey() {
|
|
||||||
log.trace "get bucket key"
|
|
||||||
if (atomicState.bucketKey == null) {
|
|
||||||
httpError(404, "Bucket key Not Found")
|
|
||||||
} else {
|
|
||||||
[
|
|
||||||
bucketKey: atomicState.bucketKey,
|
|
||||||
bucketName: atomicState.bucketName
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def setBucketKey() {
|
|
||||||
log.trace "set bucket key"
|
|
||||||
def newBucketKey = request.JSON?.bucketKey
|
|
||||||
def newBucketName = request.JSON?.bucketName
|
|
||||||
|
|
||||||
log.debug "bucket name: $newBucketName"
|
|
||||||
log.debug "bucket key: $newBucketKey"
|
|
||||||
|
|
||||||
if (newBucketKey && (newBucketKey != atomicState.bucketKey || newBucketName != atomicState.bucketName)) {
|
|
||||||
atomicState.bucketKey = "$newBucketKey"
|
|
||||||
atomicState.bucketName = "$newBucketName"
|
|
||||||
atomicState.isBucketCreated = false
|
|
||||||
}
|
|
||||||
|
|
||||||
tryCreateBucket()
|
|
||||||
}
|
|
||||||
|
|
||||||
def setAccessKey() {
|
|
||||||
log.trace "set access key"
|
|
||||||
def newAccessKey = request.JSON?.accessKey
|
|
||||||
def newGrokerSubdomain = request.JSON?.grokerSubdomain
|
|
||||||
|
|
||||||
if (newGrokerSubdomain && newGrokerSubdomain != "" && newGrokerSubdomain != atomicState.grokerSubdomain) {
|
|
||||||
atomicState.grokerSubdomain = "$newGrokerSubdomain"
|
|
||||||
atomicState.isBucketCreated = false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newAccessKey && newAccessKey != atomicState.accessKey) {
|
|
||||||
atomicState.accessKey = "$newAccessKey"
|
|
||||||
atomicState.isBucketCreated = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def installed() {
|
def installed() {
|
||||||
atomicState.version = "1.0.18"
|
atomicState.version = "1.1.0"
|
||||||
|
|
||||||
|
atomicState.isBucketCreated = false
|
||||||
|
atomicState.grokerSubdomain = "groker"
|
||||||
|
|
||||||
subscribeToEvents()
|
subscribeToEvents()
|
||||||
|
|
||||||
atomicState.isBucketCreated = false
|
atomicState.isBucketCreated = false
|
||||||
atomicState.grokerSubdomain = "groker"
|
atomicState.grokerSubdomain = "groker"
|
||||||
atomicState.eventBuffer = []
|
|
||||||
|
|
||||||
runEvery15Minutes(flushBuffer)
|
|
||||||
|
|
||||||
log.debug "installed (version $atomicState.version)"
|
log.debug "installed (version $atomicState.version)"
|
||||||
}
|
}
|
||||||
|
|
||||||
def updated() {
|
def updated() {
|
||||||
atomicState.version = "1.0.18"
|
atomicState.version = "1.1.0"
|
||||||
unsubscribe()
|
unsubscribe()
|
||||||
|
|
||||||
if (atomicState.bucketKey != null && atomicState.accessKey != null) {
|
if (atomicState.bucketKey != null && atomicState.accessKey != null) {
|
||||||
atomicState.isBucketCreated = false
|
atomicState.isBucketCreated = false
|
||||||
}
|
}
|
||||||
if (atomicState.eventBuffer == null) {
|
|
||||||
atomicState.eventBuffer = []
|
|
||||||
}
|
|
||||||
if (atomicState.grokerSubdomain == null || atomicState.grokerSubdomain == "") {
|
if (atomicState.grokerSubdomain == null || atomicState.grokerSubdomain == "") {
|
||||||
atomicState.grokerSubdomain = "groker"
|
atomicState.grokerSubdomain = "groker"
|
||||||
}
|
}
|
||||||
@@ -262,7 +260,7 @@ def uninstalled() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def tryCreateBucket() {
|
def tryCreateBucket() {
|
||||||
|
|
||||||
// can't ship events if there is no grokerSubdomain
|
// can't ship events if there is no grokerSubdomain
|
||||||
if (atomicState.grokerSubdomain == null || atomicState.grokerSubdomain == "") {
|
if (atomicState.grokerSubdomain == null || atomicState.grokerSubdomain == "") {
|
||||||
log.error "streaming url is currently null"
|
log.error "streaming url is currently null"
|
||||||
@@ -327,60 +325,40 @@ def genericHandler(evt) {
|
|||||||
eventHandler(key, value)
|
eventHandler(key, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is a handler function for flushing the event buffer
|
|
||||||
// after a specified amount of time to reduce the load on ST servers
|
|
||||||
def flushBuffer() {
|
|
||||||
log.trace "About to flush the buffer on schedule"
|
|
||||||
if (atomicState.eventBuffer != null && atomicState.eventBuffer.size() > 0) {
|
|
||||||
tryShipEvents()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def eventHandler(name, value) {
|
def eventHandler(name, value) {
|
||||||
log.debug atomicState.eventBuffer
|
|
||||||
|
|
||||||
def eventBuffer = atomicState.eventBuffer
|
|
||||||
def epoch = now() / 1000
|
def epoch = now() / 1000
|
||||||
|
|
||||||
// if for some reason this code block is being run
|
|
||||||
// but the SmartApp wasn't propery setup during install
|
|
||||||
// we need to set initialize the eventBuffer.
|
|
||||||
if (!atomicState.eventBuffer) {
|
|
||||||
atomicState.eventBuffer = []
|
|
||||||
}
|
|
||||||
eventBuffer << [key: "$name", value: "$value", epoch: "$epoch"]
|
|
||||||
|
|
||||||
log.debug eventBuffer
|
|
||||||
|
|
||||||
atomicState.eventBuffer = eventBuffer
|
def event = new JsonSlurper().parseText("{\"key\": \"$name\", \"value\": \"$value\", \"epoch\": \"$epoch\"}")
|
||||||
|
|
||||||
if (eventBuffer.size() >= 10) {
|
tryShipEvents(event)
|
||||||
tryShipEvents()
|
|
||||||
}
|
log.debug "Shipped Event: " + event
|
||||||
}
|
}
|
||||||
|
|
||||||
// a helper function for shipping the atomicState.eventBuffer to Initial State
|
def tryShipEvents(event) {
|
||||||
def tryShipEvents() {
|
|
||||||
|
|
||||||
|
def grokerSubdomain = atomicState.grokerSubdomain
|
||||||
// can't ship events if there is no grokerSubdomain
|
// can't ship events if there is no grokerSubdomain
|
||||||
if (atomicState.grokerSubdomain == null || atomicState.grokerSubdomain == "") {
|
if (grokerSubdomain == null || grokerSubdomain == "") {
|
||||||
log.error "streaming url is currently null"
|
log.error "streaming url is currently null"
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
def accessKey = atomicState.accessKey
|
||||||
|
def bucketKey = atomicState.bucketKey
|
||||||
// can't ship if access key and bucket key are null, so finish trying
|
// can't ship if access key and bucket key are null, so finish trying
|
||||||
if (atomicState.accessKey == null || atomicState.bucketKey == null) {
|
if (accessKey == null || bucketKey == null) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
def eventPost = [
|
def eventPost = [
|
||||||
uri: "https://${atomicState.grokerSubdomain}.initialstate.com/api/events",
|
uri: "https://${grokerSubdomain}.initialstate.com/api/events",
|
||||||
headers: [
|
headers: [
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"X-IS-BucketKey": "${atomicState.bucketKey}",
|
"X-IS-BucketKey": "${bucketKey}",
|
||||||
"X-IS-AccessKey": "${atomicState.accessKey}",
|
"X-IS-AccessKey": "${accessKey}",
|
||||||
"Accept-Version": "0.0.2"
|
"Accept-Version": "0.0.2"
|
||||||
],
|
],
|
||||||
body: atomicState.eventBuffer
|
body: event
|
||||||
]
|
]
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -389,13 +367,10 @@ def tryShipEvents() {
|
|||||||
log.debug "shipped events and got ${resp.status}"
|
log.debug "shipped events and got ${resp.status}"
|
||||||
if (resp.status >= 400) {
|
if (resp.status >= 400) {
|
||||||
log.error "shipping failed... ${resp.data}"
|
log.error "shipping failed... ${resp.data}"
|
||||||
} else {
|
|
||||||
// clear the buffer
|
|
||||||
atomicState.eventBuffer = []
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.error "shipping events failed: $e"
|
log.error "shipping events failed: $e"
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -13,7 +13,7 @@ preferences{
|
|||||||
input "lock1", "capability.lock", required: true
|
input "lock1", "capability.lock", required: true
|
||||||
}
|
}
|
||||||
section("Select the door contact sensor:") {
|
section("Select the door contact sensor:") {
|
||||||
input "contact", "capability.contactSensor", required: true
|
input "contact", "capability.contactSensor", required: true
|
||||||
}
|
}
|
||||||
section("Automatically lock the door when closed...") {
|
section("Automatically lock the door when closed...") {
|
||||||
input "minutesLater", "number", title: "Delay (in minutes):", required: true
|
input "minutesLater", "number", title: "Delay (in minutes):", required: true
|
||||||
@@ -22,9 +22,10 @@ preferences{
|
|||||||
input "secondsLater", "number", title: "Delay (in seconds):", required: true
|
input "secondsLater", "number", title: "Delay (in seconds):", required: true
|
||||||
}
|
}
|
||||||
section( "Notifications" ) {
|
section( "Notifications" ) {
|
||||||
input "sendPushMessage", "enum", title: "Send a push notification?", metadata:[values:["Yes", "No"]], required: false
|
input("recipients", "contact", title: "Send notifications to", required: false) {
|
||||||
input "phoneNumber", "phone", title: "Enter phone number to send text notification.", required: false
|
input "phoneNumber", "phone", title: "Warn with text message (optional)", description: "Phone Number", required: false
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def installed(){
|
def installed(){
|
||||||
@@ -42,55 +43,73 @@ def initialize(){
|
|||||||
subscribe(lock1, "lock", doorHandler, [filterEvents: false])
|
subscribe(lock1, "lock", doorHandler, [filterEvents: false])
|
||||||
subscribe(lock1, "unlock", doorHandler, [filterEvents: false])
|
subscribe(lock1, "unlock", doorHandler, [filterEvents: false])
|
||||||
subscribe(contact, "contact.open", doorHandler)
|
subscribe(contact, "contact.open", doorHandler)
|
||||||
subscribe(contact, "contact.closed", doorHandler)
|
subscribe(contact, "contact.closed", doorHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
def lockDoor(){
|
def lockDoor(){
|
||||||
log.debug "Locking the door."
|
log.debug "Locking the door."
|
||||||
lock1.lock()
|
lock1.lock()
|
||||||
log.debug ( "Sending Push Notification..." )
|
if(location.contactBookEnabled) {
|
||||||
if ( sendPushMessage != "No" ) sendPush( "${lock1} locked after ${contact} was closed for ${minutesLater} minutes!" )
|
if ( recipients ) {
|
||||||
log.debug("Sending text message...")
|
log.debug ( "Sending Push Notification..." )
|
||||||
if ( phoneNumber != "0" ) sendSms( phoneNumber, "${lock1} locked after ${contact} was closed for ${minutesLater} minutes!" )
|
sendNotificationToContacts( "${lock1} locked after ${contact} was closed for ${minutesLater} minutes!", recipients)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (phoneNumber) {
|
||||||
|
log.debug("Sending text message...")
|
||||||
|
sendSms( phoneNumber, "${lock1} locked after ${contact} was closed for ${minutesLater} minutes!")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def unlockDoor(){
|
def unlockDoor(){
|
||||||
log.debug "Unlocking the door."
|
log.debug "Unlocking the door."
|
||||||
lock1.unlock()
|
lock1.unlock()
|
||||||
log.debug ( "Sending Push Notification..." )
|
if(location.contactBookEnabled) {
|
||||||
if ( sendPushMessage != "No" ) sendPush( "${lock1} unlocked after ${contact} was opened for ${secondsLater} seconds!" )
|
if ( recipients ) {
|
||||||
log.debug("Sending text message...")
|
log.debug ( "Sending Push Notification..." )
|
||||||
if ( phoneNumber != "0" ) sendSms( phoneNumber, "${lock1} unlocked after ${contact} was opened for ${secondsLater} seconds!" )
|
sendNotificationToContacts( "${lock1} unlocked after ${contact} was opened for ${secondsLater} seconds!", recipients)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ( phoneNumber ) {
|
||||||
|
log.debug("Sending text message...")
|
||||||
|
sendSms( phoneNumber, "${lock1} unlocked after ${contact} was opened for ${secondsLater} seconds!")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def doorHandler(evt){
|
def doorHandler(evt){
|
||||||
if ((contact.latestValue("contact") == "open") && (evt.value == "locked")) { // If the door is open and a person locks the door then...
|
if ((contact.latestValue("contact") == "open") && (evt.value == "locked")) { // If the door is open and a person locks the door then...
|
||||||
def delay = (secondsLater) // runIn uses seconds
|
//def delay = (secondsLater) // runIn uses seconds
|
||||||
runIn( delay, unlockDoor ) // ...schedule (in minutes) to unlock... We don't want the door to be closed while the lock is engaged.
|
runIn( secondsLater, unlockDoor ) // ...schedule (in minutes) to unlock... We don't want the door to be closed while the lock is engaged.
|
||||||
}
|
}
|
||||||
else if ((contact.latestValue("contact") == "open") && (evt.value == "unlocked")) { // If the door is open and a person unlocks it then...
|
else if ((contact.latestValue("contact") == "open") && (evt.value == "unlocked")) { // If the door is open and a person unlocks it then...
|
||||||
unschedule( unlockDoor ) // ...we don't need to unlock it later.
|
unschedule( unlockDoor ) // ...we don't need to unlock it later.
|
||||||
}
|
}
|
||||||
else if ((contact.latestValue("contact") == "closed") && (evt.value == "locked")) { // If the door is closed and a person manually locks it then...
|
else if ((contact.latestValue("contact") == "closed") && (evt.value == "locked")) { // If the door is closed and a person manually locks it then...
|
||||||
unschedule( lockDoor ) // ...we don't need to lock it later.
|
unschedule( lockDoor ) // ...we don't need to lock it later.
|
||||||
}
|
}
|
||||||
else if ((contact.latestValue("contact") == "closed") && (evt.value == "unlocked")) { // If the door is closed and a person unlocks it then...
|
else if ((contact.latestValue("contact") == "closed") && (evt.value == "unlocked")) { // If the door is closed and a person unlocks it then...
|
||||||
def delay = (minutesLater * 60) // runIn uses seconds
|
//def delay = (minutesLater * 60) // runIn uses seconds
|
||||||
runIn( delay, lockDoor ) // ...schedule (in minutes) to lock.
|
runIn( (minutesLater * 60), lockDoor ) // ...schedule (in minutes) to lock.
|
||||||
}
|
}
|
||||||
else if ((lock1.latestValue("lock") == "unlocked") && (evt.value == "open")) { // If a person opens an unlocked door...
|
else if ((lock1.latestValue("lock") == "unlocked") && (evt.value == "open")) { // If a person opens an unlocked door...
|
||||||
unschedule( lockDoor ) // ...we don't need to lock it later.
|
unschedule( lockDoor ) // ...we don't need to lock it later.
|
||||||
}
|
}
|
||||||
else if ((lock1.latestValue("lock") == "unlocked") && (evt.value == "closed")) { // If a person closes an unlocked door...
|
else if ((lock1.latestValue("lock") == "unlocked") && (evt.value == "closed")) { // If a person closes an unlocked door...
|
||||||
def delay = (minutesLater * 60) // runIn uses seconds
|
//def delay = (minutesLater * 60) // runIn uses seconds
|
||||||
runIn( delay, lockDoor ) // ...schedule (in minutes) to lock.
|
runIn( (minutesLater * 60), lockDoor ) // ...schedule (in minutes) to lock.
|
||||||
}
|
}
|
||||||
else { //Opening or Closing door when locked (in case you have a handle lock)
|
else { //Opening or Closing door when locked (in case you have a handle lock)
|
||||||
log.debug "Unlocking the door."
|
log.debug "Unlocking the door."
|
||||||
lock1.unlock()
|
lock1.unlock()
|
||||||
log.debug ( "Sending Push Notification..." )
|
if(location.contactBookEnabled) {
|
||||||
if ( sendPushMessage != "No" ) sendPush( "${lock1} unlocked after ${contact} was opened or closed when ${lock1} was locked!" )
|
if ( recipients ) {
|
||||||
log.debug("Sending text message...")
|
log.debug ( "Sending Push Notification..." )
|
||||||
if ( phoneNumber != "0" ) sendSms( phoneNumber, "${lock1} unlocked after ${contact} was opened or closed when ${lock1} was locked!" )
|
sendNotificationToContacts( "${lock1} unlocked after ${contact} was opened or closed when ${lock1} was locked!", recipients)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if ( phoneNumber ) {
|
||||||
|
log.debug("Sending text message...")
|
||||||
|
sendSms( phoneNumber, "${lock1} unlocked after ${contact} was opened or closed when ${lock1} was locked!")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,389 @@
|
|||||||
|
/**
|
||||||
|
* Required PlantLink Connector
|
||||||
|
* This SmartApp forwards the raw data of the deviceType to myplantlink.com
|
||||||
|
* and returns it back to your device after calculating soil and plant type.
|
||||||
|
*
|
||||||
|
* Copyright 2015 Oso Technologies
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
import groovy.json.JsonBuilder
|
||||||
|
import java.util.regex.Matcher
|
||||||
|
import java.util.regex.Pattern
|
||||||
|
|
||||||
|
definition(
|
||||||
|
name: "PlantLink Connector",
|
||||||
|
namespace: "Osotech",
|
||||||
|
author: "Oso Technologies",
|
||||||
|
description: "This SmartApp connects to myplantlink.com and forwards the device data to it so it can calculate easy to read plant status for your specific plant's needs.",
|
||||||
|
category: "Convenience",
|
||||||
|
iconUrl: "https://dashboard.myplantlink.com/images/apple-touch-icon-76x76-precomposed.png",
|
||||||
|
iconX2Url: "https://dashboard.myplantlink.com/images/apple-touch-icon-120x120-precomposed.png",
|
||||||
|
iconX3Url: "https://dashboard.myplantlink.com/images/apple-touch-icon-152x152-precomposed.png"
|
||||||
|
) {
|
||||||
|
appSetting "client_id"
|
||||||
|
appSetting "client_secret"
|
||||||
|
appSetting "https_plantLinkServer"
|
||||||
|
}
|
||||||
|
|
||||||
|
preferences {
|
||||||
|
page(name: "auth", title: "Step 1 of 2", nextPage:"deviceList", content:"authPage")
|
||||||
|
page(name: "deviceList", title: "Step 2 of 2", install:true, uninstall:false){
|
||||||
|
section {
|
||||||
|
input "plantlinksensors", "capability.sensor", title: "Select PlantLink sensors", multiple: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mappings {
|
||||||
|
path("/swapToken") {
|
||||||
|
action: [
|
||||||
|
GET: "swapToken"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def authPage(){
|
||||||
|
if(!atomicState.accessToken){
|
||||||
|
createAccessToken()
|
||||||
|
atomicState.accessToken = state.accessToken
|
||||||
|
}
|
||||||
|
|
||||||
|
def redirectUrl = oauthInitUrl()
|
||||||
|
def uninstallAllowed = false
|
||||||
|
def oauthTokenProvided = false
|
||||||
|
if(atomicState.authToken){
|
||||||
|
uninstallAllowed = true
|
||||||
|
oauthTokenProvided = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!oauthTokenProvided) {
|
||||||
|
return dynamicPage(name: "auth", title: "Step 1 of 2", nextPage:null, uninstall:uninstallAllowed) {
|
||||||
|
section(){
|
||||||
|
href(name:"login",
|
||||||
|
url:redirectUrl,
|
||||||
|
style:"embedded",
|
||||||
|
title:"PlantLink",
|
||||||
|
image:"https://dashboard.myplantlink.com/images/PLlogo.png",
|
||||||
|
description:"Tap to login to myplantlink.com")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
return dynamicPage(name: "auth", title: "Step 1 of 2 - Completed", nextPage:"deviceList", uninstall:uninstallAllowed) {
|
||||||
|
section(){
|
||||||
|
paragraph "You are logged in to myplantlink.com, tap next to continue", image: iconUrl
|
||||||
|
href(url:redirectUrl, title:"Or", description:"tap to switch accounts")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def installed() {
|
||||||
|
log.debug "Installed with settings: ${settings}"
|
||||||
|
initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
def updated() {
|
||||||
|
log.debug "Updated with settings: ${settings}"
|
||||||
|
unsubscribe()
|
||||||
|
initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
def uninstalled() {
|
||||||
|
if (plantlinksensors){
|
||||||
|
plantlinksensors.each{ sensor_device ->
|
||||||
|
sensor_device.setInstallSmartApp("needSmartApp")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def initialize() {
|
||||||
|
atomicState.attached_sensors = [:]
|
||||||
|
if (plantlinksensors){
|
||||||
|
subscribe(plantlinksensors, "moisture_status", moistureHandler)
|
||||||
|
subscribe(plantlinksensors, "battery_status", batteryHandler)
|
||||||
|
plantlinksensors.each{ sensor_device ->
|
||||||
|
sensor_device.setStatusIcon("Waiting on First Measurement")
|
||||||
|
sensor_device.setInstallSmartApp("connectedToSmartApp")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def dock_sensor(device_serial, expected_plant_name) {
|
||||||
|
def docking_body_json_builder = new JsonBuilder([version: '1c', smartthings_device_id: device_serial])
|
||||||
|
def docking_params = [
|
||||||
|
uri : appSettings.https_plantLinkServer,
|
||||||
|
path : "/api/v1/smartthings/links",
|
||||||
|
headers : ["Content-Type": "application/json", "Authorization": "Bearer ${atomicState.authToken}"],
|
||||||
|
contentType: "application/json",
|
||||||
|
body: docking_body_json_builder.toString()
|
||||||
|
]
|
||||||
|
def plant_post_body_map = [
|
||||||
|
plant_type_key: 999999,
|
||||||
|
soil_type_key : 1000004
|
||||||
|
]
|
||||||
|
def plant_post_params = [
|
||||||
|
uri : appSettings.https_plantLinkServer,
|
||||||
|
path : "/api/v1/plants",
|
||||||
|
headers : ["Content-Type": "application/json", "Authorization": "Bearer ${atomicState.authToken}"],
|
||||||
|
contentType: "application/json",
|
||||||
|
]
|
||||||
|
log.debug "Creating new plant on myplantlink.com - ${expected_plant_name}"
|
||||||
|
httpPost(docking_params) { docking_response ->
|
||||||
|
if (parse_api_response(docking_response, "Docking a link")) {
|
||||||
|
if (docking_response.data.plants.size() == 0) {
|
||||||
|
log.debug "creating plant for - ${expected_plant_name}"
|
||||||
|
plant_post_body_map["name"] = expected_plant_name
|
||||||
|
plant_post_body_map['links_key'] = [docking_response.data.key]
|
||||||
|
def plant_post_body_json_builder = new JsonBuilder(plant_post_body_map)
|
||||||
|
plant_post_params["body"] = plant_post_body_json_builder.toString()
|
||||||
|
httpPost(plant_post_params) { plant_post_response ->
|
||||||
|
if(parse_api_response(plant_post_response, 'creating plant')){
|
||||||
|
def attached_map = atomicState.attached_sensors
|
||||||
|
attached_map[device_serial] = plant_post_response.data
|
||||||
|
atomicState.attached_sensors = attached_map
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
def plant = docking_response.data.plants[0]
|
||||||
|
def attached_map = atomicState.attached_sensors
|
||||||
|
attached_map[device_serial] = plant
|
||||||
|
atomicState.attached_sensors = attached_map
|
||||||
|
checkAndUpdatePlantIfNeeded(plant, expected_plant_name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
def checkAndUpdatePlantIfNeeded(plant, expected_plant_name){
|
||||||
|
def plant_put_params = [
|
||||||
|
uri : appSettings.https_plantLinkServer,
|
||||||
|
headers : ["Content-Type": "application/json", "Authorization": "Bearer ${atomicState.authToken}"],
|
||||||
|
contentType : "application/json"
|
||||||
|
]
|
||||||
|
if (plant.name != expected_plant_name) {
|
||||||
|
log.debug "updating plant for - ${expected_plant_name}"
|
||||||
|
plant_put_params["path"] = "/api/v1/plants/${plant.key}"
|
||||||
|
def plant_put_body_map = [
|
||||||
|
name: expected_plant_name
|
||||||
|
]
|
||||||
|
def plant_put_body_json_builder = new JsonBuilder(plant_put_body_map)
|
||||||
|
plant_put_params["body"] = plant_put_body_json_builder.toString()
|
||||||
|
httpPut(plant_put_params) { plant_put_response ->
|
||||||
|
parse_api_response(plant_put_response, 'updating plant name')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def moistureHandler(event){
|
||||||
|
def expected_plant_name = "SmartThings - ${event.displayName}"
|
||||||
|
def device_serial = getDeviceSerialFromEvent(event)
|
||||||
|
|
||||||
|
if (!atomicState.attached_sensors.containsKey(device_serial)){
|
||||||
|
dock_sensor(device_serial, expected_plant_name)
|
||||||
|
}else{
|
||||||
|
def measurement_post_params = [
|
||||||
|
uri: appSettings.https_plantLinkServer,
|
||||||
|
path: "/api/v1/smartthings/links/${device_serial}/measurements",
|
||||||
|
headers: ["Content-Type": "application/json", "Authorization": "Bearer ${atomicState.authToken}"],
|
||||||
|
contentType: "application/json",
|
||||||
|
body: event.value
|
||||||
|
]
|
||||||
|
httpPost(measurement_post_params) { measurement_post_response ->
|
||||||
|
if (parse_api_response(measurement_post_response, 'creating moisture measurement') &&
|
||||||
|
measurement_post_response.data.size() >0){
|
||||||
|
def measurement = measurement_post_response.data[0]
|
||||||
|
def plant = measurement.plant
|
||||||
|
log.debug plant
|
||||||
|
checkAndUpdatePlantIfNeeded(plant, expected_plant_name)
|
||||||
|
plantlinksensors.each{ sensor_device ->
|
||||||
|
if (sensor_device.id == event.deviceId){
|
||||||
|
sensor_device.setStatusIcon(plant.status)
|
||||||
|
if (plant.last_measurements && plant.last_measurements[0].moisture){
|
||||||
|
sensor_device.setPlantFuelLevel(plant.last_measurements[0].moisture * 100 as int)
|
||||||
|
}
|
||||||
|
if (plant.last_measurements && plant.last_measurements[0].battery){
|
||||||
|
sensor_device.setBatteryLevel(plant.last_measurements[0].battery * 100 as int)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def batteryHandler(event){
|
||||||
|
def expected_plant_name = "SmartThings - ${event.displayName}"
|
||||||
|
def device_serial = getDeviceSerialFromEvent(event)
|
||||||
|
|
||||||
|
if (!atomicState.attached_sensors.containsKey(device_serial)){
|
||||||
|
dock_sensor(device_serial, expected_plant_name)
|
||||||
|
}else{
|
||||||
|
def measurement_post_params = [
|
||||||
|
uri: appSettings.https_plantLinkServer,
|
||||||
|
path: "/api/v1/smartthings/links/${device_serial}/measurements",
|
||||||
|
headers: ["Content-Type": "application/json", "Authorization": "Bearer ${atomicState.authToken}"],
|
||||||
|
contentType: "application/json",
|
||||||
|
body: event.value
|
||||||
|
]
|
||||||
|
httpPost(measurement_post_params) { measurement_post_response ->
|
||||||
|
parse_api_response(measurement_post_response, 'creating battery measurement')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def getDeviceSerialFromEvent(event){
|
||||||
|
def pattern = /.*"zigbeedeviceid"\s*:\s*"(\w+)".*/
|
||||||
|
def match_result = (event.value =~ pattern)
|
||||||
|
return match_result[0][1]
|
||||||
|
}
|
||||||
|
|
||||||
|
def oauthInitUrl(){
|
||||||
|
atomicState.oauthInitState = UUID.randomUUID().toString()
|
||||||
|
def oauthParams = [
|
||||||
|
response_type: "code",
|
||||||
|
client_id: appSettings.client_id,
|
||||||
|
state: atomicState.oauthInitState,
|
||||||
|
redirect_uri: buildRedirectUrl()
|
||||||
|
]
|
||||||
|
return appSettings.https_plantLinkServer + "/oauth/oauth2/authorize?" + toQueryString(oauthParams)
|
||||||
|
}
|
||||||
|
|
||||||
|
def buildRedirectUrl(){
|
||||||
|
return getServerUrl() + "/api/token/${atomicState.accessToken}/smartapps/installations/${app.id}/swapToken"
|
||||||
|
}
|
||||||
|
|
||||||
|
def swapToken(){
|
||||||
|
def code = params.code
|
||||||
|
def oauthState = params.state
|
||||||
|
def stcid = appSettings.client_id
|
||||||
|
def postParams = [
|
||||||
|
method: 'POST',
|
||||||
|
uri: "https://oso-tech.appspot.com",
|
||||||
|
path: "/api/v1/oauth-token",
|
||||||
|
query: [grant_type:'authorization_code', code:params.code, client_id:stcid,
|
||||||
|
client_secret:appSettings.client_secret, redirect_uri: buildRedirectUrl()],
|
||||||
|
]
|
||||||
|
|
||||||
|
def jsonMap
|
||||||
|
httpPost(postParams) { resp ->
|
||||||
|
jsonMap = resp.data
|
||||||
|
}
|
||||||
|
|
||||||
|
atomicState.refreshToken = jsonMap.refresh_token
|
||||||
|
atomicState.authToken = jsonMap.access_token
|
||||||
|
|
||||||
|
def html = """
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<style>
|
||||||
|
.container {
|
||||||
|
padding:25px;
|
||||||
|
}
|
||||||
|
.flex1 {
|
||||||
|
width:33%;
|
||||||
|
float:left;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
font-size: 2em;
|
||||||
|
font-family: Verdana, Geneva, sans-serif;
|
||||||
|
text-align: center;
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="flex1"><img src="https://dashboard.myplantlink.com/images/PLlogo.png" alt="PlantLink" height="75"/></div>
|
||||||
|
<div class="flex1"><img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/connected-device-icn%402x.png" alt="connected to" height="25" style="padding-top:25px;" /></div>
|
||||||
|
<div class="flex1"><img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/st-logo%402x.png" alt="SmartThings" height="75"/></div>
|
||||||
|
<br clear="all">
|
||||||
|
</div>
|
||||||
|
<div class="container">
|
||||||
|
<p>Your PlantLink Account is now connected to SmartThings!</p>
|
||||||
|
<p style="color:green;">Click <strong>Done</strong> at the top right to finish setup.</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
render contentType: 'text/html', data: html
|
||||||
|
}
|
||||||
|
|
||||||
|
private refreshAuthToken() {
|
||||||
|
def stcid = appSettings.client_id
|
||||||
|
def refreshParams = [
|
||||||
|
method: 'POST',
|
||||||
|
uri: "https://hardware-dot-oso-tech.appspot.com",
|
||||||
|
path: "/api/v1/oauth-token",
|
||||||
|
query: [grant_type:'refresh_token', code:"${atomicState.refreshToken}", client_id:stcid,
|
||||||
|
client_secret:appSettings.client_secret],
|
||||||
|
]
|
||||||
|
try{
|
||||||
|
def jsonMap
|
||||||
|
httpPost(refreshParams) { resp ->
|
||||||
|
if(resp.status == 200){
|
||||||
|
log.debug "OAuth Token refreshed"
|
||||||
|
jsonMap = resp.data
|
||||||
|
if (resp.data) {
|
||||||
|
atomicState.refreshToken = resp?.data?.refresh_token
|
||||||
|
atomicState.authToken = resp?.data?.access_token
|
||||||
|
if (data?.action && data?.action != "") {
|
||||||
|
log.debug data.action
|
||||||
|
"{data.action}"()
|
||||||
|
data.action = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data.action = ""
|
||||||
|
}else{
|
||||||
|
log.debug "refresh failed ${resp.status} : ${resp.status.code}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(Exception e){
|
||||||
|
log.debug "caught exception refreshing auth token: " + e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse_api_response(resp, message) {
|
||||||
|
if (resp.status == 200) {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
log.error "sent ${message} Json & got http status ${resp.status} - ${resp.status.code}"
|
||||||
|
if (resp.status == 401) {
|
||||||
|
refreshAuthToken()
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
debugEvent("Authentication error, invalid authentication method, lack of credentials, etc.", true)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def getServerUrl() {
|
||||||
|
return "https://graph.api.smartthings.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
def debugEvent(message, displayEvent) {
|
||||||
|
def results = [
|
||||||
|
name: "appdebug",
|
||||||
|
descriptionText: message,
|
||||||
|
displayed: displayEvent
|
||||||
|
]
|
||||||
|
log.debug "Generating AppDebug Event: ${results}"
|
||||||
|
sendEvent (results)
|
||||||
|
}
|
||||||
|
|
||||||
|
def toQueryString(Map m){
|
||||||
|
return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&")
|
||||||
|
}
|
||||||
@@ -0,0 +1,341 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
* Bose® SoundTouch® Control
|
||||||
|
*
|
||||||
|
* Author: SmartThings & Joe Geiger
|
||||||
|
*
|
||||||
|
* Date: 2015-30-09
|
||||||
|
*/
|
||||||
|
definition(
|
||||||
|
name: "Bose® SoundTouch® Control",
|
||||||
|
namespace: "smartthings",
|
||||||
|
author: "SmartThings & Joe Geiger",
|
||||||
|
description: "Control your Bose® SoundTouch® when certain actions take place in your home.",
|
||||||
|
category: "SmartThings Labs",
|
||||||
|
iconUrl: "https://d3azp77rte0gip.cloudfront.net/smartapps/fcf1d93a-ba0b-4324-b96f-e5b5487dfaf5/images/BoseST_icon.png",
|
||||||
|
iconX2Url: "https://d3azp77rte0gip.cloudfront.net/smartapps/fcf1d93a-ba0b-4324-b96f-e5b5487dfaf5/images/BoseST_icon@2x.png",
|
||||||
|
iconX3Url: "https://d3azp77rte0gip.cloudfront.net/smartapps/fcf1d93a-ba0b-4324-b96f-e5b5487dfaf5/images/BoseST_icon@2x-1.png"
|
||||||
|
)
|
||||||
|
|
||||||
|
preferences {
|
||||||
|
page(name: "mainPage", title: "Control your Bose® SoundTouch® when something happens", install: true, uninstall: true)
|
||||||
|
page(name: "timeIntervalInput", title: "Only during a certain time") {
|
||||||
|
section {
|
||||||
|
input "starting", "time", title: "Starting", required: false
|
||||||
|
input "ending", "time", title: "Ending", required: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def mainPage() {
|
||||||
|
dynamicPage(name: "mainPage") {
|
||||||
|
def anythingSet = anythingSet()
|
||||||
|
if (anythingSet) {
|
||||||
|
section("When..."){
|
||||||
|
ifSet "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true
|
||||||
|
ifSet "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true
|
||||||
|
ifSet "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true
|
||||||
|
ifSet "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true
|
||||||
|
ifSet "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true
|
||||||
|
ifSet "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true
|
||||||
|
ifSet "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true
|
||||||
|
ifSet "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true
|
||||||
|
ifSet "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true
|
||||||
|
ifSet "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true
|
||||||
|
ifSet "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production
|
||||||
|
ifSet "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true
|
||||||
|
ifSet "timeOfDay", "time", title: "At a Scheduled Time", required: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
section(anythingSet ? "Select additional triggers" : "When...", hideable: anythingSet, hidden: true){
|
||||||
|
ifUnset "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true
|
||||||
|
ifUnset "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true
|
||||||
|
ifUnset "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true
|
||||||
|
ifUnset "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true
|
||||||
|
ifUnset "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true
|
||||||
|
ifUnset "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true
|
||||||
|
ifUnset "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true
|
||||||
|
ifUnset "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true
|
||||||
|
ifUnset "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true
|
||||||
|
ifUnset "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true
|
||||||
|
ifUnset "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production
|
||||||
|
ifUnset "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true
|
||||||
|
ifUnset "timeOfDay", "time", title: "At a Scheduled Time", required: false
|
||||||
|
}
|
||||||
|
section("Perform this action"){
|
||||||
|
input "actionType", "enum", title: "Action?", required: true, defaultValue: "play", options: [
|
||||||
|
"Turn On & Play",
|
||||||
|
"Turn Off",
|
||||||
|
"Toggle Play/Pause",
|
||||||
|
"Skip to Next Track",
|
||||||
|
"Skip to Beginning/Previous Track",
|
||||||
|
"Play Preset 1",
|
||||||
|
"Play Preset 2",
|
||||||
|
"Play Preset 3",
|
||||||
|
"Play Preset 4",
|
||||||
|
"Play Preset 5",
|
||||||
|
"Play Preset 6"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
section {
|
||||||
|
input "bose", "capability.musicPlayer", title: "Bose® SoundTouch® music player", required: true
|
||||||
|
}
|
||||||
|
section("More options", hideable: true, hidden: true) {
|
||||||
|
input "volume", "number", title: "Set the volume volume", description: "0-100%", required: false
|
||||||
|
input "frequency", "decimal", title: "Minimum time between actions (defaults to every event)", description: "Minutes", required: false
|
||||||
|
href "timeIntervalInput", title: "Only during a certain time", description: timeLabel ?: "Tap to set", state: timeLabel ? "complete" : "incomplete"
|
||||||
|
input "days", "enum", title: "Only on certain days of the week", multiple: true, required: false,
|
||||||
|
options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
|
||||||
|
if (settings.modes) {
|
||||||
|
input "modes", "mode", title: "Only when mode is", multiple: true, required: false
|
||||||
|
}
|
||||||
|
input "oncePerDay", "bool", title: "Only once per day", required: false, defaultValue: false
|
||||||
|
}
|
||||||
|
section([mobileOnly:true]) {
|
||||||
|
label title: "Assign a name", required: false
|
||||||
|
mode title: "Set for specific mode(s)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private anythingSet() {
|
||||||
|
for (name in ["motion","contact","contactClosed","acceleration","mySwitch","mySwitchOff","arrivalPresence","departurePresence","smoke","water","button1","triggerModes","timeOfDay"]) {
|
||||||
|
if (settings[name]) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private ifUnset(Map options, String name, String capability) {
|
||||||
|
if (!settings[name]) {
|
||||||
|
input(options, name, capability)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ifSet(Map options, String name, String capability) {
|
||||||
|
if (settings[name]) {
|
||||||
|
input(options, name, capability)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def installed() {
|
||||||
|
log.debug "Installed with settings: ${settings}"
|
||||||
|
subscribeToEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
def updated() {
|
||||||
|
log.debug "Updated with settings: ${settings}"
|
||||||
|
unsubscribe()
|
||||||
|
unschedule()
|
||||||
|
subscribeToEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
def subscribeToEvents() {
|
||||||
|
log.trace "subscribeToEvents()"
|
||||||
|
subscribe(app, appTouchHandler)
|
||||||
|
subscribe(contact, "contact.open", eventHandler)
|
||||||
|
subscribe(contactClosed, "contact.closed", eventHandler)
|
||||||
|
subscribe(acceleration, "acceleration.active", eventHandler)
|
||||||
|
subscribe(motion, "motion.active", eventHandler)
|
||||||
|
subscribe(mySwitch, "switch.on", eventHandler)
|
||||||
|
subscribe(mySwitchOff, "switch.off", eventHandler)
|
||||||
|
subscribe(arrivalPresence, "presence.present", eventHandler)
|
||||||
|
subscribe(departurePresence, "presence.not present", eventHandler)
|
||||||
|
subscribe(smoke, "smoke.detected", eventHandler)
|
||||||
|
subscribe(smoke, "smoke.tested", eventHandler)
|
||||||
|
subscribe(smoke, "carbonMonoxide.detected", eventHandler)
|
||||||
|
subscribe(water, "water.wet", eventHandler)
|
||||||
|
subscribe(button1, "button.pushed", eventHandler)
|
||||||
|
|
||||||
|
if (triggerModes) {
|
||||||
|
subscribe(location, modeChangeHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeOfDay) {
|
||||||
|
schedule(timeOfDay, scheduledTimeHandler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def eventHandler(evt) {
|
||||||
|
if (allOk) {
|
||||||
|
def lastTime = state[frequencyKey(evt)]
|
||||||
|
if (oncePerDayOk(lastTime)) {
|
||||||
|
if (frequency) {
|
||||||
|
if (lastTime == null || now() - lastTime >= frequency * 60000) {
|
||||||
|
takeAction(evt)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
log.debug "Not taking action because $frequency minutes have not elapsed since last action"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
takeAction(evt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
log.debug "Not taking action because it was already taken today"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def modeChangeHandler(evt) {
|
||||||
|
log.trace "modeChangeHandler $evt.name: $evt.value ($triggerModes)"
|
||||||
|
if (evt.value in triggerModes) {
|
||||||
|
eventHandler(evt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def scheduledTimeHandler() {
|
||||||
|
eventHandler(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
def appTouchHandler(evt) {
|
||||||
|
takeAction(evt)
|
||||||
|
}
|
||||||
|
|
||||||
|
private takeAction(evt) {
|
||||||
|
log.debug "takeAction($actionType)"
|
||||||
|
def options = [:]
|
||||||
|
if (volume) {
|
||||||
|
bose.setLevel(volume as Integer)
|
||||||
|
options.delay = 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (actionType) {
|
||||||
|
case "Turn On & Play":
|
||||||
|
options ? bose.on(options) : bose.on()
|
||||||
|
break
|
||||||
|
case "Turn Off":
|
||||||
|
options ? bose.off(options) : bose.off()
|
||||||
|
break
|
||||||
|
case "Toggle Play/Pause":
|
||||||
|
def currentStatus = bose.currentValue("playpause")
|
||||||
|
if (currentStatus == "play") {
|
||||||
|
options ? bose.pause(options) : bose.pause()
|
||||||
|
}
|
||||||
|
else if (currentStatus == "pause") {
|
||||||
|
options ? bose.play(options) : bose.play()
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case "Skip to Next Track":
|
||||||
|
options ? bose.nextTrack(options) : bose.nextTrack()
|
||||||
|
break
|
||||||
|
case "Skip to Beginning/Previous Track":
|
||||||
|
options ? bose.previousTrack(options) : bose.previousTrack()
|
||||||
|
break
|
||||||
|
case "Play Preset 1":
|
||||||
|
options ? bose.preset1(options) : bose.preset1()
|
||||||
|
break
|
||||||
|
case "Play Preset 2":
|
||||||
|
options ? bose.preset2(options) : bose.preset2()
|
||||||
|
break
|
||||||
|
case "Play Preset 3":
|
||||||
|
options ? bose.preset3(options) : bose.preset3()
|
||||||
|
break
|
||||||
|
case "Play Preset 4":
|
||||||
|
options ? bose.preset4(options) : bose.preset4()
|
||||||
|
break
|
||||||
|
case "Play Preset 5":
|
||||||
|
options ? bose.preset5(options) : bose.preset5()
|
||||||
|
break
|
||||||
|
case "Play Preset 6":
|
||||||
|
options ? bose.preset6(options) : bose.preset6()
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
log.error "Action type '$actionType' not defined"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frequency) {
|
||||||
|
state.lastActionTimeStamp = now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private frequencyKey(evt) {
|
||||||
|
//evt.deviceId ?: evt.value
|
||||||
|
"lastActionTimeStamp"
|
||||||
|
}
|
||||||
|
|
||||||
|
private dayString(Date date) {
|
||||||
|
def df = new java.text.SimpleDateFormat("yyyy-MM-dd")
|
||||||
|
if (location.timeZone) {
|
||||||
|
df.setTimeZone(location.timeZone)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
|
||||||
|
}
|
||||||
|
df.format(date)
|
||||||
|
}
|
||||||
|
|
||||||
|
private oncePerDayOk(Long lastTime) {
|
||||||
|
def result = true
|
||||||
|
if (oncePerDay) {
|
||||||
|
result = lastTime ? dayString(new Date()) != dayString(new Date(lastTime)) : true
|
||||||
|
log.trace "oncePerDayOk = $result"
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO - centralize somehow
|
||||||
|
private getAllOk() {
|
||||||
|
modeOk && daysOk && timeOk
|
||||||
|
}
|
||||||
|
|
||||||
|
private getModeOk() {
|
||||||
|
def result = !modes || modes.contains(location.mode)
|
||||||
|
log.trace "modeOk = $result"
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
private getDaysOk() {
|
||||||
|
def result = true
|
||||||
|
if (days) {
|
||||||
|
def df = new java.text.SimpleDateFormat("EEEE")
|
||||||
|
if (location.timeZone) {
|
||||||
|
df.setTimeZone(location.timeZone)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
|
||||||
|
}
|
||||||
|
def day = df.format(new Date())
|
||||||
|
result = days.contains(day)
|
||||||
|
}
|
||||||
|
log.trace "daysOk = $result"
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
private getTimeOk() {
|
||||||
|
def result = true
|
||||||
|
if (starting && ending) {
|
||||||
|
def currTime = now()
|
||||||
|
def start = timeToday(starting, location?.timeZone).time
|
||||||
|
def stop = timeToday(ending, location?.timeZone).time
|
||||||
|
result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start
|
||||||
|
}
|
||||||
|
log.trace "timeOk = $result"
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
private hhmm(time, fmt = "h:mm a")
|
||||||
|
{
|
||||||
|
def t = timeToday(time, location.timeZone)
|
||||||
|
def f = new java.text.SimpleDateFormat(fmt)
|
||||||
|
f.setTimeZone(location.timeZone ?: timeZone(time))
|
||||||
|
f.format(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
private timeIntervalLabel()
|
||||||
|
{
|
||||||
|
(starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z") : ""
|
||||||
|
}
|
||||||
|
// TODO - End Centralize
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 4.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 7.8 KiB |
@@ -246,6 +246,9 @@ def toggle(devices) {
|
|||||||
else if (devices*.currentValue('lock').contains('locked')) {
|
else if (devices*.currentValue('lock').contains('locked')) {
|
||||||
devices.unlock()
|
devices.unlock()
|
||||||
}
|
}
|
||||||
|
else if (devices*.currentValue('lock').contains('unlocked')) {
|
||||||
|
devices.lock()
|
||||||
|
}
|
||||||
else if (devices*.currentValue('alarm').contains('off')) {
|
else if (devices*.currentValue('alarm').contains('off')) {
|
||||||
devices.siren()
|
devices.siren()
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Every Element
|
* Every Element
|
||||||
*
|
*
|
||||||
* Copyright 2014 SmartThings
|
* Copyright 2015 SmartThings
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
* 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:
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
@@ -14,349 +14,555 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
definition(
|
definition(
|
||||||
name: "Every Element",
|
name: "Every Element",
|
||||||
namespace: "smartthings/examples",
|
namespace: "smartthings/examples",
|
||||||
author: "SmartThings",
|
author: "SmartThings",
|
||||||
description: "Every element demonstration app",
|
description: "Every element demonstration app",
|
||||||
category: "SmartThings Internal",
|
category: "SmartThings Internal",
|
||||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
|
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
|
||||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png"
|
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png"
|
||||||
)
|
)
|
||||||
|
|
||||||
preferences {
|
preferences {
|
||||||
page(name: "firstPage")
|
// landing page
|
||||||
page(name: "inputPage")
|
page(name: "firstPage")
|
||||||
page(name: "appPage")
|
|
||||||
page(name: "labelPage")
|
// PageKit
|
||||||
page(name: "modePage")
|
page(name: "buttonsPage")
|
||||||
page(name: "paragraphPage")
|
page(name: "imagePage")
|
||||||
page(name: "iconPage")
|
page(name: "inputPage")
|
||||||
page(name: "hrefPage")
|
page(name: "inputBooleanPage")
|
||||||
page(name: "buttonsPage")
|
page(name: "inputIconPage")
|
||||||
page(name: "imagePage")
|
page(name: "inputImagePage")
|
||||||
page(name: "videoPage")
|
page(name: "inputDevicePage")
|
||||||
page(name: "deadEnd", title: "Nothing to see here, move along.", content: "foo")
|
page(name: "inputCapabilityPage")
|
||||||
page(name: "flattenedPage")
|
page(name: "inputRoomPage")
|
||||||
|
page(name: "inputModePage")
|
||||||
|
page(name: "inputSelectionPage")
|
||||||
|
page(name: "inputHubPage")
|
||||||
|
page(name: "inputContactBookPage")
|
||||||
|
page(name: "inputTextPage")
|
||||||
|
page(name: "inputTimePage")
|
||||||
|
page(name: "appPage")
|
||||||
|
page(name: "hrefPage")
|
||||||
|
page(name: "paragraphPage")
|
||||||
|
page(name: "videoPage")
|
||||||
|
page(name: "labelPage")
|
||||||
|
page(name: "modePage")
|
||||||
|
|
||||||
|
// Every element helper pages
|
||||||
|
page(name: "deadEnd", title: "Nothing to see here, move along.", content: "foo")
|
||||||
|
page(name: "flattenedPage")
|
||||||
}
|
}
|
||||||
|
|
||||||
def firstPage() {
|
def firstPage() {
|
||||||
dynamicPage(name: "firstPage", title: "Where to first?", install: true, uninstall: true) {
|
dynamicPage(name: "firstPage", title: "Where to first?", install: true, uninstall: true) {
|
||||||
section() {
|
section {
|
||||||
href(page: "inputPage", title: "Element: 'input'")
|
href(page: "appPage", title: "Element: 'app'")
|
||||||
href(page: "appPage", title: "Element: 'app'")
|
href(page: "buttonsPage", title: "Element: 'buttons'")
|
||||||
href(page: "labelPage", title: "Element: 'label'")
|
href(page: "hrefPage", title: "Element: 'href'")
|
||||||
href(page: "modePage", title: "Element: 'mode'")
|
href(page: "imagePage", title: "Element: 'image'")
|
||||||
href(page: "paragraphPage", title: "Element: 'paragraph'")
|
href(page: "inputPage", title: "Element: 'input'")
|
||||||
href(page: "iconPage", title: "Element: 'icon'")
|
href(page: "labelPage", title: "Element: 'label'")
|
||||||
href(page: "hrefPage", title: "Element: 'href'")
|
href(page: "modePage", title: "Element: 'mode'")
|
||||||
href(page: "buttonsPage", title: "Element: 'buttons'")
|
href(page: "paragraphPage", title: "Element: 'paragraph'")
|
||||||
href(page: "imagePage", title: "Element: 'image'")
|
href(page: "videoPage", title: "Element: 'video'")
|
||||||
href(page: "videoPage", title: "Element: 'video'")
|
}
|
||||||
}
|
section {
|
||||||
section() {
|
href(page: "flattenedPage", title: "All of the above elements on a single page")
|
||||||
href(page: "flattenedPage", title: "All of the above elements on a single page")
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def inputPage() {
|
def inputPage() {
|
||||||
dynamicPage(name: "inputPage", title: "Every 'input' type") {
|
dynamicPage(name: "inputPage", title: "Links to every 'input' element") {
|
||||||
section("enum") {
|
section {
|
||||||
input(type: "enum", name: "enumRefresh", title: "submitOnChange:true", required: false, multiple: true, options: ["one", "two", "three"], submitOnChange: true)
|
href(page: "inputBooleanPage", title: "to boolean page")
|
||||||
if (enumRefresh) {
|
href(page: "inputIconPage", title: "to icon page")
|
||||||
paragraph "${enumRefresh}"
|
href(page: "inputImagePage", title: "to image page")
|
||||||
}
|
href(page: "inputSelectionPage", title: "to selection page")
|
||||||
input(type: "enum", name: "enumSegmented", title: "style:segmented", required: false, multiple: true, options: ["one", "two", "three"], style: "segmented")
|
href(page: "inputTextPage", title: "to text page")
|
||||||
input(type: "enum", name: "enum", title: "required:false, multiple:false", required: false, multiple: false, options: ["one", "two", "three"])
|
href(page: "inputTimePage", title: "to time page")
|
||||||
input(type: "enum", name: "enumRequired", title: "required:true", required: true, multiple: false, options: ["one", "two", "three"])
|
}
|
||||||
input(type: "enum", name: "enumMultiple", title: "multiple:true", required: false, multiple: true, options: ["one", "two", "three"])
|
section("subsets of selection input") {
|
||||||
input(type: "enum", name: "enumWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, multiple: true, options: ["one", "two", "three"], image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
href(page: "inputDevicePage", title: "to device selection page")
|
||||||
input(type: "enum", name: "enumWithGroupedOptions", title: "groupedOptions", description: "This enum has grouped options", required: false, multiple: true, groupedOptions: [
|
href(page: "inputCapabilityPage", title: "to capability selection page")
|
||||||
[
|
href(page: "inputRoomPage", title: "to room selection page")
|
||||||
title : "the group title that is displayed",
|
href(page: "inputModePage", title: "to mode selection page")
|
||||||
order : 0, // the order of the group; 0-based
|
href(page: "inputHubPage", title: "to hub selection page")
|
||||||
image : "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", // not yet supported
|
href(page: "inputContactBookPage", title: "to contact-book selection page")
|
||||||
values: [
|
}
|
||||||
[
|
}
|
||||||
key : "the value that will be placed in SmartApp settings.", // such as a device id
|
}
|
||||||
value: "the title of the selectable option that is displayed", // such as a device name
|
|
||||||
order: 0 // the order of the option
|
def inputBooleanPage() {
|
||||||
]
|
dynamicPage(name: "inputBooleanPage") {
|
||||||
]
|
section {
|
||||||
],
|
paragraph "The `required` and `multiple` attributes have no effect because the value will always be either `true` or `false`"
|
||||||
[
|
}
|
||||||
title : "the second group title that is displayed",
|
section {
|
||||||
order : 1, // the order of the group; 0-based
|
input(type: "boolean", name: "booleanWithoutDescription", title: "without description", description: null)
|
||||||
image : null, // not yet supported
|
input(type: "boolean", name: "booleanWithDescription", title: "with description", description: "This has a description")
|
||||||
values: [
|
}
|
||||||
[
|
section("defaultValue: 'true'") {
|
||||||
key : "some_device_id",
|
input(type: "boolean", name: "booleanWithDefaultValue", title: "", description: "", defaultValue: "true")
|
||||||
value: "some_device_name",
|
}
|
||||||
order: 1 // the order of the option. This option will appear second in the list even though it is the first option defined in this map
|
section("with image") {
|
||||||
],
|
input(type: "boolean", name: "booleanWithoutDescriptionWithImage", title: "without description", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", description: null)
|
||||||
[
|
input(type: "boolean", name: "booleanWithDescriptionWithImage", title: "with description", description: "This has a description", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
||||||
key : "some_other_device_id",
|
}
|
||||||
value: "some_other_device_name",
|
}
|
||||||
order: 0 // the order of the option. This option will appear first in the list even though it is not the first option defined in this map
|
}
|
||||||
]
|
def inputIconPage() {
|
||||||
]
|
dynamicPage(name: "inputIconPage") {
|
||||||
]
|
section {
|
||||||
])
|
paragraph "`description` is not displayed for icon elements"
|
||||||
}
|
paragraph "`multiple` has no effect because you can only choose a single icon"
|
||||||
section("text") {
|
}
|
||||||
input(type: "text", name: "text", title: "required:false, multiple:false", required: false, multiple: false)
|
section("required: true") {
|
||||||
input(type: "text", name: "textRequired", title: "required:true", required: true, multiple: false)
|
input(type: "icon", name: "iconRequired", title: "without description", required: true)
|
||||||
input(type: "text", name: "textWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
input(type: "icon", name: "iconRequiredWithDescription", title: "with description", description: "this is a description", required: true)
|
||||||
}
|
}
|
||||||
section("number") {
|
section("with image") {
|
||||||
input(type: "number", name: "number", title: "required:false, multiple:false", required: false, multiple: false)
|
paragraph "The image specified will be replaced after an icon is selected"
|
||||||
input(type: "number", name: "numberRequired", title: "required:true", required: true, multiple: false)
|
input(type: "icon", name: "iconwithImage", title: "without description", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
||||||
input(type: "number", name: "numberWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
}
|
||||||
}
|
}
|
||||||
section("boolean") {
|
}
|
||||||
input(type: "boolean", name: "boolean", title: "required:false, multiple:false", required: false, multiple: false)
|
def inputImagePage() {
|
||||||
input(type: "boolean", name: "booleanWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
dynamicPage(name: "inputImagePage") {
|
||||||
}
|
section {
|
||||||
section("password") {
|
paragraph "This only exists in DeviceTypes. Someone should do something about that. (glares at MikeDave)"
|
||||||
input(type: "password", name: "password", title: "required:false, multiple:false", required: false, multiple: false)
|
paragraph "Go to the device preferences of a Mobile Presence device to see it in action"
|
||||||
input(type: "password", name: "passwordRequired", title: "required:true", required: true, multiple: false)
|
paragraph "If you try to set the value of this, it will not behave as it would in Device Preferences"
|
||||||
input(type: "password", name: "passwordWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
input(type: "image", title: "This is kind of what it looks like", required: false)
|
||||||
}
|
}
|
||||||
section("phone") {
|
}
|
||||||
input(type: "phone", name: "phone", title: "required:false, multiple:false", required: false, multiple: false)
|
}
|
||||||
input(type: "phone", name: "phoneRequired", title: "required:true", required: true, multiple: false)
|
|
||||||
input(type: "phone", name: "phoneWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
|
||||||
}
|
def optionsGroup(List groups, String title) {
|
||||||
section("email") {
|
def group = [values:[], order: groups.size()]
|
||||||
input(type: "email", name: "email", title: "required:false, multiple:false", required: false, multiple: false)
|
group.title = title ?: ""
|
||||||
input(type: "email", name: "emailRequired", title: "required:true", required: true, multiple: false)
|
groups << group
|
||||||
input(type: "email", name: "emailWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
return groups
|
||||||
}
|
}
|
||||||
section("decimal") {
|
def addValues(List groups, String key, String value) {
|
||||||
input(type: "decimal", name: "decimal", title: "required:false, multiple:false", required: false, multiple: false)
|
def lastGroup = groups[-1]
|
||||||
input(type: "decimal", name: "decimalRequired", title: "required:true", required: true, multiple: false)
|
lastGroup["values"] << [
|
||||||
input(type: "decimal", name: "decimalWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
key: key,
|
||||||
}
|
value: value,
|
||||||
section("mode") {
|
order: lastGroup["values"].size()
|
||||||
input(type: "mode", name: "mode", title: "required:false, multiple:false", required: false, multiple: false)
|
]
|
||||||
input(type: "mode", name: "modeRequired", title: "required:true", required: true, multiple: false)
|
return groups
|
||||||
input(type: "mode", name: "modeMultiple", title: "multiple:true", required: false, multiple: true)
|
}
|
||||||
input(type: "mode", name: "iconWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, multiple: true, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
def listToMap(List original) {
|
||||||
}
|
original.inject([:]) { result, v ->
|
||||||
section("icon") {
|
result[v] = v
|
||||||
input(type: "icon", name: "icon", title: "required:false, multiple:false", required: false, multiple: false)
|
return result
|
||||||
input(type: "icon", name: "iconRequired", title: "required:true", required: true, multiple: false)
|
}
|
||||||
input(type: "icon", name: "iconWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
}
|
||||||
}
|
def addGroup(List groups, String title, values) {
|
||||||
section("capability") {
|
if (values instanceof List) {
|
||||||
input(type: "capability.switch", name: "capability", title: "required:false, multiple:false", required: false, multiple: false)
|
values = listToMap(values)
|
||||||
input(type: "capability.switch", name: "capabilityRequired", title: "required:true", required: true, multiple: false)
|
}
|
||||||
input(type: "capability.switch", name: "capabilityMultiple", title: "multiple:true", required: false, multiple: true)
|
|
||||||
input(type: "capability.switch", name: "capabilityWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, multiple: true, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
values.inject(optionsGroup(groups, title)) { result, k, v ->
|
||||||
}
|
return addValues(result, k, v)
|
||||||
section("hub") {
|
}
|
||||||
input(type: "hub", name: "hub", title: "required:false, multiple:false", required: false, multiple: false)
|
return groups
|
||||||
input(type: "hub", name: "hubRequired", title: "required:true", required: true, multiple: false)
|
}
|
||||||
input(type: "hub", name: "hubMultiple", title: "multiple:true", required: false, multiple: true)
|
def addGroup(values) {
|
||||||
input(type: "hub", name: "hubWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, multiple: true, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
addGroup([], null, values)
|
||||||
}
|
}
|
||||||
section("device") {
|
/* Example usage of options builder
|
||||||
input(type: "device.switch", name: "device", title: "required:false, multiple:false", required: false, multiple: false)
|
|
||||||
input(type: "device.switch", name: "deviceRequired", title: "required:true", required: true, multiple: false)
|
// Creating grouped options
|
||||||
input(type: "device.switch", name: "deviceMultiple", title: "multiple:true", required: false, multiple: true)
|
def newGroups = []
|
||||||
input(type: "device.switch", name: "deviceWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, multiple: true, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
addGroup(newGroups, "first group", ["foo", "bar", "baz"])
|
||||||
}
|
addGroup(newGroups, "second group", [zero: "zero", one: "uno", two: "dos", three: "tres"])
|
||||||
section("time") {
|
|
||||||
input(type: "time", name: "time", title: "required:false, multiple:false", required: false, multiple: false)
|
// simple list
|
||||||
input(type: "time", name: "timeRequired", title: "required:true", required: true, multiple: false)
|
addGroup(["a", "b", "c"])
|
||||||
input(type: "time", name: "timeWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
|
||||||
}
|
// simple map
|
||||||
section("contact-book") {
|
addGroup(["a": "yes", "b": "no", "c": "maybe"])
|
||||||
input("recipients", "contact", title: "Notify", description: "Send notifications to") {
|
*/
|
||||||
input(type: "phone", name: "phone", title: "Send text message to", required: false, multiple: false)
|
|
||||||
input(type: "boolean", name: "boolean", title: "Send push notification", required: false, multiple: false)
|
|
||||||
}
|
def inputSelectionPage() {
|
||||||
}
|
|
||||||
}
|
def englishOptions = ["One", "Two", "Three"]
|
||||||
|
def spanishOptions = ["Uno", "Dos", "Tres"]
|
||||||
|
def groupedOptions = []
|
||||||
|
addGroup(groupedOptions, "English", englishOptions)
|
||||||
|
addGroup(groupedOptions, "Spanish", spanishOptions)
|
||||||
|
|
||||||
|
dynamicPage(name: "inputSelectionPage") {
|
||||||
|
|
||||||
|
section("options variations") {
|
||||||
|
paragraph "tap these elements and look at the differences when selecting an option"
|
||||||
|
input(type: "enum", name: "selectionSimple", title: "Simple options", description: "no separators in the selectable options", groupedOptions: addGroup(englishOptions + spanishOptions))
|
||||||
|
input(type: "enum", name: "selectionGrouped", title: "Grouped options", description: "separate groups of options with headers", groupedOptions: groupedOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
section("list vs map") {
|
||||||
|
paragraph "These should be identical in UI, but are different in code and will produce different settings"
|
||||||
|
input(type: "enum", name: "selectionList", title: "Choose a device", description: "settings will be something like ['Device1 Label']", groupedOptions: addGroup(["Device1 Label", "Device2 Label"]))
|
||||||
|
input(type: "enum", name: "selectionMap", title: "Choose a device", description: "settings will be something like ['device1-id']", groupedOptions: addGroup(["device1-id": "Device1 Label", "device2-id": "Device2 Label"]))
|
||||||
|
}
|
||||||
|
|
||||||
|
section("segmented") {
|
||||||
|
paragraph "segmented should only work if there are either 2 or 3 options to choose from"
|
||||||
|
input(type: "enum", name: "selectionSegmented1", style: "segmented", title: "1 option", groupedOptions: addGroup(["One"]))
|
||||||
|
input(type: "enum", name: "selectionSegmented4", style: "segmented", title: "4 options", groupedOptions: addGroup(["One", "Two", "Three", "Four"]))
|
||||||
|
|
||||||
|
paragraph "multiple and required will have no effect on segmented selection elements. There will always be exactly 1 option selected"
|
||||||
|
input(type: "enum", name: "selectionSegmented2", style: "segmented", title: "2 options", options: ["One", "Two"])
|
||||||
|
input(type: "enum", name: "selectionSegmented3", style: "segmented", title: "3 options", options: ["One", "Two", "Three"])
|
||||||
|
|
||||||
|
paragraph "specifying defaultValue still works with segmented selection elements"
|
||||||
|
input(type: "enum", name: "selectionSegmentedWithDefault", title: "defaulted to 'two'", groupedOptions: addGroup(["One", "Two", "Three"]), defaultValue: "Two")
|
||||||
|
}
|
||||||
|
|
||||||
|
section("required: true") {
|
||||||
|
input(type: "enum", name: "selectionRequired", title: "This is required", description: "It should look different when nothing is selected", groupedOptions: addGroup(["only option"]), required: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
section("multiple: true") {
|
||||||
|
input(type: "enum", name: "selectionMultiple", title: "This allows multiple selections", description: "It should look different when nothing is selected", groupedOptions: addGroup(["an option", "another option", "no way, one more?"]), multiple: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
section("with image") {
|
||||||
|
input(type: "enum", name: "selectionWithImage", title: "This has an image", description: "and a description", groupedOptions: addGroup(["an option", "another option", "no way, one more?"]), image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
def inputTextPage() {
|
||||||
|
dynamicPage(name: "inputTextPage", title: "Every 'text' variation") {
|
||||||
|
section("style and functional differences") {
|
||||||
|
input(type: "text", name: "textRequired", title: "required: true", description: "This should look different when nothing has been entered", required: true)
|
||||||
|
input(type: "text", name: "textWithImage", title: "with image", description: "This should look different when nothing has been entered", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", required: false)
|
||||||
|
}
|
||||||
|
section("text") {
|
||||||
|
input(type: "text", name: "text", title: "This has an alpha-numeric keyboard", description: "no special formatting", required: false)
|
||||||
|
}
|
||||||
|
section("password") {
|
||||||
|
input(type: "password", name: "password", title: "This has an alpha-numeric keyboard", description: "masks value", required: false)
|
||||||
|
}
|
||||||
|
section("email") {
|
||||||
|
input(type: "email", name: "email", title: "This has an email-specific keyboard", description: "no special formatting", required: false)
|
||||||
|
}
|
||||||
|
section("phone") {
|
||||||
|
input(type: "phone", name: "phone", title: "This has a numeric keyboard", description: "formatted for phone numbers", required: false)
|
||||||
|
}
|
||||||
|
section("decimal") {
|
||||||
|
input(type: "decimal", name: "decimal", title: "This has an numeric keyboard with decimal point", description: "no special formatting", required: false)
|
||||||
|
}
|
||||||
|
section("number") {
|
||||||
|
input(type: "number", name: "number", title: "This has an numeric keyboard without decimal point", description: "no special formatting", required: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
section("specified ranges") {
|
||||||
|
paragraph "You can limit number and decimal inputs to a specific range."
|
||||||
|
input(range: "50..150", type: "decimal", name: "decimalRange50..150", title: "only values between 50 and 150 will pass validation", description: "no special formatting", required: false)
|
||||||
|
paragraph "Negative limits will add a negative symbol to the keyboard."
|
||||||
|
input(range: "-50..50", type: "number", name: "numberRange-50..50", title: "only values between -50 and 50 will pass validation", description: "no special formatting", required: false)
|
||||||
|
paragraph "Specify * to not limit one side or the other."
|
||||||
|
input(range: "*..0", type: "decimal", name: "decimalRange*..0", title: "only negative values will pass validation", description: "no special formatting", required: false)
|
||||||
|
input(range: "*..*", type: "number", name: "numberRange*..*", title: "only positive values will pass validation", description: "no special formatting", required: false)
|
||||||
|
paragraph "If you don't specify a range, it defaults to 0..*"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
def inputTimePage() {
|
||||||
|
dynamicPage(name: "inputTimePage") {
|
||||||
|
section {
|
||||||
|
input(type: "time", name: "timeWithDescription", title: "a time picker", description: "with a description", required: false)
|
||||||
|
input(type: "time", name: "timeWithoutDescription", title: "without a description", description: null, required: false)
|
||||||
|
input(type: "time", name: "timeRequired", title: "required: true", required: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// selection subsets
|
||||||
|
def inputDevicePage() {
|
||||||
|
|
||||||
|
dynamicPage(name: "inputDevicePage") {
|
||||||
|
|
||||||
|
section("required: true") {
|
||||||
|
input(type: "device.switch", name: "deviceRequired", title: "This is required", description: "It should look different when nothing is selected")
|
||||||
|
}
|
||||||
|
|
||||||
|
section("multiple: true") {
|
||||||
|
input(type: "device.switch", name: "deviceMultiple", title: "This is required", description: "It should look different when nothing is selected", multiple: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
section("with image") {
|
||||||
|
input(type: "device.switch", name: "deviceRequired", title: "This has an image", description: "and a description", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
def inputCapabilityPage() {
|
||||||
|
|
||||||
|
dynamicPage(name: "inputCapabilityPage") {
|
||||||
|
|
||||||
|
section("required: true") {
|
||||||
|
input(type: "capability.switch", name: "capabilityRequired", title: "This is required", description: "It should look different when nothing is selected")
|
||||||
|
}
|
||||||
|
|
||||||
|
section("multiple: true") {
|
||||||
|
input(type: "capability.switch", name: "capabilityMultiple", title: "This is required", description: "It should look different when nothing is selected", multiple: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
section("with image") {
|
||||||
|
input(type: "capability.switch", name: "capabilityRequired", title: "This has an image", description: "and a description", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
def inputRoomPage() {
|
||||||
|
|
||||||
|
dynamicPage(name: "inputRoomPage") {
|
||||||
|
|
||||||
|
section("required: true") {
|
||||||
|
input(type: "room", name: "roomRequired", title: "This is required", description: "It should look different when nothing is selected")
|
||||||
|
}
|
||||||
|
|
||||||
|
section("multiple: true") {
|
||||||
|
input(type: "room", name: "roomMultiple", title: "This is required", description: "It should look different when nothing is selected", multiple: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
section("with image") {
|
||||||
|
input(type: "room", name: "roomRequired", title: "This has an image", description: "and a description", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
def inputModePage() {
|
||||||
|
|
||||||
|
dynamicPage(name: "inputModePage") {
|
||||||
|
|
||||||
|
section("required: true") {
|
||||||
|
input(type: "mode", name: "modeRequired", title: "This is required", description: "It should look different when nothing is selected")
|
||||||
|
}
|
||||||
|
|
||||||
|
section("multiple: true") {
|
||||||
|
input(type: "mode", name: "modeMultiple", title: "This is required", description: "It should look different when nothing is selected", multiple: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
section("with image") {
|
||||||
|
input(type: "mode", name: "modeRequired", title: "This has an image", description: "and a description", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
def inputHubPage() {
|
||||||
|
|
||||||
|
dynamicPage(name: "inputHubPage") {
|
||||||
|
|
||||||
|
section("required: true") {
|
||||||
|
input(type: "hub", name: "hubRequired", title: "This is required", description: "It should look different when nothing is selected")
|
||||||
|
}
|
||||||
|
|
||||||
|
section("multiple: true") {
|
||||||
|
input(type: "hub", name: "hubMultiple", title: "This is required", description: "It should look different when nothing is selected", multiple: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
section("with image") {
|
||||||
|
input(type: "hub", name: "hubRequired", title: "This has an image", description: "and a description", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
def inputContactBookPage() {
|
||||||
|
|
||||||
|
dynamicPage(name: "inputContactBookPage") {
|
||||||
|
|
||||||
|
section("required: true") {
|
||||||
|
input(type: "contact", name: "contactRequired", title: "This is required", description: "It should look different when nothing is selected")
|
||||||
|
}
|
||||||
|
|
||||||
|
section("multiple: true") {
|
||||||
|
input(type: "contact", name: "contactMultiple", title: "This is required", description: "It should look different when nothing is selected", multiple: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
section("with image") {
|
||||||
|
input(type: "contact", name: "contactRequired", title: "This has an image", description: "and a description", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def appPage() {
|
def appPage() {
|
||||||
dynamicPage(name: "appPage", title: "Every 'app' type") {
|
dynamicPage(name: "appPage", title: "Every 'app' type") {
|
||||||
section {
|
section {
|
||||||
paragraph "These won't work unless you create a child SmartApp to link to... Sorry."
|
paragraph "These won't work unless you create a child SmartApp to link to... Sorry."
|
||||||
}
|
}
|
||||||
section("app") {
|
section("app") {
|
||||||
app(
|
app(
|
||||||
name: "app",
|
name: "app",
|
||||||
title: "required:false, multiple:false",
|
title: "required:false, multiple:false",
|
||||||
required: false,
|
required: false,
|
||||||
multiple: false,
|
multiple: false,
|
||||||
namespace: "Steve",
|
namespace: "Steve",
|
||||||
appName: "Child SmartApp"
|
appName: "Child SmartApp"
|
||||||
)
|
)
|
||||||
app(name: "appRequired", title: "required:true", required: true, multiple: false, namespace: "Steve", appName: "Child SmartApp")
|
app(name: "appRequired", title: "required:true", required: true, multiple: false, namespace: "Steve", appName: "Child SmartApp")
|
||||||
app(name: "appComplete", title: "state:complete", required: false, multiple: false, namespace: "Steve", appName: "Child SmartApp", state: "complete")
|
app(name: "appComplete", title: "state:complete", required: false, multiple: false, namespace: "Steve", appName: "Child SmartApp", state: "complete")
|
||||||
app(name: "appWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, multiple: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", namespace: "Steve", appName: "Child SmartApp")
|
app(name: "appWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, multiple: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", namespace: "Steve", appName: "Child SmartApp")
|
||||||
}
|
}
|
||||||
section("multiple:true") {
|
section("multiple:true") {
|
||||||
app(name: "appMultiple", title: "multiple:true", required: false, multiple: true, namespace: "Steve", appName: "Child SmartApp")
|
app(name: "appMultiple", title: "multiple:true", required: false, multiple: true, namespace: "Steve", appName: "Child SmartApp")
|
||||||
}
|
}
|
||||||
section("multiple:true with image") {
|
section("multiple:true with image") {
|
||||||
app(name: "appMultipleWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, multiple: true, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", namespace: "Steve", appName: "Child SmartApp")
|
app(name: "appMultipleWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, multiple: true, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", namespace: "Steve", appName: "Child SmartApp")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def labelPage() {
|
def labelPage() {
|
||||||
dynamicPage(name: "labelPage", title: "Every 'Label' type") {
|
dynamicPage(name: "labelPage", title: "Every 'Label' type") {
|
||||||
section("label") {
|
section("label") {
|
||||||
label(name: "label", title: "required:false, multiple:false", required: false, multiple: false)
|
paragraph "The difference between a label element and a text input element is that the label element will effect the SmartApp directly by setting the label. An input element will place the set value in the SmartApp's settings."
|
||||||
label(name: "labelRequired", title: "required:true", required: true, multiple: false)
|
paragraph "There are 3 here as an example. Never use more than 1 label element on a page."
|
||||||
label(name: "labelWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
label(name: "label", title: "required:false, multiple:false", required: false, multiple: false)
|
||||||
}
|
label(name: "labelRequired", title: "required:true", required: true, multiple: false)
|
||||||
}
|
label(name: "labelWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def modePage() {
|
def modePage() {
|
||||||
dynamicPage(name: "modePage", title: "Every 'mode' type") { // TODO: finish this
|
dynamicPage(name: "modePage", title: "Every 'mode' type") { // TODO: finish this
|
||||||
section("mode") {
|
section("mode") {
|
||||||
mode(name: "mode", title: "required:false, multiple:false", required: false, multiple: false)
|
paragraph "The difference between a mode element and a mode input element is that the mode element will effect the SmartApp directly by setting the modes it executes in. A mode input element will place the set value in the SmartApp's settings."
|
||||||
mode(name: "modeRequired", title: "required:true", required: true, multiple: false)
|
paragraph "Another difference is that you can select 'All Modes' when choosing which mode the SmartApp should execute in. This is the same as selecting no modes. When a SmartApp does not have modes specified, it will execute in all modes."
|
||||||
mode(name: "modeMultiple", title: "multiple:true", required: false, multiple: true)
|
paragraph "There are 4 here as an example. Never use more than 1 mode element on a page."
|
||||||
mode(name: "modeWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, multiple: true, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
mode(name: "mode", title: "required:false, multiple:false", required: false, multiple: false)
|
||||||
}
|
mode(name: "modeRequired", title: "required:true", required: true, multiple: false)
|
||||||
}
|
mode(name: "modeMultiple", title: "multiple:true", required: false, multiple: true)
|
||||||
|
mode(name: "modeWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, multiple: true, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def paragraphPage() {
|
def paragraphPage() {
|
||||||
dynamicPage(name: "paragraphPage", title: "Every 'paragraph' type") {
|
dynamicPage(name: "paragraphPage", title: "Every 'paragraph' type") {
|
||||||
section("paragraph") {
|
section("paragraph") {
|
||||||
paragraph "This us how you should make a paragraph element"
|
paragraph "This is how you should make a paragraph element"
|
||||||
paragraph image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", "This is a long description, blah, blah, blah."
|
paragraph image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", "This is a long description, blah, blah, blah."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
def iconPage() {
|
|
||||||
dynamicPage(name: "iconPage", title: "Every 'icon' type") { // TODO: finish this
|
|
||||||
section("icon") {
|
|
||||||
icon(name: "icon", title: "required:false, multiple:false", required: false, multiple: false)
|
|
||||||
icon(name: "iconRequired", title: "required:true", required: true, multiple: false)
|
|
||||||
icon(name: "iconWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def hrefPage() {
|
def hrefPage() {
|
||||||
dynamicPage(name: "hrefPage", title: "Every 'href' type") {
|
dynamicPage(name: "hrefPage", title: "Every 'href' variation") {
|
||||||
section("page") {
|
section("stylistic differences") {
|
||||||
href(name: "hrefPage", title: "required:false, multiple:false", required: false, multiple: false, page: "deadEnd")
|
href(page: "deadEnd", title: "state: 'complete'", description: "gives the appearance of an input that has been filled out", state: "complete")
|
||||||
href(name: "hrefPageRequired", title: "required:true", required: true, multiple: false, page: "deadEnd", description: "Don't make hrefs required")
|
href(page: "deadEnd", title: "with image", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
||||||
href(name: "hrefPageComplete", title: "state:complete", required: false, multiple: false, page: "deadEnd", state: "complete")
|
href(page: "deadEnd", title: "with image and description", description: "and state: 'complete'", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", state: "complete")
|
||||||
href(name: "hrefPageWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", page: "deadEnd",)
|
}
|
||||||
}
|
section("functional differences") {
|
||||||
section("external") {
|
href(page: "deadEnd", title: "to a page within the app")
|
||||||
href(name: "hrefExternal", title: "required:false, multiple:false", required: false, multiple: false, style: "external", url: "http://smartthings.com/")
|
href(url: "http://www.google.com", title: "to a url using all defaults")
|
||||||
href(name: "hrefExternalRequired", title: "required:true", required: true, multiple: false, style: "external", url: "http://smartthings.com/", description: "Don't make hrefs required")
|
href(url: "http://www.google.com", title: "external: true", description: "takes you outside the app", external: true)
|
||||||
href(name: "hrefExternalComplete", title: "state:complete", required: false, multiple: true, style: "external", url: "http://smartthings.com/", state: "complete")
|
}
|
||||||
href(name: "hrefExternalWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", url: "http://smartthings.com/")
|
}
|
||||||
}
|
|
||||||
section("embedded") {
|
|
||||||
href(name: "hrefEmbedded", title: "required:false, multiple:false", required: false, multiple: false, style: "embedded", url: "http://smartthings.com/")
|
|
||||||
href(name: "hrefEmbeddedRequired", title: "required:true", required: true, multiple: false, style: "embedded", url: "http://smartthings.com/", description: "Don't make hrefs required")
|
|
||||||
href(name: "hrefEmbeddedComplete", title: "state:complete", required: false, multiple: true, style: "embedded", url: "http://smartthings.com/", state: "complete")
|
|
||||||
href(name: "hrefEmbeddedWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", url: "http://smartthings.com/")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def buttonsPage() {
|
def buttonsPage() {
|
||||||
dynamicPage(name: "buttonsPage", title: "Every 'button' type") {
|
dynamicPage(name: "buttonsPage", title: "Every 'button' type") {
|
||||||
section("buttons") {
|
section("Simple Buttons") {
|
||||||
buttons(name: "buttons", title: "required:false, multiple:false", required: false, multiple: false, buttons: [
|
paragraph "If there are an odd number of buttons, the last button will span the entire view area."
|
||||||
[label: "foo", action: "foo"],
|
buttons(name: "buttons1", title: "1 button", buttons: [
|
||||||
[label: "bar", action: "bar"]
|
[label: "foo", action: "foo"]
|
||||||
])
|
])
|
||||||
buttons(name: "buttonsRequired", title: "required:true", required: true, multiple: false, buttons: [
|
buttons(name: "buttons2", title: "2 buttons", buttons: [
|
||||||
[label: "foo", action: "foo"],
|
[label: "foo", action: "foo"],
|
||||||
[label: "bar", action: "bar"]
|
[label: "bar", action: "bar"]
|
||||||
])
|
])
|
||||||
buttons(name: "buttonsWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", buttons: [
|
buttons(name: "buttons3", title: "3 buttons", buttons: [
|
||||||
[label: "foo", action: "foo"],
|
[label: "foo", action: "foo"],
|
||||||
[label: "bar", action: "bar"]
|
[label: "bar", action: "bar"],
|
||||||
])
|
[label: "baz", action: "baz"]
|
||||||
}
|
])
|
||||||
section("Colored Buttons") {
|
buttons(name: "buttonsWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", buttons: [
|
||||||
buttons(name: "buttonsColoredSpecial", title: "special strings", description: "SmartThings highly recommends using these colors", buttons: [
|
[label: "foo", action: "foo"],
|
||||||
[label: "complete", action: "bar", backgroundColor: "complete"],
|
[label: "bar", action: "bar"]
|
||||||
[label: "required", action: "bar", backgroundColor: "required"]
|
])
|
||||||
])
|
}
|
||||||
buttons(name: "buttonsColoredHex", title: "hex values work", buttons: [
|
section("Colored Buttons") {
|
||||||
[label: "bg: #000dff", action: "foo", backgroundColor: "#000dff"],
|
buttons(name: "buttonsColoredSpecial", title: "special strings", description: "SmartThings highly recommends using these colors", buttons: [
|
||||||
[label: "fg: #ffac00", action: "foo", color: "#ffac00"],
|
[label: "complete", action: "bar", backgroundColor: "complete"],
|
||||||
[label: "both fg and bg", action: "foo", color: "#ffac00", backgroundColor: "#000dff"]
|
[label: "required", action: "bar", backgroundColor: "required"]
|
||||||
])
|
])
|
||||||
buttons(name: "buttonsColoredString", title: "strings work too", buttons: [
|
buttons(name: "buttonsColoredHex", title: "hex values work", buttons: [
|
||||||
[label: "green", action: "foo", backgroundColor: "green"],
|
[label: "bg: #000dff", action: "foo", backgroundColor: "#000dff"],
|
||||||
[label: "red", action: "foo", backgroundColor: "red"],
|
[label: "fg: #ffac00", action: "foo", color: "#ffac00"],
|
||||||
[label: "both fg and bg", action: "foo", color: "red", backgroundColor: "green"]
|
[label: "both fg and bg", action: "foo", color: "#ffac00", backgroundColor: "#000dff"]
|
||||||
])
|
])
|
||||||
}
|
buttons(name: "buttonsColoredString", title: "strings work too", buttons: [
|
||||||
}
|
[label: "green", action: "foo", backgroundColor: "green"],
|
||||||
|
[label: "red", action: "foo", backgroundColor: "red"],
|
||||||
|
[label: "both fg and bg", action: "foo", color: "red", backgroundColor: "green"]
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def imagePage() {
|
def imagePage() {
|
||||||
dynamicPage(name: "imagePage", title: "Every 'image' type") { // TODO: finish thise
|
dynamicPage(name: "imagePage", title: "Every 'image' type") { // TODO: finish thise
|
||||||
section("image") {
|
section("image") {
|
||||||
image "http://f.cl.ly/items/1k1S0A0m3805402o3O12/20130915-191127.jpg"
|
image "http://f.cl.ly/items/1k1S0A0m3805402o3O12/20130915-191127.jpg"
|
||||||
image(name: "imageWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
image(name: "imageWithMultipleImages", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, images: ["https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", "http://f.cl.ly/items/1k1S0A0m3805402o3O12/20130915-191127.jpg"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def videoPage() {
|
def videoPage() {
|
||||||
dynamicPage(name: "imagePage", title: "Every 'image' type") { // TODO: finish this
|
dynamicPage(name: "videoPage", title: "Every 'video' type") { // TODO: finish this
|
||||||
section("video") {
|
section("video") {
|
||||||
// TODO: update this when there is a videoElement method
|
// TODO: update this when there is a videoElement method
|
||||||
element(name: "videoElement", element: "video", type: "video", title: "this is a video!", description: "I am setting long title and descriptions to test the offset", required: false, image: "http://ec2-54-161-144-215.compute-1.amazonaws.com:8081/jesse/cam1/54aafcd1c198347511c26321.jpg", video: "http://ec2-54-161-144-215.compute-1.amazonaws.com:8081/jesse/cam1/54aafcd1c198347511c2631f.mp4")
|
element(name: "videoElement", element: "video", type: "video", title: "this is a video!", description: "I am setting long title and descriptions to test the offset", required: false, image: "http://f.cl.ly/items/0w0D1p0K2D0d190F3H3N/Image%202015-12-14%20at%207.57.27%20AM.jpg", video: "http://f.cl.ly/items/3O2L03471l2K3E3l3K1r/Zombie%20Kid%20Likes%20Turtles.mp4")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def flattenedPage() {
|
def flattenedPage() {
|
||||||
def allSections = []
|
def allSections = []
|
||||||
firstPage().sections.each { section ->
|
firstPage().sections[0].body.each { hrefElement ->
|
||||||
section.body.each { hrefElement ->
|
if (hrefElement.name != "inputPage") {
|
||||||
if (hrefElement.page != "flattenedPage") {
|
// inputPage is a bunch of hrefs
|
||||||
allSections += "${hrefElement.page}"().sections
|
allSections += "${hrefElement.page}"().sections
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
// collect the input elements
|
||||||
def flattenedPage = dynamicPage(name: "flattenedPage", title: "All elements in one page!") {}
|
inputPage().sections.each { section ->
|
||||||
flattenedPage.sections = allSections
|
section.body.each { hrefElement ->
|
||||||
return flattenedPage
|
allSections += "${hrefElement.page}"().sections
|
||||||
|
}
|
||||||
|
}
|
||||||
|
def flattenedPage = dynamicPage(name: "flattenedPage", title: "All elements in one page!") {}
|
||||||
|
flattenedPage.sections = allSections
|
||||||
|
return flattenedPage
|
||||||
}
|
}
|
||||||
|
|
||||||
def foo() {
|
def foo() {
|
||||||
dynamicPage(name: "deadEnd") {
|
dynamicPage(name: "deadEnd") {
|
||||||
|
section { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def installed() {
|
def installed() {
|
||||||
log.debug "Installed with settings: ${settings}"
|
log.debug "Installed with settings: ${settings}"
|
||||||
|
|
||||||
initialize()
|
initialize()
|
||||||
}
|
}
|
||||||
|
|
||||||
def updated() {
|
def updated() {
|
||||||
log.debug "Updated with settings: ${settings}"
|
log.debug "Updated with settings: ${settings}"
|
||||||
|
|
||||||
unsubscribe()
|
unsubscribe()
|
||||||
initialize()
|
initialize()
|
||||||
}
|
}
|
||||||
|
|
||||||
def initialize() {
|
def initialize() {
|
||||||
// TODO: subscribe to attributes, devices, locations, etc.
|
// TODO: subscribe to attributes, devices, locations, etc.
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright 2015 SmartThings
|
* Copyright 2016 SmartThings
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
* 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:
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
@@ -38,37 +38,75 @@ preferences {
|
|||||||
page(name: "schedulingPage")
|
page(name: "schedulingPage")
|
||||||
page(name: "completionPage")
|
page(name: "completionPage")
|
||||||
page(name: "numbersPage")
|
page(name: "numbersPage")
|
||||||
|
page(name: "controllerExplanationPage")
|
||||||
}
|
}
|
||||||
|
|
||||||
def rootPage() {
|
def rootPage() {
|
||||||
dynamicPage(name: "rootPage", title: "", install: true, uninstall: true) {
|
dynamicPage(name: "rootPage", title: "", install: true, uninstall: true) {
|
||||||
|
|
||||||
section {
|
section("What to dim") {
|
||||||
input(name: "dimmers", type: "capability.switchLevel", title: "Dimmers", description: null, multiple: true, required: true, submitOnChange: true)
|
input(name: "dimmers", type: "capability.switchLevel", title: "Dimmers", description: null, multiple: true, required: true, submitOnChange: true)
|
||||||
|
if (dimmers) {
|
||||||
|
href(name: "toNumbersPage", page: "numbersPage", title: "Duration & Direction", description: numbersPageHrefDescription(), state: "complete")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dimmers) {
|
if (dimmers) {
|
||||||
|
|
||||||
section {
|
section("Gentle Wake Up Has A Controller") {
|
||||||
href(name: "toNumbersPage", page: "numbersPage", title: "Duration & Direction", description: numbersPageHrefDescription(), state: "complete")
|
href(title: "Learn how to control Gentle Wake Up", page: "controllerExplanationPage", description: null)
|
||||||
}
|
}
|
||||||
|
|
||||||
section {
|
section("Rules For Dimming") {
|
||||||
href(name: "toSchedulingPage", page: "schedulingPage", title: "Rules For Automatically Dimming Your Lights", description: schedulingHrefDescription(), state: schedulingHrefDescription() ? "complete" : "")
|
href(name: "toSchedulingPage", page: "schedulingPage", title: "Automation", description: schedulingHrefDescription() ?: "Set rules for when to start", state: schedulingHrefDescription() ? "complete" : "")
|
||||||
}
|
input(name: "manualOverride", type: "enum", options: ["cancel": "Cancel dimming", "jumpTo": "Jump to the end"], title: "When one of the dimmers is manually turned off…", description: "dimming will continue", required: false, multiple: false)
|
||||||
|
href(name: "toCompletionPage", title: "Completion Actions", page: "completionPage", state: completionHrefDescription() ? "complete" : "", description: completionHrefDescription() ?: "Set rules for what to do when dimming completes")
|
||||||
section {
|
|
||||||
href(name: "toCompletionPage", title: "Completion Actions (Optional)", page: "completionPage", state: completionHrefDescription() ? "complete" : "", description: completionHrefDescription())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
section {
|
section {
|
||||||
// TODO: fancy label
|
// TODO: fancy label
|
||||||
label(title: "Label this SmartApp", required: false, defaultValue: "")
|
label(title: "Label This SmartApp", required: false, defaultValue: "", description: "Highly recommended", submitOnChange: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def controllerExplanationPage() {
|
||||||
|
dynamicPage(name: "controllerExplanationPage", title: "How To Control Gentle Wake Up") {
|
||||||
|
|
||||||
|
section("With other SmartApps", hideable: true, hidden: false) {
|
||||||
|
paragraph "When this SmartApp is installed, it will create a controller device which you can use in other SmartApps for even more customizable automation!"
|
||||||
|
paragraph "The controller acts like a switch so any SmartApp that can control a switch can control Gentle Wake Up, too!"
|
||||||
|
paragraph "Routines and 'Smart Lighting' are great ways to automate Gentle Wake Up."
|
||||||
|
}
|
||||||
|
|
||||||
|
section("More about the controller", hideable: true, hidden: true) {
|
||||||
|
paragraph "You can find the controller with your other 'Things'. It will look like this."
|
||||||
|
image "http://f.cl.ly/items/2O0v0h41301U14042z3i/GentleWakeUpController-tile-stopped.png"
|
||||||
|
paragraph "You can start and stop Gentle Wake up by tapping the control on the right."
|
||||||
|
image "http://f.cl.ly/items/3W323J3M1b3K0k0V3X3a/GentleWakeUpController-tile-running.png"
|
||||||
|
paragraph "If you look at the device details screen, you will find even more information about Gentle Wake Up and more fine grain controls."
|
||||||
|
image "http://f.cl.ly/items/291s3z2I2Q0r2q0x171H/GentleWakeUpController-richTile-stopped.png"
|
||||||
|
paragraph "The slider allows you to jump to any point in the dimming process. Think of it as a percentage. If Gentle Wake Up is set to dim down as you fall asleep, but your book is just too good to put down; simply drag the slider to the left and Gentle Wake Up will give you more time to finish your chapter and drift off to sleep."
|
||||||
|
image "http://f.cl.ly/items/0F0N2G0S3v1q0L0R3J3Y/GentleWakeUpController-richTile-running.png"
|
||||||
|
paragraph "In the lower left, you will see the amount of time remaining in the dimming cycle. It does not count down evenly. Instead, it will update whenever the slider is updated; typically every 6-18 seconds depending on the duration of your dimming cycle."
|
||||||
|
paragraph "Of course, you may also tap the middle to start or stop the dimming cycle at any time."
|
||||||
|
}
|
||||||
|
|
||||||
|
section("Starting and stopping the SmartApp itself", hideable: true, hidden: true) {
|
||||||
|
paragraph "Tap the 'play' button on the SmartApp to start or stop dimming."
|
||||||
|
image "http://f.cl.ly/items/0R2u1Z2H30393z2I2V3S/GentleWakeUp-appTouch2.png"
|
||||||
|
}
|
||||||
|
|
||||||
|
section("Turning off devices while dimming", hideable: true, hidden: true) {
|
||||||
|
paragraph "It's best to use other Devices and SmartApps for triggering the Controller device. However, that isn't always an option."
|
||||||
|
paragraph "If you turn off a switch that is being dimmed, it will either continue to dim, stop dimming, or jump to the end of the dimming cycle depending on your settings."
|
||||||
|
paragraph "Unfortunately, some switches take a little time to turn off and may not finish turning off before Gentle Wake Up sets its dim level again. You may need to try a few times to get it to stop."
|
||||||
|
paragraph "That's why it's best to use devices that aren't currently dimming. Remember that you can use other SmartApps to toggle the controller. :)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
def numbersPage() {
|
def numbersPage() {
|
||||||
dynamicPage(name:"numbersPage", title:"") {
|
dynamicPage(name:"numbersPage", title:"") {
|
||||||
|
|
||||||
@@ -128,24 +166,33 @@ def endLevelLabel() {
|
|||||||
return "${endLevel}%"
|
return "${endLevel}%"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def weekdays() {
|
||||||
|
["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]
|
||||||
|
}
|
||||||
|
|
||||||
|
def weekends() {
|
||||||
|
["Saturday", "Sunday"]
|
||||||
|
}
|
||||||
|
|
||||||
def schedulingPage() {
|
def schedulingPage() {
|
||||||
dynamicPage(name: "schedulingPage", title: "Rules For Automatically Dimming Your Lights") {
|
dynamicPage(name: "schedulingPage", title: "Rules For Automatically Dimming Your Lights") {
|
||||||
|
|
||||||
section {
|
section("Use Other SmartApps!") {
|
||||||
input(name: "days", type: "enum", title: "Allow Automatic Dimming On These Days", description: "Every day", required: false, multiple: true, options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"])
|
href(title: "Learn how to control Gentle Wake Up", page: "controllerExplanationPage", description: null)
|
||||||
}
|
}
|
||||||
|
|
||||||
section {
|
section("Allow Automatic Dimming") {
|
||||||
input(name: "modeStart", title: "Start when entering this mode", type: "mode", required: false, mutliple: false, submitOnChange: true)
|
input(name: "days", type: "enum", title: "On These Days", description: "Every day", required: false, multiple: true, options: weekdays() + weekends())
|
||||||
|
}
|
||||||
|
|
||||||
|
section("Start Dimming...") {
|
||||||
|
input(name: "startTime", type: "time", title: "At This Time", description: null, required: false)
|
||||||
|
input(name: "modeStart", title: "When Entering This Mode", type: "mode", required: false, mutliple: false, submitOnChange: true, description: null)
|
||||||
if (modeStart) {
|
if (modeStart) {
|
||||||
input(name: "modeStop", title: "Stop when leaving '${modeStart}' mode", type: "bool", required: false)
|
input(name: "modeStop", title: "Stop when leaving '${modeStart}' mode", type: "bool", required: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
section {
|
|
||||||
input(name: "startTime", type: "time", title: "Start Dimming At This Time", description: null, required: false)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,11 +241,16 @@ def updated() {
|
|||||||
log.debug "Updating 'Gentle Wake Up' with settings: ${settings}"
|
log.debug "Updating 'Gentle Wake Up' with settings: ${settings}"
|
||||||
unschedule()
|
unschedule()
|
||||||
|
|
||||||
|
def controller = getController()
|
||||||
|
if (controller) {
|
||||||
|
controller.label = app.label
|
||||||
|
}
|
||||||
|
|
||||||
initialize()
|
initialize()
|
||||||
}
|
}
|
||||||
|
|
||||||
private initialize() {
|
private initialize() {
|
||||||
stop()
|
stop("settingsChange")
|
||||||
|
|
||||||
if (startTime) {
|
if (startTime) {
|
||||||
log.debug "scheduling dimming routine to run at $startTime"
|
log.debug "scheduling dimming routine to run at $startTime"
|
||||||
@@ -209,15 +261,27 @@ private initialize() {
|
|||||||
subscribe(app, appHandler)
|
subscribe(app, appHandler)
|
||||||
|
|
||||||
subscribe(location, locationHandler)
|
subscribe(location, locationHandler)
|
||||||
|
|
||||||
|
if (manualOverride) {
|
||||||
|
subscribe(dimmers, "switch.off", stopDimmersHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!getAllChildDevices()) {
|
||||||
|
// create controller device and set name to the label used here
|
||||||
|
def dni = "${new Date().getTime()}"
|
||||||
|
log.debug "app.label: ${app.label}"
|
||||||
|
addChildDevice("smartthings", "Gentle Wake Up Controller", dni, null, ["label": app.label])
|
||||||
|
state.controllerDni = dni
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def appHandler(evt) {
|
def appHandler(evt) {
|
||||||
log.debug "appHandler evt: ${evt.value}"
|
log.debug "appHandler evt: ${evt.value}"
|
||||||
if (evt.value == "touch") {
|
if (evt.value == "touch") {
|
||||||
if (atomicState.running) {
|
if (atomicState.running) {
|
||||||
stop()
|
stop("appTouch")
|
||||||
} else {
|
} else {
|
||||||
start()
|
start("appTouch")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -233,26 +297,47 @@ def locationHandler(evt) {
|
|||||||
def modeStopIsTrue = (modeStop && modeStop != "false")
|
def modeStopIsTrue = (modeStop && modeStop != "false")
|
||||||
|
|
||||||
if (isSpecifiedMode && canStartAutomatically()) {
|
if (isSpecifiedMode && canStartAutomatically()) {
|
||||||
start()
|
start("modeChange")
|
||||||
} else if (!isSpecifiedMode && modeStopIsTrue) {
|
} else if (!isSpecifiedMode && modeStopIsTrue) {
|
||||||
stop()
|
stop("modeChange")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def stopDimmersHandler(evt) {
|
||||||
|
log.trace "stopDimmersHandler evt: ${evt.value}"
|
||||||
|
def percentComplete = completionPercentage()
|
||||||
|
// Often times, the first thing we do is turn lights on or off so make sure we don't stop as soon as we start
|
||||||
|
if (percentComplete > 2 && percentComplete < 98) {
|
||||||
|
if (manualOverride == "cancel") {
|
||||||
|
log.debug "STOPPING in stopDimmersHandler"
|
||||||
|
stop("manualOverride")
|
||||||
|
} else if (manualOverride == "jumpTo") {
|
||||||
|
def end = dynamicEndLevel()
|
||||||
|
log.debug "Jumping to 99% complete in stopDimmersHandler"
|
||||||
|
jumpTo(99)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
log.debug "not stopping in stopDimmersHandler"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ========================================================
|
// ========================================================
|
||||||
// Scheduling
|
// Scheduling
|
||||||
// ========================================================
|
// ========================================================
|
||||||
|
|
||||||
def scheduledStart() {
|
def scheduledStart() {
|
||||||
if (canStartAutomatically()) {
|
if (canStartAutomatically()) {
|
||||||
start()
|
start("schedule")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def start() {
|
public def start(source) {
|
||||||
log.trace "START"
|
log.trace "START"
|
||||||
|
|
||||||
|
sendStartEvent(source)
|
||||||
|
|
||||||
setLevelsInState()
|
setLevelsInState()
|
||||||
|
|
||||||
atomicState.running = true
|
atomicState.running = true
|
||||||
@@ -263,9 +348,11 @@ def start() {
|
|||||||
increment()
|
increment()
|
||||||
}
|
}
|
||||||
|
|
||||||
def stop() {
|
public def stop(source) {
|
||||||
log.trace "STOP"
|
log.trace "STOP"
|
||||||
|
|
||||||
|
sendStopEvent(source)
|
||||||
|
|
||||||
atomicState.running = false
|
atomicState.running = false
|
||||||
atomicState.start = 0
|
atomicState.start = 0
|
||||||
|
|
||||||
@@ -282,6 +369,110 @@ private healthCheck() {
|
|||||||
increment()
|
increment()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========================================================
|
||||||
|
// Controller
|
||||||
|
// ========================================================
|
||||||
|
|
||||||
|
def sendStartEvent(source) {
|
||||||
|
log.trace "sendStartEvent(${source})"
|
||||||
|
def eventData = [
|
||||||
|
name: "sessionStatus",
|
||||||
|
value: "running",
|
||||||
|
descriptionText: "${app.label} has started dimming",
|
||||||
|
displayed: true,
|
||||||
|
linkText: app.label,
|
||||||
|
isStateChange: true
|
||||||
|
]
|
||||||
|
if (source == "modeChange") {
|
||||||
|
eventData.descriptionText += " because of a mode change"
|
||||||
|
} else if (source == "schedule") {
|
||||||
|
eventData.descriptionText += " as scheduled"
|
||||||
|
} else if (source == "appTouch") {
|
||||||
|
eventData.descriptionText += " because you pressed play on the app"
|
||||||
|
} else if (source == "controller") {
|
||||||
|
eventData.descriptionText += " because you pressed play on the controller"
|
||||||
|
}
|
||||||
|
|
||||||
|
sendControllerEvent(eventData)
|
||||||
|
}
|
||||||
|
|
||||||
|
def sendStopEvent(source) {
|
||||||
|
log.trace "sendStopEvent(${source})"
|
||||||
|
def eventData = [
|
||||||
|
name: "sessionStatus",
|
||||||
|
value: "stopped",
|
||||||
|
descriptionText: "${app.label} has stopped dimming",
|
||||||
|
displayed: true,
|
||||||
|
linkText: app.label,
|
||||||
|
isStateChange: true
|
||||||
|
]
|
||||||
|
if (source == "modeChange") {
|
||||||
|
eventData.descriptionText += " because of a mode change"
|
||||||
|
eventData.value += "cancelled"
|
||||||
|
} else if (source == "schedule") {
|
||||||
|
eventData.descriptionText = "${app.label} has finished dimming"
|
||||||
|
} else if (source == "appTouch") {
|
||||||
|
eventData.descriptionText += " because you pressed play on the app"
|
||||||
|
eventData.value += "cancelled"
|
||||||
|
} else if (source == "controller") {
|
||||||
|
eventData.descriptionText += " because you pressed stop on the controller"
|
||||||
|
eventData.value += "cancelled"
|
||||||
|
} else if (source == "settingsChange") {
|
||||||
|
eventData.descriptionText += " because the settings have changed"
|
||||||
|
eventData.value += "cancelled"
|
||||||
|
} else if (source == "manualOverride") {
|
||||||
|
eventData.descriptionText += " because the dimmer was manually turned off"
|
||||||
|
eventData.value += "cancelled"
|
||||||
|
}
|
||||||
|
|
||||||
|
sendControllerEvent(eventData)
|
||||||
|
sendTimeRemainingEvent(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
def sendTimeRemainingEvent(percentComplete) {
|
||||||
|
log.trace "sendTimeRemainingEvent(${percentComplete})"
|
||||||
|
|
||||||
|
def percentCompleteEventData = [
|
||||||
|
name: "percentComplete",
|
||||||
|
value: percentComplete as int,
|
||||||
|
displayed: true,
|
||||||
|
isStateChange: true
|
||||||
|
]
|
||||||
|
sendControllerEvent(percentCompleteEventData)
|
||||||
|
|
||||||
|
def duration = sanitizeInt(duration, 30)
|
||||||
|
def timeRemaining = duration - (duration * (percentComplete / 100))
|
||||||
|
def timeRemainingEventData = [
|
||||||
|
name: "timeRemaining",
|
||||||
|
value: displayableTime(timeRemaining),
|
||||||
|
displayed: true,
|
||||||
|
isStateChange: true
|
||||||
|
]
|
||||||
|
sendControllerEvent(timeRemainingEventData)
|
||||||
|
}
|
||||||
|
|
||||||
|
def sendControllerEvent(eventData) {
|
||||||
|
def controller = getController()
|
||||||
|
if (controller) {
|
||||||
|
controller.controllerEvent(eventData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def getController() {
|
||||||
|
def dni = state.controllerDni
|
||||||
|
if (!dni) {
|
||||||
|
log.warn "no controller dni"
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
def controller = getChildDevice(dni)
|
||||||
|
if (!controller) {
|
||||||
|
log.warn "no controller"
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
log.debug "controller: ${controller}"
|
||||||
|
return controller
|
||||||
|
}
|
||||||
|
|
||||||
// ========================================================
|
// ========================================================
|
||||||
// Setting levels
|
// Setting levels
|
||||||
// ========================================================
|
// ========================================================
|
||||||
@@ -349,6 +540,8 @@ def updateDimmers(percentComplete) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sendTimeRemainingEvent(percentComplete)
|
||||||
}
|
}
|
||||||
|
|
||||||
int dynamicLevel(dimmer, percentComplete) {
|
int dynamicLevel(dimmer, percentComplete) {
|
||||||
@@ -377,7 +570,7 @@ private completion() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
stop()
|
stop("schedule")
|
||||||
|
|
||||||
handleCompletionSwitches()
|
handleCompletionSwitches()
|
||||||
|
|
||||||
@@ -385,6 +578,7 @@ private completion() {
|
|||||||
|
|
||||||
handleCompletionModesAndPhrases()
|
handleCompletionModesAndPhrases()
|
||||||
|
|
||||||
|
sendTimeRemainingEvent(100)
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleCompletionSwitches() {
|
private handleCompletionSwitches() {
|
||||||
@@ -493,22 +687,65 @@ def completionPercentage() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
int now = new Date().getTime()
|
def now = new Date().getTime()
|
||||||
int diff = now - atomicState.start
|
def timeElapsed = now - atomicState.start
|
||||||
int totalRunTime = totalRunTimeMillis()
|
def totalRunTime = totalRunTimeMillis()
|
||||||
int percentOfRunTime = (diff / totalRunTime) * 100
|
def percentComplete = timeElapsed / totalRunTime * 100
|
||||||
log.debug "percentOfRunTime: ${percentOfRunTime}"
|
log.debug "percentComplete: ${percentComplete}"
|
||||||
|
|
||||||
percentOfRunTime
|
return percentComplete
|
||||||
}
|
}
|
||||||
|
|
||||||
int totalRunTimeMillis() {
|
int totalRunTimeMillis() {
|
||||||
int minutes = sanitizeInt(duration, 30)
|
int minutes = sanitizeInt(duration, 30)
|
||||||
|
convertToMillis(minutes)
|
||||||
|
}
|
||||||
|
|
||||||
|
int convertToMillis(minutes) {
|
||||||
def seconds = minutes * 60
|
def seconds = minutes * 60
|
||||||
def millis = seconds * 1000
|
def millis = seconds * 1000
|
||||||
return millis as int
|
return millis
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def timeRemaining(percentComplete) {
|
||||||
|
def normalizedPercentComplete = percentComplete / 100
|
||||||
|
def duration = sanitizeInt(duration, 30)
|
||||||
|
def timeElapsed = duration * normalizedPercentComplete
|
||||||
|
def timeRemaining = duration - timeElapsed
|
||||||
|
return timeRemaining
|
||||||
|
}
|
||||||
|
|
||||||
|
int millisToEnd(percentComplete) {
|
||||||
|
convertToMillis(timeRemaining(percentComplete))
|
||||||
|
}
|
||||||
|
|
||||||
|
String displayableTime(timeRemaining) {
|
||||||
|
def timeString = "${timeRemaining}"
|
||||||
|
def parts = timeString.split(/\./)
|
||||||
|
if (!parts.size()) {
|
||||||
|
return "0:00"
|
||||||
|
}
|
||||||
|
def minutes = parts[0]
|
||||||
|
if (parts.size() == 1) {
|
||||||
|
return "${minutes}:00"
|
||||||
|
}
|
||||||
|
def fraction = "0.${parts[1]}" as double
|
||||||
|
def seconds = "${60 * fraction as int}".padRight(2, "0")
|
||||||
|
return "${minutes}:${seconds}"
|
||||||
|
}
|
||||||
|
|
||||||
|
def jumpTo(percentComplete) {
|
||||||
|
def millisToEnd = millisToEnd(percentComplete)
|
||||||
|
def endTime = new Date().getTime() + millisToEnd
|
||||||
|
def duration = sanitizeInt(duration, 30)
|
||||||
|
def durationMillis = convertToMillis(duration)
|
||||||
|
def shiftedStart = endTime - durationMillis
|
||||||
|
atomicState.start = shiftedStart
|
||||||
|
updateDimmers(percentComplete)
|
||||||
|
sendTimeRemainingEvent(percentComplete)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
int dynamicEndLevel() {
|
int dynamicEndLevel() {
|
||||||
if (usesOldSettings()) {
|
if (usesOldSettings()) {
|
||||||
if (direction && direction == "Down") {
|
if (direction && direction == "Down") {
|
||||||
@@ -673,7 +910,13 @@ def schedulingHrefDescription() {
|
|||||||
|
|
||||||
def descriptionParts = []
|
def descriptionParts = []
|
||||||
if (days) {
|
if (days) {
|
||||||
descriptionParts << "On ${fancyString(days)},"
|
if (days == weekdays()) {
|
||||||
|
descriptionParts << "On weekdays,"
|
||||||
|
} else if (days == weekends()) {
|
||||||
|
descriptionParts << "On weekends,"
|
||||||
|
} else {
|
||||||
|
descriptionParts << "On ${fancyString(days)},"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
descriptionParts << "${fancyDeviceString(dimmers)} will start dimming"
|
descriptionParts << "${fancyDeviceString(dimmers)} will start dimming"
|
||||||
@@ -759,15 +1002,15 @@ def completionHrefDescription() {
|
|||||||
|
|
||||||
def numbersPageHrefDescription() {
|
def numbersPageHrefDescription() {
|
||||||
def title = "All dimmers will dim for ${duration ?: '30'} minutes from ${startLevelLabel()} to ${endLevelLabel()}"
|
def title = "All dimmers will dim for ${duration ?: '30'} minutes from ${startLevelLabel()} to ${endLevelLabel()}"
|
||||||
if (colorize) {
|
if (colorize) {
|
||||||
def colorDimmers = dimmersWithSetColorCommand()
|
def colorDimmers = dimmersWithSetColorCommand()
|
||||||
if (colorDimmers == dimmers) {
|
if (colorDimmers == dimmers) {
|
||||||
title += " and will gradually change color."
|
title += " and will gradually change color."
|
||||||
} else {
|
} else {
|
||||||
title += ".\n${fancyDeviceString(colorDimmers)} will gradually change color."
|
title += ".\n${fancyDeviceString(colorDimmers)} will gradually change color."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return title
|
return title
|
||||||
}
|
}
|
||||||
|
|
||||||
def hueSatToHex(h, s) {
|
def hueSatToHex(h, s) {
|
||||||
|
|||||||
@@ -258,8 +258,8 @@ def installed() {
|
|||||||
|
|
||||||
def updated() {
|
def updated() {
|
||||||
log.trace "Updated with settings: ${settings}"
|
log.trace "Updated with settings: ${settings}"
|
||||||
unschedule()
|
|
||||||
unsubscribe()
|
unsubscribe()
|
||||||
|
unschedule()
|
||||||
initialize()
|
initialize()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,6 +325,7 @@ def addBulbs() {
|
|||||||
} else {
|
} else {
|
||||||
d = addChildDevice("smartthings", "Hue Bulb", dni, newHueBulb?.value.hub, ["label":newHueBulb?.value.name])
|
d = addChildDevice("smartthings", "Hue Bulb", dni, newHueBulb?.value.hub, ["label":newHueBulb?.value.name])
|
||||||
}
|
}
|
||||||
|
log.debug "created ${d.displayName} with id $dni"
|
||||||
} else {
|
} else {
|
||||||
log.debug "$dni in not longer paired to the Hue Bridge or ID changed"
|
log.debug "$dni in not longer paired to the Hue Bridge or ID changed"
|
||||||
}
|
}
|
||||||
@@ -333,8 +334,6 @@ def addBulbs() {
|
|||||||
newHueBulb = bulbs.find { (app.id + "/" + it.id) == dni }
|
newHueBulb = bulbs.find { (app.id + "/" + it.id) == dni }
|
||||||
d = addChildDevice("smartthings", "Hue Bulb", dni, newHueBulb?.hub, ["label":newHueBulb?.name])
|
d = addChildDevice("smartthings", "Hue Bulb", dni, newHueBulb?.hub, ["label":newHueBulb?.name])
|
||||||
}
|
}
|
||||||
|
|
||||||
log.debug "created ${d.displayName} with id $dni"
|
|
||||||
d.refresh()
|
d.refresh()
|
||||||
} else {
|
} else {
|
||||||
log.debug "found ${d.displayName} with id $dni already exists, type: '$d.typeName'"
|
log.debug "found ${d.displayName} with id $dni already exists, type: '$d.typeName'"
|
||||||
@@ -775,4 +774,4 @@ private Boolean hasAllHubsOver(String desiredFirmware) {
|
|||||||
|
|
||||||
private List getRealHubFirmwareVersions() {
|
private List getRealHubFirmwareVersions() {
|
||||||
return location.hubs*.firmwareVersionString.findAll { it }
|
return location.hubs*.firmwareVersionString.findAll { it }
|
||||||
}
|
}
|
||||||
@@ -592,7 +592,7 @@ def updated() {
|
|||||||
// log.debug "External Id=${app.id}:${member.id}"
|
// log.debug "External Id=${app.id}:${member.id}"
|
||||||
|
|
||||||
// create the device
|
// create the device
|
||||||
def childDevice = addChildDevice("smartthings", "life360-user", "${app.id}.${member.id}",null,[name:member.firstName, completedSetup: true])
|
def childDevice = addChildDevice("smartthings", "Life360 User", "${app.id}.${member.id}",null,[name:member.firstName, completedSetup: true])
|
||||||
// childDevice.setMemberId(member.id)
|
// childDevice.setMemberId(member.id)
|
||||||
|
|
||||||
if (childDevice)
|
if (childDevice)
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ preferences {
|
|||||||
section("Via a push notification and/or an SMS message"){
|
section("Via a push notification and/or an SMS message"){
|
||||||
input("recipients", "contact", title: "Send notifications to") {
|
input("recipients", "contact", title: "Send notifications to") {
|
||||||
input "phone", "phone", title: "Phone Number (for SMS, optional)", required: false
|
input "phone", "phone", title: "Phone Number (for SMS, optional)", required: false
|
||||||
|
paragraph "If outside the US please make sure to enter the proper country code"
|
||||||
input "pushAndPhone", "enum", title: "Both Push and SMS?", required: false, options: ["Yes", "No"]
|
input "pushAndPhone", "enum", title: "Both Push and SMS?", required: false, options: ["Yes", "No"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,24 +10,24 @@
|
|||||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
* 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.
|
* for the specific language governing permissions and limitations under the License.
|
||||||
*
|
*
|
||||||
* Sonos Control
|
* Speaker Control
|
||||||
*
|
*
|
||||||
* Author: SmartThings
|
* Author: SmartThings
|
||||||
*
|
*
|
||||||
* Date: 2013-12-10
|
* Date: 2013-12-10
|
||||||
*/
|
*/
|
||||||
definition(
|
definition(
|
||||||
name: "Sonos Control",
|
name: "Speaker Control",
|
||||||
namespace: "smartthings",
|
namespace: "smartthings",
|
||||||
author: "SmartThings",
|
author: "SmartThings",
|
||||||
description: "Play or pause your Sonos when certain actions take place in your home.",
|
description: "Play or pause your Speaker when certain actions take place in your home.",
|
||||||
category: "SmartThings Labs",
|
category: "SmartThings Labs",
|
||||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos.png",
|
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos.png",
|
||||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos@2x.png"
|
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos@2x.png"
|
||||||
)
|
)
|
||||||
|
|
||||||
preferences {
|
preferences {
|
||||||
page(name: "mainPage", title: "Control your Sonos when something happens", install: true, uninstall: true)
|
page(name: "mainPage", title: "Control your Speaker when something happens", install: true, uninstall: true)
|
||||||
page(name: "timeIntervalInput", title: "Only during a certain time") {
|
page(name: "timeIntervalInput", title: "Only during a certain time") {
|
||||||
section {
|
section {
|
||||||
input "starting", "time", title: "Starting", required: false
|
input "starting", "time", title: "Starting", required: false
|
||||||
@@ -81,7 +81,7 @@ def mainPage() {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
section {
|
section {
|
||||||
input "sonos", "capability.musicPlayer", title: "Sonos music player", required: true
|
input "sonos", "capability.musicPlayer", title: "Speaker music player", required: true
|
||||||
}
|
}
|
||||||
section("More options", hideable: true, hidden: true) {
|
section("More options", hideable: true, hidden: true) {
|
||||||
input "volume", "number", title: "Set the volume volume", description: "0-100%", required: false
|
input "volume", "number", title: "Set the volume volume", description: "0-100%", required: false
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
* 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.
|
* for the specific language governing permissions and limitations under the License.
|
||||||
*
|
*
|
||||||
* Sonos Mood Music
|
* Speaker Mood Music
|
||||||
*
|
*
|
||||||
* Author: SmartThings
|
* Author: SmartThings
|
||||||
* Date: 2014-02-12
|
* Date: 2014-02-12
|
||||||
@@ -65,7 +65,7 @@ private saveSelectedSong() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
definition(
|
definition(
|
||||||
name: "Sonos Mood Music",
|
name: "Speaker Mood Music",
|
||||||
namespace: "smartthings",
|
namespace: "smartthings",
|
||||||
author: "SmartThings",
|
author: "SmartThings",
|
||||||
description: "Plays a selected song or station.",
|
description: "Plays a selected song or station.",
|
||||||
@@ -75,7 +75,7 @@ definition(
|
|||||||
)
|
)
|
||||||
|
|
||||||
preferences {
|
preferences {
|
||||||
page(name: "mainPage", title: "Play a selected song or station on your Sonos when something happens", nextPage: "chooseTrack", uninstall: true)
|
page(name: "mainPage", title: "Play a selected song or station on your Speaker when something happens", nextPage: "chooseTrack", uninstall: true)
|
||||||
page(name: "chooseTrack", title: "Select a song", install: true)
|
page(name: "chooseTrack", title: "Select a song", install: true)
|
||||||
page(name: "timeIntervalInput", title: "Only during a certain time") {
|
page(name: "timeIntervalInput", title: "Only during a certain time") {
|
||||||
section {
|
section {
|
||||||
@@ -125,7 +125,7 @@ def mainPage() {
|
|||||||
ifUnset "timeOfDay", "time", title: "At a Scheduled Time", required: false
|
ifUnset "timeOfDay", "time", title: "At a Scheduled Time", required: false
|
||||||
}
|
}
|
||||||
section {
|
section {
|
||||||
input "sonos", "capability.musicPlayer", title: "On this Sonos player", required: true
|
input "sonos", "capability.musicPlayer", title: "On this Speaker player", required: true
|
||||||
}
|
}
|
||||||
section("More options", hideable: true, hidden: true) {
|
section("More options", hideable: true, hidden: true) {
|
||||||
input "volume", "number", title: "Set the volume", description: "0-100%", required: false
|
input "volume", "number", title: "Set the volume", description: "0-100%", required: false
|
||||||
@@ -10,23 +10,23 @@
|
|||||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
* 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.
|
* for the specific language governing permissions and limitations under the License.
|
||||||
*
|
*
|
||||||
* Sonos Custom Message
|
* Speaker Custom Message
|
||||||
*
|
*
|
||||||
* Author: SmartThings
|
* Author: SmartThings
|
||||||
* Date: 2014-1-29
|
* Date: 2014-1-29
|
||||||
*/
|
*/
|
||||||
definition(
|
definition(
|
||||||
name: "Sonos Notify with Sound",
|
name: "Speaker Notify with Sound",
|
||||||
namespace: "smartthings",
|
namespace: "smartthings",
|
||||||
author: "SmartThings",
|
author: "SmartThings",
|
||||||
description: "Play a sound or custom message through your Sonos when the mode changes or other events occur.",
|
description: "Play a sound or custom message through your Speaker when the mode changes or other events occur.",
|
||||||
category: "SmartThings Labs",
|
category: "SmartThings Labs",
|
||||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos.png",
|
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos.png",
|
||||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos@2x.png"
|
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos@2x.png"
|
||||||
)
|
)
|
||||||
|
|
||||||
preferences {
|
preferences {
|
||||||
page(name: "mainPage", title: "Play a message on your Sonos when something happens", install: true, uninstall: true)
|
page(name: "mainPage", title: "Play a message on your Speaker when something happens", install: true, uninstall: true)
|
||||||
page(name: "chooseTrack", title: "Select a song or station")
|
page(name: "chooseTrack", title: "Select a song or station")
|
||||||
page(name: "timeIntervalInput", title: "Only during a certain time") {
|
page(name: "timeIntervalInput", title: "Only during a certain time") {
|
||||||
section {
|
section {
|
||||||
@@ -75,8 +75,8 @@ def mainPage() {
|
|||||||
ifUnset "timeOfDay", "time", title: "At a Scheduled Time", required: false
|
ifUnset "timeOfDay", "time", title: "At a Scheduled Time", required: false
|
||||||
}
|
}
|
||||||
section{
|
section{
|
||||||
input "actionType", "enum", title: "Action?", required: true, defaultValue: "Custom Message", options: [
|
input "actionType", "enum", title: "Action?", required: true, defaultValue: "Bell 1", options: [
|
||||||
"Custom Message",
|
//"Custom Message",
|
||||||
"Bell 1",
|
"Bell 1",
|
||||||
"Bell 2",
|
"Bell 2",
|
||||||
"Dogs Barking",
|
"Dogs Barking",
|
||||||
@@ -89,10 +89,10 @@ def mainPage() {
|
|||||||
"Someone is arriving",
|
"Someone is arriving",
|
||||||
"Piano",
|
"Piano",
|
||||||
"Lightsaber"]
|
"Lightsaber"]
|
||||||
input "message","text",title:"Play this message", required:false, multiple: false
|
//input "message","text",title:"Play this message", required:false, multiple: false
|
||||||
}
|
}
|
||||||
section {
|
section {
|
||||||
input "sonos", "capability.musicPlayer", title: "On this Sonos player", required: true
|
input "sonos", "capability.musicPlayer", title: "On this Speaker player", required: true
|
||||||
}
|
}
|
||||||
section("More options", hideable: true, hidden: true) {
|
section("More options", hideable: true, hidden: true) {
|
||||||
input "resumePlaying", "bool", title: "Resume currently playing music after notification", required: false, defaultValue: true
|
input "resumePlaying", "bool", title: "Resume currently playing music after notification", required: false, defaultValue: true
|
||||||
@@ -409,12 +409,13 @@ private loadText() {
|
|||||||
state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/lightsaber.mp3", duration: "10"]
|
state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/lightsaber.mp3", duration: "10"]
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
if (message) {
|
/*if (message) {
|
||||||
state.sound = textToSpeech(message instanceof List ? message[0] : message) // not sure why this is (sometimes) needed)
|
state.sound = textToSpeech(message instanceof List ? message[0] : message) // not sure why this is (sometimes) needed)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
state.sound = textToSpeech("You selected the custom message option but did not enter a message in the $app.label Smart App")
|
state.sound = textToSpeech("You selected the custom message option but did not enter a message in the $app.label Smart App")
|
||||||
}
|
}*/
|
||||||
|
state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/bell1.mp3", duration: "10"]
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10,23 +10,23 @@
|
|||||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
* 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.
|
* for the specific language governing permissions and limitations under the License.
|
||||||
*
|
*
|
||||||
* Sonos Weather Forecast
|
* Speaker Weather Forecast
|
||||||
*
|
*
|
||||||
* Author: SmartThings
|
* Author: SmartThings
|
||||||
* Date: 2014-1-29
|
* Date: 2014-1-29
|
||||||
*/
|
*/
|
||||||
definition(
|
definition(
|
||||||
name: "Sonos Weather Forecast",
|
name: "Speaker Weather Forecast",
|
||||||
namespace: "smartthings",
|
namespace: "smartthings",
|
||||||
author: "SmartThings",
|
author: "SmartThings",
|
||||||
description: "Play a weather report through your Sonos when the mode changes or other events occur",
|
description: "Play a weather report through your Speaker when the mode changes or other events occur",
|
||||||
category: "SmartThings Labs",
|
category: "SmartThings Labs",
|
||||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos.png",
|
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos.png",
|
||||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos@2x.png"
|
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos@2x.png"
|
||||||
)
|
)
|
||||||
|
|
||||||
preferences {
|
preferences {
|
||||||
page(name: "mainPage", title: "Play the weather report on your sonos", install: true, uninstall: true)
|
page(name: "mainPage", title: "Play the weather report on your speaker", install: true, uninstall: true)
|
||||||
page(name: "chooseTrack", title: "Select a song or station")
|
page(name: "chooseTrack", title: "Select a song or station")
|
||||||
page(name: "timeIntervalInput", title: "Only during a certain time") {
|
page(name: "timeIntervalInput", title: "Only during a certain time") {
|
||||||
section {
|
section {
|
||||||
@@ -85,7 +85,7 @@ def mainPage() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
section {
|
section {
|
||||||
input "sonos", "capability.musicPlayer", title: "On this Sonos player", required: true
|
input "sonos", "capability.musicPlayer", title: "On this Speaker player", required: true
|
||||||
}
|
}
|
||||||
section("More options", hideable: true, hidden: true) {
|
section("More options", hideable: true, hidden: true) {
|
||||||
input "resumePlaying", "bool", title: "Resume currently playing music after weather report finishes", required: false, defaultValue: true
|
input "resumePlaying", "bool", title: "Resume currently playing music after weather report finishes", required: false, defaultValue: true
|
||||||
@@ -78,7 +78,7 @@ def firstPage()
|
|||||||
def motionsDiscovered = motionsDiscovered()
|
def motionsDiscovered = motionsDiscovered()
|
||||||
def lightSwitchesDiscovered = lightSwitchesDiscovered()
|
def lightSwitchesDiscovered = lightSwitchesDiscovered()
|
||||||
|
|
||||||
return dynamicPage(name:"firstPage", title:"Discovery Started!", nextPage:"", refreshInterval: refreshInterval, install:true, uninstall: selectedSwitches != null || selectedMotions != null || selectedLightSwitches != null) {
|
return dynamicPage(name:"firstPage", title:"Discovery Started!", nextPage:"", refreshInterval: refreshInterval, install:true, uninstall: true) {
|
||||||
section("Select a device...") {
|
section("Select a device...") {
|
||||||
input "selectedSwitches", "enum", required:false, title:"Select Wemo Switches \n(${switchesDiscovered.size() ?: 0} found)", multiple:true, options:switchesDiscovered
|
input "selectedSwitches", "enum", required:false, title:"Select Wemo Switches \n(${switchesDiscovered.size() ?: 0} found)", multiple:true, options:switchesDiscovered
|
||||||
input "selectedMotions", "enum", required:false, title:"Select Wemo Motions \n(${motionsDiscovered.size() ?: 0} found)", multiple:true, options:motionsDiscovered
|
input "selectedMotions", "enum", required:false, title:"Select Wemo Motions \n(${motionsDiscovered.size() ?: 0} found)", multiple:true, options:motionsDiscovered
|
||||||
|
|||||||
Reference in New Issue
Block a user