mirror of
https://github.com/mtan93/SmartThingsPublic.git
synced 2026-04-27 06:06:19 +01:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7669bec0bc | |||
| 3a7abd6169 | |||
| 7def620f04 | |||
| 5b0b239caa | |||
| 3ea70fecad | |||
| f420907043 | |||
| bf915b49dc | |||
| 1c2a65e313 | |||
| 075fdf0974 |
+62
-17
@@ -371,21 +371,50 @@ def getTemperature(value) {
|
||||
|
||||
def refresh() {
|
||||
log.debug "Refreshing Values "
|
||||
def refreshCmds = [
|
||||
|
||||
/* sensitivity - default value (8) */
|
||||
def refreshCmds = []
|
||||
|
||||
"zcl mfg-code ${manufacturerCode}", "delay 200",
|
||||
"zcl global write 0xFC02 0 0x20 {02}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 400",
|
||||
if (device.getDataValue("manufacturer") == "SmartThings") {
|
||||
|
||||
log.debug "Refreshing Values for manufacturer: SmartThings "
|
||||
refreshCmds = refreshCmds + [
|
||||
|
||||
"st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200",
|
||||
"st rattr 0x${device.deviceNetworkId} 1 1 0x20", "delay 200",
|
||||
/* These values of Motion Threshold Multiplier(01) and Motion Threshold (D200)
|
||||
seem to be giving pretty accurate results for the XYZ co-ordinates for this manufacturer.
|
||||
Separating these out in a separate if-else because I do not want to touch Centralite part
|
||||
as of now.
|
||||
*/
|
||||
|
||||
"zcl mfg-code ${manufacturerCode}", "delay 200",
|
||||
"zcl global read 0xFC02 0x0010",
|
||||
"send 0x${device.deviceNetworkId} 1 1","delay 400"
|
||||
]
|
||||
"zcl mfg-code ${manufacturerCode}", "delay 200",
|
||||
"zcl global write 0xFC02 0 0x20 {01}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 400",
|
||||
|
||||
"zcl mfg-code ${manufacturerCode}", "delay 200",
|
||||
"zcl global write 0xFC02 2 0x21 {D200}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 400",
|
||||
|
||||
]
|
||||
|
||||
|
||||
} else {
|
||||
refreshCmds = refreshCmds + [
|
||||
|
||||
/* sensitivity - default value (8) */
|
||||
"zcl mfg-code ${manufacturerCode}", "delay 200",
|
||||
"zcl global write 0xFC02 0 0x20 {02}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 400",
|
||||
]
|
||||
}
|
||||
|
||||
//Common refresh commands
|
||||
refreshCmds = refreshCmds + [
|
||||
"st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200",
|
||||
"st rattr 0x${device.deviceNetworkId} 1 1 0x20", "delay 200",
|
||||
|
||||
"zcl mfg-code ${manufacturerCode}", "delay 200",
|
||||
"zcl global read 0xFC02 0x0010",
|
||||
"send 0x${device.deviceNetworkId} 1 1","delay 400"
|
||||
]
|
||||
|
||||
return refreshCmds + enrollResponse()
|
||||
}
|
||||
@@ -461,19 +490,34 @@ private Map parseAxis(String description) {
|
||||
xyzResults.x = signedX
|
||||
log.debug "X Part: ${signedX}"
|
||||
}
|
||||
// Y and the Z axes are interchanged between SmartThings's implementation and Centralite's implementation
|
||||
else if (part.startsWith("13")) {
|
||||
def unsignedY = hexToInt(part.split("13")[1].trim())
|
||||
def signedY = unsignedY > 32767 ? unsignedY - 65536 : unsignedY
|
||||
xyzResults.y = signedY
|
||||
log.debug "Y Part: ${signedY}"
|
||||
if (device.getDataValue("manufacturer") == "SmartThings") {
|
||||
xyzResults.z = -signedY
|
||||
log.debug "Z Part: ${xyzResults.z}"
|
||||
if (garageSensor == "Yes")
|
||||
garageEvent(xyzResults.z)
|
||||
}
|
||||
else {
|
||||
xyzResults.y = signedY
|
||||
log.debug "Y Part: ${signedY}"
|
||||
}
|
||||
}
|
||||
else if (part.startsWith("14")) {
|
||||
def unsignedZ = hexToInt(part.split("14")[1].trim())
|
||||
def signedZ = unsignedZ > 32767 ? unsignedZ - 65536 : unsignedZ
|
||||
xyzResults.z = signedZ
|
||||
log.debug "Z Part: ${signedZ}"
|
||||
if (garageSensor == "Yes")
|
||||
garageEvent(signedZ)
|
||||
if (device.getDataValue("manufacturer") == "SmartThings") {
|
||||
xyzResults.y = signedZ
|
||||
log.debug "Y Part: ${signedZ}"
|
||||
} else {
|
||||
xyzResults.z = signedZ
|
||||
log.debug "Z Part: ${signedZ}"
|
||||
if (garageSensor == "Yes")
|
||||
garageEvent(signedZ)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -553,3 +597,4 @@ private byte[] reverseArray(byte[] array) {
|
||||
return array
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
metadata {
|
||||
definition (name: "Timevalve Smart", namespace: "timevalve.gaslock.t-08", author: "ruinnel") {
|
||||
capability "Valve"
|
||||
capability "Refresh"
|
||||
capability "Battery"
|
||||
capability "Temperature Measurement"
|
||||
|
||||
command "setRemaining"
|
||||
command "setTimeout"
|
||||
command "setTimeout10"
|
||||
command "setTimeout20"
|
||||
command "setTimeout30"
|
||||
command "setTimeout40"
|
||||
|
||||
command "remainingLevel"
|
||||
|
||||
attribute "remaining", "number"
|
||||
attribute "remainingText", "String"
|
||||
attribute "timeout", "number"
|
||||
|
||||
//raw desc : 0 0 0x1006 0 0 0 7 0x5E 0x86 0x72 0x5A 0x73 0x98 0x80
|
||||
//fingerprint deviceId:"0x1006", inClusters:"0x5E, 0x86, 0x72, 0x5A, 0x73, 0x98, 0x80"
|
||||
}
|
||||
|
||||
tiles (scale: 2) {
|
||||
multiAttributeTile(name:"statusTile", type:"generic", width:6, height:4) {
|
||||
tileAttribute("device.contact", key: "PRIMARY_CONTROL") {
|
||||
attributeState "open", label: '${name}', action: "close", icon:"st.contact.contact.open", backgroundColor:"#ffa81e"
|
||||
attributeState "closed", label:'${name}', action: "", icon:"st.contact.contact.closed", backgroundColor:"#79b821"
|
||||
}
|
||||
tileAttribute("device.remainingText", key: "SECONDARY_CONTROL") {
|
||||
attributeState "open", label: '${currentValue}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e"
|
||||
attributeState "closed", label:'', icon:"st.contact.contact.closed", backgroundColor:"#79b821"
|
||||
}
|
||||
}
|
||||
|
||||
standardTile("refreshTile", "command.refresh", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
controlTile("remainingSliderTile", "device.remaining", "slider", inactiveLabel: false, range:"(0..590)", height: 2, width: 4) {
|
||||
state "level", action:"remainingLevel"
|
||||
}
|
||||
valueTile("setRemaining", "device.remainingText", inactiveLabel: false, decoration: "flat", height: 2, width: 2){
|
||||
state "remainingText", label:'${currentValue}\nRemaining'//, action: "setRemaining"//, icon: "st.Office.office6"
|
||||
}
|
||||
|
||||
standardTile("setTimeout10", "device.remaining", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:'10Min', action: "setTimeout10", icon:"st.Health & Wellness.health7", defaultState: true
|
||||
state "10", label:'10Min', action: "setTimeout10", icon:"st.Office.office13"
|
||||
}
|
||||
standardTile("setTimeout20", "device.remaining", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:'20Min', action: "setTimeout20", icon:"st.Health & Wellness.health7", defaultState: true
|
||||
state "20", label:'20Min', action: "setTimeout20", icon:"st.Office.office13"
|
||||
}
|
||||
standardTile("setTimeout30", "device.remaining", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:'30Min', action: "setTimeout30", icon:"st.Health & Wellness.health7", defaultState: true
|
||||
state "30", label:'30Min', action: "setTimeout30", icon:"st.Office.office13"
|
||||
}
|
||||
standardTile("setTimeout40", "device.remaining", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:'40Min', action: "setTimeout40", icon:"st.Health & Wellness.health7", defaultState: true
|
||||
state "40", label:'40Min', action: "setTimeout40", icon:"st.Office.office13"
|
||||
}
|
||||
|
||||
valueTile("batteryTile", "device.battery", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||
state "battery", label:'${currentValue}% battery', unit:""
|
||||
}
|
||||
|
||||
main (["statusTile"])
|
||||
// details (["statusTile", "remainingSliderTile", "setRemaining", "setTimeout10", "setTimeout20", "batteryTile", "refreshTile", "setTimeout30", "setTimeout40"])
|
||||
// details (["statusTile", "batteryTile", "setRemaining", "refreshTile"])
|
||||
details (["statusTile", "batteryTile", "refreshTile"])
|
||||
}
|
||||
}
|
||||
|
||||
def parse(description) {
|
||||
// log.debug "parse - " + description
|
||||
def result = null
|
||||
if (description.startsWith("Err 106")) {
|
||||
state.sec = 0
|
||||
result = createEvent(descriptionText: description, isStateChange: true)
|
||||
} else if (description != "updated") {
|
||||
def cmd = zwave.parse(description, [0x20: 1, 0x25: 1, 0x70: 1, 0x71: 1, 0x98: 1])
|
||||
if (cmd) {
|
||||
log.debug "parsed cmd = " + cmd
|
||||
result = zwaveEvent(cmd)
|
||||
//log.debug("'$description' parsed to $result")
|
||||
} else {
|
||||
log.debug("Couldn't zwave.parse '$description'")
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// 복호화 후 zwaveEvent() 호출
|
||||
def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) {
|
||||
//log.debug "SecurityMessageEncapsulation - " + cmd
|
||||
def encapsulatedCommand = cmd.encapsulatedCommand([0x20: 1, 0x25: 1, 0x70: 1, 0x71: 1, 0x98: 1])
|
||||
if (encapsulatedCommand) {
|
||||
state.sec = 1
|
||||
log.debug "encapsulatedCommand = " + encapsulatedCommand
|
||||
zwaveEvent(encapsulatedCommand)
|
||||
}
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) {
|
||||
//log.debug "switch status - " + cmd.value
|
||||
createEvent(name:"contact", value: cmd.value ? "open" : "closed")
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) {
|
||||
def map = [ name: "battery", unit: "%" ]
|
||||
if (cmd.batteryLevel == 0xFF) { // Special value for low battery alert
|
||||
map.value = 1
|
||||
map.descriptionText = "${device.displayName} has a low battery"
|
||||
map.isStateChange = true
|
||||
} else {
|
||||
map.value = cmd.batteryLevel
|
||||
}
|
||||
|
||||
log.debug "battery - ${map.value}${map.unit}"
|
||||
// Store time of last battery update so we don't ask every wakeup, see WakeUpNotification handler
|
||||
state.lastbatt = new Date().time
|
||||
createEvent(map)
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.Command cmd) {
|
||||
//log.debug "zwaveEvent - ${device.displayName}: ${cmd}"
|
||||
createEvent(descriptionText: "${device.displayName}: ${cmd}")
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.configurationv1.ConfigurationReport cmd) {
|
||||
def result = []
|
||||
log.info "zwave.configurationV1.configurationGet - " + cmd
|
||||
def array = cmd.configurationValue
|
||||
def value = ( (array[0] * 0x1000000) + (array[1] * 0x10000) + (array[2] * 0x100) + array[3] ).intdiv(60)
|
||||
if (device.currentValue("contact") == "open") {
|
||||
value = ( (array[0] * 0x1000000) + (array[1] * 0x10000) + (array[2] * 0x100) + array[3] ).intdiv(60)
|
||||
} else {
|
||||
value = 0
|
||||
}
|
||||
|
||||
if (device.currentValue('contact') == 'open') {
|
||||
def hour = value.intdiv(60);
|
||||
def min = (value % 60).toString().padLeft(2, '0');
|
||||
def text = "${hour}:${min}M"
|
||||
|
||||
log.info "remain - " + text
|
||||
result.add( createEvent(name: "remaining", value: value, displayed: false, isStateChange: true) )
|
||||
result.add( createEvent(name: "remainingText", value: text, displayed: false, isStateChange: true) )
|
||||
} else {
|
||||
result.add( createEvent(name: "timeout", value: value, displayed: false, isStateChange: true) )
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) {
|
||||
def type = cmd.notificationType
|
||||
if (type == cmd.NOTIFICATION_TYPE_HEAT) {
|
||||
log.info "NotificationReport - ${type}"
|
||||
createEvent(name: "temperature", value: 999, unit: "C", descriptionText: "${device.displayName} is over heat!", displayed: true, isStateChange: true)
|
||||
}
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.alarmv1.AlarmReport cmd) {
|
||||
def type = cmd.alarmType
|
||||
def level = cmd.alarmLevel
|
||||
|
||||
log.info "AlarmReport - type : ${type}, level : ${level}"
|
||||
def msg = "${device.displayName} is over heat!"
|
||||
def result = createEvent(name: "temperature", value: 999, unit: "C", descriptionText: msg, displayed: true, isStateChange: true)
|
||||
if (sendPushMessage) {
|
||||
sendPushMessage(msg)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// remote open not allow
|
||||
def open() {}
|
||||
|
||||
def close() {
|
||||
// log.debug 'cmd - close()'
|
||||
commands([
|
||||
zwave.switchBinaryV1.switchBinarySet(switchValue: 0x00),
|
||||
zwave.switchBinaryV1.switchBinaryGet()
|
||||
])
|
||||
}
|
||||
|
||||
def setTimeout10() { setTimeout(10) }
|
||||
def setTimeout20() { setTimeout(20) }
|
||||
def setTimeout30() { setTimeout(30) }
|
||||
def setTimeout40() { setTimeout(40) }
|
||||
|
||||
|
||||
def setTimeout(value) {
|
||||
// log.debug "setDefaultTime($value)"
|
||||
commands([
|
||||
zwave.configurationV1.configurationSet(parameterNumber: 0x01, size: 4, scaledConfigurationValue: value * 60),
|
||||
zwave.configurationV1.configurationGet(parameterNumber: 0x01)
|
||||
]);
|
||||
}
|
||||
|
||||
def remainingLevel(value) {
|
||||
// log.debug "remainingLevel($value)"
|
||||
def hour = value.intdiv(60);
|
||||
def min = (value % 60).toString().padLeft(2, '0');
|
||||
def text = "${hour}:${min}M"
|
||||
sendEvent(name: "remaining", value: value, displayed: false, isStateChange: true)
|
||||
sendEvent(name: "remainingText", value: text, displayed: false, isStateChange: true)
|
||||
}
|
||||
|
||||
def setRemaining() {
|
||||
def remaining = device.currentValue("remaining")
|
||||
// log.debug "setConfiguration() - remaining : $remaining"
|
||||
commands([
|
||||
zwave.configurationV1.configurationSet(parameterNumber: 0x03, size: 4, scaledConfigurationValue: remaining * 60),
|
||||
zwave.configurationV1.configurationGet(parameterNumber: 0x03)
|
||||
]);
|
||||
}
|
||||
|
||||
private command(physicalgraph.zwave.Command cmd) {
|
||||
if (state.sec != 0 && !(cmd instanceof physicalgraph.zwave.commands.batteryv1.BatteryGet)) {
|
||||
log.debug "cmd = " + cmd + ", encapsulation"
|
||||
zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format()
|
||||
} else {
|
||||
log.debug "cmd = " + cmd + ", plain"
|
||||
cmd.format()
|
||||
}
|
||||
}
|
||||
|
||||
private commands(commands, delay=200) {
|
||||
delayBetween(commands.collect{ command(it) }, delay)
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
// log.debug 'cmd - refresh()'
|
||||
commands([
|
||||
zwave.batteryV1.batteryGet(),
|
||||
zwave.switchBinaryV1.switchBinaryGet(),
|
||||
zwave.configurationV1.configurationGet(parameterNumber: 0x01),
|
||||
zwave.configurationV1.configurationGet(parameterNumber: 0x03)
|
||||
], 400)
|
||||
}
|
||||
@@ -1,427 +1,164 @@
|
||||
/**
|
||||
* Nobody Home
|
||||
*
|
||||
* Author: brian@bevey.org, raychi@gmail.com
|
||||
* Date: 12/02/2015
|
||||
* Author: brian@bevey.org
|
||||
* Date: 12/19/14
|
||||
*
|
||||
* 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.
|
||||
* Monitors a set of presence detectors and triggers a mode change when everyone has left.
|
||||
* When everyone has left, sets mode to a new defined mode.
|
||||
* When at least one person returns home, set the mode back to a new defined mode.
|
||||
* When someone is home - or upon entering the home, their mode may change dependent on sunrise / sunset.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Monitors a set of presence sensors and trigger appropriate mode
|
||||
* based on configured modes and sunrise/sunset time.
|
||||
*
|
||||
* - When everyone is away [Away]
|
||||
* - When someone is home during the day [Home]
|
||||
* - When someone is home at the night [Night]
|
||||
*/
|
||||
|
||||
// ********** App related functions **********
|
||||
|
||||
// The definition provides metadata about the App to SmartThings.
|
||||
definition (
|
||||
name: "Nobody Home",
|
||||
namespace: "imbrianj",
|
||||
author: "brian@bevey.org",
|
||||
description: "Automatically set Away/Home/Night mode based on a set of presence sensors and sunrise/sunset time.",
|
||||
category: "Mode Magic",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
|
||||
iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png"
|
||||
definition(
|
||||
name: "Nobody Home",
|
||||
namespace: "imbrianj",
|
||||
author: "brian@bevey.org",
|
||||
description: "When everyone leaves, change mode. If at least one person home, switch mode based on sun position.",
|
||||
category: "Mode Magic",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience%402x.png"
|
||||
)
|
||||
|
||||
// The preferences defines information the App needs from the user.
|
||||
preferences {
|
||||
section("Presence sensors to monitor") {
|
||||
input "people", "capability.presenceSensor", multiple: true
|
||||
}
|
||||
section("When all of these people leave home") {
|
||||
input "people", "capability.presenceSensor", multiple: true
|
||||
}
|
||||
|
||||
section("Mode setting") {
|
||||
input "newAwayMode", "mode", title: "Everyone is away"
|
||||
input "newSunriseMode", "mode", title: "Someone is home during the day"
|
||||
input "newSunsetMode", "mode", title: "Someone is home at night"
|
||||
}
|
||||
section("Change to this mode to...") {
|
||||
input "newAwayMode", "mode", title: "Everyone is away"
|
||||
input "newSunsetMode", "mode", title: "At least one person home and nightfall"
|
||||
input "newSunriseMode", "mode", title: "At least one person home and sunrise"
|
||||
}
|
||||
|
||||
section("Mode change delay (minutes)") {
|
||||
input "awayThreshold", "decimal", title: "Away delay [5m]", required: false
|
||||
input "arrivalThreshold", "decimal", title: "Arrival delay [2m]", required: false
|
||||
}
|
||||
section("Away threshold (defaults to 10 min)") {
|
||||
input "awayThreshold", "decimal", title: "Number of minutes", required: false
|
||||
}
|
||||
|
||||
section("Notifications") {
|
||||
input "sendPushMessage", "bool", title: "Push notification", required:false
|
||||
}
|
||||
section("Notifications") {
|
||||
input "sendPushMessage", "enum", title: "Send a push notification?", metadata:[values:["Yes","No"]], required:false
|
||||
}
|
||||
}
|
||||
|
||||
// called when the user installs the App
|
||||
def installed()
|
||||
{
|
||||
log.debug("installed() @${location.name}: ${settings}")
|
||||
initialize(true)
|
||||
def installed() {
|
||||
init()
|
||||
}
|
||||
|
||||
// called when the user installs the app, or changes the App
|
||||
// preference
|
||||
def updated()
|
||||
{
|
||||
log.debug("updated() @${location.name}: ${settings}")
|
||||
unsubscribe()
|
||||
initialize(false)
|
||||
def updated() {
|
||||
unsubscribe()
|
||||
init()
|
||||
}
|
||||
|
||||
def initialize(isInstall)
|
||||
{
|
||||
// subscribe to all the events we care about
|
||||
log.debug("Subscribing to events ...")
|
||||
def init() {
|
||||
subscribe(people, "presence", presence)
|
||||
subscribe(location, "sunrise", setSunrise)
|
||||
subscribe(location, "sunset", setSunset)
|
||||
|
||||
// thing to subscribe, attribute/state we care about, and callback fn
|
||||
subscribe(people, "presence", presenceHandler)
|
||||
subscribe(location, "sunrise", sunriseHandler)
|
||||
subscribe(location, "sunset", sunsetHandler)
|
||||
|
||||
// set the optional parameter values. these are not available
|
||||
// directly until the app has initialized (that is,
|
||||
// installed/updated has returned). so here we access them through
|
||||
// the settings object, as otherwise will get an exception.
|
||||
|
||||
// store information we need in state object so we can get access
|
||||
// to it later in our event handlers.
|
||||
|
||||
// calculate the away threshold in seconds. can't use the simpler
|
||||
// default falsy value, as value of 0 (no delay) is evaluated to
|
||||
// false (not specified), but we want 0 to represent no delay. so
|
||||
// we compare against null explicitly to see if the user has set a
|
||||
// value or not.
|
||||
if (settings.awayThreshold == null) {
|
||||
settings.awayThreshold = 5 // default away 5 minute
|
||||
}
|
||||
state.awayDelay = (int) settings.awayThreshold * 60
|
||||
log.debug("awayThreshold set to " + state.awayDelay + " second(s)")
|
||||
|
||||
if (settings.arrivalThreshold == null) {
|
||||
settings.arrivalThreshold = 2 // default arrival 2 minute
|
||||
}
|
||||
state.arrivalDelay = (int) settings.arrivalThreshold * 60
|
||||
log.debug("arrivalThreshold set to " + state.arrivalDelay + " second(s)")
|
||||
|
||||
// get push notification setting
|
||||
state.isPush = settings.sendPushMessage ? true : false
|
||||
log.debug("sendPushMessage set to " + state.isPush)
|
||||
|
||||
// on install (not update), figure out what mode we should be in
|
||||
// IF someone's home. This value is needed so that when a presence
|
||||
// sensor is triggered, we know what mode to set the system to, as
|
||||
// the sunrise/sunset event handler may not be triggered yet after
|
||||
// a fresh install.
|
||||
if (isInstall) {
|
||||
// TODO: for now, we simply assume daytime. a better approach
|
||||
// would be to figure out whether current time is day or
|
||||
// night, and set it appropriately. However there
|
||||
// doesn't seem to be a way to query this directly
|
||||
// without a zip code. This will become the correct
|
||||
// value at the next sunrise/sunset event.
|
||||
log.debug("No sun info yet, assuming daytime")
|
||||
state.modeIfHome = newSunriseMode
|
||||
|
||||
// set keep a separate sun mode state so we can show a more
|
||||
// helpful message when sun events are triggered.
|
||||
state.currentSunMode = "sunUnknown"
|
||||
|
||||
state.eventDevice = "" // last event device
|
||||
|
||||
// device that triggered timer. This is not necessarily the
|
||||
// eventDevice. For example, if A arrives, kick off timer,
|
||||
// then b arrives before timer elapsed, we want the
|
||||
// notification message to reference A, not B.
|
||||
state.timerDevice = null
|
||||
|
||||
// anything in flight? We use this to avoid scheduling
|
||||
// duplicate timers (so we don't extend the timer).
|
||||
state.pendingOp = "init"
|
||||
|
||||
// now set the correct mode for the location. This way, we
|
||||
// don't need to wait for the next sun/presence event.
|
||||
|
||||
// we schedule this action to run after the app has fully
|
||||
// initialized. This way, the app install is faster and the
|
||||
// user customized app name is used in the notification.
|
||||
runIn(7, "setInitialMode")
|
||||
}
|
||||
// On update, we don't change state.modeIfHome. This is so that we
|
||||
// preserve the current sun rise/set state we obtained in earlier
|
||||
// sunset/sunrise handler. This way the app remains in the correct
|
||||
// sun state when the user reconfigures it.
|
||||
state.sunMode = location.mode
|
||||
}
|
||||
|
||||
def setInitialMode()
|
||||
{
|
||||
changeSunMode(state.modeIfHome)
|
||||
state.pendingOp = null
|
||||
def setSunrise(evt) {
|
||||
changeSunMode(newSunriseMode)
|
||||
}
|
||||
|
||||
// ********** sunrise/sunset handling **********
|
||||
|
||||
// event handler when the sunrise time is reached
|
||||
def sunriseHandler(evt)
|
||||
{
|
||||
// we store the mode we should be in, IF someone's home
|
||||
state.modeIfHome = newSunriseMode
|
||||
state.currentSunMode = "sunRise"
|
||||
|
||||
// change mode if someone's home, otherwise set to away
|
||||
changeSunMode(newSunriseMode)
|
||||
def setSunset(evt) {
|
||||
changeSunMode(newSunsetMode)
|
||||
}
|
||||
|
||||
// event handler when the sunset time is reached
|
||||
def sunsetHandler(evt)
|
||||
{
|
||||
// we store the mode we should be in, IF someone's home
|
||||
state.modeIfHome = newSunsetMode
|
||||
state.currentSunMode = "sunSet"
|
||||
def changeSunMode(newMode) {
|
||||
state.sunMode = newMode
|
||||
|
||||
// change mode if someone's home, otherwise set to away
|
||||
changeSunMode(newSunsetMode)
|
||||
if(everyoneIsAway() && (location.mode == newAwayMode)) {
|
||||
log.debug("Mode is away, not evaluating")
|
||||
}
|
||||
|
||||
else if(location.mode != newMode) {
|
||||
def message = "${app.label} changed your mode to '${newMode}'"
|
||||
send(message)
|
||||
setLocationMode(newMode)
|
||||
}
|
||||
|
||||
else {
|
||||
log.debug("Mode is the same, not evaluating")
|
||||
}
|
||||
}
|
||||
|
||||
def changeSunMode(newMode)
|
||||
{
|
||||
// if everyone is away, we need to check and ensure the system is
|
||||
// in away mode.
|
||||
if (isEveryoneAway()) {
|
||||
// this shouldn not happen normally as the mode should already
|
||||
// be changed during presenceHandler, but in case this is not
|
||||
// done, such as when app is initially installed while away,
|
||||
// and system is not in away mode, then we toggle it to away
|
||||
// at the sun rise/set event.
|
||||
changeMode(newAwayMode, " because no one is present")
|
||||
} else {
|
||||
// someone is home, we update the mode depending on
|
||||
// sunrise/sunset.
|
||||
if (state.currentSunMode == "sunRise") {
|
||||
changeMode(newMode, " because it's sunrise")
|
||||
} else if (state.currentSunMode == "sunSet") {
|
||||
changeMode(newMode, " because it's sunset")
|
||||
} else {
|
||||
changeMode(newMode)
|
||||
}
|
||||
def presence(evt) {
|
||||
if(evt.value == "not present") {
|
||||
log.debug("Checking if everyone is away")
|
||||
|
||||
if(everyoneIsAway()) {
|
||||
log.info("Starting ${newAwayMode} sequence")
|
||||
def delay = (awayThreshold != null && awayThreshold != "") ? awayThreshold * 60 : 10 * 60
|
||||
runIn(delay, "setAway")
|
||||
}
|
||||
}
|
||||
|
||||
else {
|
||||
if(location.mode != state.sunMode) {
|
||||
log.debug("Checking if anyone is home")
|
||||
|
||||
if(anyoneIsHome()) {
|
||||
log.info("Starting ${state.sunMode} sequence")
|
||||
|
||||
changeSunMode(state.sunMode)
|
||||
}
|
||||
}
|
||||
|
||||
else {
|
||||
log.debug("Mode is the same, not evaluating")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ********** presence handling **********
|
||||
|
||||
// event handler when presence sensor changes state
|
||||
def presenceHandler(evt)
|
||||
{
|
||||
// get the device name that resulted in the change
|
||||
state.eventDevice= evt.device?.displayName
|
||||
|
||||
// is setInitialMode() still pending?
|
||||
if (state.pendingOp == "init") {
|
||||
log.debug("Pending ${state.pendingOp} op still in progress, ignoring presence event")
|
||||
return
|
||||
def setAway() {
|
||||
if(everyoneIsAway()) {
|
||||
if(location.mode != newAwayMode) {
|
||||
def message = "${app.label} changed your mode to '${newAwayMode}' because everyone left home"
|
||||
log.info(message)
|
||||
send(message)
|
||||
setLocationMode(newAwayMode)
|
||||
}
|
||||
|
||||
if (evt.value == "not present") {
|
||||
handleDeparture()
|
||||
} else {
|
||||
handleArrival()
|
||||
else {
|
||||
log.debug("Mode is the same, not evaluating")
|
||||
}
|
||||
}
|
||||
|
||||
else {
|
||||
log.info("Somebody returned home before we set to '${newAwayMode}'")
|
||||
}
|
||||
}
|
||||
|
||||
def handleDeparture()
|
||||
{
|
||||
log.info("${state.eventDevice} left ${location.name}")
|
||||
private everyoneIsAway() {
|
||||
def result = true
|
||||
|
||||
// do nothing if someone's still home
|
||||
if (!isEveryoneAway()) {
|
||||
log.info("Someone is still present, no actions needed")
|
||||
return
|
||||
}
|
||||
if(people.findAll { it?.currentPresence == "present" }) {
|
||||
result = false
|
||||
}
|
||||
|
||||
// Now we set away mode. We perform the following actions even if
|
||||
// home is already in away mode because an arrival timer may be
|
||||
// pending, and scheduling delaySetMode() has the nice effect of
|
||||
// canceling any previous pending timer, which is what we want to
|
||||
// do. So we do this even if delay is 0.
|
||||
log.info("Scheduling ${newAwayMode} mode in " + state.awayDelay + "s")
|
||||
state.pendingOp = "away"
|
||||
state.timerDevice = state.eventDevice
|
||||
// we always use runIn(). This has the benefit of automatically
|
||||
// replacing any pending arrival/away timer. if any arrival timer
|
||||
// is active, it will be clobbered with this away timer. If any
|
||||
// away timer is active, it will be extended with this new timeout
|
||||
// (though normally it should not happen)
|
||||
runIn(state.awayDelay, "delaySetMode")
|
||||
log.debug("everyoneIsAway: ${result}")
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
def handleArrival()
|
||||
{
|
||||
// someone returned home, set home/night mode after delay
|
||||
log.info("${state.eventDevice} arrived at ${location.name}")
|
||||
private anyoneIsHome() {
|
||||
def result = false
|
||||
|
||||
def numHome = isAnyoneHome()
|
||||
if (!numHome) {
|
||||
// no one home, do nothing for now (should NOT happen)
|
||||
log.warn("${deviceName} arrived, but isAnyoneHome() returned false!")
|
||||
return
|
||||
}
|
||||
if(people.findAll { it?.currentPresence == "present" }) {
|
||||
result = true
|
||||
}
|
||||
|
||||
if (numHome > 1) {
|
||||
// not the first one home, do nothing, as any action that
|
||||
// should happen would've happened when the first sensor
|
||||
// arrived. this is the opposite of isEveryoneAway() where we
|
||||
// don't do anything if someone's still home.
|
||||
log.debug("Someone is already present, no actions needed")
|
||||
return
|
||||
}
|
||||
log.debug("anyoneIsHome: ${result}")
|
||||
|
||||
// check if any pending arrival timer is already active. we want
|
||||
// the timer to trigger when the 1st person arrives, but not
|
||||
// extended when the 2nd person arrives later. this should not
|
||||
// happen because of the >1 check above, but just in case.
|
||||
if (state.pendingOp == "arrive") {
|
||||
log.debug("Pending ${state.pendingOp} op already in progress, do nothing")
|
||||
return
|
||||
}
|
||||
|
||||
// now we set home/night mode
|
||||
log.info("Scheduling ${state.modeIfHome} mode in " + state.arrivalDelay + "s")
|
||||
state.pendingOp = "arrive"
|
||||
state.timerDevice = state.eventDevice
|
||||
// if any away timer is active, it will be clobbered with
|
||||
// this arrival timer
|
||||
runIn(state.arrivalDelay, "delaySetMode")
|
||||
return result
|
||||
}
|
||||
|
||||
private send(msg) {
|
||||
if(sendPushMessage != "No") {
|
||||
log.debug("Sending push message")
|
||||
sendPush(msg)
|
||||
}
|
||||
|
||||
// ********** helper functions **********
|
||||
|
||||
// change the system to the new mode, unless its already in that mode.
|
||||
def changeMode(newMode, reason="")
|
||||
{
|
||||
if (location.mode != newMode) {
|
||||
// notification message
|
||||
def message = "${location.name} changed mode from '${location.mode}' to '${newMode}'" + reason
|
||||
setLocationMode(newMode)
|
||||
send(message) // send message after changing mode
|
||||
} else {
|
||||
log.debug("${location.name} is already in ${newMode} mode, no actions needed")
|
||||
}
|
||||
log.debug(msg)
|
||||
}
|
||||
|
||||
// create a useful departure/arrival reason string
|
||||
def reasonStr(isAway, delaySec, delayMin)
|
||||
{
|
||||
def reason
|
||||
|
||||
// if we are invoked by timer, use the stored timer trigger
|
||||
// device, otherwise use the last event device
|
||||
if (state.timerDevice) {
|
||||
reason = " because ${state.timerDevice} "
|
||||
} else {
|
||||
reason = " because ${state.eventDevice} "
|
||||
}
|
||||
|
||||
if (isAway) {
|
||||
reason += "left"
|
||||
} else {
|
||||
reason += "arrived"
|
||||
}
|
||||
|
||||
if (delaySec) {
|
||||
if (delaySec > 60) {
|
||||
if (delayMin == null) {
|
||||
delayMin = (int) delaySec / 60
|
||||
}
|
||||
reason += " ${delayMin} minutes ago"
|
||||
} else {
|
||||
reason += " ${delaySec}s ago"
|
||||
}
|
||||
}
|
||||
|
||||
return reason
|
||||
}
|
||||
|
||||
// http://docs.smartthings.com/en/latest/smartapp-developers-guide/scheduling.html#schedule-from-now
|
||||
//
|
||||
// By default, if a method is scheduled to run in the future, and then
|
||||
// another call to runIn with the same method is made, the last one
|
||||
// overwrites the previously scheduled method.
|
||||
//
|
||||
// We use the above property to schedule our arrval/departure delay
|
||||
// using the same function so we don't have to worry about
|
||||
// arrival/departure timer firing independently and complicating code.
|
||||
def delaySetMode()
|
||||
{
|
||||
def newMode = null
|
||||
def reason = ""
|
||||
|
||||
// timer has elapsed, check presence status to figure out what we
|
||||
// need to do
|
||||
if (isEveryoneAway()) {
|
||||
reason = reasonStr(true, state.awayDelay, awayThreshold)
|
||||
newMode = newAwayMode
|
||||
if (state.pendingOp) {
|
||||
log.debug("${state.pendingOp} timer elapsed: everyone is away")
|
||||
}
|
||||
} else {
|
||||
reason = reasonStr(false, state.arrivalDelay, arrivalThreshold)
|
||||
newMode = state.modeIfHome
|
||||
if (state.pendingOp) {
|
||||
log.debug("${state.pendingOp} timer elapsed: someone is home")
|
||||
}
|
||||
}
|
||||
|
||||
// now change the mode
|
||||
changeMode(newMode, reason);
|
||||
|
||||
state.pendingOp = null
|
||||
state.timerDevice = null
|
||||
}
|
||||
|
||||
private isEveryoneAway()
|
||||
{
|
||||
def result = true
|
||||
|
||||
if (people.findAll { it?.currentPresence == "present" }) {
|
||||
result = false
|
||||
}
|
||||
// log.debug("isEveryoneAway: ${result}")
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// return the number of people that are home
|
||||
private isAnyoneHome()
|
||||
{
|
||||
def result = 0
|
||||
// iterate over our people variable that we defined
|
||||
// in the preferences method
|
||||
for (person in people) {
|
||||
if (person.currentPresence == "present") {
|
||||
result++
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private send(msg)
|
||||
{
|
||||
if (state.isPush) {
|
||||
log.debug("Sending push notification")
|
||||
sendPush(msg)
|
||||
} else {
|
||||
log.debug("Sending notification")
|
||||
sendNotificationEvent(msg)
|
||||
}
|
||||
log.info(msg)
|
||||
}
|
||||
@@ -22,9 +22,9 @@ definition(
|
||||
author: "SmartThings & Joe Geiger",
|
||||
description: "Control your Bose® SoundTouch® when certain actions take place in your home.",
|
||||
category: "SmartThings Labs",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience%402x.png",
|
||||
iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience%402x.png"
|
||||
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 {
|
||||
|
||||
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 |
Reference in New Issue
Block a user