mirror of
https://github.com/mtan93/SmartThingsPublic.git
synced 2026-03-16 13:10:51 +00:00
Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d69abb64bd | ||
|
|
7429ecc83b | ||
|
|
112a35f5db | ||
|
|
c297564665 | ||
|
|
26ab32565b | ||
|
|
9733947fea | ||
|
|
6abf8c7f20 | ||
|
|
fe505ddc9f | ||
|
|
f4034f5ccf | ||
|
|
c1c2431299 | ||
|
|
39f0c49ea6 | ||
|
|
ed5a409c63 | ||
|
|
8453292038 | ||
|
|
e98a04a1b4 | ||
|
|
41e95b9248 | ||
|
|
63f20c912d | ||
|
|
837d2d0cfd | ||
|
|
629c4cc231 | ||
|
|
f12684565c | ||
|
|
51e727b91a | ||
|
|
49a858eb5c | ||
|
|
112a4087b0 | ||
|
|
132d8fc9d8 | ||
|
|
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 | ||
|
|
96f2c5ed8b | ||
|
|
dcfc16cf12 |
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,153 @@
|
|||||||
|
/**
|
||||||
|
* 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleReportAttributeMessage(String description) {
|
||||||
|
def descMap = zigbee.parseDescriptionAsMap(description)
|
||||||
|
|
||||||
|
if (descMap.clusterInt == 0x0001 && descMap.attrInt == 0x0020) {
|
||||||
|
handleBatteryEvent(Integer.parseInt(descMap.value, 16))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleBatteryEvent(rawValue) {
|
||||||
|
def linkText = getLinkText(device)
|
||||||
|
|
||||||
|
def eventMap = [
|
||||||
|
name: 'battery',
|
||||||
|
value: '--'
|
||||||
|
]
|
||||||
|
|
||||||
|
def volts = rawValue / 10
|
||||||
|
if (volts > 0){
|
||||||
|
def minVolts = 2.0
|
||||||
|
def maxVolts = 2.8
|
||||||
|
|
||||||
|
if (volts < minVolts)
|
||||||
|
volts = minVolts
|
||||||
|
else if (volts > maxVolts)
|
||||||
|
volts = maxVolts
|
||||||
|
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
||||||
|
|
||||||
|
eventMap.value = Math.round(pct * 100)
|
||||||
|
eventMap.descriptionText = "${linkText} battery was ${eventMap.value}%"
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug "Creating battery event: ${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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -67,7 +67,7 @@ metadata {
|
|||||||
state "setpoint", action:"raiseSetpoint", 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"
|
||||||
@@ -99,7 +99,7 @@ metadata {
|
|||||||
}
|
}
|
||||||
|
|
||||||
preferences {
|
preferences {
|
||||||
input "holdType", "enum", title: "Hold Type", description: "When changing temperature, use Temporary or Permanent hold (default)", required: false, options:["Temporary", "Permanent"]
|
input "holdType", "enum", title: "Hold Type", description: "When changing temperature, use Temporary (Until next transition) or Permanent hold (default)", required: false, options:["Temporary", "Permanent"]
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ metadata {
|
|||||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3315-S", deviceJoinName: "Water Leak Sensor"
|
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3315-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 {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ metadata {
|
|||||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3305"
|
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "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 {
|
||||||
@@ -352,4 +353,4 @@ private byte[] reverseArray(byte[] array) {
|
|||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
return array
|
return array
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3320"
|
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "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"
|
||||||
}
|
}
|
||||||
@@ -203,8 +204,10 @@ private List parseReportAttributeMessage(String description) {
|
|||||||
}
|
}
|
||||||
result << getAccelerationResult(descMap.value)
|
result << getAccelerationResult(descMap.value)
|
||||||
}
|
}
|
||||||
else if (descMap.cluster == "FC02" && descMap.attrId == "0012") {
|
else if (descMap.cluster == "FC02" && descMap.attrId == "0012" && descMap.value.size() == 24) {
|
||||||
result << 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") {
|
||||||
result << getBatteryResult(Integer.parseInt(descMap.value, 16))
|
result << getBatteryResult(Integer.parseInt(descMap.value, 16))
|
||||||
@@ -370,33 +373,50 @@ def getTemperature(value) {
|
|||||||
|
|
||||||
def refresh() {
|
def refresh() {
|
||||||
log.debug "Refreshing Values "
|
log.debug "Refreshing Values "
|
||||||
def refreshCmds = [
|
|
||||||
|
|
||||||
/* sensitivity - default value (8) */
|
def refreshCmds = []
|
||||||
|
|
||||||
"zcl mfg-code 0x104E", "delay 200",
|
if (device.getDataValue("manufacturer") == "SmartThings") {
|
||||||
"zcl global write 0xFC02 0 0x20 {02}", "delay 200",
|
|
||||||
"send 0x${device.deviceNetworkId} 1 1", "delay 400",
|
log.debug "Refreshing Values for manufacturer: SmartThings "
|
||||||
|
refreshCmds = refreshCmds + [
|
||||||
|
|
||||||
"st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200",
|
/* These values of Motion Threshold Multiplier(01) and Motion Threshold (7602)
|
||||||
"st rattr 0x${device.deviceNetworkId} 1 1 0x20", "delay 200",
|
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 0x104E", "delay 200",
|
"zcl mfg-code ${manufacturerCode}", "delay 200",
|
||||||
"zcl global read 0xFC02 0x0010",
|
"zcl global write 0xFC02 0 0x20 {01}", "delay 200",
|
||||||
"send 0x${device.deviceNetworkId} 1 1","delay 400",
|
"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",
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
"zcl mfg-code 0x104E", "delay 200",
|
} else {
|
||||||
"zcl global read 0xFC02 0x0012",
|
refreshCmds = refreshCmds + [
|
||||||
"send 0x${device.deviceNetworkId} 1 1","delay 400",
|
|
||||||
|
/* 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",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
"zcl mfg-code 0x104E", "delay 200",
|
//Common refresh commands
|
||||||
"zcl global read 0xFC02 0x0013",
|
refreshCmds = refreshCmds + [
|
||||||
"send 0x${device.deviceNetworkId} 1 1","delay 400",
|
"st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200",
|
||||||
|
"st rattr 0x${device.deviceNetworkId} 1 1 0x20", "delay 200",
|
||||||
"zcl mfg-code 0x104E", "delay 200",
|
|
||||||
"zcl global read 0xFC02 0x0014",
|
"zcl mfg-code ${manufacturerCode}", "delay 200",
|
||||||
"send 0x${device.deviceNetworkId} 1 1", "delay 400"
|
"zcl global read 0xFC02 0x0010",
|
||||||
]
|
"send 0x${device.deviceNetworkId} 1 1","delay 400"
|
||||||
|
]
|
||||||
|
|
||||||
return refreshCmds + enrollResponse()
|
return refreshCmds + enrollResponse()
|
||||||
}
|
}
|
||||||
@@ -420,19 +440,19 @@ def getTemperature(value) {
|
|||||||
"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}",
|
||||||
"zcl global send-me-a-report 0xFC02 0x0010 0x18 10 3600 {01}",
|
"zcl global send-me-a-report 0xFC02 0x0010 0x18 10 3600 {01}",
|
||||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||||
|
|
||||||
"zcl mfg-code 0x104E",
|
"zcl mfg-code ${manufacturerCode}",
|
||||||
"zcl global send-me-a-report 0xFC02 0x0012 0x29 1 3600 {01}",
|
"zcl global send-me-a-report 0xFC02 0x0012 0x29 1 3600 {01}",
|
||||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||||
|
|
||||||
"zcl mfg-code 0x104E",
|
"zcl mfg-code ${manufacturerCode}",
|
||||||
"zcl global send-me-a-report 0xFC02 0x0013 0x29 1 3600 {01}",
|
"zcl global send-me-a-report 0xFC02 0x0013 0x29 1 3600 {01}",
|
||||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||||
|
|
||||||
"zcl mfg-code 0x104E",
|
"zcl mfg-code ${manufacturerCode}",
|
||||||
"zcl global send-me-a-report 0xFC02 0x0014 0x29 1 3600 {01}",
|
"zcl global send-me-a-report 0xFC02 0x0014 0x29 1 3600 {01}",
|
||||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500"
|
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500"
|
||||||
|
|
||||||
@@ -458,35 +478,34 @@ def enrollResponse() {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
@@ -530,6 +549,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)
|
||||||
}
|
}
|
||||||
@@ -556,3 +583,4 @@ private byte[] reverseArray(byte[] array) {
|
|||||||
return array
|
return array
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ metadata {
|
|||||||
|
|
||||||
tiles {
|
tiles {
|
||||||
|
|
||||||
valueTile("power", "device.power") {
|
valueTile("power", "device.power", canChangeIcon: true) {
|
||||||
state "power", label: '${currentValue} W'
|
state "power", label: '${currentValue} W'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
@@ -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} {}"
|
||||||
}
|
}
|
||||||
|
|||||||
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()
|
||||||
|
}
|
||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
@@ -90,7 +90,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)
|
||||||
}
|
}
|
||||||
@@ -218,7 +218,7 @@ def setAccessKey() {
|
|||||||
atomicState.grokerSubdomain = "$newGrokerSubdomain"
|
atomicState.grokerSubdomain = "$newGrokerSubdomain"
|
||||||
atomicState.isBucketCreated = false
|
atomicState.isBucketCreated = false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newAccessKey && newAccessKey != atomicState.accessKey) {
|
if (newAccessKey && newAccessKey != atomicState.accessKey) {
|
||||||
atomicState.accessKey = "$newAccessKey"
|
atomicState.accessKey = "$newAccessKey"
|
||||||
atomicState.isBucketCreated = false
|
atomicState.isBucketCreated = false
|
||||||
@@ -262,7 +262,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"
|
||||||
@@ -330,57 +330,57 @@ def genericHandler(evt) {
|
|||||||
// This is a handler function for flushing the event buffer
|
// This is a handler function for flushing the event buffer
|
||||||
// after a specified amount of time to reduce the load on ST servers
|
// after a specified amount of time to reduce the load on ST servers
|
||||||
def flushBuffer() {
|
def flushBuffer() {
|
||||||
|
def eventBuffer = atomicState.eventBuffer
|
||||||
log.trace "About to flush the buffer on schedule"
|
log.trace "About to flush the buffer on schedule"
|
||||||
if (atomicState.eventBuffer != null && atomicState.eventBuffer.size() > 0) {
|
if (eventBuffer != null && eventBuffer.size() > 0) {
|
||||||
tryShipEvents()
|
atomicState.eventBuffer = []
|
||||||
|
tryShipEvents(eventBuffer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def eventHandler(name, value) {
|
def eventHandler(name, value) {
|
||||||
log.debug atomicState.eventBuffer
|
|
||||||
|
|
||||||
def eventBuffer = atomicState.eventBuffer
|
|
||||||
def epoch = now() / 1000
|
def epoch = now() / 1000
|
||||||
|
def eventBuffer = atomicState.eventBuffer ?: []
|
||||||
// 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"]
|
eventBuffer << [key: "$name", value: "$value", epoch: "$epoch"]
|
||||||
|
|
||||||
log.debug eventBuffer
|
|
||||||
|
|
||||||
atomicState.eventBuffer = eventBuffer
|
|
||||||
|
|
||||||
if (eventBuffer.size() >= 10) {
|
if (eventBuffer.size() >= 10) {
|
||||||
tryShipEvents()
|
// Clear eventBuffer right away since we've already pulled it off of atomicState to reduce the risk of missing
|
||||||
|
// events. This assumes the grokerSubdomain, accessKey, and bucketKey are set correctly to avoid the eventBuffer
|
||||||
|
// from growing unbounded.
|
||||||
|
atomicState.eventBuffer = []
|
||||||
|
tryShipEvents(eventBuffer)
|
||||||
|
} else {
|
||||||
|
// Make sure we persist the updated eventBuffer with the new event added back to atomicState
|
||||||
|
atomicState.eventBuffer = eventBuffer
|
||||||
}
|
}
|
||||||
|
log.debug "Event added to buffer: " + eventBuffer
|
||||||
}
|
}
|
||||||
|
|
||||||
// a helper function for shipping the atomicState.eventBuffer to Initial State
|
// a helper function for shipping the atomicState.eventBuffer to Initial State
|
||||||
def tryShipEvents() {
|
def tryShipEvents(eventBuffer) {
|
||||||
|
|
||||||
|
def grokerSubdomain = atomicState.grokerSubdomain
|
||||||
// can't ship events if there is no grokerSubdomain
|
// 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: eventBuffer
|
||||||
]
|
]
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -389,13 +389,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 |
@@ -1,4 +1,15 @@
|
|||||||
/**
|
/**
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
* Ecobee Service Manager
|
* Ecobee Service Manager
|
||||||
*
|
*
|
||||||
* Author: scott
|
* Author: scott
|
||||||
|
|||||||
@@ -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.
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,7 +89,7 @@ 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 Speaker player", required: true
|
input "sonos", "capability.musicPlayer", title: "On this Speaker player", required: 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user