mirror of
https://github.com/mtan93/SmartThingsPublic.git
synced 2026-03-18 05:10:52 +00:00
Initial commit
This commit is contained in:
314
smartapps/smartthings/beacon-control.src/beacon-control.groovy
Normal file
314
smartapps/smartthings/beacon-control.src/beacon-control.groovy
Normal file
@@ -0,0 +1,314 @@
|
||||
/**
|
||||
* Beacon Control
|
||||
*
|
||||
* Copyright 2014 Physical Graph Corporation
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
definition(
|
||||
name: "Beacon Control",
|
||||
category: "SmartThings Internal",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Execute a Hello, Home phrase, turn on or off some lights, and/or lock or unlock your door when you enter or leave a monitored region",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/MiscHacking/mindcontrol.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/MiscHacking/mindcontrol@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
page(name: "mainPage")
|
||||
|
||||
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", install: true, uninstall: true) {
|
||||
|
||||
section("Where do you want to watch?") {
|
||||
input name: "beacons", type: "capability.beacon", title: "Select your beacon(s)",
|
||||
multiple: true, required: true
|
||||
}
|
||||
|
||||
section("Who do you want to watch for?") {
|
||||
input name: "phones", type: "device.mobilePresence", title: "Select your phone(s)",
|
||||
multiple: true, required: true
|
||||
}
|
||||
|
||||
section("What do you want to do on arrival?") {
|
||||
input name: "arrivalPhrase", type: "enum", title: "Execute a phrase",
|
||||
options: listPhrases(), required: false
|
||||
input "arrivalOnSwitches", "capability.switch", title: "Turn on some switches",
|
||||
multiple: true, required: false
|
||||
input "arrivalOffSwitches", "capability.switch", title: "Turn off some switches",
|
||||
multiple: true, required: false
|
||||
input "arrivalLocks", "capability.lock", title: "Unlock the door",
|
||||
multiple: true, required: false
|
||||
}
|
||||
|
||||
section("What do you want to do on departure?") {
|
||||
input name: "departPhrase", type: "enum", title: "Execute a phrase",
|
||||
options: listPhrases(), required: false
|
||||
input "departOnSwitches", "capability.switch", title: "Turn on some switches",
|
||||
multiple: true, required: false
|
||||
input "departOffSwitches", "capability.switch", title: "Turn off some switches",
|
||||
multiple: true, required: false
|
||||
input "departLocks", "capability.lock", title: "Lock the door",
|
||||
multiple: true, required: false
|
||||
}
|
||||
|
||||
section("Do you want to be notified?") {
|
||||
input "pushNotification", "bool", title: "Send a push notification"
|
||||
input "phone", "phone", title: "Send a text message", description: "Tap to enter phone number",
|
||||
required: false
|
||||
}
|
||||
|
||||
section {
|
||||
label title: "Give your automation a name", description: "e.g. Goodnight Home, Wake Up"
|
||||
}
|
||||
|
||||
def timeLabel = timeIntervalLabel()
|
||||
section(title: "More options", hidden: hideOptionsSection(), hideable: true) {
|
||||
href "timeIntervalInput", title: "Only during a certain time",
|
||||
description: timeLabel ?: "Tap to set", state: timeLabel ? "complete" : "incomplete"
|
||||
|
||||
input "days", "enum", title: "Only on certain days of the week", multiple: true, required: false,
|
||||
options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
|
||||
|
||||
input "modes", "mode", title: "Only when mode is", multiple: true, required: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Lifecycle management
|
||||
def installed() {
|
||||
log.debug "<beacon-control> Installed with settings: ${settings}"
|
||||
initialize()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "<beacon-control> Updated with settings: ${settings}"
|
||||
unsubscribe()
|
||||
initialize()
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
subscribe(beacons, "presence", beaconHandler)
|
||||
}
|
||||
|
||||
// Event handlers
|
||||
def beaconHandler(evt) {
|
||||
log.debug "<beacon-control> beaconHandler: $evt"
|
||||
|
||||
if (allOk) {
|
||||
def data = new groovy.json.JsonSlurper().parseText(evt.data)
|
||||
log.debug "<beacon-control> data: $data - phones: " + phones*.deviceNetworkId
|
||||
|
||||
def beaconName = getBeaconName(evt)
|
||||
log.debug "<beacon-control> beaconName: $beaconName"
|
||||
|
||||
def phoneName = getPhoneName(data)
|
||||
log.debug "<beacon-control> phoneName: $phoneName"
|
||||
if (phoneName != null) {
|
||||
def action = data.presence == "1" ? "arrived" : "left"
|
||||
def msg = "$phoneName has $action ${action == 'arrived' ? 'at ' : ''}the $beaconName"
|
||||
|
||||
if (action == "arrived") {
|
||||
msg = arriveActions(msg)
|
||||
}
|
||||
else if (action == "left") {
|
||||
msg = departActions(msg)
|
||||
}
|
||||
log.debug "<beacon-control> msg: $msg"
|
||||
|
||||
if (pushNotification || phone) {
|
||||
def options = [
|
||||
method: (pushNotification && phone) ? "both" : (pushNotification ? "push" : "sms"),
|
||||
phone: phone
|
||||
]
|
||||
sendNotification(msg, options)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helpers
|
||||
private arriveActions(msg) {
|
||||
if (arrivalPhrase || arrivalOnSwitches || arrivalOffSwitches || arrivalLocks) msg += ", so"
|
||||
|
||||
if (arrivalPhrase) {
|
||||
log.debug "<beacon-control> executing: $arrivalPhrase"
|
||||
executePhrase(arrivalPhrase)
|
||||
msg += " ${prefix('executed')} $arrivalPhrase."
|
||||
}
|
||||
if (arrivalOnSwitches) {
|
||||
log.debug "<beacon-control> turning on: $arrivalOnSwitches"
|
||||
arrivalOnSwitches.on()
|
||||
msg += " ${prefix('turned')} ${list(arrivalOnSwitches)} on."
|
||||
}
|
||||
if (arrivalOffSwitches) {
|
||||
log.debug "<beacon-control> turning off: $arrivalOffSwitches"
|
||||
arrivalOffSwitches.off()
|
||||
msg += " ${prefix('turned')} ${list(arrivalOffSwitches)} off."
|
||||
}
|
||||
if (arrivalLocks) {
|
||||
log.debug "<beacon-control> unlocking: $arrivalLocks"
|
||||
arrivalLocks.unlock()
|
||||
msg += " ${prefix('unlocked')} ${list(arrivalLocks)}."
|
||||
}
|
||||
msg
|
||||
}
|
||||
|
||||
private departActions(msg) {
|
||||
if (departPhrase || departOnSwitches || departOffSwitches || departLocks) msg += ", so"
|
||||
|
||||
if (departPhrase) {
|
||||
log.debug "<beacon-control> executing: $departPhrase"
|
||||
executePhrase(departPhrase)
|
||||
msg += " ${prefix('executed')} $departPhrase."
|
||||
}
|
||||
if (departOnSwitches) {
|
||||
log.debug "<beacon-control> turning on: $departOnSwitches"
|
||||
departOnSwitches.on()
|
||||
msg += " ${prefix('turned')} ${list(departOnSwitches)} on."
|
||||
}
|
||||
if (departOffSwitches) {
|
||||
log.debug "<beacon-control> turning off: $departOffSwitches"
|
||||
departOffSwitches.off()
|
||||
msg += " ${prefix('turned')} ${list(departOffSwitches)} off."
|
||||
}
|
||||
if (departLocks) {
|
||||
log.debug "<beacon-control> unlocking: $departLocks"
|
||||
departLocks.lock()
|
||||
msg += " ${prefix('locked')} ${list(departLocks)}."
|
||||
}
|
||||
msg
|
||||
}
|
||||
|
||||
private prefix(word) {
|
||||
def result
|
||||
def index = settings.prefixIndex == null ? 0 : settings.prefixIndex + 1
|
||||
switch (index) {
|
||||
case 0:
|
||||
result = "I $word"
|
||||
break
|
||||
case 1:
|
||||
result = "I also $word"
|
||||
break
|
||||
case 2:
|
||||
result = "And I $word"
|
||||
break
|
||||
default:
|
||||
result = "And $word"
|
||||
break
|
||||
}
|
||||
|
||||
settings.prefixIndex = index
|
||||
log.trace "prefix($word'): $result"
|
||||
result
|
||||
}
|
||||
|
||||
private listPhrases() {
|
||||
location.helloHome.getPhrases().label
|
||||
}
|
||||
|
||||
private executePhrase(phraseName) {
|
||||
if (phraseName) {
|
||||
location.helloHome.execute(phraseName)
|
||||
log.debug "<beacon-control> executed phrase: $phraseName"
|
||||
}
|
||||
}
|
||||
|
||||
private getBeaconName(evt) {
|
||||
def beaconName = beacons.find { b -> b.id == evt.deviceId }
|
||||
return beaconName
|
||||
}
|
||||
|
||||
private getPhoneName(data) {
|
||||
def phoneName = phones.find { phone ->
|
||||
// Work around DNI bug in data
|
||||
def pParts = phone.deviceNetworkId.split('\\|')
|
||||
def dParts = data.dni.split('\\|')
|
||||
pParts[0] == dParts[0]
|
||||
}
|
||||
return phoneName
|
||||
}
|
||||
|
||||
private hideOptionsSection() {
|
||||
(starting || ending || days || modes) ? false : true
|
||||
}
|
||||
|
||||
private getAllOk() {
|
||||
modeOk && daysOk && timeOk
|
||||
}
|
||||
|
||||
private getModeOk() {
|
||||
def result = !modes || modes.contains(location.mode)
|
||||
log.trace "<beacon-control> 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 "<beacon-control> 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 "<beacon-control> 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") : ""
|
||||
}
|
||||
|
||||
private list(List names) {
|
||||
switch (names.size()) {
|
||||
case 0:
|
||||
return null
|
||||
case 1:
|
||||
return names[0]
|
||||
case 2:
|
||||
return "${names[0]} and ${names[1]}"
|
||||
default:
|
||||
return "${names[0..-2].join(', ')}, and ${names[-1]}"
|
||||
}
|
||||
}
|
||||
54
smartapps/smartthings/big-turn-off.src/big-turn-off.groovy
Normal file
54
smartapps/smartthings/big-turn-off.src/big-turn-off.groovy
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Big Turn OFF
|
||||
*
|
||||
* Author: SmartThings
|
||||
*/
|
||||
definition(
|
||||
name: "Big Turn OFF",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Turn your lights off when the SmartApp is tapped or activated",
|
||||
category: "Convenience",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/light_outlet.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/light_outlet@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("When I touch the app, turn off...") {
|
||||
input "switches", "capability.switch", multiple: true
|
||||
}
|
||||
}
|
||||
|
||||
def installed()
|
||||
{
|
||||
subscribe(location, changedLocationMode)
|
||||
subscribe(app, appTouch)
|
||||
}
|
||||
|
||||
def updated()
|
||||
{
|
||||
unsubscribe()
|
||||
subscribe(location, changedLocationMode)
|
||||
subscribe(app, appTouch)
|
||||
}
|
||||
|
||||
def changedLocationMode(evt) {
|
||||
log.debug "changedLocationMode: $evt"
|
||||
switches?.off()
|
||||
}
|
||||
|
||||
def appTouch(evt) {
|
||||
log.debug "appTouch: $evt"
|
||||
switches?.off()
|
||||
}
|
||||
55
smartapps/smartthings/big-turn-on.src/big-turn-on.groovy
Normal file
55
smartapps/smartthings/big-turn-on.src/big-turn-on.groovy
Normal file
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Big Turn ON
|
||||
*
|
||||
* Author: SmartThings
|
||||
*/
|
||||
|
||||
definition(
|
||||
name: "Big Turn ON",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Turn your lights on when the SmartApp is tapped or activated.",
|
||||
category: "Convenience",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/light_outlet.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/light_outlet@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("When I touch the app, turn on...") {
|
||||
input "switches", "capability.switch", multiple: true
|
||||
}
|
||||
}
|
||||
|
||||
def installed()
|
||||
{
|
||||
subscribe(location, changedLocationMode)
|
||||
subscribe(app, appTouch)
|
||||
}
|
||||
|
||||
def updated()
|
||||
{
|
||||
unsubscribe()
|
||||
subscribe(location, changedLocationMode)
|
||||
subscribe(app, appTouch)
|
||||
}
|
||||
|
||||
def changedLocationMode(evt) {
|
||||
log.debug "changedLocationMode: $evt"
|
||||
switches?.on()
|
||||
}
|
||||
|
||||
def appTouch(evt) {
|
||||
log.debug "appTouch: $evt"
|
||||
switches?.on()
|
||||
}
|
||||
145
smartapps/smartthings/bon-voyage.src/bon-voyage.groovy
Normal file
145
smartapps/smartthings/bon-voyage.src/bon-voyage.groovy
Normal file
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Bon Voyage
|
||||
*
|
||||
* Author: SmartThings
|
||||
* Date: 2013-03-07
|
||||
*
|
||||
* Monitors a set of presence detectors and triggers a mode change when everyone has left.
|
||||
*/
|
||||
|
||||
definition(
|
||||
name: "Bon Voyage",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Monitors a set of SmartSense Presence tags or smartphones and triggers a mode change when everyone has left. Used in conjunction with Big Turn Off or Make It So to turn off lights, appliances, adjust the thermostat, turn on security apps, and more.",
|
||||
category: "Mode Magic",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/App-LightUpMyWorld.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/App-LightUpMyWorld@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("When all of these people leave home") {
|
||||
input "people", "capability.presenceSensor", multiple: true
|
||||
}
|
||||
section("Change to this mode") {
|
||||
input "newMode", "mode", title: "Mode?"
|
||||
}
|
||||
section("False alarm threshold (defaults to 10 min)") {
|
||||
input "falseAlarmThreshold", "decimal", title: "Number of minutes", required: false
|
||||
}
|
||||
section( "Notifications" ) {
|
||||
input("recipients", "contact", title: "Send notifications to", required: false) {
|
||||
input "sendPushMessage", "enum", title: "Send a push notification?", options: ["Yes", "No"], required: false
|
||||
input "phone", "phone", title: "Send a Text Message?", required: false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def installed() {
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
log.debug "Current mode = ${location.mode}, people = ${people.collect{it.label + ': ' + it.currentPresence}}"
|
||||
subscribe(people, "presence", presence)
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
log.debug "Current mode = ${location.mode}, people = ${people.collect{it.label + ': ' + it.currentPresence}}"
|
||||
unsubscribe()
|
||||
subscribe(people, "presence", presence)
|
||||
}
|
||||
|
||||
def presence(evt)
|
||||
{
|
||||
log.debug "evt.name: $evt.value"
|
||||
if (evt.value == "not present") {
|
||||
if (location.mode != newMode) {
|
||||
log.debug "checking if everyone is away"
|
||||
if (everyoneIsAway()) {
|
||||
log.debug "starting sequence"
|
||||
runIn(findFalseAlarmThreshold() * 60, "takeAction", [overwrite: false])
|
||||
}
|
||||
}
|
||||
else {
|
||||
log.debug "mode is the same, not evaluating"
|
||||
}
|
||||
}
|
||||
else {
|
||||
log.debug "present; doing nothing"
|
||||
}
|
||||
}
|
||||
|
||||
def takeAction()
|
||||
{
|
||||
if (everyoneIsAway()) {
|
||||
def threshold = 1000 * 60 * findFalseAlarmThreshold() - 1000
|
||||
def awayLongEnough = people.findAll { person ->
|
||||
def presenceState = person.currentState("presence")
|
||||
if (!presenceState) {
|
||||
// This device has yet to check in and has no presence state, treat it as not away long enough
|
||||
return false
|
||||
}
|
||||
def elapsed = now() - presenceState.rawDateCreated.time
|
||||
elapsed >= threshold
|
||||
}
|
||||
log.debug "Found ${awayLongEnough.size()} out of ${people.size()} person(s) who were away long enough"
|
||||
if (awayLongEnough.size() == people.size()) {
|
||||
// TODO -- uncomment when app label is available
|
||||
def message = "SmartThings changed your mode to '${newMode}' because everyone left home"
|
||||
log.info message
|
||||
send(message)
|
||||
setLocationMode(newMode)
|
||||
} else {
|
||||
log.debug "not everyone has been away long enough; doing nothing"
|
||||
}
|
||||
} else {
|
||||
log.debug "not everyone is away; doing nothing"
|
||||
}
|
||||
}
|
||||
|
||||
private everyoneIsAway()
|
||||
{
|
||||
def result = true
|
||||
for (person in people) {
|
||||
if (person.currentPresence == "present") {
|
||||
result = false
|
||||
break
|
||||
}
|
||||
}
|
||||
log.debug "everyoneIsAway: $result"
|
||||
return result
|
||||
}
|
||||
|
||||
private send(msg) {
|
||||
if (location.contactBookEnabled) {
|
||||
log.debug("sending notifications to: ${recipients?.size()}")
|
||||
sendNotificationToContacts(msg, recipients)
|
||||
}
|
||||
else {
|
||||
if (sendPushMessage != "No") {
|
||||
log.debug("sending push message")
|
||||
sendPush(msg)
|
||||
}
|
||||
|
||||
if (phone) {
|
||||
log.debug("sending text message")
|
||||
sendSms(phone, msg)
|
||||
}
|
||||
}
|
||||
log.debug msg
|
||||
}
|
||||
|
||||
private findFalseAlarmThreshold() {
|
||||
(falseAlarmThreshold != null && falseAlarmThreshold != "") ? falseAlarmThreshold : 10
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Brighten Dark Places
|
||||
*
|
||||
* Author: SmartThings
|
||||
*/
|
||||
definition(
|
||||
name: "Brighten Dark Places",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Turn your lights on when a open/close sensor opens and the space is dark.",
|
||||
category: "Convenience",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/light_contact-outlet-luminance.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/light_contact-outlet-luminance@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("When the door opens...") {
|
||||
input "contact1", "capability.contactSensor", title: "Where?"
|
||||
}
|
||||
section("And it's dark...") {
|
||||
input "luminance1", "capability.illuminanceMeasurement", title: "Where?"
|
||||
}
|
||||
section("Turn on a light...") {
|
||||
input "switch1", "capability.switch"
|
||||
}
|
||||
}
|
||||
|
||||
def installed()
|
||||
{
|
||||
subscribe(contact1, "contact.open", contactOpenHandler)
|
||||
}
|
||||
|
||||
def updated()
|
||||
{
|
||||
unsubscribe()
|
||||
subscribe(contact1, "contact.open", contactOpenHandler)
|
||||
}
|
||||
|
||||
def contactOpenHandler(evt) {
|
||||
def lightSensorState = luminance1.currentIlluminance
|
||||
log.debug "SENSOR = $lightSensorState"
|
||||
if (lightSensorState != null && lightSensorState < 10) {
|
||||
log.trace "light.on() ... [luminance: ${lightSensorState}]"
|
||||
switch1.on()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Brighten My Path
|
||||
*
|
||||
* Author: SmartThings
|
||||
*/
|
||||
definition(
|
||||
name: "Brighten My Path",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Turn your lights on when motion is detected.",
|
||||
category: "Convenience",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/light_motion-outlet.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/light_motion-outlet@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("When there's movement...") {
|
||||
input "motion1", "capability.motionSensor", title: "Where?", multiple: true
|
||||
}
|
||||
section("Turn on a light...") {
|
||||
input "switch1", "capability.switch", multiple: true
|
||||
}
|
||||
}
|
||||
|
||||
def installed()
|
||||
{
|
||||
subscribe(motion1, "motion.active", motionActiveHandler)
|
||||
}
|
||||
|
||||
def updated()
|
||||
{
|
||||
unsubscribe()
|
||||
subscribe(motion1, "motion.active", motionActiveHandler)
|
||||
}
|
||||
|
||||
def motionActiveHandler(evt) {
|
||||
switch1.on()
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Button Controller
|
||||
*
|
||||
* Author: SmartThings
|
||||
* Date: 2014-5-21
|
||||
*/
|
||||
definition(
|
||||
name: "Button Controller",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Control devices with buttons like the Aeon Labs Minimote",
|
||||
category: "Convenience",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/MyApps/Cat-MyApps.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/MyApps/Cat-MyApps@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
page(name: "selectButton")
|
||||
page(name: "configureButton1")
|
||||
page(name: "configureButton2")
|
||||
page(name: "configureButton3")
|
||||
page(name: "configureButton4")
|
||||
|
||||
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 selectButton() {
|
||||
dynamicPage(name: "selectButton", title: "First, select your button device", nextPage: "configureButton1", uninstall: configured()) {
|
||||
section {
|
||||
input "buttonDevice", "capability.button", title: "Button", multiple: false, required: true
|
||||
}
|
||||
|
||||
section(title: "More options", hidden: hideOptionsSection(), hideable: true) {
|
||||
|
||||
def timeLabel = timeIntervalLabel()
|
||||
|
||||
href "timeIntervalInput", title: "Only during a certain time", description: timeLabel ?: "Tap to set", state: timeLabel ? "complete" : null
|
||||
|
||||
input "days", "enum", title: "Only on certain days of the week", multiple: true, required: false,
|
||||
options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
|
||||
|
||||
input "modes", "mode", title: "Only when mode is", multiple: true, required: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def configureButton1() {
|
||||
dynamicPage(name: "configureButton1", title: "Now let's decide how to use the first button",
|
||||
nextPage: "configureButton2", uninstall: configured(), getButtonSections(1))
|
||||
}
|
||||
def configureButton2() {
|
||||
dynamicPage(name: "configureButton2", title: "If you have a second button, set it up here",
|
||||
nextPage: "configureButton3", uninstall: configured(), getButtonSections(2))
|
||||
}
|
||||
|
||||
def configureButton3() {
|
||||
dynamicPage(name: "configureButton3", title: "If you have a third button, you can do even more here",
|
||||
nextPage: "configureButton4", uninstall: configured(), getButtonSections(3))
|
||||
}
|
||||
def configureButton4() {
|
||||
dynamicPage(name: "configureButton4", title: "If you have a fourth button, you rule, and can set it up here",
|
||||
install: true, uninstall: true, getButtonSections(4))
|
||||
}
|
||||
|
||||
def getButtonSections(buttonNumber) {
|
||||
return {
|
||||
section("Lights") {
|
||||
input "lights_${buttonNumber}_pushed", "capability.switch", title: "Pushed", multiple: true, required: false
|
||||
input "lights_${buttonNumber}_held", "capability.switch", title: "Held", multiple: true, required: false
|
||||
}
|
||||
section("Locks") {
|
||||
input "locks_${buttonNumber}_pushed", "capability.lock", title: "Pushed", multiple: true, required: false
|
||||
input "locks_${buttonNumber}_held", "capability.lock", title: "Held", multiple: true, required: false
|
||||
}
|
||||
section("Sonos") {
|
||||
input "sonos_${buttonNumber}_pushed", "capability.musicPlayer", title: "Pushed", multiple: true, required: false
|
||||
input "sonos_${buttonNumber}_held", "capability.musicPlayer", title: "Held", multiple: true, required: false
|
||||
}
|
||||
section("Modes") {
|
||||
input "mode_${buttonNumber}_pushed", "mode", title: "Pushed", required: false
|
||||
input "mode_${buttonNumber}_held", "mode", title: "Held", required: false
|
||||
}
|
||||
def phrases = location.helloHome?.getPhrases()*.label
|
||||
if (phrases) {
|
||||
section("Hello Home Actions") {
|
||||
log.trace phrases
|
||||
input "phrase_${buttonNumber}_pushed", "enum", title: "Pushed", required: false, options: phrases
|
||||
input "phrase_${buttonNumber}_held", "enum", title: "Held", required: false, options: phrases
|
||||
}
|
||||
}
|
||||
section("Sirens") {
|
||||
input "sirens_${buttonNumber}_pushed","capability.alarm" ,title: "Pushed", multiple: true, required: false
|
||||
input "sirens_${buttonNumber}_held", "capability.alarm", title: "Held", multiple: true, required: false
|
||||
}
|
||||
|
||||
section("Custom Message") {
|
||||
input "textMessage_${buttonNumber}", "text", title: "Message", required: false
|
||||
}
|
||||
|
||||
section("Push Notifications") {
|
||||
input "notifications_${buttonNumber}_pushed","bool" ,title: "Pushed", required: false, defaultValue: false
|
||||
input "notifications_${buttonNumber}_held", "bool", title: "Held", required: false, defaultValue: false
|
||||
}
|
||||
|
||||
section("Sms Notifications") {
|
||||
input "phone_${buttonNumber}_pushed","phone" ,title: "Pushed", required: false
|
||||
input "phone_${buttonNumber}_held", "phone", title: "Held", required: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
initialize()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
unsubscribe()
|
||||
initialize()
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
subscribe(buttonDevice, "button", buttonEvent)
|
||||
}
|
||||
|
||||
def configured() {
|
||||
return buttonDevice || buttonConfigured(1) || buttonConfigured(2) || buttonConfigured(3) || buttonConfigured(4)
|
||||
}
|
||||
|
||||
def buttonConfigured(idx) {
|
||||
return settings["lights_$idx_pushed"] ||
|
||||
settings["locks_$idx_pushed"] ||
|
||||
settings["sonos_$idx_pushed"] ||
|
||||
settings["mode_$idx_pushed"] ||
|
||||
settings["notifications_$idx_pushed"] ||
|
||||
settings["sirens_$idx_pushed"] ||
|
||||
settings["notifications_$idx_pushed"] ||
|
||||
settings["phone_$idx_pushed"]
|
||||
}
|
||||
|
||||
def buttonEvent(evt){
|
||||
if(allOk) {
|
||||
def buttonNumber = evt.data // why doesn't jsonData work? always returning [:]
|
||||
def value = evt.value
|
||||
log.debug "buttonEvent: $evt.name = $evt.value ($evt.data)"
|
||||
log.debug "button: $buttonNumber, value: $value"
|
||||
|
||||
def recentEvents = buttonDevice.eventsSince(new Date(now() - 3000)).findAll{it.value == evt.value && it.data == evt.data}
|
||||
log.debug "Found ${recentEvents.size()?:0} events in past 3 seconds"
|
||||
|
||||
if(recentEvents.size <= 1){
|
||||
switch(buttonNumber) {
|
||||
case ~/.*1.*/:
|
||||
executeHandlers(1, value)
|
||||
break
|
||||
case ~/.*2.*/:
|
||||
executeHandlers(2, value)
|
||||
break
|
||||
case ~/.*3.*/:
|
||||
executeHandlers(3, value)
|
||||
break
|
||||
case ~/.*4.*/:
|
||||
executeHandlers(4, value)
|
||||
break
|
||||
}
|
||||
} else {
|
||||
log.debug "Found recent button press events for $buttonNumber with value $value"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def executeHandlers(buttonNumber, value) {
|
||||
log.debug "executeHandlers: $buttonNumber - $value"
|
||||
|
||||
def lights = find('lights', buttonNumber, value)
|
||||
if (lights != null) toggle(lights)
|
||||
|
||||
def locks = find('locks', buttonNumber, value)
|
||||
if (locks != null) toggle(locks)
|
||||
|
||||
def sonos = find('sonos', buttonNumber, value)
|
||||
if (sonos != null) toggle(sonos)
|
||||
|
||||
def mode = find('mode', buttonNumber, value)
|
||||
if (mode != null) changeMode(mode)
|
||||
|
||||
def phrase = find('phrase', buttonNumber, value)
|
||||
if (phrase != null) location.helloHome.execute(phrase)
|
||||
|
||||
def textMessage = findMsg('textMessage', buttonNumber)
|
||||
|
||||
def notifications = find('notifications', buttonNumber, value)
|
||||
if (notifications?.toBoolean()) sendPush(textMessage ?: "Button $buttonNumber was pressed" )
|
||||
|
||||
def phone = find('phone', buttonNumber, value)
|
||||
if (phone != null) sendSms(phone, textMessage ?:"Button $buttonNumber was pressed")
|
||||
|
||||
def sirens = find('sirens', buttonNumber, value)
|
||||
if (sirens != null) toggle(sirens)
|
||||
}
|
||||
|
||||
def find(type, buttonNumber, value) {
|
||||
def preferenceName = type + "_" + buttonNumber + "_" + value
|
||||
def pref = settings[preferenceName]
|
||||
if(pref != null) {
|
||||
log.debug "Found: $pref for $preferenceName"
|
||||
}
|
||||
|
||||
return pref
|
||||
}
|
||||
|
||||
def findMsg(type, buttonNumber) {
|
||||
def preferenceName = type + "_" + buttonNumber
|
||||
def pref = settings[preferenceName]
|
||||
if(pref != null) {
|
||||
log.debug "Found: $pref for $preferenceName"
|
||||
}
|
||||
|
||||
return pref
|
||||
}
|
||||
|
||||
def toggle(devices) {
|
||||
log.debug "toggle: $devices = ${devices*.currentValue('switch')}"
|
||||
|
||||
if (devices*.currentValue('switch').contains('on')) {
|
||||
devices.off()
|
||||
}
|
||||
else if (devices*.currentValue('switch').contains('off')) {
|
||||
devices.on()
|
||||
}
|
||||
else if (devices*.currentValue('lock').contains('locked')) {
|
||||
devices.unlock()
|
||||
}
|
||||
else if (devices*.currentValue('alarm').contains('off')) {
|
||||
devices.siren()
|
||||
}
|
||||
else {
|
||||
devices.on()
|
||||
}
|
||||
}
|
||||
|
||||
def changeMode(mode) {
|
||||
log.debug "changeMode: $mode, location.mode = $location.mode, location.modes = $location.modes"
|
||||
|
||||
if (location.mode != mode && location.modes?.find { it.name == mode }) {
|
||||
setLocationMode(mode)
|
||||
}
|
||||
}
|
||||
|
||||
// execution filter methods
|
||||
private getAllOk() {
|
||||
modeOk && daysOk && timeOk
|
||||
}
|
||||
|
||||
private getModeOk() {
|
||||
def result = !modes || modes.contains(location.mode)
|
||||
log.trace "modeOk = $result"
|
||||
result
|
||||
}
|
||||
|
||||
private getDaysOk() {
|
||||
def result = true
|
||||
if (days) {
|
||||
def df = new java.text.SimpleDateFormat("EEEE")
|
||||
if (location.timeZone) {
|
||||
df.setTimeZone(location.timeZone)
|
||||
}
|
||||
else {
|
||||
df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
|
||||
}
|
||||
def day = df.format(new Date())
|
||||
result = days.contains(day)
|
||||
}
|
||||
log.trace "daysOk = $result"
|
||||
result
|
||||
}
|
||||
|
||||
private getTimeOk() {
|
||||
def result = true
|
||||
if (starting && ending) {
|
||||
def currTime = now()
|
||||
def start = timeToday(starting).time
|
||||
def stop = timeToday(ending).time
|
||||
result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start
|
||||
}
|
||||
log.trace "timeOk = $result"
|
||||
result
|
||||
}
|
||||
|
||||
private hhmm(time, fmt = "h:mm a")
|
||||
{
|
||||
def t = timeToday(time, location.timeZone)
|
||||
def f = new java.text.SimpleDateFormat(fmt)
|
||||
f.setTimeZone(location.timeZone ?: timeZone(time))
|
||||
f.format(t)
|
||||
}
|
||||
|
||||
private hideOptionsSection() {
|
||||
(starting || ending || days || modes) ? false : true
|
||||
}
|
||||
|
||||
private timeIntervalLabel() {
|
||||
(starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z") : ""
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Schedule the Camera Power
|
||||
*
|
||||
* Author: danny@smartthings.com
|
||||
* Date: 2013-10-07
|
||||
*/
|
||||
|
||||
definition(
|
||||
name: "Camera Power Scheduler",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Turn the power on and off at a specific time. ",
|
||||
category: "Available Beta Apps",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/dropcam-on-off-schedule.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/dropcam-on-off-schedule@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("Camera power..."){
|
||||
input "switch1", "capability.switch", multiple: true
|
||||
}
|
||||
section("Turn the Camera On at..."){
|
||||
input "startTime", "time", title: "Start Time", required:false
|
||||
}
|
||||
section("Turn the Camera Off at..."){
|
||||
input "endTime", "time", title: "End Time", required:false
|
||||
}
|
||||
}
|
||||
|
||||
def installed()
|
||||
{
|
||||
initialize()
|
||||
}
|
||||
|
||||
def updated()
|
||||
{
|
||||
unschedule()
|
||||
initialize()
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
/*
|
||||
def tz = location.timeZone
|
||||
|
||||
//if it's after the startTime but before the end time, turn it on
|
||||
if(startTime && timeToday(startTime,tz).time > timeToday(now,tz).time){
|
||||
|
||||
if(endTime && timeToday(endTime,tz).time < timeToday(now,tz).time){
|
||||
switch1.on()
|
||||
}
|
||||
else{
|
||||
switch1.off()
|
||||
}
|
||||
}
|
||||
else if(endTime && timeToday(endtime,tz).time > timeToday(now,tz).time)
|
||||
{
|
||||
switch1.off()
|
||||
}
|
||||
*/
|
||||
|
||||
if(startTime)
|
||||
runDaily(startTime, turnOnCamera)
|
||||
if(endTime)
|
||||
runDaily(endTime,turnOffCamera)
|
||||
}
|
||||
|
||||
def turnOnCamera()
|
||||
{
|
||||
log.info "turned on camera"
|
||||
switch1.on()
|
||||
}
|
||||
|
||||
def turnOffCamera()
|
||||
{
|
||||
log.info "turned off camera"
|
||||
switch1.off()
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Cameras On When I'm Away
|
||||
*
|
||||
* Author: danny@smartthings.com
|
||||
* Date: 2013-10-07
|
||||
*/
|
||||
|
||||
definition(
|
||||
name: "Cameras On When I'm Away",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Turn cameras on when I'm away",
|
||||
category: "Available Beta Apps",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/dropcam-on-off-presence.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/dropcam-on-off-presence@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("When all of these people are home...") {
|
||||
input "people", "capability.presenceSensor", multiple: true
|
||||
}
|
||||
section("Turn off camera power..."){
|
||||
input "switches1", "capability.switch", multiple: true
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
log.debug "Current people = ${people.collect{it.label + ': ' + it.currentPresence}}"
|
||||
subscribe(people, "presence", presence)
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
log.debug "Current people = ${people.collect{it.label + ': ' + it.currentPresence}}"
|
||||
unsubscribe()
|
||||
subscribe(people, "presence", presence)
|
||||
}
|
||||
|
||||
def presence(evt)
|
||||
{
|
||||
log.debug "evt.name: $evt.value"
|
||||
if (evt.value == "not present") {
|
||||
|
||||
log.debug "checking if everyone is away"
|
||||
if (everyoneIsAway()) {
|
||||
log.debug "starting on Sequence"
|
||||
|
||||
runIn(60*2, "turnOn") //two minute delay after everyone has left
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (!everyoneIsAway()) {
|
||||
turnOff()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def turnOff()
|
||||
{
|
||||
log.debug "canceling On requests"
|
||||
unschedule("turnOn")
|
||||
|
||||
log.info "turning off the camera"
|
||||
switches1.off()
|
||||
}
|
||||
|
||||
def turnOn()
|
||||
{
|
||||
|
||||
log.info "turned on the camera"
|
||||
switches1.on()
|
||||
|
||||
unschedule("turnOn") // Temporary work-around to scheduling bug
|
||||
}
|
||||
|
||||
private everyoneIsAway()
|
||||
{
|
||||
def result = true
|
||||
for (person in people) {
|
||||
if (person.currentPresence == "present") {
|
||||
result = false
|
||||
break
|
||||
}
|
||||
}
|
||||
log.debug "everyoneIsAway: $result"
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* title: Carpool Notifier
|
||||
*
|
||||
* description:
|
||||
* Do you carpool to work with your spouse? Do you pick your children up from school? Have they been waiting in doors for you? Let them know you've arrived with Carpool Notifier.
|
||||
*
|
||||
* This SmartApp is designed to send notifications to your carpooling buddies when you arrive to pick them up. What separates this SmartApp from other notification SmartApps is that it will only send a notification if your carpool buddy is not with you.
|
||||
*
|
||||
* category: Family
|
||||
|
||||
* icon: https://s3.amazonaws.com/smartapp-icons/Family/App-IMadeIt.png
|
||||
* icon2X: https://s3.amazonaws.com/smartapp-icons/Family/App-IMadeIt%402x.png
|
||||
*
|
||||
* Author: steve
|
||||
* Date: 2013-11-19
|
||||
*/
|
||||
|
||||
definition(
|
||||
name: "Carpool Notifier",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "This SmartApp is designed to send notifications to your carpooling buddies when you arrive to pick them up. What separates this SmartApp from other notification SmartApps is that it will only send a notification if your carpool buddy is not with you. If the person you are picking up is present, and has been for 5 minutes or more, they will get a notification when you become present.",
|
||||
category: "Green Living",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Family/App-IMadeIt.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Family/App-IMadeIt@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section() {
|
||||
input(name: "driver", type: "capability.presenceSensor", required: true, multiple: false, title: "When this person arrives", description: "Who's driving?")
|
||||
input("recipients", "contact", title: "Notify", description: "Send notifications to") {
|
||||
input(name: "phoneNumber", type: "phone", required: true, multiple: false, title: "Send a text to", description: "Phone number")
|
||||
}
|
||||
input(name: "message", type: "text", required: false, multiple: false, title: "With the message:", description: "Your ride is here!")
|
||||
input(name: "rider", type: "capability.presenceSensor", required: true, multiple: false, title: "But only when this person is not with you", description: "Who are you picking up?")
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
|
||||
initialize()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
|
||||
unsubscribe()
|
||||
initialize()
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
subscribe(driver, "presence.present", presence)
|
||||
}
|
||||
|
||||
def presence(evt) {
|
||||
|
||||
if (evt.value == "present" && riderIsHome())
|
||||
{
|
||||
// log.debug "Rider Is Home; Send A Text"
|
||||
sendText()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def riderIsHome() {
|
||||
|
||||
// log.debug "rider presence: ${rider.currentPresence}"
|
||||
|
||||
if (rider.currentPresence != "present")
|
||||
{
|
||||
return false
|
||||
}
|
||||
|
||||
def riderState = rider.currentState("presence")
|
||||
// log.debug "riderState: ${riderState}"
|
||||
if (!riderState)
|
||||
{
|
||||
return true
|
||||
}
|
||||
|
||||
def latestState = rider.latestState("presence")
|
||||
|
||||
def now = new Date()
|
||||
def minusFive = new Date(minutes: now.minutes - 5)
|
||||
|
||||
|
||||
if (minusFive > latestState.date)
|
||||
{
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
def sendText() {
|
||||
if (location.contactBookEnabled) {
|
||||
sendNotificationToContacts(message ?: "Your ride is here!", recipients)
|
||||
}
|
||||
else {
|
||||
sendSms(phoneNumber, message ?: "Your ride is here!")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Close a valve if moisture is detected
|
||||
*
|
||||
* Copyright 2014 SmartThings
|
||||
*
|
||||
* Author: Juan Risso
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
definition(
|
||||
name: "Close The Valve",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Close a selected valve if moisture is detected, and get notified by SMS and push notification.",
|
||||
category: "Safety & Security",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Developers/dry-the-wet-spot.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Developers/dry-the-wet-spot@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("When water is sensed...") {
|
||||
input "sensor", "capability.waterSensor", title: "Where?", required: true, multiple: true
|
||||
}
|
||||
section("Close the valve...") {
|
||||
input "valve", "capability.valve", title: "Which?", required: true, multiple: false
|
||||
}
|
||||
section("Send this message (optional, sends standard status message if not specified)"){
|
||||
input "messageText", "text", title: "Message Text", required: false
|
||||
}
|
||||
section("Via a push notification and/or an SMS message"){
|
||||
input "phone", "phone", title: "Phone Number (for SMS, optional)", required: false
|
||||
input "pushAndPhone", "enum", title: "Both Push and SMS?", required: false, options: ["Yes","No"]
|
||||
}
|
||||
section("Minimum time between messages (optional)") {
|
||||
input "frequency", "decimal", title: "Minutes", required: false
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
subscribe(sensor, "water", waterHandler)
|
||||
}
|
||||
|
||||
def updated() {
|
||||
unsubscribe()
|
||||
subscribe(sensor, "water", waterHandler)
|
||||
}
|
||||
|
||||
def waterHandler(evt) {
|
||||
log.debug "Sensor says ${evt.value}"
|
||||
if (evt.value == "wet") {
|
||||
valve.close()
|
||||
}
|
||||
if (frequency) {
|
||||
def lastTime = state[evt.deviceId]
|
||||
if (lastTime == null || now() - lastTime >= frequency * 60000) {
|
||||
sendMessage(evt)
|
||||
}
|
||||
}
|
||||
else {
|
||||
sendMessage(evt)
|
||||
}
|
||||
}
|
||||
|
||||
private sendMessage(evt) {
|
||||
def msg = messageText ?: "We closed the valve because moisture was detected"
|
||||
log.debug "$evt.name:$evt.value, pushAndPhone:$pushAndPhone, '$msg'"
|
||||
|
||||
if (!phone || pushAndPhone != "No") {
|
||||
log.debug "sending push"
|
||||
sendPush(msg)
|
||||
}
|
||||
if (phone) {
|
||||
log.debug "sending SMS"
|
||||
sendSms(phone, msg)
|
||||
}
|
||||
if (frequency) {
|
||||
state[evt.deviceId] = now()
|
||||
}
|
||||
}
|
||||
114
smartapps/smartthings/curling-iron.src/curling-iron.groovy
Normal file
114
smartapps/smartthings/curling-iron.src/curling-iron.groovy
Normal file
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Curling Iron
|
||||
*
|
||||
* Author: SmartThings
|
||||
* Date: 2013-03-20
|
||||
*/
|
||||
definition(
|
||||
name: "Curling Iron",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Turns on an outlet when the user is present and off after a period of time",
|
||||
category: "Convenience",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/temp_thermo-switch.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/temp_thermo-switch@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("When someone's around because of...") {
|
||||
input name: "motionSensors", title: "Motion here", type: "capability.motionSensor", multiple: true, required: false
|
||||
input name: "presenceSensors", title: "And (optionally) these sensors being present", type: "capability.presenceSensor", multiple: true, required: false
|
||||
}
|
||||
section("Turn on these outlet(s)") {
|
||||
input name: "outlets", title: "Which?", type: "capability.switch", multiple: true
|
||||
}
|
||||
section("For this amount of time") {
|
||||
input name: "minutes", title: "Minutes?", type: "number", multiple: false
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
subscribeToEvents()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
unsubscribe()
|
||||
subscribeToEvents()
|
||||
}
|
||||
|
||||
def subscribeToEvents() {
|
||||
subscribe(motionSensors, "motion.active", motionActive)
|
||||
subscribe(motionSensors, "motion.inactive", motionInactive)
|
||||
subscribe(presenceSensors, "presence.not present", notPresent)
|
||||
}
|
||||
|
||||
def motionActive(evt) {
|
||||
log.debug "$evt.name: $evt.value"
|
||||
if (anyHere()) {
|
||||
outletsOn()
|
||||
}
|
||||
}
|
||||
|
||||
def motionInactive(evt) {
|
||||
log.debug "$evt.name: $evt.value"
|
||||
if (allQuiet()) {
|
||||
outletsOff()
|
||||
}
|
||||
}
|
||||
|
||||
def notPresent(evt) {
|
||||
log.debug "$evt.name: $evt.value"
|
||||
if (!anyHere()) {
|
||||
outletsOff()
|
||||
}
|
||||
}
|
||||
|
||||
def allQuiet() {
|
||||
def result = true
|
||||
for (it in motionSensors) {
|
||||
if (it.currentMotion == "active") {
|
||||
result = false
|
||||
break
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
def anyHere() {
|
||||
def result = true
|
||||
for (it in presenceSensors) {
|
||||
if (it.currentPresence == "not present") {
|
||||
result = false
|
||||
break
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
def outletsOn() {
|
||||
outlets.on()
|
||||
unschedule("scheduledTurnOff")
|
||||
}
|
||||
|
||||
def outletsOff() {
|
||||
def delay = minutes * 60
|
||||
runIn(delay, "scheduledTurnOff")
|
||||
}
|
||||
|
||||
def scheduledTurnOff() {
|
||||
outlets.off()
|
||||
unschedule("scheduledTurnOff") // Temporary work-around to scheduling bug
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Darken Behind Me
|
||||
*
|
||||
* Author: SmartThings
|
||||
*/
|
||||
definition(
|
||||
name: "Darken Behind Me",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Turn your lights off after a period of no motion being observed.",
|
||||
category: "Convenience",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/light_motion-outlet.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/light_motion-outlet@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("When there's no movement...") {
|
||||
input "motion1", "capability.motionSensor", title: "Where?"
|
||||
}
|
||||
section("Turn off a light...") {
|
||||
input "switch1", "capability.switch", multiple: true
|
||||
}
|
||||
}
|
||||
|
||||
def installed()
|
||||
{
|
||||
subscribe(motion1, "motion.inactive", motionInactiveHandler)
|
||||
}
|
||||
|
||||
def updated()
|
||||
{
|
||||
unsubscribe()
|
||||
subscribe(motion1, "motion.inactive", motionInactiveHandler)
|
||||
}
|
||||
|
||||
def motionInactiveHandler(evt) {
|
||||
switch1.off()
|
||||
}
|
||||
100
smartapps/smartthings/double-tap.src/double-tap.groovy
Normal file
100
smartapps/smartthings/double-tap.src/double-tap.groovy
Normal file
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Double Tap
|
||||
*
|
||||
* Author: SmartThings
|
||||
*/
|
||||
definition(
|
||||
name: "Double Tap",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Turn on or off any number of switches when an existing switch is tapped twice in a row.",
|
||||
category: "Convenience",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/light_outlet.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/light_outlet@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("When this switch is double-tapped...") {
|
||||
input "master", "capability.switch", title: "Where?"
|
||||
}
|
||||
section("Turn on or off all of these switches as well") {
|
||||
input "switches", "capability.switch", multiple: true, required: false
|
||||
}
|
||||
section("And turn off but not on all of these switches") {
|
||||
input "offSwitches", "capability.switch", multiple: true, required: false
|
||||
}
|
||||
section("And turn on but not off all of these switches") {
|
||||
input "onSwitches", "capability.switch", multiple: true, required: false
|
||||
}
|
||||
}
|
||||
|
||||
def installed()
|
||||
{
|
||||
subscribe(master, "switch", switchHandler, [filterEvents: false])
|
||||
}
|
||||
|
||||
def updated()
|
||||
{
|
||||
unsubscribe()
|
||||
subscribe(master, "switch", switchHandler, [filterEvents: false])
|
||||
}
|
||||
|
||||
def switchHandler(evt) {
|
||||
log.info evt.value
|
||||
|
||||
// use Event rather than DeviceState because we may be changing DeviceState to only store changed values
|
||||
def recentStates = master.eventsSince(new Date(now() - 4000), [all:true, max: 10]).findAll{it.name == "switch"}
|
||||
log.debug "${recentStates?.size()} STATES FOUND, LAST AT ${recentStates ? recentStates[0].dateCreated : ''}"
|
||||
|
||||
if (evt.physical) {
|
||||
if (evt.value == "on" && lastTwoStatesWere("on", recentStates, evt)) {
|
||||
log.debug "detected two taps, turn on other light(s)"
|
||||
onSwitches()*.on()
|
||||
} else if (evt.value == "off" && lastTwoStatesWere("off", recentStates, evt)) {
|
||||
log.debug "detected two taps, turn off other light(s)"
|
||||
offSwitches()*.off()
|
||||
}
|
||||
}
|
||||
else {
|
||||
log.trace "Skipping digital on/off event"
|
||||
}
|
||||
}
|
||||
|
||||
private onSwitches() {
|
||||
(switches + onSwitches).findAll{it}
|
||||
}
|
||||
|
||||
private offSwitches() {
|
||||
(switches + offSwitches).findAll{it}
|
||||
}
|
||||
|
||||
private lastTwoStatesWere(value, states, evt) {
|
||||
def result = false
|
||||
if (states) {
|
||||
|
||||
log.trace "unfiltered: [${states.collect{it.dateCreated + ':' + it.value}.join(', ')}]"
|
||||
def onOff = states.findAll { it.physical || !it.type }
|
||||
log.trace "filtered: [${onOff.collect{it.dateCreated + ':' + it.value}.join(', ')}]"
|
||||
|
||||
// This test was needed before the change to use Event rather than DeviceState. It should never pass now.
|
||||
if (onOff[0].date.before(evt.date)) {
|
||||
log.warn "Last state does not reflect current event, evt.date: ${evt.dateCreated}, state.date: ${onOff[0].dateCreated}"
|
||||
result = evt.value == value && onOff[0].value == value
|
||||
}
|
||||
else {
|
||||
result = onOff.size() > 1 && onOff[0].value == value && onOff[1].value == value
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Dry the Wetspot
|
||||
*
|
||||
* Copyright 2014 Scottin Pollock
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
definition(
|
||||
name: "Dry the Wetspot",
|
||||
namespace: "smartthings",
|
||||
author: "Scottin Pollock",
|
||||
description: "Turns switch on and off based on moisture sensor input.",
|
||||
category: "Safety & Security",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Developers/dry-the-wet-spot.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Developers/dry-the-wet-spot@2x.png"
|
||||
)
|
||||
|
||||
|
||||
preferences {
|
||||
section("When water is sensed...") {
|
||||
input "sensor", "capability.waterSensor", title: "Where?", required: true
|
||||
}
|
||||
section("Turn on a pump...") {
|
||||
input "pump", "capability.switch", title: "Which?", required: true
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
subscribe(sensor, "water.dry", waterHandler)
|
||||
subscribe(sensor, "water.wet", waterHandler)
|
||||
}
|
||||
|
||||
def updated() {
|
||||
unsubscribe()
|
||||
subscribe(sensor, "water.dry", waterHandler)
|
||||
subscribe(sensor, "water.wet", waterHandler)
|
||||
}
|
||||
|
||||
def waterHandler(evt) {
|
||||
log.debug "Sensor says ${evt.value}"
|
||||
if (evt.value == "wet") {
|
||||
pump.on()
|
||||
} else if (evt.value == "dry") {
|
||||
pump.off()
|
||||
}
|
||||
}
|
||||
|
||||
919
smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy
Normal file
919
smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy
Normal file
@@ -0,0 +1,919 @@
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
* Author: scott
|
||||
* Date: 2013-08-07
|
||||
*
|
||||
* Last Modification:
|
||||
* JLH - 01-23-2014 - Update for Correct SmartApp URL Format
|
||||
* JLH - 02-15-2014 - Fuller use of ecobee API
|
||||
*/
|
||||
definition(
|
||||
name: "Ecobee (Connect)",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Connect your Ecobee thermostat to SmartThings.",
|
||||
category: "SmartThings Labs",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/ecobee.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/ecobee@2x.png"
|
||||
) {
|
||||
appSetting "clientId"
|
||||
appSetting "serverUrl"
|
||||
}
|
||||
|
||||
preferences {
|
||||
page(name: "auth", title: "ecobee", nextPage:"deviceList", content:"authPage", uninstall: true)
|
||||
page(name: "deviceList", title: "ecobee", content:"ecobeeDeviceList", install:true)
|
||||
}
|
||||
|
||||
mappings {
|
||||
path("/auth") {
|
||||
action: [
|
||||
GET: "auth"
|
||||
]
|
||||
}
|
||||
path("/swapToken") {
|
||||
action: [
|
||||
GET: "swapToken"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
def auth() {
|
||||
redirect location: oauthInitUrl()
|
||||
}
|
||||
|
||||
def authPage()
|
||||
{
|
||||
log.debug "authPage()"
|
||||
|
||||
if(!atomicState.accessToken)
|
||||
{
|
||||
log.debug "about to create access token"
|
||||
createAccessToken()
|
||||
atomicState.accessToken = state.accessToken
|
||||
}
|
||||
|
||||
|
||||
def description = "Required"
|
||||
def uninstallAllowed = false
|
||||
def oauthTokenProvided = false
|
||||
|
||||
if(atomicState.authToken)
|
||||
{
|
||||
// TODO: Check if it's valid
|
||||
if(true)
|
||||
{
|
||||
description = "You are connected."
|
||||
uninstallAllowed = true
|
||||
oauthTokenProvided = true
|
||||
}
|
||||
else
|
||||
{
|
||||
description = "Required" // Worth differentiating here vs. not having atomicState.authToken?
|
||||
oauthTokenProvided = false
|
||||
}
|
||||
}
|
||||
|
||||
def redirectUrl = buildRedirectUrl("auth")
|
||||
|
||||
log.debug "RedirectUrl = ${redirectUrl}"
|
||||
|
||||
// get rid of next button until the user is actually auth'd
|
||||
|
||||
if (!oauthTokenProvided) {
|
||||
|
||||
return dynamicPage(name: "auth", title: "Login", nextPage:null, uninstall:uninstallAllowed) {
|
||||
section(){
|
||||
paragraph "Tap below to log in to the ecobee service and authorize SmartThings access. Be sure to scroll down on page 2 and press the 'Allow' button."
|
||||
href url:redirectUrl, style:"embedded", required:true, title:"ecobee", description:description
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
return dynamicPage(name: "auth", title: "Log In", nextPage:"deviceList", uninstall:uninstallAllowed) {
|
||||
section(){
|
||||
paragraph "Tap Next to continue to setup your thermostats."
|
||||
href url:redirectUrl, style:"embedded", state:"complete", title:"ecobee", description:description
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def ecobeeDeviceList()
|
||||
{
|
||||
log.debug "ecobeeDeviceList()"
|
||||
|
||||
def stats = getEcobeeThermostats()
|
||||
|
||||
log.debug "device list: $stats"
|
||||
|
||||
def p = dynamicPage(name: "deviceList", title: "Select Your Thermostats", uninstall: true) {
|
||||
section(""){
|
||||
paragraph "Tap below to see the list of ecobee thermostats available in your ecobee account and select the ones you want to connect to SmartThings."
|
||||
input(name: "thermostats", title:"", type: "enum", required:true, multiple:true, description: "Tap to choose", metadata:[values:stats])
|
||||
}
|
||||
}
|
||||
|
||||
log.debug "list p: $p"
|
||||
return p
|
||||
}
|
||||
|
||||
def getEcobeeThermostats()
|
||||
{
|
||||
log.debug "getting device list"
|
||||
|
||||
def requestBody = '{"selection":{"selectionType":"registered","selectionMatch":"","includeRuntime":true}}'
|
||||
|
||||
def deviceListParams = [
|
||||
uri: "https://api.ecobee.com",
|
||||
path: "/1/thermostat",
|
||||
headers: ["Content-Type": "text/json", "Authorization": "Bearer ${atomicState.authToken}"],
|
||||
query: [format: 'json', body: requestBody]
|
||||
]
|
||||
|
||||
log.debug "_______AUTH______ ${atomicState.authToken}"
|
||||
log.debug "device list params: $deviceListParams"
|
||||
|
||||
def stats = [:]
|
||||
httpGet(deviceListParams) { resp ->
|
||||
|
||||
if(resp.status == 200)
|
||||
{
|
||||
resp.data.thermostatList.each { stat ->
|
||||
def dni = [ app.id, stat.identifier ].join('.')
|
||||
stats[dni] = getThermostatDisplayName(stat)
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
log.debug "http status: ${resp.status}"
|
||||
|
||||
//refresh the auth token
|
||||
if (resp.status == 500 && resp.data.status.code == 14)
|
||||
{
|
||||
log.debug "Storing the failed action to try later"
|
||||
atomicState.action = "getEcobeeThermostats"
|
||||
log.debug "Refreshing your auth_token!"
|
||||
refreshAuthToken()
|
||||
}
|
||||
else
|
||||
{
|
||||
log.error "Authentication error, invalid authentication method, lack of credentials, etc."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.debug "thermostats: $stats"
|
||||
|
||||
return stats
|
||||
}
|
||||
|
||||
def getThermostatDisplayName(stat)
|
||||
{
|
||||
log.debug "getThermostatDisplayName"
|
||||
if(stat?.name)
|
||||
{
|
||||
return stat.name.toString()
|
||||
}
|
||||
|
||||
return (getThermostatTypeName(stat) + " (${stat.identifier})").toString()
|
||||
}
|
||||
|
||||
def getThermostatTypeName(stat)
|
||||
{
|
||||
log.debug "getThermostatTypeName"
|
||||
return stat.modelNumber == "siSmart" ? "Smart Si" : "Smart"
|
||||
}
|
||||
|
||||
def installed() {
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
|
||||
// createAccessToken()
|
||||
|
||||
|
||||
initialize()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
|
||||
unsubscribe()
|
||||
initialize()
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
// TODO: subscribe to attributes, devices, locations, etc.
|
||||
log.debug "initialize"
|
||||
def devices = thermostats.collect { dni ->
|
||||
|
||||
def d = getChildDevice(dni)
|
||||
|
||||
if(!d)
|
||||
{
|
||||
d = addChildDevice(getChildNamespace(), getChildName(), dni)
|
||||
log.debug "created ${d.displayName} with id $dni"
|
||||
}
|
||||
else
|
||||
{
|
||||
log.debug "found ${d.displayName} with id $dni already exists"
|
||||
}
|
||||
|
||||
return d
|
||||
}
|
||||
|
||||
log.debug "created ${devices.size()} thermostats"
|
||||
|
||||
def delete
|
||||
// Delete any that are no longer in settings
|
||||
if(!thermostats)
|
||||
{
|
||||
log.debug "delete thermostats"
|
||||
delete = getAllChildDevices()
|
||||
}
|
||||
else
|
||||
{
|
||||
delete = getChildDevices().findAll { !thermostats.contains(it.deviceNetworkId) }
|
||||
}
|
||||
|
||||
log.debug "deleting ${delete.size()} thermostats"
|
||||
delete.each { deleteChildDevice(it.deviceNetworkId) }
|
||||
|
||||
atomicState.thermostatData = [:]
|
||||
|
||||
pollHandler()
|
||||
|
||||
// schedule ("0 0/15 * 1/1 * ? *", pollHandler)
|
||||
}
|
||||
|
||||
|
||||
def oauthInitUrl()
|
||||
{
|
||||
log.debug "oauthInitUrl"
|
||||
// def oauth_url = "https://api.ecobee.com/authorize?response_type=code&client_id=qqwy6qo0c2lhTZGytelkQ5o8vlHgRsrO&redirect_uri=http://localhost/&scope=smartRead,smartWrite&state=abc123"
|
||||
def stcid = getSmartThingsClientId();
|
||||
|
||||
atomicState.oauthInitState = UUID.randomUUID().toString()
|
||||
|
||||
def oauthParams = [
|
||||
response_type: "code",
|
||||
scope: "smartRead,smartWrite",
|
||||
client_id: stcid,
|
||||
state: atomicState.oauthInitState,
|
||||
redirect_uri: buildRedirectUrl()
|
||||
]
|
||||
|
||||
return "https://api.ecobee.com/authorize?" + toQueryString(oauthParams)
|
||||
}
|
||||
|
||||
def buildRedirectUrl(action = "swapToken")
|
||||
{
|
||||
log.debug "buildRedirectUrl"
|
||||
// return serverUrl + "/api/smartapps/installations/${app.id}/token/${atomicState.accessToken}"
|
||||
return serverUrl + "/api/token/${atomicState.accessToken}/smartapps/installations/${app.id}/${action}"
|
||||
}
|
||||
|
||||
def swapToken()
|
||||
{
|
||||
log.debug "swapping token: $params"
|
||||
debugEvent ("swapping token: $params")
|
||||
|
||||
def code = params.code
|
||||
def oauthState = params.state
|
||||
|
||||
// TODO: verify oauthState == atomicState.oauthInitState
|
||||
|
||||
|
||||
|
||||
// https://www.ecobee.com/home/token?grant_type=authorization_code&code=aliOpagDm3BqbRplugcs1AwdJE0ohxdB&client_id=qqwy6qo0c2lhTZGytelkQ5o8vlHgRsrO&redirect_uri=https://graph.api.smartthings.com/
|
||||
def stcid = getSmartThingsClientId()
|
||||
|
||||
def tokenParams = [
|
||||
grant_type: "authorization_code",
|
||||
code: params.code,
|
||||
client_id: stcid,
|
||||
redirect_uri: buildRedirectUrl()
|
||||
]
|
||||
|
||||
def tokenUrl = "https://www.ecobee.com/home/token?" + toQueryString(tokenParams)
|
||||
|
||||
log.debug "SCOTT: swapping token $params"
|
||||
|
||||
def jsonMap
|
||||
httpPost(uri:tokenUrl) { resp ->
|
||||
jsonMap = resp.data
|
||||
}
|
||||
|
||||
log.debug "SCOTT: swapped token for $jsonMap"
|
||||
debugEvent ("swapped token for $jsonMap")
|
||||
|
||||
atomicState.refreshToken = jsonMap.refresh_token
|
||||
atomicState.authToken = jsonMap.access_token
|
||||
|
||||
def html = """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=640">
|
||||
<title>Withings Connection</title>
|
||||
<style type="text/css">
|
||||
@font-face {
|
||||
font-family: 'Swiss 721 W01 Thin';
|
||||
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot');
|
||||
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot?#iefix') format('embedded-opentype'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.woff') format('woff'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.ttf') format('truetype'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.svg#swis721_th_btthin') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Swiss 721 W01 Light';
|
||||
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot');
|
||||
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot?#iefix') format('embedded-opentype'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.woff') format('woff'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.ttf') format('truetype'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.svg#swis721_lt_btlight') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
.container {
|
||||
width: 560px;
|
||||
padding: 40px;
|
||||
/*background: #eee;*/
|
||||
text-align: center;
|
||||
}
|
||||
img {
|
||||
vertical-align: middle;
|
||||
}
|
||||
img:nth-child(2) {
|
||||
margin: 0 30px;
|
||||
}
|
||||
p {
|
||||
font-size: 2.2em;
|
||||
font-family: 'Swiss 721 W01 Thin';
|
||||
text-align: center;
|
||||
color: #666666;
|
||||
padding: 0 40px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
/*
|
||||
p:last-child {
|
||||
margin-top: 0px;
|
||||
}
|
||||
*/
|
||||
span {
|
||||
font-family: 'Swiss 721 W01 Light';
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<img src="https://s3.amazonaws.com/smartapp-icons/Partner/ecobee%402x.png" alt="ecobee icon" />
|
||||
<img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/connected-device-icn%402x.png" alt="connected device icon" />
|
||||
<img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/st-logo%402x.png" alt="SmartThings logo" />
|
||||
<p>Your ecobee Account is now connected to SmartThings!</p>
|
||||
<p>Click 'Done' to finish setup.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
render contentType: 'text/html', data: html
|
||||
}
|
||||
|
||||
def getPollRateMillis() { return 15 * 60 * 1000 }
|
||||
|
||||
// Poll Child is invoked from the Child Device itself as part of the Poll Capability
|
||||
def pollChild( child )
|
||||
{
|
||||
log.debug "poll child"
|
||||
debugEvent ("poll child")
|
||||
def now = new Date().time
|
||||
|
||||
debugEvent ("Last Poll Millis = ${atomicState.lastPollMillis}")
|
||||
def last = atomicState.lastPollMillis ?: 0
|
||||
def next = last + pollRateMillis
|
||||
|
||||
log.debug "pollChild( ${child.device.deviceNetworkId} ): $now > $next ?? w/ current state: ${atomicState.thermostats}"
|
||||
debugEvent ("pollChild( ${child.device.deviceNetworkId} ): $now > $next ?? w/ current state: ${atomicState.thermostats}")
|
||||
|
||||
// if( now > next )
|
||||
if( true ) // for now let's always poll/refresh
|
||||
{
|
||||
log.debug "polling children because $now > $next"
|
||||
debugEvent("polling children because $now > $next")
|
||||
|
||||
pollChildren()
|
||||
|
||||
log.debug "polled children and looking for ${child.device.deviceNetworkId} from ${atomicState.thermostats}"
|
||||
debugEvent ("polled children and looking for ${child.device.deviceNetworkId} from ${atomicState.thermostats}")
|
||||
|
||||
def currentTime = new Date().time
|
||||
debugEvent ("Current Time = ${currentTime}")
|
||||
atomicState.lastPollMillis = currentTime
|
||||
|
||||
def tData = atomicState.thermostats[child.device.deviceNetworkId]
|
||||
|
||||
if(!tData)
|
||||
{
|
||||
log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId} after polling"
|
||||
|
||||
// TODO: flag device as in error state
|
||||
// child.errorState = true
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
tData.updated = currentTime
|
||||
|
||||
return tData.data
|
||||
}
|
||||
else if(atomicState.thermostats[child.device.deviceNetworkId] != null)
|
||||
{
|
||||
log.debug "not polling children, found child ${child.device.deviceNetworkId} "
|
||||
|
||||
def tData = atomicState.thermostats[child.device.deviceNetworkId]
|
||||
if(!tData.updated)
|
||||
{
|
||||
// we have pulled new data for this thermostat, but it has not asked us for it
|
||||
// track it and return the data
|
||||
tData.updated = new Date().time
|
||||
return tData.data
|
||||
}
|
||||
return null
|
||||
}
|
||||
else if(atomicState.thermostats[child.device.deviceNetworkId] == null)
|
||||
{
|
||||
log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId}"
|
||||
|
||||
// TODO: flag device as in error state
|
||||
// child.errorState = true
|
||||
|
||||
return null
|
||||
}
|
||||
else
|
||||
{
|
||||
// it's not time to poll again and this thermostat already has its latest values
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
def availableModes(child)
|
||||
{
|
||||
|
||||
debugEvent ("atomicState.Thermos = ${atomicState.thermostats}")
|
||||
|
||||
debugEvent ("Child DNI = ${child.device.deviceNetworkId}")
|
||||
|
||||
def tData = atomicState.thermostats[child.device.deviceNetworkId]
|
||||
|
||||
debugEvent("Data = ${tData}")
|
||||
|
||||
if(!tData)
|
||||
{
|
||||
log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId} after polling"
|
||||
|
||||
// TODO: flag device as in error state
|
||||
// child.errorState = true
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
def modes = ["off"]
|
||||
|
||||
if (tData.data.heatMode) modes.add("heat")
|
||||
if (tData.data.coolMode) modes.add("cool")
|
||||
if (tData.data.autoMode) modes.add("auto")
|
||||
if (tData.data.auxHeatMode) modes.add("auxHeatOnly")
|
||||
|
||||
modes
|
||||
|
||||
}
|
||||
|
||||
|
||||
def currentMode(child)
|
||||
{
|
||||
|
||||
debugEvent ("atomicState.Thermos = ${atomicState.thermostats}")
|
||||
|
||||
debugEvent ("Child DNI = ${child.device.deviceNetworkId}")
|
||||
|
||||
def tData = atomicState.thermostats[child.device.deviceNetworkId]
|
||||
|
||||
debugEvent("Data = ${tData}")
|
||||
|
||||
if(!tData)
|
||||
{
|
||||
log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId} after polling"
|
||||
|
||||
// TODO: flag device as in error state
|
||||
// child.errorState = true
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
def mode = tData.data.thermostatMode
|
||||
|
||||
mode
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
def pollChildren()
|
||||
{
|
||||
log.debug "polling children"
|
||||
def thermostatIdsString = getChildDeviceIdsString()
|
||||
|
||||
log.debug "polling children: $thermostatIdsString"
|
||||
|
||||
|
||||
def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + thermostatIdsString + '","includeExtendedRuntime":"true","includeSettings":"true","includeRuntime":"true"}}'
|
||||
|
||||
// // TODO: test this:
|
||||
//
|
||||
// def jsonRequestBody = toJson([
|
||||
// selection:[
|
||||
// selectionType: "thermostats",
|
||||
// selectionMatch: getChildDeviceIdsString(),
|
||||
// includeRuntime: true
|
||||
// ]
|
||||
// ])
|
||||
log.debug "json Request: " + jsonRequestBody
|
||||
|
||||
log.debug "State AuthToken: ${atomicState.authToken}"
|
||||
debugEvent "State AuthToken: ${atomicState.authToken}"
|
||||
|
||||
|
||||
def pollParams = [
|
||||
uri: "https://api.ecobee.com",
|
||||
path: "/1/thermostat",
|
||||
headers: ["Content-Type": "text/json", "Authorization": "Bearer ${atomicState.authToken}"],
|
||||
query: [format: 'json', body: jsonRequestBody]
|
||||
]
|
||||
|
||||
debugEvent ("Before HTTPGET to ecobee.")
|
||||
|
||||
try{
|
||||
httpGet(pollParams) { resp ->
|
||||
|
||||
if (resp.data) {
|
||||
debugEvent ("Response from ecobee GET = ${resp.data}")
|
||||
debugEvent ("Response Status = ${resp.status}")
|
||||
}
|
||||
|
||||
if(resp.status == 200) {
|
||||
log.debug "poll results returned"
|
||||
|
||||
atomicState.thermostats = resp.data.thermostatList.inject([:]) { collector, stat ->
|
||||
|
||||
def dni = [ app.id, stat.identifier ].join('.')
|
||||
|
||||
log.debug "updating dni $dni"
|
||||
|
||||
def data = [
|
||||
coolMode: (stat.settings.coolStages > 0),
|
||||
heatMode: (stat.settings.heatStages > 0),
|
||||
autoMode: stat.settings.autoHeatCoolFeatureEnabled,
|
||||
auxHeatMode: (stat.settings.hasHeatPump) && (stat.settings.hasForcedAir || stat.settings.hasElectric || stat.settings.hasBoiler),
|
||||
temperature: stat.runtime.actualTemperature / 10,
|
||||
heatingSetpoint: stat.runtime.desiredHeat / 10,
|
||||
coolingSetpoint: stat.runtime.desiredCool / 10,
|
||||
thermostatMode: stat.settings.hvacMode
|
||||
]
|
||||
|
||||
debugEvent ("Event Data = ${data}")
|
||||
|
||||
collector[dni] = [data:data]
|
||||
return collector
|
||||
}
|
||||
|
||||
log.debug "updated ${atomicState.thermostats?.size()} stats: ${atomicState.thermostats}"
|
||||
}
|
||||
else
|
||||
{
|
||||
log.error "polling children & got http status ${resp.status}"
|
||||
|
||||
//refresh the auth token
|
||||
if (resp.status == 500 && resp.data.status.code == 14)
|
||||
{
|
||||
log.debug "Storing the failed action to try later"
|
||||
atomicState.action = "pollChildren";
|
||||
log.debug "Refreshing your auth_token!"
|
||||
refreshAuthToken()
|
||||
}
|
||||
else
|
||||
{
|
||||
log.error "Authentication error, invalid authentication method, lack of credentials, etc."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
log.debug "___exception polling children: " + e
|
||||
debugEvent ("${e}")
|
||||
|
||||
refreshAuthToken()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def pollHandler() {
|
||||
|
||||
debugEvent ("in Poll() method.")
|
||||
pollChildren() // Hit the ecobee API for update on all thermostats
|
||||
|
||||
atomicState.thermostats.each {stat ->
|
||||
|
||||
def dni = stat.key
|
||||
|
||||
log.debug ("DNI = ${dni}")
|
||||
debugEvent ("DNI = ${dni}")
|
||||
|
||||
def d = getChildDevice(dni)
|
||||
|
||||
if(d)
|
||||
{
|
||||
log.debug ("Found Child Device.")
|
||||
debugEvent ("Found Child Device.")
|
||||
debugEvent("Event Data before generate event call = ${stat}")
|
||||
|
||||
d.generateEvent(atomicState.thermostats[dni].data)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def getChildDeviceIdsString()
|
||||
{
|
||||
return thermostats.collect { it.split(/\./).last() }.join(',')
|
||||
}
|
||||
|
||||
def toJson(Map m)
|
||||
{
|
||||
return new org.json.JSONObject(m).toString()
|
||||
}
|
||||
|
||||
def toQueryString(Map m)
|
||||
{
|
||||
return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&")
|
||||
}
|
||||
|
||||
private refreshAuthToken() {
|
||||
log.debug "refreshing auth token"
|
||||
debugEvent("refreshing OAUTH token")
|
||||
|
||||
if(!atomicState.refreshToken) {
|
||||
log.warn "Can not refresh OAuth token since there is no refreshToken stored"
|
||||
} else {
|
||||
def stcid = getSmartThingsClientId()
|
||||
|
||||
def refreshParams = [
|
||||
method: 'POST',
|
||||
uri : "https://api.ecobee.com",
|
||||
path : "/token",
|
||||
query : [grant_type: 'refresh_token', code: "${atomicState.refreshToken}", client_id: stcid],
|
||||
|
||||
//data?.refreshToken
|
||||
]
|
||||
|
||||
log.debug refreshParams
|
||||
|
||||
//changed to httpPost
|
||||
try {
|
||||
def jsonMap
|
||||
httpPost(refreshParams) { resp ->
|
||||
|
||||
if(resp.status == 200) {
|
||||
log.debug "Token refreshed...calling saved RestAction now!"
|
||||
|
||||
debugEvent("Token refreshed ... calling saved RestAction now!")
|
||||
|
||||
log.debug resp
|
||||
|
||||
jsonMap = resp.data
|
||||
|
||||
if(resp.data) {
|
||||
|
||||
log.debug resp.data
|
||||
debugEvent("Response = ${resp.data}")
|
||||
|
||||
atomicState.refreshToken = resp?.data?.refresh_token
|
||||
atomicState.authToken = resp?.data?.access_token
|
||||
|
||||
debugEvent("Refresh Token = ${atomicState.refreshToken}")
|
||||
debugEvent("OAUTH Token = ${atomicState.authToken}")
|
||||
|
||||
if(atomicState.action && atomicState.action != "") {
|
||||
log.debug "Executing next action: ${atomicState.action}"
|
||||
|
||||
"{atomicState.action}"()
|
||||
|
||||
//remove saved action
|
||||
atomicState.action = ""
|
||||
}
|
||||
|
||||
}
|
||||
atomicState.action = ""
|
||||
} else {
|
||||
log.debug "refresh failed ${resp.status} : ${resp.status.code}"
|
||||
}
|
||||
}
|
||||
|
||||
// atomicState.refreshToken = jsonMap.refresh_token
|
||||
// atomicState.authToken = jsonMap.access_token
|
||||
}
|
||||
catch(Exception e) {
|
||||
log.debug "caught exception refreshing auth token: " + e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def resumeProgram(child)
|
||||
{
|
||||
|
||||
def thermostatIdsString = getChildDeviceIdsString()
|
||||
log.debug "resumeProgram children: $thermostatIdsString"
|
||||
|
||||
def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + thermostatIdsString + '","includeRuntime":true},"functions": [{"type": "resumeProgram"}]}'
|
||||
//, { "type": "sendMessage", "params": { "text": "Setpoint Updated" } }
|
||||
sendJson(jsonRequestBody)
|
||||
}
|
||||
|
||||
def setHold(child, heating, cooling)
|
||||
{
|
||||
|
||||
int h = heating * 10
|
||||
int c = cooling * 10
|
||||
|
||||
log.debug "setpoints____________ - h: $heating - $h, c: $cooling - $c"
|
||||
def thermostatIdsString = getChildDeviceIdsString()
|
||||
log.debug "setCoolingSetpoint children: $thermostatIdsString"
|
||||
|
||||
|
||||
|
||||
def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + thermostatIdsString + '","includeRuntime":true},"functions": [{ "type": "setHold", "params": { "coolHoldTemp": '+c+',"heatHoldTemp": '+h+', "holdType": "indefinite" } } ]}'
|
||||
|
||||
// def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + thermostatIdsString + '","includeRuntime":true},"functions": [{"type": "resumeProgram"}, { "type": "setHold", "params": { "coolHoldTemp": '+c+',"heatHoldTemp": '+h+', "holdType": "indefinite" } } ]}'
|
||||
|
||||
sendJson(jsonRequestBody)
|
||||
}
|
||||
|
||||
def setMode(child, mode)
|
||||
{
|
||||
log.debug "requested mode = ${mode}"
|
||||
def thermostatIdsString = getChildDeviceIdsString()
|
||||
log.debug "setCoolingSetpoint children: $thermostatIdsString"
|
||||
|
||||
|
||||
def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + thermostatIdsString + '","includeRuntime":true},"thermostat": {"settings":{"hvacMode":"'+"${mode}"+'"}}}'
|
||||
|
||||
log.debug "Mode Request Body = ${jsonRequestBody}"
|
||||
debugEvent ("Mode Request Body = ${jsonRequestBody}")
|
||||
|
||||
def result = sendJson(jsonRequestBody)
|
||||
|
||||
if (result) {
|
||||
def tData = atomicState.thermostats[child.device.deviceNetworkId]
|
||||
tData.data.thermostatMode = mode
|
||||
}
|
||||
|
||||
return(result)
|
||||
}
|
||||
|
||||
def changeSetpoint (child, amount)
|
||||
{
|
||||
def tData = atomicState.thermostats[child.device.deviceNetworkId]
|
||||
|
||||
log.debug "In changeSetpoint."
|
||||
debugEvent ("In changeSetpoint.")
|
||||
|
||||
if (tData) {
|
||||
|
||||
def thermostat = tData.data
|
||||
|
||||
log.debug "Thermostat=${thermostat}"
|
||||
debugEvent ("Thermostat=${thermostat}")
|
||||
|
||||
if (thermostat.thermostatMode == "heat") {
|
||||
thermostat.heatingSetpoint = thermostat.heatingSetpoint + amount
|
||||
child.setHeatingSetpoint (thermostat.heatingSetpoint)
|
||||
|
||||
log.debug "New Heating Setpoint = ${thermostat.heatingSetpoint}"
|
||||
debugEvent ("New Heating Setpoint = ${thermostat.heatingSetpoint}")
|
||||
|
||||
}
|
||||
else if (thermostat.thermostatMode == "cool") {
|
||||
thermostat.coolingSetpoint = thermostat.coolingSetpoint + amount
|
||||
child.setCoolingSetpoint (thermostat.coolingSetpoint)
|
||||
|
||||
log.debug "New Cooling Setpoint = ${thermostat.coolingSetpoint}"
|
||||
debugEvent ("New Cooling Setpoint = ${thermostat.coolingSetpoint}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def sendJson(String jsonBody)
|
||||
{
|
||||
|
||||
//log.debug "_____AUTH_____ ${atomicState.authToken}"
|
||||
|
||||
def cmdParams = [
|
||||
uri: "https://api.ecobee.com",
|
||||
|
||||
path: "/1/thermostat",
|
||||
headers: ["Content-Type": "application/json", "Authorization": "Bearer ${atomicState.authToken}"],
|
||||
body: jsonBody
|
||||
]
|
||||
|
||||
def returnStatus = -1
|
||||
|
||||
try{
|
||||
httpPost(cmdParams) { resp ->
|
||||
|
||||
if(resp.status == 200) {
|
||||
|
||||
log.debug "updated ${resp.data}"
|
||||
debugEvent("updated ${resp.data}")
|
||||
returnStatus = resp.data.status.code
|
||||
if (resp.data.status.code == 0)
|
||||
log.debug "Successful call to ecobee API."
|
||||
else {
|
||||
log.debug "Error return code = ${resp.data.status.code}"
|
||||
debugEvent("Error return code = ${resp.data.status.code}")
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
log.error "sent Json & got http status ${resp.status} - ${resp.status.code}"
|
||||
debugEvent ("sent Json & got http status ${resp.status} - ${resp.status.code}")
|
||||
|
||||
//refresh the auth token
|
||||
if (resp.status == 500 && resp.status.code == 14)
|
||||
{
|
||||
//log.debug "Storing the failed action to try later"
|
||||
log.debug "Refreshing your auth_token!"
|
||||
debugEvent ("Refreshing OAUTH Token")
|
||||
refreshAuthToken()
|
||||
return false
|
||||
}
|
||||
else
|
||||
{
|
||||
debugEvent ("Authentication error, invalid authentication method, lack of credentials, etc.")
|
||||
log.error "Authentication error, invalid authentication method, lack of credentials, etc."
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
log.debug "Exception Sending Json: " + e
|
||||
debugEvent ("Exception Sending JSON: " + e)
|
||||
return false
|
||||
}
|
||||
|
||||
if (returnStatus == 0)
|
||||
return true
|
||||
else
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
def getChildNamespace() { "smartthings" }
|
||||
def getChildName() { "Ecobee Thermostat" }
|
||||
|
||||
def getServerUrl() { return appSettings.serverUrl }
|
||||
def getSmartThingsClientId() { appSettings.clientId }
|
||||
|
||||
def debugEvent(message, displayEvent = false) {
|
||||
|
||||
def results = [
|
||||
name: "appdebug",
|
||||
descriptionText: message,
|
||||
displayed: displayEvent
|
||||
]
|
||||
log.debug "Generating AppDebug Event: ${results}"
|
||||
sendEvent (results)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Elder Care
|
||||
*
|
||||
* Author: SmartThings
|
||||
* Date: 2013-03-06
|
||||
*
|
||||
* Stay connected to your loved ones. Get notified if they are not up and moving around
|
||||
* by a specified time and/or if they have not opened a cabinet or door according to a set schedule.
|
||||
*/
|
||||
|
||||
definition(
|
||||
name: "Elder Care: Daily Routine",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Stay connected to your loved ones. Get notified if they are not up and moving around by a specified time and/or if they have not opened a cabinet or door according to a set schedule.",
|
||||
category: "Family",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/calendar_contact-accelerometer.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/calendar_contact-accelerometer@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("Who are you checking on?") {
|
||||
input "person1", "text", title: "Name?"
|
||||
}
|
||||
section("If there's no movement (optional, leave blank to not require)...") {
|
||||
input "motion1", "capability.motionSensor", title: "Where?", required: false
|
||||
}
|
||||
section("or a door or cabinet hasn't been opened (optional, leave blank to not require)...") {
|
||||
input "contact1", "capability.contactSensor", required: false
|
||||
}
|
||||
section("between these times...") {
|
||||
input "time0", "time", title: "From what time?"
|
||||
input "time1", "time", title: "Until what time?"
|
||||
}
|
||||
section("then alert the following people...") {
|
||||
input("recipients", "contact", title: "People to notify", description: "Send notifications to") {
|
||||
input "phone1", "phone", title: "Phone number?", required: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
schedule(time1, "scheduleCheck")
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
unsubscribe() //TODO no longer subscribe like we used to - clean this up after all apps updated
|
||||
unschedule()
|
||||
schedule(time1, "scheduleCheck")
|
||||
}
|
||||
|
||||
def scheduleCheck()
|
||||
{
|
||||
if(noRecentContact() && noRecentMotion()) {
|
||||
def person = person1 ?: "your elder"
|
||||
def msg = "Alert! There has been no activity at ${person}'s place ${timePhrase}"
|
||||
log.debug msg
|
||||
|
||||
if (location.contactBookEnabled) {
|
||||
sendNotificationToContacts(msg, recipients)
|
||||
}
|
||||
else {
|
||||
if (phone1) {
|
||||
sendSms(phone1, msg)
|
||||
} else {
|
||||
sendPush(msg)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.debug "There has been activity ${timePhrase}, not sending alert"
|
||||
}
|
||||
}
|
||||
|
||||
private noRecentMotion()
|
||||
{
|
||||
if(motion1) {
|
||||
def motionEvents = motion1.eventsSince(sinceTime)
|
||||
log.trace "Found ${motionEvents?.size() ?: 0} motion events"
|
||||
if (motionEvents.find { it.value == "active" }) {
|
||||
log.debug "There have been recent 'active' events"
|
||||
return false
|
||||
} else {
|
||||
log.debug "There have not been any recent 'active' events"
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
log.debug "Motion sensor not enabled"
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private noRecentContact()
|
||||
{
|
||||
if(contact1) {
|
||||
def contactEvents = contact1.eventsSince(sinceTime)
|
||||
log.trace "Found ${contactEvents?.size() ?: 0} door events"
|
||||
if (contactEvents.find { it.value == "open" }) {
|
||||
log.debug "There have been recent 'open' events"
|
||||
return false
|
||||
} else {
|
||||
log.debug "There have not been any recent 'open' events"
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
log.debug "Contact sensor not enabled"
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private getSinceTime() {
|
||||
if (time0) {
|
||||
return timeToday(time0, location?.timeZone)
|
||||
}
|
||||
else {
|
||||
return new Date(now() - 21600000)
|
||||
}
|
||||
}
|
||||
|
||||
private getTimePhrase() {
|
||||
def interval = now() - sinceTime.time
|
||||
if (interval < 3600000) {
|
||||
return "in the past ${Math.round(interval/60000)} minutes"
|
||||
}
|
||||
else if (interval < 7200000) {
|
||||
return "in the past hour"
|
||||
}
|
||||
else {
|
||||
return "in the past ${Math.round(interval/3600000)} hours"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Elder Care: Slip & Fall
|
||||
*
|
||||
* Author: SmartThings
|
||||
* Date: 2013-04-07
|
||||
*
|
||||
*/
|
||||
|
||||
definition(
|
||||
name: "Elder Care: Slip & Fall",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Monitors motion sensors in bedroom and bathroom during the night and detects if occupant does not return from the bathroom after a specified period of time.",
|
||||
category: "Family",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/calendar_contact-accelerometer.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/calendar_contact-accelerometer@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("Bedroom motion detector(s)") {
|
||||
input "bedroomMotion", "capability.motionSensor", multiple: true
|
||||
}
|
||||
section("Bathroom motion detector") {
|
||||
input "bathroomMotion", "capability.motionSensor"
|
||||
}
|
||||
section("Active between these times") {
|
||||
input "startTime", "time", title: "Start Time"
|
||||
input "stopTime", "time", title: "Stop Time"
|
||||
}
|
||||
section("Send message when no return within specified time period") {
|
||||
input "warnMessage", "text", title: "Warning Message"
|
||||
input "threshold", "number", title: "Minutes"
|
||||
}
|
||||
section("To these contacts") {
|
||||
input("recipients", "contact", title: "Recipients", description: "Send notifications to") {
|
||||
input "phone1", "phone", required: false
|
||||
input "phone2", "phone", required: false
|
||||
input "phone3", "phone", required: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
initialize()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
|
||||
unsubscribe()
|
||||
initialize()
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
state.active = 0
|
||||
subscribe(bedroomMotion, "motion.active", bedroomActive)
|
||||
subscribe(bathroomMotion, "motion.active", bathroomActive)
|
||||
}
|
||||
|
||||
def bedroomActive(evt) {
|
||||
def start = timeToday(startTime, location?.timeZone)
|
||||
def stop = timeToday(stopTime, location?.timeZone)
|
||||
def now = new Date()
|
||||
log.debug "bedroomActive, status: $state.ststus, start: $start, stop: $stop, now: $now"
|
||||
if (state.status == "waiting") {
|
||||
log.debug "motion detected in bedroom, disarming"
|
||||
unschedule("sendMessage")
|
||||
state.status = null
|
||||
}
|
||||
else {
|
||||
if (start.before(now) && stop.after(now)) {
|
||||
log.debug "motion in bedroom, look for bathroom motion"
|
||||
state.status = "pending"
|
||||
}
|
||||
else {
|
||||
log.debug "Not in time window"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def bathroomActive(evt) {
|
||||
log.debug "bathroomActive, status: $state.status"
|
||||
if (state.status == "pending") {
|
||||
def delay = threshold.toInteger() * 60
|
||||
state.status = "waiting"
|
||||
log.debug "runIn($delay)"
|
||||
runIn(delay, sendMessage)
|
||||
}
|
||||
}
|
||||
|
||||
def sendMessage() {
|
||||
log.debug "sendMessage"
|
||||
def msg = warnMessage
|
||||
log.info msg
|
||||
|
||||
if (location.contactBookEnabled) {
|
||||
sendNotificationToContacts(msg, recipients)
|
||||
}
|
||||
else {
|
||||
sendPush msg
|
||||
if (phone1) {
|
||||
sendSms phone1, msg
|
||||
}
|
||||
if (phone2) {
|
||||
sendSms phone2, msg
|
||||
}
|
||||
if (phone3) {
|
||||
sendSms phone3, msg
|
||||
}
|
||||
}
|
||||
state.status = null
|
||||
}
|
||||
101
smartapps/smartthings/energy-alerts.src/energy-alerts.groovy
Normal file
101
smartapps/smartthings/energy-alerts.src/energy-alerts.groovy
Normal file
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* Energy Saver
|
||||
*
|
||||
* Copyright 2014 SmartThings
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
definition(
|
||||
name: "Energy Alerts",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Get notified if you're using too much energy",
|
||||
category: "Green Living",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/text.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/text@2x.png",
|
||||
iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Meta/text@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section {
|
||||
input(name: "meter", type: "capability.powerMeter", title: "When This Power Meter...", required: true, multiple: false, description: null)
|
||||
input(name: "aboveThreshold", type: "number", title: "Reports Above...", required: true, description: "in either watts or kw.")
|
||||
input(name: "belowThreshold", type: "number", title: "Or Reports Below...", required: true, description: "in either watts or kw.")
|
||||
}
|
||||
section {
|
||||
input("recipients", "contact", title: "Send notifications to") {
|
||||
input(name: "sms", type: "phone", title: "Send A Text To", description: null, required: false)
|
||||
input(name: "pushNotification", type: "bool", title: "Send a push notification", description: null, defaultValue: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
initialize()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
unsubscribe()
|
||||
initialize()
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
subscribe(meter, "power", meterHandler)
|
||||
}
|
||||
|
||||
def meterHandler(evt) {
|
||||
|
||||
def meterValue = evt.value as double
|
||||
|
||||
if (!atomicState.lastValue) {
|
||||
atomicState.lastValue = meterValue
|
||||
}
|
||||
|
||||
def lastValue = atomicState.lastValue as double
|
||||
atomicState.lastValue = meterValue
|
||||
|
||||
def aboveThresholdValue = aboveThreshold as int
|
||||
if (meterValue > aboveThresholdValue) {
|
||||
if (lastValue < aboveThresholdValue) { // only send notifications when crossing the threshold
|
||||
def msg = "${meter} reported ${evt.value} ${evt.unit} which is above your threshold of ${aboveThreshold}."
|
||||
sendMessage(msg)
|
||||
} else {
|
||||
// log.debug "not sending notification for ${evt.description} because the threshold (${aboveThreshold}) has already been crossed"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def belowThresholdValue = belowThreshold as int
|
||||
if (meterValue < belowThresholdValue) {
|
||||
if (lastValue > belowThresholdValue) { // only send notifications when crossing the threshold
|
||||
def msg = "${meter} reported ${evt.value} ${evt.unit} which is below your threshold of ${belowThreshold}."
|
||||
sendMessage(msg)
|
||||
} else {
|
||||
// log.debug "not sending notification for ${evt.description} because the threshold (${belowThreshold}) has already been crossed"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def sendMessage(msg) {
|
||||
if (location.contactBookEnabled) {
|
||||
sendNotificationToContacts(msg, recipients)
|
||||
}
|
||||
else {
|
||||
if (sms) {
|
||||
sendSms(sms, msg)
|
||||
}
|
||||
if (pushNotification) {
|
||||
sendPush(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
59
smartapps/smartthings/energy-saver.src/energy-saver.groovy
Normal file
59
smartapps/smartthings/energy-saver.src/energy-saver.groovy
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Energy Saver
|
||||
*
|
||||
* Copyright 2014 SmartThings
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
definition(
|
||||
name: "Energy Saver",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Turn things off if you're using too much energy",
|
||||
category: "Green Living",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/light_outlet.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/light_outlet@2x.png",
|
||||
iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Meta/light_outlet@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section {
|
||||
input(name: "meter", type: "capability.powerMeter", title: "When This Power Meter...", required: true, multiple: false, description: null)
|
||||
input(name: "threshold", type: "number", title: "Reports Above...", required: true, description: "in either watts or kw.")
|
||||
}
|
||||
section {
|
||||
input(name: "switches", type: "capability.switch", title: "Turn Off These Switches", required: true, multiple: true, description: null)
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
initialize()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
unsubscribe()
|
||||
initialize()
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
subscribe(meter, "power", meterHandler)
|
||||
}
|
||||
|
||||
def meterHandler(evt) {
|
||||
def meterValue = evt.value as double
|
||||
def thresholdValue = threshold as int
|
||||
if (meterValue > thresholdValue) {
|
||||
log.debug "${meter} reported energy consumption above ${threshold}. Turning of switches."
|
||||
switches.off()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,362 @@
|
||||
/**
|
||||
* Every Element
|
||||
*
|
||||
* Copyright 2014 SmartThings
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
definition(
|
||||
name: "Every Element",
|
||||
namespace: "smartthings/examples",
|
||||
author: "SmartThings",
|
||||
description: "Every element demonstration app",
|
||||
category: "SmartThings Internal",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
page(name: "firstPage")
|
||||
page(name: "inputPage")
|
||||
page(name: "appPage")
|
||||
page(name: "labelPage")
|
||||
page(name: "modePage")
|
||||
page(name: "paragraphPage")
|
||||
page(name: "iconPage")
|
||||
page(name: "hrefPage")
|
||||
page(name: "buttonsPage")
|
||||
page(name: "imagePage")
|
||||
page(name: "videoPage")
|
||||
page(name: "deadEnd", title: "Nothing to see here, move along.", content: "foo")
|
||||
page(name: "flattenedPage")
|
||||
}
|
||||
|
||||
def firstPage() {
|
||||
dynamicPage(name: "firstPage", title: "Where to first?", install: true, uninstall: true) {
|
||||
section() {
|
||||
href(page: "inputPage", title: "Element: 'input'")
|
||||
href(page: "appPage", title: "Element: 'app'")
|
||||
href(page: "labelPage", title: "Element: 'label'")
|
||||
href(page: "modePage", title: "Element: 'mode'")
|
||||
href(page: "paragraphPage", title: "Element: 'paragraph'")
|
||||
href(page: "iconPage", title: "Element: 'icon'")
|
||||
href(page: "hrefPage", title: "Element: 'href'")
|
||||
href(page: "buttonsPage", title: "Element: 'buttons'")
|
||||
href(page: "imagePage", title: "Element: 'image'")
|
||||
href(page: "videoPage", title: "Element: 'video'")
|
||||
}
|
||||
section() {
|
||||
href(page: "flattenedPage", title: "All of the above elements on a single page")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def inputPage() {
|
||||
dynamicPage(name: "inputPage", title: "Every 'input' type") {
|
||||
section("enum") {
|
||||
input(type: "enum", name: "enumRefresh", title: "submitOnChange:true", required: false, multiple: true, options: ["one", "two", "three"], submitOnChange: true)
|
||||
if (enumRefresh) {
|
||||
paragraph "${enumRefresh}"
|
||||
}
|
||||
input(type: "enum", name: "enumSegmented", title: "style:segmented", required: false, multiple: true, options: ["one", "two", "three"], style: "segmented")
|
||||
input(type: "enum", name: "enum", title: "required:false, multiple:false", required: false, multiple: false, options: ["one", "two", "three"])
|
||||
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"])
|
||||
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")
|
||||
input(type: "enum", name: "enumWithGroupedOptions", title: "groupedOptions", description: "This enum has grouped options", required: false, multiple: true, groupedOptions: [
|
||||
[
|
||||
title : "the group title that is displayed",
|
||||
order : 0, // the order of the group; 0-based
|
||||
image : "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", // not yet supported
|
||||
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
|
||||
]
|
||||
]
|
||||
],
|
||||
[
|
||||
title : "the second group title that is displayed",
|
||||
order : 1, // the order of the group; 0-based
|
||||
image : null, // not yet supported
|
||||
values: [
|
||||
[
|
||||
key : "some_device_id",
|
||||
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
|
||||
],
|
||||
[
|
||||
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
|
||||
]
|
||||
]
|
||||
]
|
||||
])
|
||||
}
|
||||
section("text") {
|
||||
input(type: "text", name: "text", title: "required:false, multiple:false", required: false, multiple: false)
|
||||
input(type: "text", name: "textRequired", title: "required:true", required: true, multiple: false)
|
||||
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")
|
||||
}
|
||||
section("number") {
|
||||
input(type: "number", name: "number", title: "required:false, multiple:false", required: false, multiple: false)
|
||||
input(type: "number", name: "numberRequired", title: "required:true", required: true, multiple: false)
|
||||
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)
|
||||
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")
|
||||
}
|
||||
section("password") {
|
||||
input(type: "password", name: "password", title: "required:false, multiple:false", required: false, multiple: false)
|
||||
input(type: "password", name: "passwordRequired", title: "required:true", required: true, multiple: false)
|
||||
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")
|
||||
}
|
||||
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")
|
||||
}
|
||||
section("email") {
|
||||
input(type: "email", name: "email", title: "required:false, multiple:false", required: false, multiple: false)
|
||||
input(type: "email", name: "emailRequired", title: "required:true", required: true, multiple: false)
|
||||
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")
|
||||
}
|
||||
section("decimal") {
|
||||
input(type: "decimal", name: "decimal", title: "required:false, multiple:false", required: false, multiple: false)
|
||||
input(type: "decimal", name: "decimalRequired", title: "required:true", required: true, multiple: false)
|
||||
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")
|
||||
}
|
||||
section("mode") {
|
||||
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)
|
||||
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")
|
||||
}
|
||||
section("icon") {
|
||||
input(type: "icon", name: "icon", title: "required:false, multiple:false", required: false, multiple: false)
|
||||
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")
|
||||
}
|
||||
section("capability") {
|
||||
input(type: "capability.switch", name: "capability", title: "required:false, multiple:false", required: false, multiple: false)
|
||||
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")
|
||||
}
|
||||
section("hub") {
|
||||
input(type: "hub", name: "hub", title: "required:false, multiple:false", required: false, multiple: false)
|
||||
input(type: "hub", name: "hubRequired", title: "required:true", required: true, multiple: false)
|
||||
input(type: "hub", name: "hubMultiple", title: "multiple:true", required: false, multiple: true)
|
||||
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")
|
||||
}
|
||||
section("device") {
|
||||
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)
|
||||
input(type: "device.switch", name: "deviceMultiple", title: "multiple:true", required: false, multiple: true)
|
||||
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")
|
||||
}
|
||||
section("time") {
|
||||
input(type: "time", name: "time", title: "required:false, multiple:false", required: false, multiple: false)
|
||||
input(type: "time", name: "timeRequired", title: "required:true", required: true, multiple: false)
|
||||
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")
|
||||
}
|
||||
section("contact-book") {
|
||||
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 appPage() {
|
||||
dynamicPage(name: "appPage", title: "Every 'app' type") {
|
||||
section {
|
||||
paragraph "These won't work unless you create a child SmartApp to link to... Sorry."
|
||||
}
|
||||
section("app") {
|
||||
app(
|
||||
name: "app",
|
||||
title: "required:false, multiple:false",
|
||||
required: false,
|
||||
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: "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") {
|
||||
app(name: "appMultiple", title: "multiple:true", required: false, multiple: true, namespace: "Steve", appName: "Child SmartApp")
|
||||
}
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def labelPage() {
|
||||
dynamicPage(name: "labelPage", title: "Every 'Label' type") {
|
||||
section("label") {
|
||||
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() {
|
||||
dynamicPage(name: "modePage", title: "Every 'mode' type") { // TODO: finish this
|
||||
section("mode") {
|
||||
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() {
|
||||
dynamicPage(name: "paragraphPage", title: "Every 'paragraph' type") {
|
||||
section("paragraph") {
|
||||
paragraph "This us 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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
dynamicPage(name: "hrefPage", title: "Every 'href' type") {
|
||||
section("page") {
|
||||
href(name: "hrefPage", title: "required:false, multiple:false", required: false, multiple: false, page: "deadEnd")
|
||||
href(name: "hrefPageRequired", title: "required:true", required: true, multiple: false, page: "deadEnd", description: "Don't make hrefs required")
|
||||
href(name: "hrefPageComplete", title: "state:complete", required: false, multiple: false, page: "deadEnd", 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("external") {
|
||||
href(name: "hrefExternal", title: "required:false, multiple:false", required: false, multiple: false, style: "external", url: "http://smartthings.com/")
|
||||
href(name: "hrefExternalRequired", title: "required:true", required: true, multiple: false, style: "external", url: "http://smartthings.com/", description: "Don't make hrefs required")
|
||||
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() {
|
||||
dynamicPage(name: "buttonsPage", title: "Every 'button' type") {
|
||||
section("buttons") {
|
||||
buttons(name: "buttons", title: "required:false, multiple:false", required: false, multiple: false, buttons: [
|
||||
[label: "foo", action: "foo"],
|
||||
[label: "bar", action: "bar"]
|
||||
])
|
||||
buttons(name: "buttonsRequired", title: "required:true", required: true, multiple: false, buttons: [
|
||||
[label: "foo", action: "foo"],
|
||||
[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: [
|
||||
[label: "foo", action: "foo"],
|
||||
[label: "bar", action: "bar"]
|
||||
])
|
||||
}
|
||||
section("Colored Buttons") {
|
||||
buttons(name: "buttonsColoredSpecial", title: "special strings", description: "SmartThings highly recommends using these colors", buttons: [
|
||||
[label: "complete", action: "bar", backgroundColor: "complete"],
|
||||
[label: "required", action: "bar", backgroundColor: "required"]
|
||||
])
|
||||
buttons(name: "buttonsColoredHex", title: "hex values work", buttons: [
|
||||
[label: "bg: #000dff", action: "foo", backgroundColor: "#000dff"],
|
||||
[label: "fg: #ffac00", action: "foo", color: "#ffac00"],
|
||||
[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() {
|
||||
dynamicPage(name: "imagePage", title: "Every 'image' type") { // TODO: finish thise
|
||||
section("image") {
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def videoPage() {
|
||||
dynamicPage(name: "imagePage", title: "Every 'image' type") { // TODO: finish this
|
||||
section("video") {
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def flattenedPage() {
|
||||
def allSections = []
|
||||
firstPage().sections.each { section ->
|
||||
section.body.each { hrefElement ->
|
||||
if (hrefElement.page != "flattenedPage") {
|
||||
allSections += "${hrefElement.page}"().sections
|
||||
}
|
||||
}
|
||||
}
|
||||
def flattenedPage = dynamicPage(name: "flattenedPage", title: "All elements in one page!") {}
|
||||
flattenedPage.sections = allSections
|
||||
return flattenedPage
|
||||
}
|
||||
|
||||
def foo() {
|
||||
dynamicPage(name: "deadEnd") {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
|
||||
initialize()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
|
||||
unsubscribe()
|
||||
initialize()
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
// TODO: subscribe to attributes, devices, locations, etc.
|
||||
}
|
||||
51
smartapps/smartthings/feed-my-pet.src/feed-my-pet.groovy
Normal file
51
smartapps/smartthings/feed-my-pet.src/feed-my-pet.groovy
Normal file
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Feed My Pet
|
||||
*
|
||||
* Author: SmartThings
|
||||
*/
|
||||
definition(
|
||||
name: "Feed My Pet",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Setup a schedule for when your pet is fed. Purchase any SmartThings certified pet food feeder and install the Feed My Pet app, and set the time. You and your pet are ready to go. Your life just got smarter.",
|
||||
category: "Pets",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/dogfood_feeder.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/dogfood_feeder@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("Choose your pet feeder...") {
|
||||
input "feeder", "device.PetFeederShield", title: "Where?"
|
||||
}
|
||||
section("Feed my pet at...") {
|
||||
input "time1", "time", title: "When?"
|
||||
}
|
||||
}
|
||||
|
||||
def installed()
|
||||
{
|
||||
schedule(time1, "scheduleCheck")
|
||||
}
|
||||
|
||||
def updated()
|
||||
{
|
||||
unschedule()
|
||||
schedule(time1, "scheduleCheck")
|
||||
}
|
||||
|
||||
def scheduleCheck()
|
||||
{
|
||||
log.trace "scheduledFeeding"
|
||||
feeder?.feed()
|
||||
}
|
||||
73
smartapps/smartthings/flood-alert.src/flood-alert.groovy
Normal file
73
smartapps/smartthings/flood-alert.src/flood-alert.groovy
Normal file
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Flood Alert
|
||||
*
|
||||
* Author: SmartThings
|
||||
*/
|
||||
definition(
|
||||
name: "Flood Alert!",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Get a push notification or text message when water is detected where it doesn't belong.",
|
||||
category: "Safety & Security",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/water_moisture.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/water_moisture@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("When there's water detected...") {
|
||||
input "alarm", "capability.waterSensor", title: "Where?"
|
||||
}
|
||||
section("Send a notification to...") {
|
||||
input("recipients", "contact", title: "Recipients", description: "Send notifications to") {
|
||||
input "phone", "phone", title: "Phone number?", required: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
subscribe(alarm, "water.wet", waterWetHandler)
|
||||
}
|
||||
|
||||
def updated() {
|
||||
unsubscribe()
|
||||
subscribe(alarm, "water.wet", waterWetHandler)
|
||||
}
|
||||
|
||||
def waterWetHandler(evt) {
|
||||
def deltaSeconds = 60
|
||||
|
||||
def timeAgo = new Date(now() - (1000 * deltaSeconds))
|
||||
def recentEvents = alarm.eventsSince(timeAgo)
|
||||
log.debug "Found ${recentEvents?.size() ?: 0} events in the last $deltaSeconds seconds"
|
||||
|
||||
def alreadySentSms = recentEvents.count { it.value && it.value == "wet" } > 1
|
||||
|
||||
if (alreadySentSms) {
|
||||
log.debug "SMS already sent to $phone within the last $deltaSeconds seconds"
|
||||
} else {
|
||||
def msg = "${alarm.displayName} is wet!"
|
||||
log.debug "$alarm is wet, texting $phone"
|
||||
|
||||
if (location.contactBookEnabled) {
|
||||
sendNotificationToContacts(msg, recipients)
|
||||
}
|
||||
else {
|
||||
sendPush(msg)
|
||||
if (phone) {
|
||||
sendSms(phone, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
247
smartapps/smartthings/foscam-connect.src/foscam-connect.groovy
Normal file
247
smartapps/smartthings/foscam-connect.src/foscam-connect.groovy
Normal file
@@ -0,0 +1,247 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Foscam (connect)
|
||||
*
|
||||
* Author: smartthings
|
||||
* Date: 2014-03-10
|
||||
*/
|
||||
|
||||
definition(
|
||||
name: "Foscam (Connect)",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Connect and take pictures using your Foscam camera from inside the Smartthings app.",
|
||||
category: "SmartThings Internal",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/foscam.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/foscam@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
page(name: "cameraDiscovery", title:"Foscam Camera Setup", content:"cameraDiscovery")
|
||||
page(name: "loginToFoscam", title: "Foscam Login")
|
||||
}
|
||||
|
||||
//PAGES
|
||||
/////////////////////////////////////
|
||||
def cameraDiscovery()
|
||||
{
|
||||
if(canInstallLabs())
|
||||
{
|
||||
int refreshCount = !state.refreshCount ? 0 : state.refreshCount as int
|
||||
state.refreshCount = refreshCount + 1
|
||||
def refreshInterval = 3
|
||||
|
||||
def options = camerasDiscovered() ?: []
|
||||
def numFound = options.size() ?: 0
|
||||
|
||||
if(!state.subscribe) {
|
||||
subscribe(location, null, locationHandler, [filterEvents:false])
|
||||
state.subscribe = true
|
||||
}
|
||||
|
||||
//bridge discovery request every
|
||||
if((refreshCount % 5) == 0) {
|
||||
discoverCameras()
|
||||
}
|
||||
|
||||
return dynamicPage(name:"cameraDiscovery", title:"Discovery Started!", nextPage:"loginToFoscam", refreshInterval:refreshInterval, uninstall: true) {
|
||||
section("Please wait while we discover your Foscam. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") {
|
||||
input "selectedFoscam", "enum", required:false, title:"Select Foscam (${numFound} found)", multiple:true, options:options
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
def upgradeNeeded = """To use Foscam, your Hub should be completely up to date.
|
||||
|
||||
To update your Hub, access Location Settings in the Main Menu (tap the gear next to your location name), select your Hub, and choose "Update Hub"."""
|
||||
|
||||
return dynamicPage(name:"cameraDiscovery", title:"Upgrade needed!", nextPage:"", install:false, uninstall: true) {
|
||||
section("Upgrade") {
|
||||
paragraph "$upgradeNeeded"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
def loginToFoscam() {
|
||||
def showUninstall = username != null && password != null
|
||||
return dynamicPage(name: "loginToFoscam", title: "Foscam", uninstall:showUninstall, install:true,) {
|
||||
section("Log in to Foscam") {
|
||||
input "username", "text", title: "Username", required: true, autoCorrect:false
|
||||
input "password", "password", title: "Password", required: true, autoCorrect:false
|
||||
}
|
||||
}
|
||||
}
|
||||
//END PAGES
|
||||
|
||||
/////////////////////////////////////
|
||||
private discoverCameras()
|
||||
{
|
||||
//add type UDP_CLIENT
|
||||
def action = new physicalgraph.device.HubAction("0b4D4F5F490000000000000000000000040000000400000000000001", physicalgraph.device.Protocol.LAN, "FFFFFFFF:2710")
|
||||
action.options = [type:"LAN_TYPE_UDPCLIENT"]
|
||||
sendHubCommand(action)
|
||||
}
|
||||
|
||||
def camerasDiscovered() {
|
||||
def cameras = getCameras()
|
||||
def map = [:]
|
||||
cameras.each {
|
||||
def value = it.value.name ?: "Foscam Camera"
|
||||
def key = it.value.ip + ":" + it.value.port
|
||||
map["${key}"] = value
|
||||
}
|
||||
map
|
||||
}
|
||||
|
||||
/////////////////////////////////////
|
||||
def getCameras()
|
||||
{
|
||||
state.cameras = state.cameras ?: [:]
|
||||
}
|
||||
|
||||
/////////////////////////////////////
|
||||
def installed() {
|
||||
//log.debug "Installed with settings: ${settings}"
|
||||
initialize()
|
||||
|
||||
runIn(300, "doDeviceSync" , [overwrite: false]) //setup ip:port syncing every 5 minutes
|
||||
|
||||
//wait 5 seconds and get the deviceInfo
|
||||
//log.info "calling 'getDeviceInfo()'"
|
||||
//runIn(5, getDeviceInfo)
|
||||
}
|
||||
|
||||
/////////////////////////////////////
|
||||
def updated() {
|
||||
//log.debug "Updated with settings: ${settings}"
|
||||
unsubscribe()
|
||||
initialize()
|
||||
}
|
||||
|
||||
/////////////////////////////////////
|
||||
def initialize() {
|
||||
// remove location subscription aftwards
|
||||
unsubscribe()
|
||||
state.subscribe = false
|
||||
|
||||
if (selectedFoscam)
|
||||
{
|
||||
addCameras()
|
||||
}
|
||||
}
|
||||
|
||||
def addCameras() {
|
||||
def cameras = getCameras()
|
||||
|
||||
selectedFoscam.each { dni ->
|
||||
def d = getChildDevice(dni)
|
||||
|
||||
if(!d)
|
||||
{
|
||||
def newFoscam = cameras.find { (it.value.ip + ":" + it.value.port) == dni }
|
||||
d = addChildDevice("smartthings", "Foscam", dni, newFoscam?.value?.hub, ["label":newFoscam?.value?.name ?: "Foscam Camera", "data":["mac": newFoscam?.value?.mac, "ip": newFoscam.value.ip, "port":newFoscam.value.port], "preferences":["username":username, "password":password]])
|
||||
|
||||
log.debug "created ${d.displayName} with id $dni"
|
||||
}
|
||||
else
|
||||
{
|
||||
log.debug "found ${d.displayName} with id $dni already exists"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def getDeviceInfo() {
|
||||
def devices = getAllChildDevices()
|
||||
devices.each { d ->
|
||||
d.getDeviceInfo()
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////////////
|
||||
def locationHandler(evt) {
|
||||
/*
|
||||
FOSCAM EXAMPLE
|
||||
4D4F5F4901000000000000000000006200000000000000 (SOF) //46
|
||||
30303632364534443042344200 (mac) //26
|
||||
466F7363616D5F44617274684D61756C0000000000 (name) //42
|
||||
0A01652C (ip) //8
|
||||
FFFFFE00 (mask) //8
|
||||
00000000 (gateway ip) //8
|
||||
00000000 (dns) //8
|
||||
01005800 (reserve) //8
|
||||
01040108 (system software version) //8
|
||||
020B0106 (app software version) //8
|
||||
0058 (port) //4
|
||||
01 (dhcp enabled) //2
|
||||
*/
|
||||
def description = evt.description
|
||||
def hub = evt?.hubId
|
||||
|
||||
log.debug "GOT LOCATION EVT: $description"
|
||||
|
||||
def parsedEvent = stringToMap(description)
|
||||
|
||||
//FOSCAM does a UDP response with camera operate protocol:“MO_I” i.e. "4D4F5F49"
|
||||
if (parsedEvent?.type == "LAN_TYPE_UDPCLIENT" && parsedEvent?.payload?.startsWith("4D4F5F49"))
|
||||
{
|
||||
def unpacked = [:]
|
||||
unpacked.mac = parsedEvent.mac.toString()
|
||||
unpacked.name = hexToString(parsedEvent.payload[72..113]).trim()
|
||||
unpacked.ip = parsedEvent.payload[114..121]
|
||||
unpacked.subnet = parsedEvent.payload[122..129]
|
||||
unpacked.gateway = parsedEvent.payload[130..137]
|
||||
unpacked.dns = parsedEvent.payload[138..145]
|
||||
unpacked.reserve = parsedEvent.payload[146..153]
|
||||
unpacked.sysVersion = parsedEvent.payload[154..161]
|
||||
unpacked.appVersion = parsedEvent.payload[162..169]
|
||||
unpacked.port = parsedEvent.payload[170..173]
|
||||
unpacked.dhcp = parsedEvent.payload[174..175]
|
||||
unpacked.hub = hub
|
||||
|
||||
def cameras = getCameras()
|
||||
if (!(cameras."${parsedEvent.mac.toString()}"))
|
||||
{
|
||||
cameras << [("${parsedEvent.mac.toString()}"):unpacked]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////////////
|
||||
private Boolean canInstallLabs()
|
||||
{
|
||||
return hasAllHubsOver("000.011.00603")
|
||||
}
|
||||
|
||||
private Boolean hasAllHubsOver(String desiredFirmware)
|
||||
{
|
||||
return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware }
|
||||
}
|
||||
|
||||
private List getRealHubFirmwareVersions()
|
||||
{
|
||||
return location.hubs*.firmwareVersionString.findAll { it }
|
||||
}
|
||||
|
||||
private String hexToString(String txtInHex)
|
||||
{
|
||||
byte [] txtInByte = new byte [txtInHex.length() / 2];
|
||||
int j = 0;
|
||||
for (int i = 0; i < txtInHex.length(); i += 2)
|
||||
{
|
||||
txtInByte[j++] = Byte.parseByte(txtInHex.substring(i, i + 2), 16);
|
||||
}
|
||||
return new String(txtInByte);
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Garage Door Monitor
|
||||
*
|
||||
* Author: SmartThings
|
||||
*/
|
||||
definition(
|
||||
name: "Garage Door Monitor",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Monitor your garage door and get a text message if it is open too long",
|
||||
category: "Safety & Security",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/garage_contact.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/garage_contact@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("When the garage door is open...") {
|
||||
input "multisensor", "capability.threeAxis", title: "Which?"
|
||||
}
|
||||
section("For too long...") {
|
||||
input "maxOpenTime", "number", title: "Minutes?"
|
||||
}
|
||||
section("Text me at (optional, sends a push notification if not specified)...") {
|
||||
input("recipients", "contact", title: "Notify", description: "Send notifications to") {
|
||||
input "phone", "phone", title: "Phone number?", required: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def installed()
|
||||
{
|
||||
subscribe(multisensor, "acceleration", accelerationHandler)
|
||||
}
|
||||
|
||||
def updated()
|
||||
{
|
||||
unsubscribe()
|
||||
subscribe(multisensor, "acceleration", accelerationHandler)
|
||||
}
|
||||
|
||||
def accelerationHandler(evt) {
|
||||
def latestThreeAxisState = multisensor.threeAxisState // e.g.: 0,0,-1000
|
||||
if (latestThreeAxisState) {
|
||||
def isOpen = Math.abs(latestThreeAxisState.xyzValue.z) > 250 // TODO: Test that this value works in most cases...
|
||||
def isNotScheduled = state.status != "scheduled"
|
||||
|
||||
if (!isOpen) {
|
||||
clearSmsHistory()
|
||||
clearStatus()
|
||||
}
|
||||
|
||||
if (isOpen && isNotScheduled) {
|
||||
runIn(maxOpenTime * 60, takeAction, [overwrite: false])
|
||||
state.status = "scheduled"
|
||||
}
|
||||
|
||||
}
|
||||
else {
|
||||
log.warn "COULD NOT FIND LATEST 3-AXIS STATE FOR: ${multisensor}"
|
||||
}
|
||||
}
|
||||
|
||||
def takeAction(){
|
||||
if (state.status == "scheduled")
|
||||
{
|
||||
def deltaMillis = 1000 * 60 * maxOpenTime
|
||||
def timeAgo = new Date(now() - deltaMillis)
|
||||
def openTooLong = multisensor.threeAxisState.dateCreated.toSystemDate() < timeAgo
|
||||
|
||||
def recentTexts = state.smsHistory.find { it.sentDate.toSystemDate() > timeAgo }
|
||||
|
||||
if (!recentTexts) {
|
||||
sendTextMessage()
|
||||
}
|
||||
runIn(maxOpenTime * 60, takeAction, [overwrite: false])
|
||||
} else {
|
||||
log.trace "Status is no longer scheduled. Not sending text."
|
||||
}
|
||||
}
|
||||
|
||||
def sendTextMessage() {
|
||||
log.debug "$multisensor was open too long, texting $phone"
|
||||
|
||||
updateSmsHistory()
|
||||
def openMinutes = maxOpenTime * (state.smsHistory?.size() ?: 1)
|
||||
def msg = "Your ${multisensor.label ?: multisensor.name} has been open for more than ${openMinutes} minutes!"
|
||||
if (location.contactBookEnabled) {
|
||||
sendNotificationToContacts(msg, recipients)
|
||||
}
|
||||
else {
|
||||
if (phone) {
|
||||
sendSms(phone, msg)
|
||||
} else {
|
||||
sendPush msg
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def updateSmsHistory() {
|
||||
if (!state.smsHistory) state.smsHistory = []
|
||||
|
||||
if(state.smsHistory.size() > 9) {
|
||||
log.debug "SmsHistory is too big, reducing size"
|
||||
state.smsHistory = state.smsHistory[-9..-1]
|
||||
}
|
||||
state.smsHistory << [sentDate: new Date().toSystemFormat()]
|
||||
}
|
||||
|
||||
def clearSmsHistory() {
|
||||
state.smsHistory = null
|
||||
}
|
||||
|
||||
def clearStatus() {
|
||||
state.status = null
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Garage Door Opener
|
||||
*
|
||||
* Author: SmartThings
|
||||
*/
|
||||
definition(
|
||||
name: "Garage Door Opener",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Open your garage door when a switch is turned on.",
|
||||
category: "Convenience",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/garage_outlet.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/garage_outlet@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("When the garage door switch is turned on, open the garage door...") {
|
||||
input "switch1", "capability.switch"
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
subscribe(app, appTouchHandler)
|
||||
subscribeToCommand(switch1, "on", onCommand)
|
||||
}
|
||||
|
||||
def updated() {
|
||||
unsubscribe()
|
||||
subscribe(app, appTouchHandler)
|
||||
subscribeToCommand(switch1, "on", onCommand)
|
||||
}
|
||||
|
||||
def appTouch(evt) {
|
||||
log.debug "appTouch: $evt.value, $evt"
|
||||
switch1?.on()
|
||||
}
|
||||
|
||||
def onCommand(evt) {
|
||||
log.debug "onCommand: $evt.value, $evt"
|
||||
switch1?.off(delay: 3000)
|
||||
}
|
||||
833
smartapps/smartthings/gentle-wake-up.src/gentle-wake-up.groovy
Normal file
833
smartapps/smartthings/gentle-wake-up.src/gentle-wake-up.groovy
Normal file
@@ -0,0 +1,833 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Gentle Wake Up
|
||||
*
|
||||
* Author: Steve Vlaminck
|
||||
* Date: 2013-03-11
|
||||
*
|
||||
* https://s3.amazonaws.com/smartapp-icons/HealthAndWellness/App-SleepyTime.png
|
||||
* https://s3.amazonaws.com/smartapp-icons/HealthAndWellness/App-SleepyTime%402x.png
|
||||
* Gentle Wake Up turns on your lights slowly, allowing you to wake up more
|
||||
* naturally. Once your lights have reached full brightness, optionally turn on
|
||||
* more things, or send yourself a text for a more gentle nudge into the waking
|
||||
* world (you may want to set your normal alarm as a backup plan).
|
||||
*
|
||||
*/
|
||||
definition(
|
||||
name: "Gentle Wake Up",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Gentle Wake Up dims your lights slowly, allowing you to wake up more naturally. Once your lights have finished dimming, optionally turn on more things or send yourself a text for a more gentle nudge into the waking world (you may want to set your normal alarm as a backup plan).",
|
||||
category: "Health & Wellness",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/HealthAndWellness/App-SleepyTime.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/HealthAndWellness/App-SleepyTime@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
page(name: "rootPage")
|
||||
page(name: "schedulingPage")
|
||||
page(name: "completionPage")
|
||||
page(name: "numbersPage")
|
||||
}
|
||||
|
||||
def rootPage() {
|
||||
dynamicPage(name: "rootPage", title: "", install: true, uninstall: true) {
|
||||
|
||||
section {
|
||||
input(name: "dimmers", type: "capability.switchLevel", title: "Dimmers", description: null, multiple: true, required: true, submitOnChange: true)
|
||||
}
|
||||
|
||||
if (dimmers) {
|
||||
|
||||
section {
|
||||
href(name: "toNumbersPage", page: "numbersPage", title: "Duration & Direction", description: numbersPageHrefDescription(), state: "complete")
|
||||
}
|
||||
|
||||
section {
|
||||
href(name: "toSchedulingPage", page: "schedulingPage", title: "Rules For Automatically Dimming Your Lights", description: schedulingHrefDescription(), state: schedulingHrefDescription() ? "complete" : "")
|
||||
}
|
||||
|
||||
section {
|
||||
href(name: "toCompletionPage", title: "Completion Actions (Optional)", page: "completionPage", state: completionHrefDescription() ? "complete" : "", description: completionHrefDescription())
|
||||
}
|
||||
|
||||
section {
|
||||
// TODO: fancy label
|
||||
label(title: "Label this SmartApp", required: false, defaultValue: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def numbersPage() {
|
||||
dynamicPage(name:"numbersPage", title:"") {
|
||||
|
||||
section {
|
||||
paragraph(name: "pGraph", title: "These lights will dim", fancyDeviceString(dimmers))
|
||||
}
|
||||
|
||||
section {
|
||||
input(name: "duration", type: "number", title: "For this many minutes", description: "30", required: false, defaultValue: 30)
|
||||
}
|
||||
|
||||
section {
|
||||
input(name: "startLevel", type: "number", range: "0..99", title: "From this level", defaultValue: defaultStart(), description: "Current Level", required: false, multiple: false)
|
||||
input(name: "endLevel", type: "number", range: "0..99", title: "To this level", defaultValue: defaultEnd(), description: "Between 0 and 99", required: true, multiple: false)
|
||||
}
|
||||
|
||||
def colorDimmers = dimmersWithSetColorCommand()
|
||||
if (colorDimmers) {
|
||||
section {
|
||||
input(name: "colorize", type: "bool", title: "Gradually change the color of ${fancyDeviceString(colorDimmers)}", description: null, required: false, defaultValue: "true")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def defaultStart() {
|
||||
if (usesOldSettings() && direction && direction == "Down") {
|
||||
return 99
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
def defaultEnd() {
|
||||
if (usesOldSettings() && direction && direction == "Down") {
|
||||
return 0
|
||||
}
|
||||
return 99
|
||||
}
|
||||
|
||||
def startLevelLabel() {
|
||||
if (usesOldSettings()) { // using old settings
|
||||
if (direction && direction == "Down") { // 99 -> 1
|
||||
return "99%"
|
||||
}
|
||||
return "0%"
|
||||
}
|
||||
return hasStartLevel() ? "${startLevel}%" : "Current Level"
|
||||
}
|
||||
|
||||
def endLevelLabel() {
|
||||
if (usesOldSettings()) {
|
||||
if (direction && direction == "Down") { // 99 -> 1
|
||||
return "0%"
|
||||
}
|
||||
return "99%"
|
||||
}
|
||||
return "${endLevel}%"
|
||||
}
|
||||
|
||||
def schedulingPage() {
|
||||
dynamicPage(name: "schedulingPage", title: "Rules For Automatically Dimming Your Lights") {
|
||||
|
||||
section {
|
||||
input(name: "days", type: "enum", title: "Allow Automatic Dimming On These Days", description: "Every day", required: false, multiple: true, options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"])
|
||||
}
|
||||
|
||||
section {
|
||||
input(name: "modeStart", title: "Start when entering this mode", type: "mode", required: false, mutliple: false, submitOnChange: true)
|
||||
if (modeStart) {
|
||||
input(name: "modeStop", title: "Stop when leaving '${modeStart}' mode", type: "bool", required: false)
|
||||
}
|
||||
}
|
||||
|
||||
section {
|
||||
input(name: "startTime", type: "time", title: "Start Dimming At This Time", description: null, required: false)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
def completionPage() {
|
||||
dynamicPage(name: "completionPage", title: "Completion Rules") {
|
||||
|
||||
section("Switches") {
|
||||
input(name: "completionSwitches", type: "capability.switch", title: "Set these switches", description: null, required: false, multiple: true, submitOnChange: true)
|
||||
if (completionSwitches || androidClient()) {
|
||||
input(name: "completionSwitchesState", type: "enum", title: "To", description: null, required: false, multiple: false, options: ["on", "off"], style: "segmented", defaultValue: "on")
|
||||
input(name: "completionSwitchesLevel", type: "number", title: "Optionally, Set Dimmer Levels To", description: null, required: false, multiple: false, range: "(0..99)")
|
||||
}
|
||||
}
|
||||
|
||||
section("Notifications") {
|
||||
input("recipients", "contact", title: "Send notifications to") {
|
||||
input(name: "completionPhoneNumber", type: "phone", title: "Text This Number", description: "Phone number", required: false)
|
||||
input(name: "completionPush", type: "bool", title: "Send A Push Notification", description: "Phone number", required: false)
|
||||
}
|
||||
input(name: "completionMusicPlayer", type: "capability.musicPlayer", title: "Speak Using This Music Player", required: false)
|
||||
input(name: "completionMessage", type: "text", title: "With This Message", description: null, required: false)
|
||||
}
|
||||
|
||||
section("Modes and Phrases") {
|
||||
input(name: "completionMode", type: "mode", title: "Change ${location.name} Mode To", description: null, required: false)
|
||||
input(name: "completionPhrase", type: "enum", title: "Execute The Phrase", description: null, required: false, multiple: false, options: location.helloHome.getPhrases().label)
|
||||
}
|
||||
|
||||
section("Delay") {
|
||||
input(name: "completionDelay", type: "number", title: "Delay This Many Minutes Before Executing These Actions", description: "0", required: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
// Handlers
|
||||
// ========================================================
|
||||
|
||||
def installed() {
|
||||
log.debug "Installing 'Gentle Wake Up' with settings: ${settings}"
|
||||
|
||||
initialize()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "Updating 'Gentle Wake Up' with settings: ${settings}"
|
||||
unschedule()
|
||||
|
||||
initialize()
|
||||
}
|
||||
|
||||
private initialize() {
|
||||
stop()
|
||||
|
||||
if (startTime) {
|
||||
log.debug "scheduling dimming routine to run at $startTime"
|
||||
schedule(startTime, "scheduledStart")
|
||||
}
|
||||
|
||||
// TODO: make this an option
|
||||
subscribe(app, appHandler)
|
||||
|
||||
subscribe(location, locationHandler)
|
||||
}
|
||||
|
||||
def appHandler(evt) {
|
||||
log.debug "appHandler evt: ${evt.value}"
|
||||
if (evt.value == "touch") {
|
||||
if (atomicState.running) {
|
||||
stop()
|
||||
} else {
|
||||
start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def locationHandler(evt) {
|
||||
log.debug "locationHandler evt: ${evt.value}"
|
||||
|
||||
if (!modeStart) {
|
||||
return
|
||||
}
|
||||
|
||||
def isSpecifiedMode = (evt.value == modeStart)
|
||||
def modeStopIsTrue = (modeStop && modeStop != "false")
|
||||
|
||||
if (isSpecifiedMode && canStartAutomatically()) {
|
||||
start()
|
||||
} else if (!isSpecifiedMode && modeStopIsTrue) {
|
||||
stop()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
// Scheduling
|
||||
// ========================================================
|
||||
|
||||
def scheduledStart() {
|
||||
if (canStartAutomatically()) {
|
||||
start()
|
||||
}
|
||||
}
|
||||
|
||||
def start() {
|
||||
log.trace "START"
|
||||
|
||||
setLevelsInState()
|
||||
|
||||
atomicState.running = true
|
||||
|
||||
atomicState.start = new Date().getTime()
|
||||
|
||||
schedule("0 * * * * ?", "healthCheck")
|
||||
increment()
|
||||
}
|
||||
|
||||
def stop() {
|
||||
log.trace "STOP"
|
||||
|
||||
atomicState.running = false
|
||||
atomicState.start = 0
|
||||
|
||||
unschedule("healthCheck")
|
||||
}
|
||||
|
||||
private healthCheck() {
|
||||
log.trace "'Gentle Wake Up' healthCheck"
|
||||
|
||||
if (!atomicState.running) {
|
||||
return
|
||||
}
|
||||
|
||||
increment()
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
// Setting levels
|
||||
// ========================================================
|
||||
|
||||
|
||||
private increment() {
|
||||
|
||||
if (!atomicState.running) {
|
||||
return
|
||||
}
|
||||
|
||||
def percentComplete = completionPercentage()
|
||||
|
||||
if (percentComplete > 99) {
|
||||
percentComplete = 99
|
||||
}
|
||||
|
||||
updateDimmers(percentComplete)
|
||||
|
||||
if (percentComplete < 99) {
|
||||
|
||||
def runAgain = stepDuration()
|
||||
log.debug "Rescheduling to run again in ${runAgain} seconds"
|
||||
|
||||
runIn(runAgain, 'increment', [overwrite: true])
|
||||
|
||||
} else {
|
||||
|
||||
int completionDelay = completionDelaySeconds()
|
||||
if (completionDelay) {
|
||||
log.debug "Finished with steps. Scheduling completion for ${completionDelay} second(s) from now"
|
||||
runIn(completionDelay, 'completion', [overwrite: true])
|
||||
unschedule("healthCheck")
|
||||
// don't let the health check start incrementing again while we wait for the delayed execution of completion
|
||||
} else {
|
||||
log.debug "Finished with steps. Execution completion"
|
||||
completion()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def updateDimmers(percentComplete) {
|
||||
dimmers.each { dimmer ->
|
||||
|
||||
def nextLevel = dynamicLevel(dimmer, percentComplete)
|
||||
|
||||
if (nextLevel == 0) {
|
||||
|
||||
dimmer.off()
|
||||
|
||||
} else {
|
||||
|
||||
def shouldChangeColors = (colorize && colorize != "false")
|
||||
def canChangeColors = hasSetColorCommand(dimmer)
|
||||
|
||||
log.debug "Setting ${deviceLabel(dimmer)} to ${nextLevel}"
|
||||
|
||||
if (shouldChangeColors && canChangeColors) {
|
||||
dimmer.setColor([hue: getHue(dimmer, nextLevel), saturation: 100, level: nextLevel])
|
||||
} else {
|
||||
dimmer.setLevel(nextLevel)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int dynamicLevel(dimmer, percentComplete) {
|
||||
def start = atomicState.startLevels[dimmer.id]
|
||||
def end = dynamicEndLevel()
|
||||
|
||||
if (!percentComplete) {
|
||||
return start
|
||||
}
|
||||
|
||||
def totalDiff = end - start
|
||||
def actualPercentage = percentComplete / 100
|
||||
def percentOfTotalDiff = totalDiff * actualPercentage
|
||||
|
||||
(start + percentOfTotalDiff) as int
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
// Completion
|
||||
// ========================================================
|
||||
|
||||
private completion() {
|
||||
log.trace "Starting completion block"
|
||||
|
||||
if (!atomicState.running) {
|
||||
return
|
||||
}
|
||||
|
||||
stop()
|
||||
|
||||
handleCompletionSwitches()
|
||||
|
||||
handleCompletionMessaging()
|
||||
|
||||
handleCompletionModesAndPhrases()
|
||||
|
||||
}
|
||||
|
||||
private handleCompletionSwitches() {
|
||||
completionSwitches.each { completionSwitch ->
|
||||
|
||||
def isDimmer = hasSetLevelCommand(completionSwitch)
|
||||
|
||||
if (completionSwitchesLevel && isDimmer) {
|
||||
completionSwitch.setLevel(completionSwitchesLevel)
|
||||
} else {
|
||||
def command = completionSwitchesState ?: "on"
|
||||
completionSwitch."${command}"()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private handleCompletionMessaging() {
|
||||
if (completionMessage) {
|
||||
if (location.contactBookEnabled) {
|
||||
sendNotificationToContacts(completionMessage, recipients)
|
||||
} else {
|
||||
if (completionPhoneNumber) {
|
||||
sendSms(completionPhoneNumber, completionMessage)
|
||||
}
|
||||
if (completionPush) {
|
||||
sendPush(completionMessage)
|
||||
}
|
||||
}
|
||||
if (completionMusicPlayer) {
|
||||
speak(completionMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private handleCompletionModesAndPhrases() {
|
||||
|
||||
if (completionMode) {
|
||||
setLocationMode(completionMode)
|
||||
}
|
||||
|
||||
if (completionPhrase) {
|
||||
location.helloHome.execute(completionPhrase)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def speak(message) {
|
||||
def sound = textToSpeech(message)
|
||||
def soundDuration = (sound.duration as Integer) + 2
|
||||
log.debug "Playing $sound.uri"
|
||||
completionMusicPlayer.playTrack(sound.uri)
|
||||
log.debug "Scheduled resume in $soundDuration sec"
|
||||
runIn(soundDuration, resumePlaying, [overwrite: true])
|
||||
}
|
||||
|
||||
def resumePlaying() {
|
||||
log.trace "resumePlaying()"
|
||||
def sonos = completionMusicPlayer
|
||||
if (sonos) {
|
||||
def currentTrack = sonos.currentState("trackData").jsonValue
|
||||
if (currentTrack.status == "playing") {
|
||||
sonos.playTrack(currentTrack)
|
||||
} else {
|
||||
sonos.setTrack(currentTrack)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
// Helpers
|
||||
// ========================================================
|
||||
|
||||
def setLevelsInState() {
|
||||
def startLevels = [:]
|
||||
dimmers.each { dimmer ->
|
||||
if (usesOldSettings()) {
|
||||
startLevels[dimmer.id] = defaultStart()
|
||||
} else if (hasStartLevel()) {
|
||||
startLevels[dimmer.id] = startLevel
|
||||
} else {
|
||||
def dimmerIsOff = dimmer.currentValue("switch") == "off"
|
||||
startLevels[dimmer.id] = dimmerIsOff ? 0 : dimmer.currentValue("level")
|
||||
}
|
||||
}
|
||||
|
||||
atomicState.startLevels = startLevels
|
||||
}
|
||||
|
||||
def canStartAutomatically() {
|
||||
|
||||
def today = new Date().format("EEEE")
|
||||
log.debug "today: ${today}, days: ${days}"
|
||||
|
||||
if (!days || days.contains(today)) {// if no days, assume every day
|
||||
return true
|
||||
}
|
||||
|
||||
log.trace "should not run"
|
||||
return false
|
||||
}
|
||||
|
||||
def completionPercentage() {
|
||||
log.trace "checkingTime"
|
||||
|
||||
if (!atomicState.running) {
|
||||
return
|
||||
}
|
||||
|
||||
int now = new Date().getTime()
|
||||
int diff = now - atomicState.start
|
||||
int totalRunTime = totalRunTimeMillis()
|
||||
int percentOfRunTime = (diff / totalRunTime) * 100
|
||||
log.debug "percentOfRunTime: ${percentOfRunTime}"
|
||||
|
||||
percentOfRunTime
|
||||
}
|
||||
|
||||
int totalRunTimeMillis() {
|
||||
int minutes = sanitizeInt(duration, 30)
|
||||
def seconds = minutes * 60
|
||||
def millis = seconds * 1000
|
||||
return millis as int
|
||||
}
|
||||
|
||||
int dynamicEndLevel() {
|
||||
if (usesOldSettings()) {
|
||||
if (direction && direction == "Down") {
|
||||
return 0
|
||||
}
|
||||
return 99
|
||||
}
|
||||
return endLevel as int
|
||||
}
|
||||
|
||||
def getHue(dimmer, level) {
|
||||
def start = atomicState.startLevels[dimmer.id] as int
|
||||
def end = dynamicEndLevel()
|
||||
if (start > end) {
|
||||
return getDownHue(level)
|
||||
} else {
|
||||
return getUpHue(level)
|
||||
}
|
||||
}
|
||||
|
||||
def getUpHue(level) {
|
||||
getBlueHue(level)
|
||||
}
|
||||
|
||||
def getDownHue(level) {
|
||||
getRedHue(level)
|
||||
}
|
||||
|
||||
private getBlueHue(level) {
|
||||
if (level < 5) return 72
|
||||
if (level < 10) return 71
|
||||
if (level < 15) return 70
|
||||
if (level < 20) return 69
|
||||
if (level < 25) return 68
|
||||
if (level < 30) return 67
|
||||
if (level < 35) return 66
|
||||
if (level < 40) return 65
|
||||
if (level < 45) return 64
|
||||
if (level < 50) return 63
|
||||
if (level < 55) return 62
|
||||
if (level < 60) return 61
|
||||
if (level < 65) return 60
|
||||
if (level < 70) return 59
|
||||
if (level < 75) return 58
|
||||
if (level < 80) return 57
|
||||
if (level < 85) return 56
|
||||
if (level < 90) return 55
|
||||
if (level < 95) return 54
|
||||
if (level >= 95) return 53
|
||||
}
|
||||
|
||||
private getRedHue(level) {
|
||||
if (level < 6) return 1
|
||||
if (level < 12) return 2
|
||||
if (level < 18) return 3
|
||||
if (level < 24) return 4
|
||||
if (level < 30) return 5
|
||||
if (level < 36) return 6
|
||||
if (level < 42) return 7
|
||||
if (level < 48) return 8
|
||||
if (level < 54) return 9
|
||||
if (level < 60) return 10
|
||||
if (level < 66) return 11
|
||||
if (level < 72) return 12
|
||||
if (level < 78) return 13
|
||||
if (level < 84) return 14
|
||||
if (level < 90) return 15
|
||||
if (level < 96) return 16
|
||||
if (level >= 96) return 17
|
||||
}
|
||||
|
||||
private hasSetLevelCommand(device) {
|
||||
def isDimmer = false
|
||||
device.supportedCommands.each {
|
||||
if (it.name.contains("setLevel")) {
|
||||
isDimmer = true
|
||||
}
|
||||
}
|
||||
return isDimmer
|
||||
}
|
||||
|
||||
private hasSetColorCommand(device) {
|
||||
def hasColor = false
|
||||
device.supportedCommands.each {
|
||||
if (it.name.contains("setColor")) {
|
||||
hasColor = true
|
||||
}
|
||||
}
|
||||
return hasColor
|
||||
}
|
||||
|
||||
private dimmersWithSetColorCommand() {
|
||||
def colorDimmers = []
|
||||
dimmers.each { dimmer ->
|
||||
if (hasSetColorCommand(dimmer)) {
|
||||
colorDimmers << dimmer
|
||||
}
|
||||
}
|
||||
return colorDimmers
|
||||
}
|
||||
|
||||
private int sanitizeInt(i, int defaultValue = 0) {
|
||||
try {
|
||||
if (!i) {
|
||||
return defaultValue
|
||||
} else {
|
||||
return i as int
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.debug e
|
||||
return defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
private completionDelaySeconds() {
|
||||
int completionDelayMinutes = sanitizeInt(completionDelay)
|
||||
int completionDelaySeconds = (completionDelayMinutes * 60)
|
||||
return completionDelaySeconds ?: 0
|
||||
}
|
||||
|
||||
private stepDuration() {
|
||||
int minutes = sanitizeInt(duration, 30)
|
||||
int stepDuration = (minutes * 60) / 100
|
||||
return stepDuration ?: 1
|
||||
}
|
||||
|
||||
private debug(message) {
|
||||
log.debug "${message}\nstate: ${state}"
|
||||
}
|
||||
|
||||
public smartThingsDateFormat() { "yyyy-MM-dd'T'HH:mm:ss.SSSZ" }
|
||||
|
||||
public humanReadableStartDate() {
|
||||
new Date().parse(smartThingsDateFormat(), startTime).format("h:mm a", timeZone(startTime))
|
||||
}
|
||||
|
||||
def fancyString(listOfStrings) {
|
||||
|
||||
def fancify = { list ->
|
||||
return list.collect {
|
||||
def label = it
|
||||
if (list.size() > 1 && it == list[-1]) {
|
||||
label = "and ${label}"
|
||||
}
|
||||
label
|
||||
}.join(", ")
|
||||
}
|
||||
|
||||
return fancify(listOfStrings)
|
||||
}
|
||||
|
||||
def fancyDeviceString(devices = []) {
|
||||
fancyString(devices.collect { deviceLabel(it) })
|
||||
}
|
||||
|
||||
def deviceLabel(device) {
|
||||
return device.label ?: device.name
|
||||
}
|
||||
|
||||
def schedulingHrefDescription() {
|
||||
|
||||
def descriptionParts = []
|
||||
if (days) {
|
||||
descriptionParts << "On ${fancyString(days)},"
|
||||
}
|
||||
|
||||
descriptionParts << "${fancyDeviceString(dimmers)} will start dimming"
|
||||
|
||||
if (startTime) {
|
||||
descriptionParts << "at ${humanReadableStartDate()}"
|
||||
}
|
||||
|
||||
if (modeStart) {
|
||||
if (startTime) {
|
||||
descriptionParts << "or"
|
||||
}
|
||||
descriptionParts << "when ${location.name} enters '${modeStart}' mode"
|
||||
}
|
||||
|
||||
if (descriptionParts.size() <= 1) {
|
||||
// dimmers will be in the list no matter what. No rules are set if only dimmers are in the list
|
||||
return null
|
||||
}
|
||||
|
||||
return descriptionParts.join(" ")
|
||||
}
|
||||
|
||||
def completionHrefDescription() {
|
||||
|
||||
def descriptionParts = []
|
||||
def example = "Switch1 will be turned on. Switch2, Switch3, and Switch4 will be dimmed to 50%. The message '<message>' will be spoken, sent as a text, and sent as a push notification. The mode will be changed to '<mode>'. The phrase '<phrase>' will be executed"
|
||||
|
||||
if (completionSwitches) {
|
||||
def switchesList = []
|
||||
def dimmersList = []
|
||||
|
||||
|
||||
completionSwitches.each {
|
||||
def isDimmer = completionSwitchesLevel ? hasSetLevelCommand(it) : false
|
||||
|
||||
if (isDimmer) {
|
||||
dimmersList << deviceLabel(it)
|
||||
}
|
||||
|
||||
if (!isDimmer) {
|
||||
switchesList << deviceLabel(it)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (switchesList) {
|
||||
descriptionParts << "${fancyString(switchesList)} will be turned ${completionSwitchesState ?: 'on'}."
|
||||
}
|
||||
|
||||
if (dimmersList) {
|
||||
descriptionParts << "${fancyString(dimmersList)} will be dimmed to ${completionSwitchesLevel}%."
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (completionMessage && (completionPhoneNumber || completionPush || completionMusicPlayer)) {
|
||||
def messageParts = []
|
||||
|
||||
if (completionMusicPlayer) {
|
||||
messageParts << "spoken"
|
||||
}
|
||||
if (completionPhoneNumber) {
|
||||
messageParts << "sent as a text"
|
||||
}
|
||||
if (completionPush) {
|
||||
messageParts << "sent as a push notification"
|
||||
}
|
||||
|
||||
descriptionParts << "The message '${completionMessage}' will be ${fancyString(messageParts)}."
|
||||
}
|
||||
|
||||
if (completionMode) {
|
||||
descriptionParts << "The mode will be changed to '${completionMode}'."
|
||||
}
|
||||
|
||||
if (completionPhrase) {
|
||||
descriptionParts << "The phrase '${completionPhrase}' will be executed."
|
||||
}
|
||||
|
||||
return descriptionParts.join(" ")
|
||||
}
|
||||
|
||||
def numbersPageHrefDescription() {
|
||||
def title = "All dimmers will dim for ${duration ?: '30'} minutes from ${startLevelLabel()} to ${endLevelLabel()}"
|
||||
if (colorize) {
|
||||
def colorDimmers = dimmersWithSetColorCommand()
|
||||
if (colorDimmers == dimmers) {
|
||||
title += " and will gradually change color."
|
||||
} else {
|
||||
title += ".\n${fancyDeviceString(colorDimmers)} will gradually change color."
|
||||
}
|
||||
}
|
||||
return title
|
||||
}
|
||||
|
||||
def hueSatToHex(h, s) {
|
||||
def convertedRGB = hslToRgb(h, s, 0.5)
|
||||
return rgbToHex(convertedRGB)
|
||||
}
|
||||
|
||||
def hslToRgb(h, s, l) {
|
||||
def r, g, b;
|
||||
|
||||
if (s == 0) {
|
||||
r = g = b = l; // achromatic
|
||||
} else {
|
||||
def hue2rgb = { p, q, t ->
|
||||
if (t < 0) t += 1;
|
||||
if (t > 1) t -= 1;
|
||||
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
||||
if (t < 1 / 2) return q;
|
||||
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
||||
return p;
|
||||
}
|
||||
|
||||
def q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
||||
def p = 2 * l - q;
|
||||
|
||||
r = hue2rgb(p, q, h + 1 / 3);
|
||||
g = hue2rgb(p, q, h);
|
||||
b = hue2rgb(p, q, h - 1 / 3);
|
||||
}
|
||||
|
||||
return [r * 255, g * 255, b * 255];
|
||||
}
|
||||
|
||||
def rgbToHex(red, green, blue) {
|
||||
def toHex = {
|
||||
int n = it as int;
|
||||
n = Math.max(0, Math.min(n, 255));
|
||||
def hexOptions = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"]
|
||||
|
||||
def firstDecimal = ((n - n % 16) / 16) as int
|
||||
def secondDecimal = (n % 16) as int
|
||||
|
||||
return "${hexOptions[firstDecimal]}${hexOptions[secondDecimal]}"
|
||||
}
|
||||
|
||||
def rgbToHex = { r, g, b ->
|
||||
return toHex(r) + toHex(g) + toHex(b)
|
||||
}
|
||||
|
||||
return rgbToHex(red, green, blue)
|
||||
}
|
||||
|
||||
def usesOldSettings() {
|
||||
!hasEndLevel()
|
||||
}
|
||||
|
||||
def hasStartLevel() {
|
||||
return (startLevel != null && startLevel != "")
|
||||
}
|
||||
|
||||
def hasEndLevel() {
|
||||
return (endLevel != null && endLevel != "")
|
||||
}
|
||||
198
smartapps/smartthings/good-night.src/good-night.groovy
Normal file
198
smartapps/smartthings/good-night.src/good-night.groovy
Normal file
@@ -0,0 +1,198 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Good Night
|
||||
*
|
||||
* Author: SmartThings
|
||||
* Date: 2013-03-07
|
||||
*/
|
||||
definition(
|
||||
name: "Good Night",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Changes mode when motion ceases after a specific time of night.",
|
||||
category: "Mode Magic",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/ModeMagic/good-night.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/ModeMagic/good-night@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("When there is no motion on any of these sensors") {
|
||||
input "motionSensors", "capability.motionSensor", title: "Where?", multiple: true
|
||||
}
|
||||
section("For this amount of time") {
|
||||
input "minutes", "number", title: "Minutes?"
|
||||
}
|
||||
section("After this time of day") {
|
||||
input "timeOfDay", "time", title: "Time?"
|
||||
}
|
||||
section("And (optionally) these switches are all off") {
|
||||
input "switches", "capability.switch", multiple: true, required: false
|
||||
}
|
||||
section("Change to this mode") {
|
||||
input "newMode", "mode", title: "Mode?"
|
||||
}
|
||||
section( "Notifications" ) {
|
||||
input("recipients", "contact", title: "Send notifications to") {
|
||||
input "sendPushMessage", "enum", title: "Send a push notification?", options: ["Yes", "No"], required: false
|
||||
input "phoneNumber", "phone", title: "Send a Text Message?", required: false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def installed() {
|
||||
log.debug "Current mode = ${location.mode}"
|
||||
createSubscriptions()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "Current mode = ${location.mode}"
|
||||
unsubscribe()
|
||||
createSubscriptions()
|
||||
}
|
||||
|
||||
def createSubscriptions()
|
||||
{
|
||||
subscribe(motionSensors, "motion.active", motionActiveHandler)
|
||||
subscribe(motionSensors, "motion.inactive", motionInactiveHandler)
|
||||
subscribe(switches, "switch.off", switchOffHandler)
|
||||
subscribe(location, modeChangeHandler)
|
||||
|
||||
if (state.modeStartTime == null) {
|
||||
state.modeStartTime = 0
|
||||
}
|
||||
}
|
||||
|
||||
def modeChangeHandler(evt) {
|
||||
state.modeStartTime = now()
|
||||
}
|
||||
|
||||
def switchOffHandler(evt) {
|
||||
if (correctMode() && correctTime()) {
|
||||
if (allQuiet() && switchesOk()) {
|
||||
takeActions()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def motionActiveHandler(evt)
|
||||
{
|
||||
log.debug "Motion active"
|
||||
}
|
||||
|
||||
def motionInactiveHandler(evt)
|
||||
{
|
||||
// for backward compatibility
|
||||
if (state.modeStartTime == null) {
|
||||
subscribe(location, modeChangeHandler)
|
||||
state.modeStartTime = 0
|
||||
}
|
||||
|
||||
if (correctMode() && correctTime()) {
|
||||
runIn(minutes * 60, scheduleCheck, [overwrite: false])
|
||||
}
|
||||
}
|
||||
|
||||
def scheduleCheck()
|
||||
{
|
||||
log.debug "scheduleCheck, currentMode = ${location.mode}, newMode = $newMode"
|
||||
|
||||
if (correctMode() && correctTime()) {
|
||||
if (allQuiet() && switchesOk()) {
|
||||
takeActions()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private takeActions() {
|
||||
def message = "Goodnight! SmartThings changed the mode to '$newMode'"
|
||||
send(message)
|
||||
setLocationMode(newMode)
|
||||
log.debug message
|
||||
}
|
||||
|
||||
private correctMode() {
|
||||
if (location.mode != newMode) {
|
||||
true
|
||||
} else {
|
||||
log.debug "Location is already in the desired mode: doing nothing"
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private correctTime() {
|
||||
def t0 = now()
|
||||
def modeStartTime = new Date(state.modeStartTime)
|
||||
def startTime = timeTodayAfter(modeStartTime, timeOfDay, location.timeZone)
|
||||
if (t0 >= startTime.time) {
|
||||
true
|
||||
} else {
|
||||
log.debug "The current time of day (${new Date(t0)}), is not in the correct time window ($startTime): doing nothing"
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private switchesOk() {
|
||||
def result = true
|
||||
for (it in (switches ?: [])) {
|
||||
if (it.currentSwitch == "on") {
|
||||
result = false
|
||||
break
|
||||
}
|
||||
}
|
||||
log.debug "Switches are all off: $result"
|
||||
result
|
||||
}
|
||||
|
||||
private allQuiet() {
|
||||
def threshold = 1000 * 60 * minutes - 1000
|
||||
def states = motionSensors.collect { it.currentState("motion") ?: [:] }.sort { a, b -> b.dateCreated <=> a.dateCreated }
|
||||
if (states) {
|
||||
if (states.find { it.value == "active" }) {
|
||||
log.debug "Found active state"
|
||||
false
|
||||
} else {
|
||||
def sensor = states.first()
|
||||
def elapsed = now() - sensor.rawDateCreated.time
|
||||
if (elapsed >= threshold) {
|
||||
log.debug "No active states, and enough time has passed"
|
||||
true
|
||||
} else {
|
||||
log.debug "No active states, but not enough time has passed"
|
||||
false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.debug "No states to check for activity"
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private send(msg) {
|
||||
if (location.contactBookEnabled) {
|
||||
sendNotificationToContacts(msg, recipients)
|
||||
}
|
||||
else {
|
||||
if (sendPushMessage != "No") {
|
||||
log.debug("sending push message")
|
||||
sendPush(msg)
|
||||
}
|
||||
|
||||
if (phoneNumber) {
|
||||
log.debug("sending text message")
|
||||
sendSms(phoneNumber, msg)
|
||||
}
|
||||
}
|
||||
|
||||
log.debug msg
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Greetings Earthling
|
||||
*
|
||||
* Author: SmartThings
|
||||
* Date: 2013-03-07
|
||||
*/
|
||||
definition(
|
||||
name: "Greetings Earthling",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Monitors a set of presence detectors and triggers a mode change when someone arrives at home.",
|
||||
category: "Mode Magic",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/App-LightUpMyWorld.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/App-LightUpMyWorld@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
|
||||
section("When one of these people arrive at home") {
|
||||
input "people", "capability.presenceSensor", multiple: true
|
||||
}
|
||||
section("Change to this mode") {
|
||||
input "newMode", "mode", title: "Mode?"
|
||||
}
|
||||
section("False alarm threshold (defaults to 10 min)") {
|
||||
input "falseAlarmThreshold", "decimal", title: "Number of minutes", required: false
|
||||
}
|
||||
section( "Notifications" ) {
|
||||
input("recipients", "contact", title: "Send notifications to") {
|
||||
input "sendPushMessage", "enum", title: "Send a push notification?", options: ["Yes", "No"], required: false
|
||||
input "phone", "phone", title: "Send a Text Message?", required: false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def installed() {
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
log.debug "Current mode = ${location.mode}, people = ${people.collect{it.label + ': ' + it.currentPresence}}"
|
||||
subscribe(people, "presence", presence)
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
log.debug "Current mode = ${location.mode}, people = ${people.collect{it.label + ': ' + it.currentPresence}}"
|
||||
unsubscribe()
|
||||
subscribe(people, "presence", presence)
|
||||
}
|
||||
|
||||
def presence(evt)
|
||||
{
|
||||
log.debug "evt.name: $evt.value"
|
||||
def threshold = (falseAlarmThreshold != null && falseAlarmThreshold != "") ? (falseAlarmThreshold * 60 * 1000) as Long : 10 * 60 * 1000L
|
||||
|
||||
if (location.mode != newMode) {
|
||||
|
||||
def t0 = new Date(now() - threshold)
|
||||
if (evt.value == "present") {
|
||||
|
||||
def person = getPerson(evt)
|
||||
def recentNotPresent = person.statesSince("presence", t0).find{it.value == "not present"}
|
||||
if (recentNotPresent) {
|
||||
log.debug "skipping notification of arrival of ${person.displayName} because last departure was only ${now() - recentNotPresent.date.time} msec ago"
|
||||
}
|
||||
else {
|
||||
def message = "${person.displayName} arrived at home, changing mode to '${newMode}'"
|
||||
log.info message
|
||||
send(message)
|
||||
setLocationMode(newMode)
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
log.debug "mode is the same, not evaluating"
|
||||
}
|
||||
}
|
||||
|
||||
private getPerson(evt)
|
||||
{
|
||||
people.find{evt.deviceId == it.id}
|
||||
}
|
||||
|
||||
private send(msg) {
|
||||
if (location.contactBookEnabled) {
|
||||
sendNotificationToContacts(msg, recipients)
|
||||
}
|
||||
else {
|
||||
if (sendPushMessage != "No") {
|
||||
log.debug("sending push message")
|
||||
sendPush(msg)
|
||||
}
|
||||
|
||||
if (phone) {
|
||||
log.debug("sending text message")
|
||||
sendSms(phone, msg)
|
||||
}
|
||||
}
|
||||
|
||||
log.debug msg
|
||||
}
|
||||
68
smartapps/smartthings/habit-helper.src/habit-helper.groovy
Normal file
68
smartapps/smartthings/habit-helper.src/habit-helper.groovy
Normal file
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Habit Helper
|
||||
* Every day at a specific time, get a text reminding you about your habit
|
||||
*
|
||||
* Author: SmartThings
|
||||
*/
|
||||
definition(
|
||||
name: "Habit Helper",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Add something you want to be reminded about each day and get a text message to help you form positive habits.",
|
||||
category: "Family",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/text.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/text@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("Remind me about..."){
|
||||
input "message1", "text", title: "What?"
|
||||
}
|
||||
section("At what time?"){
|
||||
input "time1", "time", title: "When?"
|
||||
}
|
||||
section("Text me at..."){
|
||||
input("recipients", "contact", title: "Send notifications to") {
|
||||
input "phone1", "phone", title: "Phone number?"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def installed()
|
||||
{
|
||||
schedule(time1, "scheduleCheck")
|
||||
}
|
||||
|
||||
def updated()
|
||||
{
|
||||
unschedule()
|
||||
schedule(time1, "scheduleCheck")
|
||||
}
|
||||
|
||||
def scheduleCheck()
|
||||
{
|
||||
log.trace "scheduledCheck"
|
||||
|
||||
def message = message1 ?: "SmartThings - Habit Helper Reminder!"
|
||||
|
||||
if (location.contactBookEnabled) {
|
||||
log.debug "Texting reminder: ($message) to contacts:${recipients?.size()}"
|
||||
sendNotificationToContacts(message, recipients)
|
||||
}
|
||||
else {
|
||||
|
||||
log.debug "Texting reminder: ($message) to $phone1"
|
||||
sendSms(phone1, message)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Has Barkley Been Fed
|
||||
*
|
||||
* Author: SmartThings
|
||||
*/
|
||||
definition(
|
||||
name: "Has Barkley Been Fed?",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Setup a schedule to be reminded to feed your pet. Purchase any SmartThings certified pet food feeder and install the Feed My Pet app, and set the time. You and your pet are ready to go. Your life just got smarter.",
|
||||
category: "Pets",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/dogfood_feeder.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/dogfood_feeder@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("Choose your pet feeder...") {
|
||||
input "feeder1", "capability.contactSensor", title: "Where?"
|
||||
}
|
||||
section("Feed my pet at...") {
|
||||
input "time1", "time", title: "When?"
|
||||
}
|
||||
section("Text me if I forget...") {
|
||||
input("recipients", "contact", title: "Send notifications to") {
|
||||
input "phone1", "phone", title: "Phone number?"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def installed()
|
||||
{
|
||||
schedule(time1, "scheduleCheck")
|
||||
}
|
||||
|
||||
def updated()
|
||||
{
|
||||
unsubscribe() //TODO no longer subscribe like we used to - clean this up after all apps updated
|
||||
unschedule()
|
||||
schedule(time1, "scheduleCheck")
|
||||
}
|
||||
|
||||
def scheduleCheck()
|
||||
{
|
||||
log.trace "scheduledCheck"
|
||||
|
||||
def midnight = (new Date()).clearTime()
|
||||
def now = new Date()
|
||||
def feederEvents = feeder1.eventsBetween(midnight, now)
|
||||
log.trace "Found ${feederEvents?.size() ?: 0} feeder events since $midnight"
|
||||
def feederOpened = feederEvents.count { it.value && it.value == "open" } > 0
|
||||
|
||||
if (feederOpened) {
|
||||
log.debug "Feeder was opened since $midnight, no SMS required"
|
||||
} else {
|
||||
if (location.contactBookEnabled) {
|
||||
log.debug "Feeder was not opened since $midnight, texting contacts:${recipients?.size()}"
|
||||
sendNotificationToContacts("No one has fed the dog", recipients)
|
||||
}
|
||||
else {
|
||||
log.debug "Feeder was not opened since $midnight, texting $phone1"
|
||||
sendSms(phone1, "No one has fed the dog")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Hub IP Notifier
|
||||
*
|
||||
* Author: luke
|
||||
* Date: 2014-01-28
|
||||
*/
|
||||
definition(
|
||||
name: "Hub IP Notifier",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Listen for local IP changes when your hub registers.",
|
||||
category: "SmartThings Internal",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/MyApps/Cat-MyApps.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/MyApps/Cat-MyApps@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
page(name: "pageWithIp", title: "Hub IP Notifier", install: true)
|
||||
|
||||
}
|
||||
|
||||
def pageWithIp() {
|
||||
def currentIp = state.localip ?: 'unknown'
|
||||
def registerDate = state.lastRegister ?: null
|
||||
dynamicPage(name: "pageWithIp", title: "Hub IP Notifier", install: true, uninstall: true) {
|
||||
section("When Hub Comes Online") {
|
||||
input "hub", "hub", title: "Select a hub"
|
||||
}
|
||||
section("Last Registration Details") {
|
||||
if(hub && registerDate) {
|
||||
paragraph """Your hub last registered with IP:
|
||||
$currentIp
|
||||
on:
|
||||
$registerDate"""
|
||||
} else if (hub && !registerDate) {
|
||||
paragraph "Your hub has not (re)registered since you installed this app"
|
||||
} else {
|
||||
paragraph "Check back here after installing to see the current IP of your hub"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
|
||||
initialize()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
|
||||
unsubscribe()
|
||||
initialize()
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
subscribe(hub, "hubInfo", registrationHandler, [filterEvents: false])
|
||||
}
|
||||
|
||||
def registrationHandler(evt) {
|
||||
def hubInfo = evt.description.split(',').inject([:]) { map, token ->
|
||||
token.split(':').with { map[it[0].trim()] = it[1] }
|
||||
map
|
||||
}
|
||||
state.localip = hubInfo.localip
|
||||
state.lastRegister = new Date()
|
||||
sendNotificationEvent("${hub.name} registered in prod with IP: ${hubInfo.localip}")
|
||||
}
|
||||
723
smartapps/smartthings/hue-connect.src/hue-connect.groovy
Normal file
723
smartapps/smartthings/hue-connect.src/hue-connect.groovy
Normal file
@@ -0,0 +1,723 @@
|
||||
/**
|
||||
* Hue Service Manager
|
||||
*
|
||||
* Author: Juan Risso (juan@smartthings.com)
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
|
||||
definition(
|
||||
name: "Hue (Connect)",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Allows you to connect your Philips Hue lights with SmartThings and control them from your Things area or Dashboard in the SmartThings Mobile app. Adjust colors by going to the Thing detail screen for your Hue lights (tap the gear on Hue tiles).\n\nPlease update your Hue Bridge first, outside of the SmartThings app, using the Philips Hue app.",
|
||||
category: "SmartThings Labs",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/hue.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/hue@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
page(name:"mainPage", title:"Hue Device Setup", content:"mainPage", refreshTimeout:5)
|
||||
page(name:"bridgeDiscovery", title:"Hue Bridge Discovery", content:"bridgeDiscovery", refreshTimeout:5)
|
||||
page(name:"bridgeBtnPush", title:"Linking with your Hue", content:"bridgeLinking", refreshTimeout:5)
|
||||
page(name:"bulbDiscovery", title:"Hue Device Setup", content:"bulbDiscovery", refreshTimeout:5)
|
||||
}
|
||||
|
||||
def mainPage() {
|
||||
if(canInstallLabs()) {
|
||||
def bridges = bridgesDiscovered()
|
||||
if (state.username && bridges) {
|
||||
return bulbDiscovery()
|
||||
} else {
|
||||
return bridgeDiscovery()
|
||||
}
|
||||
} else {
|
||||
def upgradeNeeded = """To use SmartThings Labs, your Hub should be completely up to date.
|
||||
|
||||
To update your Hub, access Location Settings in the Main Menu (tap the gear next to your location name), select your Hub, and choose "Update Hub"."""
|
||||
|
||||
return dynamicPage(name:"bridgeDiscovery", title:"Upgrade needed!", nextPage:"", install:false, uninstall: true) {
|
||||
section("Upgrade") {
|
||||
paragraph "$upgradeNeeded"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def bridgeDiscovery(params=[:])
|
||||
{
|
||||
def bridges = bridgesDiscovered()
|
||||
int bridgeRefreshCount = !state.bridgeRefreshCount ? 0 : state.bridgeRefreshCount as int
|
||||
state.bridgeRefreshCount = bridgeRefreshCount + 1
|
||||
def refreshInterval = 3
|
||||
|
||||
def options = bridges ?: []
|
||||
def numFound = options.size() ?: 0
|
||||
|
||||
if(!state.subscribe) {
|
||||
subscribe(location, null, locationHandler, [filterEvents:false])
|
||||
state.subscribe = true
|
||||
}
|
||||
|
||||
//bridge discovery request every 15 //25 seconds
|
||||
if((bridgeRefreshCount % 5) == 0) {
|
||||
discoverBridges()
|
||||
}
|
||||
|
||||
//setup.xml request every 3 seconds except on discoveries
|
||||
if(((bridgeRefreshCount % 1) == 0) && ((bridgeRefreshCount % 5) != 0)) {
|
||||
verifyHueBridges()
|
||||
}
|
||||
|
||||
return dynamicPage(name:"bridgeDiscovery", title:"Discovery Started!", nextPage:"bridgeBtnPush", refreshInterval:refreshInterval, uninstall: true) {
|
||||
section("Please wait while we discover your Hue Bridge. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") {
|
||||
input "selectedHue", "enum", required:false, title:"Select Hue Bridge (${numFound} found)", multiple:false, options:options
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def bridgeLinking()
|
||||
{
|
||||
int linkRefreshcount = !state.linkRefreshcount ? 0 : state.linkRefreshcount as int
|
||||
state.linkRefreshcount = linkRefreshcount + 1
|
||||
def refreshInterval = 3
|
||||
|
||||
def nextPage = ""
|
||||
def title = "Linking with your Hue"
|
||||
def paragraphText = "Press the button on your Hue Bridge to setup a link."
|
||||
if (state.username) { //if discovery worked
|
||||
nextPage = "bulbDiscovery"
|
||||
title = "Success! - click 'Next'"
|
||||
paragraphText = "Linking to your hub was a success! Please click 'Next'!"
|
||||
}
|
||||
|
||||
if((linkRefreshcount % 2) == 0 && !state.username) {
|
||||
sendDeveloperReq()
|
||||
}
|
||||
|
||||
return dynamicPage(name:"bridgeBtnPush", title:title, nextPage:nextPage, refreshInterval:refreshInterval) {
|
||||
section("Button Press") {
|
||||
paragraph """${paragraphText}"""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def bulbDiscovery()
|
||||
{
|
||||
int bulbRefreshCount = !state.bulbRefreshCount ? 0 : state.bulbRefreshCount as int
|
||||
state.bulbRefreshCount = bulbRefreshCount + 1
|
||||
def refreshInterval = 3
|
||||
|
||||
def options = bulbsDiscovered() ?: []
|
||||
def numFound = options.size() ?: 0
|
||||
|
||||
if((bulbRefreshCount % 3) == 0) {
|
||||
discoverHueBulbs()
|
||||
}
|
||||
|
||||
return dynamicPage(name:"bulbDiscovery", title:"Bulb Discovery Started!", nextPage:"", refreshInterval:refreshInterval, install:true, uninstall: true) {
|
||||
section("Please wait while we discover your Hue Bulbs. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") {
|
||||
input "selectedBulbs", "enum", required:false, title:"Select Hue Bulbs (${numFound} found)", multiple:true, options:options
|
||||
}
|
||||
section {
|
||||
def title = getBridgeIP() ? "Hue bridge (${getBridgeIP()})" : "Find bridges"
|
||||
href "bridgeDiscovery", title: title, description: "", state: selectedHue ? "complete" : "incomplete", params: [override: true]
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private discoverBridges() {
|
||||
sendHubCommand(new physicalgraph.device.HubAction("lan discovery urn:schemas-upnp-org:device:basic:1", physicalgraph.device.Protocol.LAN))
|
||||
}
|
||||
|
||||
private sendDeveloperReq() {
|
||||
def token = app.id
|
||||
def host = getBridgeIP()
|
||||
sendHubCommand(new physicalgraph.device.HubAction([
|
||||
method: "POST",
|
||||
path: "/api",
|
||||
headers: [
|
||||
HOST: host
|
||||
],
|
||||
body: [devicetype: "$token-0", username: "$token-0"]], "${selectedHue}"))
|
||||
}
|
||||
|
||||
private discoverHueBulbs() {
|
||||
def host = getBridgeIP()
|
||||
sendHubCommand(new physicalgraph.device.HubAction([
|
||||
method: "GET",
|
||||
path: "/api/${state.username}/lights",
|
||||
headers: [
|
||||
HOST: host
|
||||
]], "${selectedHue}"))
|
||||
}
|
||||
|
||||
private verifyHueBridge(String deviceNetworkId, String host) {
|
||||
sendHubCommand(new physicalgraph.device.HubAction([
|
||||
method: "GET",
|
||||
path: "/description.xml",
|
||||
headers: [
|
||||
HOST: host
|
||||
]], deviceNetworkId))
|
||||
}
|
||||
|
||||
private verifyHueBridges() {
|
||||
def devices = getHueBridges().findAll { it?.value?.verified != true }
|
||||
devices.each {
|
||||
def ip = convertHexToIP(it.value.networkAddress)
|
||||
def port = convertHexToInt(it.value.deviceAddress)
|
||||
verifyHueBridge("${it.value.mac}", (ip + ":" + port))
|
||||
}
|
||||
}
|
||||
|
||||
Map bridgesDiscovered() {
|
||||
def vbridges = getVerifiedHueBridges()
|
||||
def map = [:]
|
||||
vbridges.each {
|
||||
def value = "${it.value.name}"
|
||||
def key = "${it.value.mac}"
|
||||
map["${key}"] = value
|
||||
}
|
||||
map
|
||||
}
|
||||
|
||||
Map bulbsDiscovered() {
|
||||
def bulbs = getHueBulbs()
|
||||
def map = [:]
|
||||
if (bulbs instanceof java.util.Map) {
|
||||
bulbs.each {
|
||||
def value = "${it?.value?.name}"
|
||||
def key = app.id +"/"+ it?.value?.id
|
||||
map["${key}"] = value
|
||||
}
|
||||
} else { //backwards compatable
|
||||
bulbs.each {
|
||||
def value = "${it?.name}"
|
||||
def key = app.id +"/"+ it?.id
|
||||
map["${key}"] = value
|
||||
}
|
||||
}
|
||||
map
|
||||
}
|
||||
|
||||
def getHueBulbs() {
|
||||
state.bulbs = state.bulbs ?: [:]
|
||||
}
|
||||
|
||||
def getHueBridges() {
|
||||
state.bridges = state.bridges ?: [:]
|
||||
}
|
||||
|
||||
def getVerifiedHueBridges() {
|
||||
getHueBridges().findAll{ it?.value?.verified == true }
|
||||
}
|
||||
|
||||
def installed() {
|
||||
log.trace "Installed with settings: ${settings}"
|
||||
initialize()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.trace "Updated with settings: ${settings}"
|
||||
unschedule()
|
||||
unsubscribe()
|
||||
initialize()
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
log.debug "Initializing"
|
||||
state.subscribe = false
|
||||
state.bridgeSelectedOverride = false
|
||||
def bridge = null
|
||||
|
||||
if (selectedHue) {
|
||||
addBridge()
|
||||
bridge = getChildDevice(selectedHue)
|
||||
subscribe(bridge, "bulbList", bulbListHandler)
|
||||
}
|
||||
|
||||
if (selectedBulbs) {
|
||||
addBulbs()
|
||||
doDeviceSync()
|
||||
runEvery5Minutes("doDeviceSync")
|
||||
}
|
||||
}
|
||||
|
||||
def manualRefresh() {
|
||||
unschedule()
|
||||
unsubscribe()
|
||||
doDeviceSync()
|
||||
runEvery5Minutes("doDeviceSync")
|
||||
}
|
||||
|
||||
def uninstalled(){
|
||||
state.bridges = [:]
|
||||
state.subscribe = false
|
||||
}
|
||||
|
||||
// Handles events to add new bulbs
|
||||
def bulbListHandler(evt) {
|
||||
def bulbs = [:]
|
||||
log.trace "Adding bulbs to state..."
|
||||
state.bridgeProcessedLightList = true
|
||||
evt.jsonData.each { k,v ->
|
||||
log.trace "$k: $v"
|
||||
if (v instanceof Map) {
|
||||
bulbs[k] = [id: k, name: v.name, type: v.type, hub:evt.value]
|
||||
}
|
||||
}
|
||||
state.bulbs = bulbs
|
||||
log.info "${bulbs.size()} bulbs found"
|
||||
}
|
||||
|
||||
def addBulbs() {
|
||||
def bulbs = getHueBulbs()
|
||||
selectedBulbs.each { dni ->
|
||||
def d = getChildDevice(dni)
|
||||
if(!d) {
|
||||
def newHueBulb
|
||||
if (bulbs instanceof java.util.Map) {
|
||||
newHueBulb = bulbs.find { (app.id + "/" + it.value.id) == dni }
|
||||
if (newHueBulb?.value?.type?.equalsIgnoreCase("Dimmable light")) {
|
||||
d = addChildDevice("smartthings", "Hue Lux Bulb", dni, newHueBulb?.value.hub, ["label":newHueBulb?.value.name])
|
||||
} else {
|
||||
d = addChildDevice("smartthings", "Hue Bulb", dni, newHueBulb?.value.hub, ["label":newHueBulb?.value.name])
|
||||
}
|
||||
} else {
|
||||
//backwards compatable
|
||||
newHueBulb = bulbs.find { (app.id + "/" + it.id) == dni }
|
||||
d = addChildDevice("smartthings", "Hue Bulb", dni, newHueBulb?.hub, ["label":newHueBulb?.name])
|
||||
}
|
||||
|
||||
log.debug "created ${d.displayName} with id $dni"
|
||||
d.refresh()
|
||||
} else {
|
||||
log.debug "found ${d.displayName} with id $dni already exists, type: '$d.typeName'"
|
||||
if (bulbs instanceof java.util.Map) {
|
||||
def newHueBulb = bulbs.find { (app.id + "/" + it.value.id) == dni }
|
||||
if (newHueBulb?.value?.type?.equalsIgnoreCase("Dimmable light") && d.typeName == "Hue Bulb") {
|
||||
d.setDeviceType("Hue Lux Bulb")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def addBridge() {
|
||||
def vbridges = getVerifiedHueBridges()
|
||||
def vbridge = vbridges.find {"${it.value.mac}" == selectedHue}
|
||||
|
||||
if(vbridge) {
|
||||
def d = getChildDevice(selectedHue)
|
||||
if(!d) {
|
||||
// compatibility with old devices
|
||||
def newbridge = true
|
||||
childDevices.each {
|
||||
if (it.getDeviceDataByName("mac")) {
|
||||
def newDNI = "${it.getDeviceDataByName("mac")}"
|
||||
if (newDNI != it.deviceNetworkId) {
|
||||
def oldDNI = it.deviceNetworkId
|
||||
log.debug "updating dni for device ${it} with $newDNI - previous DNI = ${it.deviceNetworkId}"
|
||||
it.setDeviceNetworkId("${newDNI}")
|
||||
if (oldDNI == selectedHue)
|
||||
app.updateSetting("selectedHue", newDNI)
|
||||
newbridge = false
|
||||
}
|
||||
}
|
||||
}
|
||||
if (newbridge) {
|
||||
d = addChildDevice("smartthings", "Hue Bridge", selectedHue, vbridge.value.hub)
|
||||
log.debug "created ${d.displayName} with id ${d.deviceNetworkId}"
|
||||
def childDevice = getChildDevice(d.deviceNetworkId)
|
||||
childDevice.sendEvent(name: "serialNumber", value: vbridge.value.serialNumber)
|
||||
if (vbridge.value.ip && vbridge.value.port) {
|
||||
if (vbridge.value.ip.contains("."))
|
||||
childDevice.sendEvent(name: "networkAddress", value: vbridge.value.ip + ":" + vbridge.value.port)
|
||||
else
|
||||
childDevice.sendEvent(name: "networkAddress", value: convertHexToIP(vbridge.value.ip) + ":" + convertHexToInt(vbridge.value.port))
|
||||
} else
|
||||
childDevice.sendEvent(name: "networkAddress", value: convertHexToIP(vbridge.value.networkAddress) + ":" + convertHexToInt(vbridge.value.deviceAddress))
|
||||
}
|
||||
} else {
|
||||
log.debug "found ${d.displayName} with id $selectedHue already exists"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def locationHandler(evt) {
|
||||
def description = evt.description
|
||||
log.trace "Location: $description"
|
||||
|
||||
def hub = evt?.hubId
|
||||
def parsedEvent = parseLanMessage(description)
|
||||
parsedEvent << ["hub":hub]
|
||||
|
||||
if (parsedEvent?.ssdpTerm?.contains("urn:schemas-upnp-org:device:basic:1")) {
|
||||
//SSDP DISCOVERY EVENTS
|
||||
log.trace "SSDP DISCOVERY EVENTS"
|
||||
def bridges = getHueBridges()
|
||||
log.trace bridges.toString()
|
||||
if (!(bridges."${parsedEvent.ssdpUSN.toString()}")) {
|
||||
//bridge does not exist
|
||||
log.trace "Adding bridge ${parsedEvent.ssdpUSN}"
|
||||
bridges << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent]
|
||||
} else {
|
||||
// update the values
|
||||
def ip = convertHexToIP(parsedEvent.networkAddress)
|
||||
def port = convertHexToInt(parsedEvent.deviceAddress)
|
||||
def host = ip + ":" + port
|
||||
log.debug "Device ($parsedEvent.mac) was already found in state with ip = $host."
|
||||
def dstate = bridges."${parsedEvent.ssdpUSN.toString()}"
|
||||
def dni = "${parsedEvent.mac}"
|
||||
def d = getChildDevice(dni)
|
||||
def networkAddress = null
|
||||
if (!d) {
|
||||
childDevices.each {
|
||||
if (it.getDeviceDataByName("mac")) {
|
||||
def newDNI = "${it.getDeviceDataByName("mac")}"
|
||||
if (newDNI != it.deviceNetworkId) {
|
||||
def oldDNI = it.deviceNetworkId
|
||||
log.debug "updating dni for device ${it} with $newDNI - previous DNI = ${it.deviceNetworkId}"
|
||||
it.setDeviceNetworkId("${newDNI}")
|
||||
if (oldDNI == selectedHue)
|
||||
app.updateSetting("selectedHue", newDNI)
|
||||
doDeviceSync()
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
networkAddress = d.latestState('networkAddress').stringValue
|
||||
log.trace "Host: $host - $networkAddress"
|
||||
if(host != networkAddress) {
|
||||
log.debug "Device's port or ip changed for device $d..."
|
||||
dstate.ip = ip
|
||||
dstate.port = port
|
||||
dstate.name = "Philips hue ($ip)"
|
||||
d.sendEvent(name:"networkAddress", value: host)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (parsedEvent.headers && parsedEvent.body) {
|
||||
log.trace "HUE BRIDGE RESPONSES"
|
||||
def headerString = parsedEvent.headers.toString()
|
||||
if (headerString?.contains("xml")) {
|
||||
log.trace "description.xml response (application/xml)"
|
||||
def body = new XmlSlurper().parseText(parsedEvent.body)
|
||||
if (body?.device?.modelName?.text().startsWith("Philips hue bridge")) {
|
||||
def bridges = getHueBridges()
|
||||
def bridge = bridges.find {it?.key?.contains(body?.device?.UDN?.text())}
|
||||
if (bridge) {
|
||||
bridge.value << [name:body?.device?.friendlyName?.text(), serialNumber:body?.device?.serialNumber?.text(), verified: true]
|
||||
} else {
|
||||
log.error "/description.xml returned a bridge that didn't exist"
|
||||
}
|
||||
}
|
||||
} else if(headerString?.contains("json")) {
|
||||
log.trace "description.xml response (application/json)"
|
||||
def body = new groovy.json.JsonSlurper().parseText(parsedEvent.body)
|
||||
if (body.success != null) {
|
||||
if (body.success[0] != null) {
|
||||
if (body.success[0].username)
|
||||
state.username = body.success[0].username
|
||||
}
|
||||
} else if (body.error != null) {
|
||||
//TODO: handle retries...
|
||||
log.error "ERROR: application/json ${body.error}"
|
||||
} else {
|
||||
//GET /api/${state.username}/lights response (application/json)
|
||||
if (!body?.state?.on) { //check if first time poll made it here by mistake
|
||||
def bulbs = getHueBulbs()
|
||||
log.debug "Adding bulbs to state!"
|
||||
body.each { k,v ->
|
||||
bulbs[k] = [id: k, name: v.name, type: v.type, hub:parsedEvent.hub]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.trace "NON-HUE EVENT $evt.description"
|
||||
}
|
||||
}
|
||||
|
||||
def doDeviceSync(){
|
||||
log.trace "Doing Hue Device Sync!"
|
||||
|
||||
//shrink the large bulb lists
|
||||
convertBulbListToMap()
|
||||
|
||||
poll()
|
||||
|
||||
if(!state.subscribe) {
|
||||
subscribe(location, null, locationHandler, [filterEvents:false])
|
||||
state.subscribe = true
|
||||
}
|
||||
|
||||
discoverBridges()
|
||||
}
|
||||
|
||||
/////////////////////////////////////
|
||||
//CHILD DEVICE METHODS
|
||||
/////////////////////////////////////
|
||||
|
||||
def parse(childDevice, description) {
|
||||
def parsedEvent = parseLanMessage(description)
|
||||
if (parsedEvent.headers && parsedEvent.body) {
|
||||
def headerString = parsedEvent.headers.toString()
|
||||
if (headerString?.contains("json")) {
|
||||
def body = new groovy.json.JsonSlurper().parseText(parsedEvent.body)
|
||||
if (body instanceof java.util.HashMap)
|
||||
{ //poll response
|
||||
def bulbs = getChildDevices()
|
||||
//for each bulb
|
||||
for (bulb in body) {
|
||||
def d = bulbs.find{it.deviceNetworkId == "${app.id}/${bulb.key}"}
|
||||
if (d) {
|
||||
if (bulb.value.state?.reachable) {
|
||||
sendEvent(d.deviceNetworkId, [name: "switch", value: bulb.value?.state?.on ? "on" : "off"])
|
||||
sendEvent(d.deviceNetworkId, [name: "level", value: Math.round(bulb.value.state.bri * 100 / 255)])
|
||||
if (bulb.value.state.sat) {
|
||||
def hue = Math.min(Math.round(bulb.value.state.hue * 100 / 65535), 65535) as int
|
||||
def sat = Math.round(bulb.value.state.sat * 100 / 255) as int
|
||||
def hex = colorUtil.hslToHex(hue, sat)
|
||||
sendEvent(d.deviceNetworkId, [name: "color", value: hex])
|
||||
sendEvent(d.deviceNetworkId, [name: "hue", value: hue])
|
||||
sendEvent(d.deviceNetworkId, [name: "saturation", value: sat])
|
||||
}
|
||||
} else {
|
||||
sendEvent(d.deviceNetworkId, [name: "switch", value: "off"])
|
||||
sendEvent(d.deviceNetworkId, [name: "level", value: 100])
|
||||
if (bulb.value.state.sat) {
|
||||
def hue = 23
|
||||
def sat = 56
|
||||
def hex = colorUtil.hslToHex(23, 56)
|
||||
sendEvent(d.deviceNetworkId, [name: "color", value: hex])
|
||||
sendEvent(d.deviceNetworkId, [name: "hue", value: hue])
|
||||
sendEvent(d.deviceNetworkId, [name: "saturation", value: sat])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
else
|
||||
{ //put response
|
||||
def hsl = [:]
|
||||
body.each { payload ->
|
||||
log.debug $payload
|
||||
if (payload?.success)
|
||||
{
|
||||
def childDeviceNetworkId = app.id + "/"
|
||||
def eventType
|
||||
body?.success[0].each { k,v ->
|
||||
childDeviceNetworkId += k.split("/")[2]
|
||||
if (!hsl[childDeviceNetworkId]) hsl[childDeviceNetworkId] = [:]
|
||||
eventType = k.split("/")[4]
|
||||
log.debug "eventType: $eventType"
|
||||
switch(eventType) {
|
||||
case "on":
|
||||
sendEvent(childDeviceNetworkId, [name: "switch", value: (v == true) ? "on" : "off"])
|
||||
break
|
||||
case "bri":
|
||||
sendEvent(childDeviceNetworkId, [name: "level", value: Math.round(v * 100 / 255)])
|
||||
break
|
||||
case "sat":
|
||||
hsl[childDeviceNetworkId].saturation = Math.round(v * 100 / 255) as int
|
||||
break
|
||||
case "hue":
|
||||
hsl[childDeviceNetworkId].hue = Math.min(Math.round(v * 100 / 65535), 65535) as int
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
else if (payload.error)
|
||||
{
|
||||
log.debug "JSON error - ${body?.error}"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
hsl.each { childDeviceNetworkId, hueSat ->
|
||||
if (hueSat.hue && hueSat.saturation) {
|
||||
def hex = colorUtil.hslToHex(hueSat.hue, hueSat.saturation)
|
||||
log.debug "sending ${hueSat} for ${childDeviceNetworkId} as ${hex}"
|
||||
sendEvent(hsl.childDeviceNetworkId, [name: "color", value: hex])
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.debug "parse - got something other than headers,body..."
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
def on(childDevice, transition = 4) {
|
||||
log.debug "Executing 'on'"
|
||||
// Assume bulb is off if no current state is found for level to avoid bulbs getting stuck in off after initial discovery
|
||||
def percent = childDevice.device?.currentValue("level") as Integer ?: 0
|
||||
def level = Math.min(Math.round(percent * 255 / 100), 255)
|
||||
put("lights/${getId(childDevice)}/state", [bri: level, on: true, transitiontime: transition])
|
||||
return "level: $percent"
|
||||
}
|
||||
|
||||
def off(childDevice, transition = 4) {
|
||||
log.debug "Executing 'off'"
|
||||
put("lights/${getId(childDevice)}/state", [on: false, transitiontime: transition])
|
||||
}
|
||||
|
||||
def setLevel(childDevice, percent) {
|
||||
log.debug "Executing 'setLevel'"
|
||||
def level = Math.min(Math.round(percent * 255 / 100), 255)
|
||||
put("lights/${getId(childDevice)}/state", [bri: level, on: percent > 0])
|
||||
}
|
||||
|
||||
def setSaturation(childDevice, percent) {
|
||||
log.debug "Executing 'setSaturation($percent)'"
|
||||
def level = Math.min(Math.round(percent * 255 / 100), 255)
|
||||
put("lights/${getId(childDevice)}/state", [sat: level])
|
||||
}
|
||||
|
||||
def setHue(childDevice, percent) {
|
||||
log.debug "Executing 'setHue($percent)'"
|
||||
def level = Math.min(Math.round(percent * 65535 / 100), 65535)
|
||||
put("lights/${getId(childDevice)}/state", [hue: level])
|
||||
}
|
||||
|
||||
def setColor(childDevice, color, alert = "none", transition = 4) {
|
||||
log.debug "Executing 'setColor($color)'"
|
||||
def hue = Math.min(Math.round(color.hue * 65535 / 100), 65535)
|
||||
def sat = Math.min(Math.round(color.saturation * 255 / 100), 255)
|
||||
|
||||
def value = [sat: sat, hue: hue, alert: alert, transitiontime: transition]
|
||||
if (color.level != null) {
|
||||
value.bri = Math.min(Math.round(color.level * 255 / 100), 255)
|
||||
value.on = value.bri > 0
|
||||
}
|
||||
|
||||
if (color.switch) {
|
||||
value.on = color.switch == "on"
|
||||
}
|
||||
|
||||
log.debug "sending command $value"
|
||||
put("lights/${getId(childDevice)}/state", value)
|
||||
}
|
||||
|
||||
def nextLevel(childDevice) {
|
||||
def level = device.latestValue("level") as Integer ?: 0
|
||||
if (level < 100) {
|
||||
level = Math.min(25 * (Math.round(level / 25) + 1), 100) as Integer
|
||||
}
|
||||
else {
|
||||
level = 25
|
||||
}
|
||||
setLevel(childDevice,level)
|
||||
}
|
||||
|
||||
private getId(childDevice) {
|
||||
if (childDevice.device?.deviceNetworkId?.startsWith("HUE")) {
|
||||
return childDevice.device?.deviceNetworkId[3..-1]
|
||||
}
|
||||
else {
|
||||
return childDevice.device?.deviceNetworkId.split("/")[-1]
|
||||
}
|
||||
}
|
||||
|
||||
private poll() {
|
||||
def host = getBridgeIP()
|
||||
def uri = "/api/${state.username}/lights/"
|
||||
log.debug "GET: $host$uri"
|
||||
sendHubCommand(new physicalgraph.device.HubAction("""GET ${uri} HTTP/1.1
|
||||
HOST: ${host}
|
||||
|
||||
""", physicalgraph.device.Protocol.LAN, selectedHue))
|
||||
}
|
||||
|
||||
private put(path, body) {
|
||||
def host = getBridgeIP()
|
||||
def uri = "/api/${state.username}/$path"
|
||||
def bodyJSON = new groovy.json.JsonBuilder(body).toString()
|
||||
def length = bodyJSON.getBytes().size().toString()
|
||||
|
||||
log.debug "PUT: $host$uri"
|
||||
log.debug "BODY: ${bodyJSON}"
|
||||
|
||||
sendHubCommand(new physicalgraph.device.HubAction("""PUT $uri HTTP/1.1
|
||||
HOST: ${host}
|
||||
Content-Length: ${length}
|
||||
|
||||
${bodyJSON}
|
||||
""", physicalgraph.device.Protocol.LAN, "${selectedHue}"))
|
||||
|
||||
}
|
||||
|
||||
private getBridgeIP() {
|
||||
def host = null
|
||||
if (selectedHue) {
|
||||
def d = getChildDevice(dni)
|
||||
if (d)
|
||||
host = d.latestState('networkAddress').stringValue
|
||||
if (host == null || host == "") {
|
||||
def serialNumber = selectedHue
|
||||
def bridge = getHueBridges().find { it?.value?.serialNumber?.equalsIgnoreCase(serialNumber) }?.value
|
||||
if (bridge?.ip && bridge?.port) {
|
||||
if (bridge?.ip.contains("."))
|
||||
host = "${bridge?.ip}:${bridge?.port}"
|
||||
else
|
||||
host = "${convertHexToIP(bridge?.ip)}:${convertHexToInt(bridge?.port)}"
|
||||
} else if (bridge?.networkAddress && bridge?.deviceAddress)
|
||||
host = "${convertHexToIP(bridge?.networkAddress)}:${convertHexToInt(bridge?.deviceAddress)}"
|
||||
}
|
||||
log.trace "Bridge: $selectedHue - Host: $host"
|
||||
}
|
||||
return host
|
||||
}
|
||||
|
||||
private Integer convertHexToInt(hex) {
|
||||
Integer.parseInt(hex,16)
|
||||
}
|
||||
|
||||
def convertBulbListToMap() {
|
||||
try {
|
||||
if (state.bulbs instanceof java.util.List) {
|
||||
def map = [:]
|
||||
state.bulbs.unique {it.id}.each { bulb ->
|
||||
map << ["${bulb.id}":["id":bulb.id, "name":bulb.name, "hub":bulb.hub]]
|
||||
}
|
||||
state.bulbs = map
|
||||
}
|
||||
}
|
||||
catch(Exception e) {
|
||||
log.error "Caught error attempting to convert bulb list to map: $e"
|
||||
}
|
||||
}
|
||||
|
||||
private String convertHexToIP(hex) {
|
||||
[convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
|
||||
}
|
||||
|
||||
private Boolean canInstallLabs() {
|
||||
return hasAllHubsOver("000.011.00603")
|
||||
}
|
||||
|
||||
private Boolean hasAllHubsOver(String desiredFirmware) {
|
||||
return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware }
|
||||
}
|
||||
|
||||
private List getRealHubFirmwareVersions() {
|
||||
return location.hubs*.firmwareVersionString.findAll { it }
|
||||
}
|
||||
@@ -0,0 +1,339 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Hue Mood Lighting
|
||||
*
|
||||
* Author: SmartThings
|
||||
* *
|
||||
* Date: 2014-02-21
|
||||
*/
|
||||
definition(
|
||||
name: "Hue Mood Lighting",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Sets the colors and brightness level of your Philips Hue lights to match your mood.",
|
||||
category: "SmartThings Labs",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/hue.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/hue@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
page(name: "mainPage", title: "Adjust the color of your Hue lights to match your mood.", 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("Set the lighting mood 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 mood lighting triggers" : "Set the lighting mood 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", description: "Select mode(s)", required: false, multiple: true
|
||||
ifUnset "timeOfDay", "time", title: "At a Scheduled Time", required: false
|
||||
}
|
||||
section("Control these bulbs...") {
|
||||
input "hues", "capability.colorControl", title: "Which Hue Bulbs?", required:true, multiple:true
|
||||
}
|
||||
section("Choose light effects...")
|
||||
{
|
||||
input "color", "enum", title: "Hue Color?", required: false, multiple:false, options: [
|
||||
["Soft White":"Soft White - Default"],
|
||||
["White":"White - Concentrate"],
|
||||
["Daylight":"Daylight - Energize"],
|
||||
["Warm White":"Warm White - Relax"],
|
||||
"Red","Green","Blue","Yellow","Orange","Purple","Pink"]
|
||||
input "lightLevel", "enum", title: "Light Level?", required: false, options: [[10:"10%"],[20:"20%"],[30:"30%"],[40:"40%"],[50:"50%"],[60:"60%"],[70:"70%"],[80:"80%"],[90:"90%"],[100:"100%"]]
|
||||
}
|
||||
|
||||
section("More options", hideable: true, hidden: true) {
|
||||
input "frequency", "decimal", title: "Minimum time between actions (defaults to every event)", description: "Minutes", required: false
|
||||
href "timeIntervalInput", title: "Only during a certain time", description: timeLabel ?: "Tap to set", state: timeLabel ? "complete" : "incomplete"
|
||||
input "days", "enum", title: "Only on certain days of the week", multiple: true, required: false,
|
||||
options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
|
||||
input "modes", "mode", title: "Only when mode is", multiple: true, required: false
|
||||
input "oncePerDay", "bool", title: "Only once per day", required: false, defaultValue: false
|
||||
}
|
||||
section([mobileOnly:true]) {
|
||||
label title: "Assign a name", required: false
|
||||
mode title: "Set for specific mode(s)", required: false
|
||||
}
|
||||
}
|
||||
}
|
||||
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() {
|
||||
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=null) {
|
||||
log.trace "Executing Mood Lighting"
|
||||
if (allOk) {
|
||||
log.trace "allOk"
|
||||
def lastTime = state[frequencyKey(evt)]
|
||||
if (oncePerDayOk(lastTime)) {
|
||||
if (frequency) {
|
||||
if (lastTime == null || now() - lastTime >= frequency * 60000) {
|
||||
takeAction(evt)
|
||||
}
|
||||
}
|
||||
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() {
|
||||
log.trace "scheduledTimeHandler()"
|
||||
eventHandler()
|
||||
}
|
||||
|
||||
def appTouchHandler(evt) {
|
||||
takeAction(evt)
|
||||
}
|
||||
|
||||
private takeAction(evt) {
|
||||
|
||||
if (frequency || oncePerDay) {
|
||||
state[frequencyKey(evt)] = now()
|
||||
}
|
||||
|
||||
def hueColor = 0
|
||||
def saturation = 100
|
||||
|
||||
switch(color) {
|
||||
case "White":
|
||||
hueColor = 52
|
||||
saturation = 19
|
||||
break;
|
||||
case "Daylight":
|
||||
hueColor = 53
|
||||
saturation = 91
|
||||
break;
|
||||
case "Soft White":
|
||||
hueColor = 23
|
||||
saturation = 56
|
||||
break;
|
||||
case "Warm White":
|
||||
hueColor = 20
|
||||
saturation = 80 //83
|
||||
break;
|
||||
case "Blue":
|
||||
hueColor = 70
|
||||
break;
|
||||
case "Green":
|
||||
hueColor = 39
|
||||
break;
|
||||
case "Yellow":
|
||||
hueColor = 25
|
||||
break;
|
||||
case "Orange":
|
||||
hueColor = 10
|
||||
break;
|
||||
case "Purple":
|
||||
hueColor = 75
|
||||
break;
|
||||
case "Pink":
|
||||
hueColor = 83
|
||||
break;
|
||||
case "Red":
|
||||
hueColor = 100
|
||||
break;
|
||||
}
|
||||
|
||||
state.previous = [:]
|
||||
|
||||
hues.each {
|
||||
state.previous[it.id] = [
|
||||
"switch": it.currentValue("switch"),
|
||||
"level" : it.currentValue("level"),
|
||||
"hue": it.currentValue("hue"),
|
||||
"saturation": it.currentValue("saturation")
|
||||
]
|
||||
}
|
||||
|
||||
log.debug "current values = $state.previous"
|
||||
|
||||
def newValue = [hue: hueColor, saturation: saturation, level: lightLevel as Integer ?: 100]
|
||||
log.debug "new value = $newValue"
|
||||
|
||||
hues*.setColor(newValue)
|
||||
}
|
||||
|
||||
private frequencyKey(evt) {
|
||||
"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 = lastTime ? dayString(new Date()) != dayString(new Date(lastTime)) : true
|
||||
log.trace "oncePerDayOk = $result - $lastTime"
|
||||
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
|
||||
230
smartapps/smartthings/ifttt.src/ifttt.groovy
Normal file
230
smartapps/smartthings/ifttt.src/ifttt.groovy
Normal file
@@ -0,0 +1,230 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* IFTTT API Access Application
|
||||
*
|
||||
* Author: SmartThings
|
||||
*
|
||||
* ---------------------+----------------+--------------------------+------------------------------------
|
||||
* Device Type | Attribute Name | Commands | Attribute Values
|
||||
* ---------------------+----------------+--------------------------+------------------------------------
|
||||
* switches | switch | on, off | on, off
|
||||
* motionSensors | motion | | active, inactive
|
||||
* contactSensors | contact | | open, closed
|
||||
* presenceSensors | presence | | present, 'not present'
|
||||
* temperatureSensors | temperature | | <numeric, F or C according to unit>
|
||||
* accelerationSensors | acceleration | | active, inactive
|
||||
* waterSensors | water | | wet, dry
|
||||
* lightSensors | illuminance | | <numeric, lux>
|
||||
* humiditySensors | humidity | | <numeric, percent>
|
||||
* alarms | alarm | strobe, siren, both, off | strobe, siren, both, off
|
||||
* locks | lock | lock, unlock | locked, unlocked
|
||||
* ---------------------+----------------+--------------------------+------------------------------------
|
||||
*/
|
||||
|
||||
definition(
|
||||
name: "IFTTT",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Put the internet to work for you.",
|
||||
category: "SmartThings Internal",
|
||||
iconUrl: "https://ifttt.com/images/channels/ifttt.png",
|
||||
iconX2Url: "https://ifttt.com/images/channels/ifttt_med.png",
|
||||
oauth: [displayName: "IFTTT", displayLink: "https://ifttt.com"]
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("Allow IFTTT to control these things...") {
|
||||
input "switches", "capability.switch", title: "Which Switches?", multiple: true, required: false
|
||||
input "motionSensors", "capability.motionSensor", title: "Which Motion Sensors?", multiple: true, required: false
|
||||
input "contactSensors", "capability.contactSensor", title: "Which Contact Sensors?", multiple: true, required: false
|
||||
input "presenceSensors", "capability.presenceSensor", title: "Which Presence Sensors?", multiple: true, required: false
|
||||
input "temperatureSensors", "capability.temperatureMeasurement", title: "Which Temperature Sensors?", multiple: true, required: false
|
||||
input "accelerationSensors", "capability.accelerationSensor", title: "Which Vibration Sensors?", multiple: true, required: false
|
||||
input "waterSensors", "capability.waterSensor", title: "Which Water Sensors?", multiple: true, required: false
|
||||
input "lightSensors", "capability.illuminanceMeasurement", title: "Which Light Sensors?", multiple: true, required: false
|
||||
input "humiditySensors", "capability.relativeHumidityMeasurement", title: "Which Relative Humidity Sensors?", multiple: true, required: false
|
||||
input "alarms", "capability.alarm", title: "Which Sirens?", multiple: true, required: false
|
||||
input "locks", "capability.lock", title: "Which Locks?", multiple: true, required: false
|
||||
}
|
||||
}
|
||||
|
||||
mappings {
|
||||
|
||||
path("/:deviceType") {
|
||||
action: [
|
||||
GET: "list"
|
||||
]
|
||||
}
|
||||
path("/:deviceType/states") {
|
||||
action: [
|
||||
GET: "listStates"
|
||||
]
|
||||
}
|
||||
path("/:deviceType/subscription") {
|
||||
action: [
|
||||
POST: "addSubscription"
|
||||
]
|
||||
}
|
||||
path("/:deviceType/subscriptions/:id") {
|
||||
action: [
|
||||
DELETE: "removeSubscription"
|
||||
]
|
||||
}
|
||||
path("/:deviceType/:id") {
|
||||
action: [
|
||||
GET: "show",
|
||||
PUT: "update"
|
||||
]
|
||||
}
|
||||
path("/subscriptions") {
|
||||
action: [
|
||||
GET: "listSubscriptions"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
log.debug settings
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug settings
|
||||
}
|
||||
|
||||
def list() {
|
||||
log.debug "[PROD] list, params: ${params}"
|
||||
def type = params.deviceType
|
||||
settings[type]?.collect{deviceItem(it)} ?: []
|
||||
}
|
||||
|
||||
def listStates() {
|
||||
log.debug "[PROD] states, params: ${params}"
|
||||
def type = params.deviceType
|
||||
def attributeName = attributeFor(type)
|
||||
settings[type]?.collect{deviceState(it, it.currentState(attributeName))} ?: []
|
||||
}
|
||||
|
||||
def listSubscriptions() {
|
||||
state
|
||||
}
|
||||
|
||||
def update() {
|
||||
def type = params.deviceType
|
||||
def data = request.JSON
|
||||
def devices = settings[type]
|
||||
def command = data.command
|
||||
|
||||
log.debug "[PROD] update, params: ${params}, request: ${data}, devices: ${devices*.id}"
|
||||
if (command) {
|
||||
def device = devices?.find { it.id == params.id }
|
||||
if (!device) {
|
||||
httpError(404, "Device not found")
|
||||
} else {
|
||||
device."$command"()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def show() {
|
||||
def type = params.deviceType
|
||||
def devices = settings[type]
|
||||
def device = devices.find { it.id == params.id }
|
||||
|
||||
log.debug "[PROD] show, params: ${params}, devices: ${devices*.id}"
|
||||
if (!device) {
|
||||
httpError(404, "Device not found")
|
||||
}
|
||||
else {
|
||||
def attributeName = attributeFor(type)
|
||||
def s = device.currentState(attributeName)
|
||||
deviceState(device, s)
|
||||
}
|
||||
}
|
||||
|
||||
def addSubscription() {
|
||||
log.debug "[PROD] addSubscription1"
|
||||
def type = params.deviceType
|
||||
def data = request.JSON
|
||||
def attribute = attributeFor(type)
|
||||
def devices = settings[type]
|
||||
def deviceId = data.deviceId
|
||||
def callbackUrl = data.callbackUrl
|
||||
def device = devices.find { it.id == deviceId }
|
||||
|
||||
log.debug "[PROD] addSubscription, params: ${params}, request: ${data}, device: ${device}"
|
||||
if (device) {
|
||||
log.debug "Adding switch subscription " + callbackUrl
|
||||
state[deviceId] = [callbackUrl: callbackUrl]
|
||||
subscribe(device, attribute, deviceHandler)
|
||||
}
|
||||
log.info state
|
||||
|
||||
}
|
||||
|
||||
def removeSubscription() {
|
||||
def type = params.deviceType
|
||||
def devices = settings[type]
|
||||
def deviceId = params.id
|
||||
def device = devices.find { it.id == deviceId }
|
||||
|
||||
log.debug "[PROD] removeSubscription, params: ${params}, request: ${data}, device: ${device}"
|
||||
if (device) {
|
||||
log.debug "Removing $device.displayName subscription"
|
||||
state.remove(device.id)
|
||||
unsubscribe(device)
|
||||
}
|
||||
log.info state
|
||||
}
|
||||
|
||||
def deviceHandler(evt) {
|
||||
def deviceInfo = state[evt.deviceId]
|
||||
if (deviceInfo) {
|
||||
try {
|
||||
httpPostJson(uri: deviceInfo.callbackUrl, path: '', body: [evt: [deviceId: evt.deviceId, name: evt.name, value: evt.value]]) {
|
||||
log.debug "[PROD IFTTT] Event data successfully posted"
|
||||
}
|
||||
} catch (groovyx.net.http.ResponseParseException e) {
|
||||
log.debug("Error parsing ifttt payload ${e}")
|
||||
}
|
||||
} else {
|
||||
log.debug "[PROD] No subscribed device found"
|
||||
}
|
||||
}
|
||||
|
||||
private deviceItem(it) {
|
||||
it ? [id: it.id, label: it.displayName] : null
|
||||
}
|
||||
|
||||
private deviceState(device, s) {
|
||||
device && s ? [id: device.id, label: device.displayName, name: s.name, value: s.value, unixTime: s.date.time] : null
|
||||
}
|
||||
|
||||
private attributeFor(type) {
|
||||
switch (type) {
|
||||
case "switches":
|
||||
log.debug "[PROD] switch type"
|
||||
return "switch"
|
||||
case "locks":
|
||||
log.debug "[PROD] lock type"
|
||||
return "lock"
|
||||
case "alarms":
|
||||
log.debug "[PROD] alarm type"
|
||||
return "alarm"
|
||||
case "lightSensors":
|
||||
log.debug "[PROD] illuminance type"
|
||||
return "illuminance"
|
||||
default:
|
||||
log.debug "[PROD] other sensor type"
|
||||
return type - "Sensors"
|
||||
}
|
||||
}
|
||||
67
smartapps/smartthings/it-moved.src/it-moved.groovy
Normal file
67
smartapps/smartthings/it-moved.src/it-moved.groovy
Normal file
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* It Moved
|
||||
*
|
||||
* Author: SmartThings
|
||||
*/
|
||||
definition(
|
||||
name: "It Moved",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Send a text when movement is detected",
|
||||
category: "Fun & Social",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/text_accelerometer.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/text_accelerometer@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("When movement is detected...") {
|
||||
input "accelerationSensor", "capability.accelerationSensor", title: "Where?"
|
||||
}
|
||||
section("Text me at...") {
|
||||
input("recipients", "contact", title: "Send notifications to") {
|
||||
input "phone1", "phone", title: "Phone number?"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
subscribe(accelerationSensor, "acceleration.active", accelerationActiveHandler)
|
||||
}
|
||||
|
||||
def updated() {
|
||||
unsubscribe()
|
||||
subscribe(accelerationSensor, "acceleration.active", accelerationActiveHandler)
|
||||
}
|
||||
|
||||
def accelerationActiveHandler(evt) {
|
||||
// Don't send a continuous stream of text messages
|
||||
def deltaSeconds = 5
|
||||
def timeAgo = new Date(now() - (1000 * deltaSeconds))
|
||||
def recentEvents = accelerationSensor.eventsSince(timeAgo)
|
||||
log.trace "Found ${recentEvents?.size() ?: 0} events in the last $deltaSeconds seconds"
|
||||
def alreadySentSms = recentEvents.count { it.value && it.value == "active" } > 1
|
||||
|
||||
if (alreadySentSms) {
|
||||
log.debug "SMS already sent to $phone1 within the last $deltaSeconds seconds"
|
||||
} else {
|
||||
if (location.contactBookEnabled) {
|
||||
log.debug "$accelerationSensor has moved, texting contacts: ${recipients?.size()}"
|
||||
sendNotificationToContacts("${accelerationSensor.label ?: accelerationSensor.name} moved", recipients)
|
||||
}
|
||||
else {
|
||||
log.debug "$accelerationSensor has moved, texting $phone1"
|
||||
sendSms(phone1, "${accelerationSensor.label ?: accelerationSensor.name} moved")
|
||||
}
|
||||
}
|
||||
}
|
||||
100
smartapps/smartthings/its-too-cold.src/its-too-cold.groovy
Normal file
100
smartapps/smartthings/its-too-cold.src/its-too-cold.groovy
Normal file
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* It's Too Cold
|
||||
*
|
||||
* Author: SmartThings
|
||||
*/
|
||||
definition(
|
||||
name: "It's Too Cold",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Monitor the temperature and when it drops below your setting get a text and/or turn on a heater or additional appliance.",
|
||||
category: "Convenience",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/temp_thermo-switch.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/temp_thermo-switch@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("Monitor the temperature...") {
|
||||
input "temperatureSensor1", "capability.temperatureMeasurement"
|
||||
}
|
||||
section("When the temperature drops below...") {
|
||||
input "temperature1", "number", title: "Temperature?"
|
||||
}
|
||||
section( "Notifications" ) {
|
||||
input("recipients", "contact", title: "Send notifications to") {
|
||||
input "sendPushMessage", "enum", title: "Send a push notification?", options: ["Yes", "No"], required: false
|
||||
input "phone1", "phone", title: "Send a Text Message?", required: false
|
||||
}
|
||||
}
|
||||
section("Turn on a heater...") {
|
||||
input "switch1", "capability.switch", required: false
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
subscribe(temperatureSensor1, "temperature", temperatureHandler)
|
||||
}
|
||||
|
||||
def updated() {
|
||||
unsubscribe()
|
||||
subscribe(temperatureSensor1, "temperature", temperatureHandler)
|
||||
}
|
||||
|
||||
def temperatureHandler(evt) {
|
||||
log.trace "temperature: $evt.value, $evt"
|
||||
|
||||
def tooCold = temperature1
|
||||
def mySwitch = settings.switch1
|
||||
|
||||
// TODO: Replace event checks with internal state (the most reliable way to know if an SMS has been sent recently or not).
|
||||
if (evt.doubleValue <= tooCold) {
|
||||
log.debug "Checking how long the temperature sensor has been reporting <= $tooCold"
|
||||
|
||||
// Don't send a continuous stream of text messages
|
||||
def deltaMinutes = 10 // TODO: Ask for "retry interval" in prefs?
|
||||
def timeAgo = new Date(now() - (1000 * 60 * deltaMinutes).toLong())
|
||||
def recentEvents = temperatureSensor1.eventsSince(timeAgo)?.findAll { it.name == "temperature" }
|
||||
log.trace "Found ${recentEvents?.size() ?: 0} events in the last $deltaMinutes minutes"
|
||||
def alreadySentSms = recentEvents.count { it.doubleValue <= tooCold } > 1
|
||||
|
||||
if (alreadySentSms) {
|
||||
log.debug "SMS already sent to $phone1 within the last $deltaMinutes minutes"
|
||||
// TODO: Send "Temperature back to normal" SMS, turn switch off
|
||||
} else {
|
||||
log.debug "Temperature dropped below $tooCold: sending SMS to $phone1 and activating $mySwitch"
|
||||
send("${temperatureSensor1.displayName} is too cold, reporting a temperature of ${evt.value}${evt.unit?:"F"}")
|
||||
switch1?.on()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private send(msg) {
|
||||
if (location.contactBookEnabled) {
|
||||
log.debug("sending notifications to: ${recipients?.size()}")
|
||||
sendNotificationToContacts(msg, recipients)
|
||||
}
|
||||
else {
|
||||
if (sendPushMessage != "No") {
|
||||
log.debug("sending push message")
|
||||
sendPush(msg)
|
||||
}
|
||||
|
||||
if (phone1) {
|
||||
log.debug("sending text message")
|
||||
sendSms(phone1, msg)
|
||||
}
|
||||
}
|
||||
|
||||
log.debug msg
|
||||
}
|
||||
100
smartapps/smartthings/its-too-hot.src/its-too-hot.groovy
Normal file
100
smartapps/smartthings/its-too-hot.src/its-too-hot.groovy
Normal file
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* It's Too Hot
|
||||
*
|
||||
* Author: SmartThings
|
||||
*/
|
||||
definition(
|
||||
name: "It's Too Hot",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Monitor the temperature and when it rises above your setting get a notification and/or turn on an A/C unit or fan.",
|
||||
category: "Convenience",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/its-too-hot.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/its-too-hot@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("Monitor the temperature...") {
|
||||
input "temperatureSensor1", "capability.temperatureMeasurement"
|
||||
}
|
||||
section("When the temperature rises above...") {
|
||||
input "temperature1", "number", title: "Temperature?"
|
||||
}
|
||||
section( "Notifications" ) {
|
||||
input("recipients", "contact", title: "Send notifications to") {
|
||||
input "sendPushMessage", "enum", title: "Send a push notification?", options: ["Yes", "No"], required: false
|
||||
input "phone1", "phone", title: "Send a Text Message?", required: false
|
||||
}
|
||||
}
|
||||
section("Turn on which A/C or fan...") {
|
||||
input "switch1", "capability.switch", required: false
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
subscribe(temperatureSensor1, "temperature", temperatureHandler)
|
||||
}
|
||||
|
||||
def updated() {
|
||||
unsubscribe()
|
||||
subscribe(temperatureSensor1, "temperature", temperatureHandler)
|
||||
}
|
||||
|
||||
def temperatureHandler(evt) {
|
||||
log.trace "temperature: $evt.value, $evt"
|
||||
|
||||
def tooHot = temperature1
|
||||
def mySwitch = settings.switch1
|
||||
|
||||
// TODO: Replace event checks with internal state (the most reliable way to know if an SMS has been sent recently or not).
|
||||
if (evt.doubleValue >= tooHot) {
|
||||
log.debug "Checking how long the temperature sensor has been reporting <= $tooHot"
|
||||
|
||||
// Don't send a continuous stream of text messages
|
||||
def deltaMinutes = 10 // TODO: Ask for "retry interval" in prefs?
|
||||
def timeAgo = new Date(now() - (1000 * 60 * deltaMinutes).toLong())
|
||||
def recentEvents = temperatureSensor1.eventsSince(timeAgo)?.findAll { it.name == "temperature" }
|
||||
log.trace "Found ${recentEvents?.size() ?: 0} events in the last $deltaMinutes minutes"
|
||||
def alreadySentSms = recentEvents.count { it.doubleValue >= tooHot } > 1
|
||||
|
||||
if (alreadySentSms) {
|
||||
log.debug "SMS already sent to $phone1 within the last $deltaMinutes minutes"
|
||||
// TODO: Send "Temperature back to normal" SMS, turn switch off
|
||||
} else {
|
||||
log.debug "Temperature rose above $tooHot: sending SMS to $phone1 and activating $mySwitch"
|
||||
send("${temperatureSensor1.displayName} is too hot, reporting a temperature of ${evt.value}${evt.unit?:"F"}")
|
||||
switch1?.on()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private send(msg) {
|
||||
if (location.contactBookEnabled) {
|
||||
log.debug("sending notifications to: ${recipients?.size()}")
|
||||
sendNotificationToContacts(msg, recipients)
|
||||
}
|
||||
else {
|
||||
if (sendPushMessage != "No") {
|
||||
log.debug("sending push message")
|
||||
sendPush(msg)
|
||||
}
|
||||
|
||||
if (phone1) {
|
||||
log.debug("sending text message")
|
||||
sendSms(phone1, msg)
|
||||
}
|
||||
}
|
||||
|
||||
log.debug msg
|
||||
}
|
||||
123
smartapps/smartthings/keep-me-cozy-ii.src/keep-me-cozy-ii.groovy
Normal file
123
smartapps/smartthings/keep-me-cozy-ii.src/keep-me-cozy-ii.groovy
Normal file
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Keep Me Cozy II
|
||||
*
|
||||
* Author: SmartThings
|
||||
*/
|
||||
|
||||
definition(
|
||||
name: "Keep Me Cozy II",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Works the same as Keep Me Cozy, but enables you to pick an alternative temperature sensor in a separate space from the thermostat. Focuses on making you comfortable where you are spending your time rather than where the thermostat is located.",
|
||||
category: "Green Living",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/temp_thermo.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/temp_thermo@2x.png"
|
||||
)
|
||||
|
||||
preferences() {
|
||||
section("Choose thermostat... ") {
|
||||
input "thermostat", "capability.thermostat"
|
||||
}
|
||||
section("Heat setting..." ) {
|
||||
input "heatingSetpoint", "decimal", title: "Degrees"
|
||||
}
|
||||
section("Air conditioning setting...") {
|
||||
input "coolingSetpoint", "decimal", title: "Degrees"
|
||||
}
|
||||
section("Optionally choose temperature sensor to use instead of the thermostat's... ") {
|
||||
input "sensor", "capability.temperatureMeasurement", title: "Temp Sensors", required: false
|
||||
}
|
||||
}
|
||||
|
||||
def installed()
|
||||
{
|
||||
log.debug "enter installed, state: $state"
|
||||
subscribeToEvents()
|
||||
}
|
||||
|
||||
def updated()
|
||||
{
|
||||
log.debug "enter updated, state: $state"
|
||||
unsubscribe()
|
||||
subscribeToEvents()
|
||||
}
|
||||
|
||||
def subscribeToEvents()
|
||||
{
|
||||
subscribe(location, changedLocationMode)
|
||||
if (sensor) {
|
||||
subscribe(sensor, "temperature", temperatureHandler)
|
||||
subscribe(thermostat, "temperature", temperatureHandler)
|
||||
subscribe(thermostat, "thermostatMode", temperatureHandler)
|
||||
}
|
||||
evaluate()
|
||||
}
|
||||
|
||||
def changedLocationMode(evt)
|
||||
{
|
||||
log.debug "changedLocationMode mode: $evt.value, heat: $heat, cool: $cool"
|
||||
evaluate()
|
||||
}
|
||||
|
||||
def temperatureHandler(evt)
|
||||
{
|
||||
evaluate()
|
||||
}
|
||||
|
||||
private evaluate()
|
||||
{
|
||||
if (sensor) {
|
||||
def threshold = 1.0
|
||||
def tm = thermostat.currentThermostatMode
|
||||
def ct = thermostat.currentTemperature
|
||||
def currentTemp = sensor.currentTemperature
|
||||
log.trace("evaluate:, mode: $tm -- temp: $ct, heat: $thermostat.currentHeatingSetpoint, cool: $thermostat.currentCoolingSetpoint -- " +
|
||||
"sensor: $currentTemp, heat: $heatingSetpoint, cool: $coolingSetpoint")
|
||||
if (tm in ["cool","auto"]) {
|
||||
// air conditioner
|
||||
if (currentTemp - coolingSetpoint >= threshold) {
|
||||
thermostat.setCoolingSetpoint(ct - 2)
|
||||
log.debug "thermostat.setCoolingSetpoint(${ct - 2}), ON"
|
||||
}
|
||||
else if (coolingSetpoint - currentTemp >= threshold && ct - thermostat.currentCoolingSetpoint >= threshold) {
|
||||
thermostat.setCoolingSetpoint(ct + 2)
|
||||
log.debug "thermostat.setCoolingSetpoint(${ct + 2}), OFF"
|
||||
}
|
||||
}
|
||||
if (tm in ["heat","emergency heat","auto"]) {
|
||||
// heater
|
||||
if (heatingSetpoint - currentTemp >= threshold) {
|
||||
thermostat.setHeatingSetpoint(ct + 2)
|
||||
log.debug "thermostat.setHeatingSetpoint(${ct + 2}), ON"
|
||||
}
|
||||
else if (currentTemp - heatingSetpoint >= threshold && thermostat.currentHeatingSetpoint - ct >= threshold) {
|
||||
thermostat.setHeatingSetpoint(ct - 2)
|
||||
log.debug "thermostat.setHeatingSetpoint(${ct - 2}), OFF"
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
thermostat.setHeatingSetpoint(heatingSetpoint)
|
||||
thermostat.setCoolingSetpoint(coolingSetpoint)
|
||||
thermostat.poll()
|
||||
}
|
||||
}
|
||||
|
||||
// for backward compatibility with existing subscriptions
|
||||
def coolingSetpointHandler(evt) {
|
||||
log.debug "coolingSetpointHandler()"
|
||||
}
|
||||
def heatingSetpointHandler (evt) {
|
||||
log.debug "heatingSetpointHandler ()"
|
||||
}
|
||||
95
smartapps/smartthings/keep-me-cozy.src/keep-me-cozy.groovy
Normal file
95
smartapps/smartthings/keep-me-cozy.src/keep-me-cozy.groovy
Normal file
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Keep Me Cozy
|
||||
*
|
||||
* Author: SmartThings
|
||||
*/
|
||||
definition(
|
||||
name: "Keep Me Cozy",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Changes your thermostat settings automatically in response to a mode change. Often used with Bon Voyage, Rise and Shine, and other Mode Magic SmartApps to automatically keep you comfortable while you're present and save you energy and money while you are away.",
|
||||
category: "Green Living",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/temp_thermo.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/temp_thermo@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("Choose thermostat... ") {
|
||||
input "thermostat", "capability.thermostat"
|
||||
}
|
||||
section("Heat setting...") {
|
||||
input "heatingSetpoint", "number", title: "Degrees?"
|
||||
}
|
||||
section("Air conditioning setting..."){
|
||||
input "coolingSetpoint", "number", title: "Degrees?"
|
||||
}
|
||||
}
|
||||
|
||||
def installed()
|
||||
{
|
||||
subscribe(thermostat, "heatingSetpoint", heatingSetpointHandler)
|
||||
subscribe(thermostat, "coolingSetpoint", coolingSetpointHandler)
|
||||
subscribe(thermostat, "temperature", temperatureHandler)
|
||||
subscribe(location, changedLocationMode)
|
||||
subscribe(app, appTouch)
|
||||
}
|
||||
|
||||
def updated()
|
||||
{
|
||||
unsubscribe()
|
||||
subscribe(thermostat, "heatingSetpoint", heatingSetpointHandler)
|
||||
subscribe(thermostat, "coolingSetpoint", coolingSetpointHandler)
|
||||
subscribe(thermostat, "temperature", temperatureHandler)
|
||||
subscribe(location, changedLocationMode)
|
||||
subscribe(app, appTouch)
|
||||
}
|
||||
|
||||
def heatingSetpointHandler(evt)
|
||||
{
|
||||
log.debug "heatingSetpoint: $evt, $settings"
|
||||
}
|
||||
|
||||
def coolingSetpointHandler(evt)
|
||||
{
|
||||
log.debug "coolingSetpoint: $evt, $settings"
|
||||
}
|
||||
|
||||
def temperatureHandler(evt)
|
||||
{
|
||||
log.debug "currentTemperature: $evt, $settings"
|
||||
}
|
||||
|
||||
def changedLocationMode(evt)
|
||||
{
|
||||
log.debug "changedLocationMode: $evt, $settings"
|
||||
|
||||
thermostat.setHeatingSetpoint(heatingSetpoint)
|
||||
thermostat.setCoolingSetpoint(coolingSetpoint)
|
||||
thermostat.poll()
|
||||
}
|
||||
|
||||
def appTouch(evt)
|
||||
{
|
||||
log.debug "appTouch: $evt, $settings"
|
||||
|
||||
thermostat.setHeatingSetpoint(heatingSetpoint)
|
||||
thermostat.setCoolingSetpoint(coolingSetpoint)
|
||||
thermostat.poll()
|
||||
}
|
||||
|
||||
// catchall
|
||||
def event(evt)
|
||||
{
|
||||
log.debug "value: $evt.value, event: $evt, settings: $settings, handlerName: ${evt.handlerName}"
|
||||
}
|
||||
182
smartapps/smartthings/laundry-monitor.src/laundry-monitor.groovy
Normal file
182
smartapps/smartthings/laundry-monitor.src/laundry-monitor.groovy
Normal file
@@ -0,0 +1,182 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Laundry Monitor
|
||||
*
|
||||
* Author: SmartThings
|
||||
*
|
||||
* Sends a message and (optionally) turns on or blinks a light to indicate that laundry is done.
|
||||
*
|
||||
* Date: 2013-02-21
|
||||
*/
|
||||
|
||||
definition(
|
||||
name: "Laundry Monitor",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Sends a message and (optionally) turns on or blinks a light to indicate that laundry is done.",
|
||||
category: "Convenience",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/FunAndSocial/App-HotTubTuner.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/FunAndSocial/App-HotTubTuner%402x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("Tell me when this washer/dryer has stopped..."){
|
||||
input "sensor1", "capability.accelerationSensor"
|
||||
}
|
||||
section("Via this number (optional, sends push notification if not specified)"){
|
||||
input("recipients", "contact", title: "Send notifications to") {
|
||||
input "phone", "phone", title: "Phone Number", required: false
|
||||
}
|
||||
}
|
||||
section("And by turning on these lights (optional)") {
|
||||
input "switches", "capability.switch", required: false, multiple: true, title: "Which lights?"
|
||||
input "lightMode", "enum", options: ["Flash Lights", "Turn On Lights"], required: false, defaultValue: "Turn On Lights", title: "Action?"
|
||||
}
|
||||
section("Time thresholds (in minutes, optional)"){
|
||||
input "cycleTime", "decimal", title: "Minimum cycle time", required: false, defaultValue: 10
|
||||
input "fillTime", "decimal", title: "Time to fill tub", required: false, defaultValue: 5
|
||||
}
|
||||
}
|
||||
|
||||
def installed()
|
||||
{
|
||||
initialize()
|
||||
}
|
||||
|
||||
def updated()
|
||||
{
|
||||
unsubscribe()
|
||||
initialize()
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
subscribe(sensor1, "acceleration.active", accelerationActiveHandler)
|
||||
subscribe(sensor1, "acceleration.inactive", accelerationInactiveHandler)
|
||||
}
|
||||
|
||||
def accelerationActiveHandler(evt) {
|
||||
log.trace "vibration"
|
||||
if (!state.isRunning) {
|
||||
log.info "Arming detector"
|
||||
state.isRunning = true
|
||||
state.startedAt = now()
|
||||
}
|
||||
state.stoppedAt = null
|
||||
}
|
||||
|
||||
def accelerationInactiveHandler(evt) {
|
||||
log.trace "no vibration, isRunning: $state.isRunning"
|
||||
if (state.isRunning) {
|
||||
log.debug "startedAt: ${state.startedAt}, stoppedAt: ${state.stoppedAt}"
|
||||
if (!state.stoppedAt) {
|
||||
state.stoppedAt = now()
|
||||
def delay = Math.floor(fillTime * 60).toInteger()
|
||||
runIn(delay, checkRunning, [overwrite: false])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def checkRunning() {
|
||||
log.trace "checkRunning()"
|
||||
if (state.isRunning) {
|
||||
def fillTimeMsec = fillTime ? fillTime * 60000 : 300000
|
||||
def sensorStates = sensor1.statesSince("acceleration", new Date((now() - fillTimeMsec) as Long))
|
||||
|
||||
if (!sensorStates.find{it.value == "active"}) {
|
||||
|
||||
def cycleTimeMsec = cycleTime ? cycleTime * 60000 : 600000
|
||||
def duration = now() - state.startedAt
|
||||
if (duration - fillTimeMsec > cycleTimeMsec) {
|
||||
log.debug "Sending notification"
|
||||
|
||||
def msg = "${sensor1.displayName} is finished"
|
||||
log.info msg
|
||||
|
||||
if (location.contactBookEnabled) {
|
||||
sendNotificationToContacts(msg, recipients)
|
||||
}
|
||||
else {
|
||||
|
||||
if (phone) {
|
||||
sendSms phone, msg
|
||||
} else {
|
||||
sendPush msg
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (switches) {
|
||||
if (lightMode?.equals("Turn On Lights")) {
|
||||
switches.on()
|
||||
} else {
|
||||
flashLights()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.debug "Not sending notification because machine wasn't running long enough $duration versus $cycleTimeMsec msec"
|
||||
}
|
||||
state.isRunning = false
|
||||
log.info "Disarming detector"
|
||||
} else {
|
||||
log.debug "skipping notification because vibration detected again"
|
||||
}
|
||||
}
|
||||
else {
|
||||
log.debug "machine no longer running"
|
||||
}
|
||||
}
|
||||
|
||||
private flashLights() {
|
||||
def doFlash = true
|
||||
def onFor = onFor ?: 1000
|
||||
def offFor = offFor ?: 1000
|
||||
def numFlashes = numFlashes ?: 3
|
||||
|
||||
log.debug "LAST ACTIVATED IS: ${state.lastActivated}"
|
||||
if (state.lastActivated) {
|
||||
def elapsed = now() - state.lastActivated
|
||||
def sequenceTime = (numFlashes + 1) * (onFor + offFor)
|
||||
doFlash = elapsed > sequenceTime
|
||||
log.debug "DO FLASH: $doFlash, ELAPSED: $elapsed, LAST ACTIVATED: ${state.lastActivated}"
|
||||
}
|
||||
|
||||
if (doFlash) {
|
||||
log.debug "FLASHING $numFlashes times"
|
||||
state.lastActivated = now()
|
||||
log.debug "LAST ACTIVATED SET TO: ${state.lastActivated}"
|
||||
def initialActionOn = switches.collect{it.currentSwitch != "on"}
|
||||
def delay = 1L
|
||||
numFlashes.times {
|
||||
log.trace "Switch on after $delay msec"
|
||||
switches.eachWithIndex {s, i ->
|
||||
if (initialActionOn[i]) {
|
||||
s.on(delay: delay)
|
||||
}
|
||||
else {
|
||||
s.off(delay:delay)
|
||||
}
|
||||
}
|
||||
delay += onFor
|
||||
log.trace "Switch off after $delay msec"
|
||||
switches.eachWithIndex {s, i ->
|
||||
if (initialActionOn[i]) {
|
||||
s.off(delay: delay)
|
||||
}
|
||||
else {
|
||||
s.on(delay:delay)
|
||||
}
|
||||
}
|
||||
delay += offFor
|
||||
}
|
||||
}
|
||||
}
|
||||
110
smartapps/smartthings/left-it-open.src/left-it-open.groovy
Normal file
110
smartapps/smartthings/left-it-open.src/left-it-open.groovy
Normal file
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Left It Open
|
||||
*
|
||||
* Author: SmartThings
|
||||
* Date: 2013-05-09
|
||||
*/
|
||||
definition(
|
||||
name: "Left It Open",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Notifies you when you have left a door or window open longer that a specified amount of time.",
|
||||
category: "Convenience",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/ModeMagic/bon-voyage.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/ModeMagic/bon-voyage%402x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
|
||||
section("Monitor this door or window") {
|
||||
input "contact", "capability.contactSensor"
|
||||
}
|
||||
section("And notify me if it's open for more than this many minutes (default 10)") {
|
||||
input "openThreshold", "number", description: "Number of minutes", required: false
|
||||
}
|
||||
section("Delay between notifications (default 10 minutes") {
|
||||
input "frequency", "number", title: "Number of minutes", description: "", required: false
|
||||
}
|
||||
section("Via text message at this number (or via push notification if not specified") {
|
||||
input("recipients", "contact", title: "Send notifications to") {
|
||||
input "phone", "phone", title: "Phone number (optional)", required: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
log.trace "installed()"
|
||||
subscribe()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.trace "updated()"
|
||||
unsubscribe()
|
||||
subscribe()
|
||||
}
|
||||
|
||||
def subscribe() {
|
||||
subscribe(contact, "contact.open", doorOpen)
|
||||
subscribe(contact, "contact.closed", doorClosed)
|
||||
}
|
||||
|
||||
def doorOpen(evt)
|
||||
{
|
||||
log.trace "doorOpen($evt.name: $evt.value)"
|
||||
def t0 = now()
|
||||
def delay = (openThreshold != null && openThreshold != "") ? openThreshold * 60 : 600
|
||||
runIn(delay, doorOpenTooLong, [overwrite: false])
|
||||
log.debug "scheduled doorOpenTooLong in ${now() - t0} msec"
|
||||
}
|
||||
|
||||
def doorClosed(evt)
|
||||
{
|
||||
log.trace "doorClosed($evt.name: $evt.value)"
|
||||
}
|
||||
|
||||
def doorOpenTooLong() {
|
||||
def contactState = contact.currentState("contact")
|
||||
def freq = (frequency != null && frequency != "") ? frequency * 60 : 600
|
||||
|
||||
if (contactState.value == "open") {
|
||||
def elapsed = now() - contactState.rawDateCreated.time
|
||||
def threshold = ((openThreshold != null && openThreshold != "") ? openThreshold * 60000 : 60000) - 1000
|
||||
if (elapsed >= threshold) {
|
||||
log.debug "Contact has stayed open long enough since last check ($elapsed ms): calling sendMessage()"
|
||||
sendMessage()
|
||||
runIn(freq, doorOpenTooLong, [overwrite: false])
|
||||
} else {
|
||||
log.debug "Contact has not stayed open long enough since last check ($elapsed ms): doing nothing"
|
||||
}
|
||||
} else {
|
||||
log.warn "doorOpenTooLong() called but contact is closed: doing nothing"
|
||||
}
|
||||
}
|
||||
|
||||
void sendMessage()
|
||||
{
|
||||
def minutes = (openThreshold != null && openThreshold != "") ? openThreshold : 10
|
||||
def msg = "${contact.displayName} has been left open for ${minutes} minutes."
|
||||
log.info msg
|
||||
if (location.contactBookEnabled) {
|
||||
sendNotificationToContacts(msg, recipients)
|
||||
}
|
||||
else {
|
||||
if (phone) {
|
||||
sendSms phone, msg
|
||||
} else {
|
||||
sendPush msg
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Let There Be Light!
|
||||
* Turn your lights on when an open/close sensor opens and off when the sensor closes.
|
||||
*
|
||||
* Author: SmartThings
|
||||
*/
|
||||
definition(
|
||||
name: "Let There Be Light!",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Turn your lights on when a SmartSense Multi is opened and turn them off when it is closed.",
|
||||
category: "Convenience",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/light_contact-outlet.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/light_contact-outlet@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("When the door opens/closes...") {
|
||||
input "contact1", "capability.contactSensor", title: "Where?"
|
||||
}
|
||||
section("Turn on/off a light...") {
|
||||
input "switch1", "capability.switch"
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
subscribe(contact1, "contact", contactHandler)
|
||||
}
|
||||
|
||||
def updated() {
|
||||
unsubscribe()
|
||||
subscribe(contact1, "contact", contactHandler)
|
||||
}
|
||||
|
||||
def contactHandler(evt) {
|
||||
log.debug "$evt.value"
|
||||
if (evt.value == "open") {
|
||||
switch1.on()
|
||||
} else if (evt.value == "closed") {
|
||||
switch1.off()
|
||||
}
|
||||
}
|
||||
768
smartapps/smartthings/life360-connect.src/life360-connect.groovy
Normal file
768
smartapps/smartthings/life360-connect.src/life360-connect.groovy
Normal file
@@ -0,0 +1,768 @@
|
||||
/**
|
||||
* life360
|
||||
*
|
||||
* Copyright 2014 Jeff's Account
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
definition(
|
||||
name: "Life360 (Connect)",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Life360 Service Manager",
|
||||
category: "SmartThings Labs",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/life360.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/life360@2x.png",
|
||||
oauth: [displayName: "Life360", displayLink: "Life360"]
|
||||
) {
|
||||
appSetting "clientId"
|
||||
appSetting "clientSecret"
|
||||
appSetting "serverUrl"
|
||||
}
|
||||
|
||||
preferences {
|
||||
page(name: "Credentials", title: "Life360 Authentication", content: "authPage", nextPage: "listCirclesPage", install: false)
|
||||
page(name: "listCirclesPage", title: "Select Life360 Circle", nextPage: "listPlacesPage", content: "listCircles", install: false)
|
||||
page(name: "listPlacesPage", title: "Select Life360 Place", nextPage: "listUsersPage", content: "listPlaces", install: false)
|
||||
page(name: "listUsersPage", title: "Select Life360 Users", content: "listUsers", install: true)
|
||||
}
|
||||
|
||||
// page(name: "Credentials", title: "Enter Life360 Credentials", content: "getCredentialsPage", nextPage: "listCirclesPage", install: false)
|
||||
// page(name: "page3", title: "Select Life360 Users", content: "listUsers")
|
||||
|
||||
mappings {
|
||||
|
||||
path("/placecallback") {
|
||||
action: [
|
||||
POST: "placeEventHandler",
|
||||
GET: "placeEventHandler"
|
||||
]
|
||||
}
|
||||
|
||||
path("/receiveToken") {
|
||||
action: [
|
||||
POST: "receiveToken",
|
||||
GET: "receiveToken"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
def authPage()
|
||||
{
|
||||
log.debug "authPage()"
|
||||
|
||||
def description = "Life360 Credentials Already Entered."
|
||||
|
||||
def uninstallOption = false
|
||||
if (app.installationState == "COMPLETE")
|
||||
uninstallOption = true
|
||||
|
||||
if(!state.life360AccessToken)
|
||||
{
|
||||
log.debug "about to create access token"
|
||||
createAccessToken()
|
||||
description = "Click to enter Life360 Credentials."
|
||||
|
||||
def redirectUrl = oauthInitUrl()
|
||||
|
||||
log.debug "RedirectURL = ${redirectUrl}"
|
||||
|
||||
return dynamicPage(name: "Credentials", title: "Life360", nextPage:"listCirclesPage", uninstall: uninstallOption, install:false) {
|
||||
section {
|
||||
href url:redirectUrl, style:"embedded", required:false, title:"Life360", description:description
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
listCircles()
|
||||
}
|
||||
}
|
||||
|
||||
def receiveToken() {
|
||||
|
||||
state.life360AccessToken = params.access_token
|
||||
|
||||
def html = """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=640">
|
||||
<title>Withings Connection</title>
|
||||
<style type="text/css">
|
||||
@font-face {
|
||||
font-family: 'Swiss 721 W01 Thin';
|
||||
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot');
|
||||
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot?#iefix') format('embedded-opentype'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.woff') format('woff'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.ttf') format('truetype'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.svg#swis721_th_btthin') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Swiss 721 W01 Light';
|
||||
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot');
|
||||
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot?#iefix') format('embedded-opentype'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.woff') format('woff'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.ttf') format('truetype'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.svg#swis721_lt_btlight') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
.container {
|
||||
width: 560px;
|
||||
padding: 40px;
|
||||
/*background: #eee;*/
|
||||
text-align: center;
|
||||
}
|
||||
img {
|
||||
vertical-align: middle;
|
||||
}
|
||||
img:nth-child(2) {
|
||||
margin: 0 30px;
|
||||
}
|
||||
p {
|
||||
font-size: 2.2em;
|
||||
font-family: 'Swiss 721 W01 Thin';
|
||||
text-align: center;
|
||||
color: #666666;
|
||||
padding: 0 40px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
/*
|
||||
p:last-child {
|
||||
margin-top: 0px;
|
||||
}
|
||||
*/
|
||||
span {
|
||||
font-family: 'Swiss 721 W01 Light';
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<img src="https://s3.amazonaws.com/smartapp-icons/Partner/life360@2x.png" alt="Life360 icon" />
|
||||
<img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/connected-device-icn%402x.png" alt="connected device icon" />
|
||||
<img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/st-logo%402x.png" alt="SmartThings logo" />
|
||||
<p>Your Life360 Account is now connected to SmartThings!</p>
|
||||
<p>Click 'Done' to finish setup.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
render contentType: 'text/html', data: html
|
||||
|
||||
}
|
||||
|
||||
def oauthInitUrl()
|
||||
{
|
||||
log.debug "oauthInitUrl"
|
||||
def stcid = getSmartThingsClientId();
|
||||
|
||||
// def oauth_url = "https://api.life360.com/v3/oauth2/authorize?client_id=pREqugabRetre4EstetherufrePumamExucrEHuc&response_type=token&redirect_uri=http%3A%2F%2Fwww.smartthings.com"
|
||||
|
||||
state.oauthInitState = UUID.randomUUID().toString()
|
||||
|
||||
def oauthParams = [
|
||||
response_type: "token",
|
||||
client_id: stcid,
|
||||
redirect_uri: buildRedirectUrl()
|
||||
]
|
||||
|
||||
return "https://api.life360.com/v3/oauth2/authorize?" + toQueryString(oauthParams)
|
||||
}
|
||||
|
||||
String toQueryString(Map m) {
|
||||
return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&")
|
||||
}
|
||||
|
||||
def getSmartThingsClientId() {
|
||||
return "pREqugabRetre4EstetherufrePumamExucrEHuc"
|
||||
}
|
||||
|
||||
def getServerUrl() { appSettings.serverUrl }
|
||||
|
||||
def buildRedirectUrl()
|
||||
{
|
||||
log.debug "buildRedirectUrl"
|
||||
// /api/token/:st_token/smartapps/installations/:id/something
|
||||
|
||||
return serverUrl + "/api/token/${state.accessToken}/smartapps/installations/${app.id}/receiveToken"
|
||||
}
|
||||
|
||||
//
|
||||
// This method is no longer used - was part of the initial username/password based authentication that has now been replaced
|
||||
// by the full OAUTH web flow
|
||||
//
|
||||
|
||||
def getCredentialsPage() {
|
||||
|
||||
dynamicPage(name: "Credentials", title: "Enter Life360 Credentials", nextPage: "listCirclesPage", uninstall: true, install:false)
|
||||
{
|
||||
section("Life 360 Credentials ...") {
|
||||
input "username", "text", title: "Life360 Username?", multiple: false, required: true
|
||||
input "password", "password", title: "Life360 Password?", multiple: false, required: true, autoCorrect: false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//
|
||||
// This method is no longer used - was part of the initial username/password based authentication that has now been replaced
|
||||
// by the full OAUTH web flow
|
||||
//
|
||||
|
||||
def getCredentialsErrorPage(String message) {
|
||||
|
||||
dynamicPage(name: "Credentials", title: "Enter Life360 Credentials", nextPage: "listCirclesPage", uninstall: true, install:false)
|
||||
{
|
||||
section("Life 360 Credentials ...") {
|
||||
input "username", "text", title: "Life360 Username?", multiple: false, required: true
|
||||
input "password", "password", title: "Life360 Password?", multiple: false, required: true, autoCorrect: false
|
||||
paragraph "${message}"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def testLife360Connection() {
|
||||
|
||||
if (state.life360AccessToken)
|
||||
true
|
||||
else
|
||||
false
|
||||
|
||||
}
|
||||
|
||||
//
|
||||
// This method is no longer used - was part of the initial username/password based authentication that has now been replaced
|
||||
// by the full OAUTH web flow
|
||||
//
|
||||
|
||||
def initializeLife360Connection() {
|
||||
|
||||
def oauthClientId = appSettings.clientId
|
||||
def oauthClientSecret = appSettings.clientSecret
|
||||
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
|
||||
initialize()
|
||||
|
||||
def username = settings.username
|
||||
def password = settings.password
|
||||
|
||||
// Base 64 encode the credentials
|
||||
|
||||
def basicCredentials = "${oauthClientId}:${oauthClientSecret}"
|
||||
def encodedCredentials = basicCredentials.encodeAsBase64().toString()
|
||||
|
||||
log.debug "Encoded Creds: ${encodedCredentials}"
|
||||
|
||||
|
||||
// call life360, get OAUTH token using password flow, save
|
||||
// curl -X POST -H "Authorization: Basic cFJFcXVnYWJSZXRyZTRFc3RldGhlcnVmcmVQdW1hbUV4dWNyRUh1YzptM2ZydXBSZXRSZXN3ZXJFQ2hBUHJFOTZxYWtFZHI0Vg=="
|
||||
// -F "grant_type=password" -F "username=jeff@hagins.us" -F "password=tondeleo" https://api.life360.com/v3/oauth2/token.json
|
||||
|
||||
|
||||
def url = "https://api.life360.com/v3/oauth2/token.json"
|
||||
|
||||
|
||||
def postBody = "grant_type=password&" +
|
||||
"username=${username}&"+
|
||||
"password=${password}"
|
||||
|
||||
log.debug "Post Body: ${postBody}"
|
||||
|
||||
def result = null
|
||||
|
||||
try {
|
||||
|
||||
httpPost(uri: url, body: postBody, headers: ["Authorization": "Basic ${encodedCredentials}" ]) {response ->
|
||||
result = response
|
||||
}
|
||||
if (result.data.access_token) {
|
||||
state.life360AccessToken = result.data.access_token
|
||||
log.debug "Access Token = ${state.life360AccessToken}"
|
||||
return true;
|
||||
}
|
||||
log.debug "Response=${result.data}"
|
||||
return false;
|
||||
|
||||
}
|
||||
catch (e) {
|
||||
log.debug e
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def listCircles (){
|
||||
|
||||
// understand whether to present the Uninstall option
|
||||
def uninstallOption = false
|
||||
if (app.installationState == "COMPLETE")
|
||||
uninstallOption = true
|
||||
|
||||
// get connected to life360 api
|
||||
|
||||
if (testLife360Connection()) {
|
||||
|
||||
// now pull back the list of Life360 circles
|
||||
// curl -X GET -H "Authorization: Bearer MmEzODQxYWQtMGZmMy00MDZhLWEwMGQtMTIzYmYxYzFmNGU3" https://api.life360.com/v3/circles.json
|
||||
|
||||
def url = "https://api.life360.com/v3/circles.json"
|
||||
|
||||
def result = null
|
||||
|
||||
httpGet(uri: url, headers: ["Authorization": "Bearer ${state.life360AccessToken}" ]) {response ->
|
||||
result = response
|
||||
}
|
||||
|
||||
log.debug "Circles=${result.data}"
|
||||
|
||||
def circles = result.data.circles
|
||||
|
||||
if (circles.size > 1) {
|
||||
return (
|
||||
dynamicPage(name: "listCirclesPage", title: "Life360 Circles", nextPage: null, uninstall: uninstallOption, install:false) {
|
||||
section("Select Life360 Circle:") {
|
||||
input "circle", "enum", multiple: false, required:true, title:"Life360 Circle: ", options: circles.collectEntries{[it.id, it.name]}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
else {
|
||||
state.circle = circles[0].id
|
||||
return (listPlaces())
|
||||
}
|
||||
}
|
||||
else {
|
||||
getCredentialsErrorPage("Invalid Usernaname or password.")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def listPlaces() {
|
||||
|
||||
// understand whether to present the Uninstall option
|
||||
def uninstallOption = false
|
||||
if (app.installationState == "COMPLETE")
|
||||
uninstallOption = true
|
||||
|
||||
if (!state?.circle)
|
||||
state.circle = settings.circle
|
||||
|
||||
// call life360 and get the list of places in the circle
|
||||
|
||||
def url = "https://api.life360.com/v3/circles/${state.circle}/places.json"
|
||||
|
||||
def result = null
|
||||
|
||||
httpGet(uri: url, headers: ["Authorization": "Bearer ${state.life360AccessToken}" ]) {response ->
|
||||
result = response
|
||||
}
|
||||
|
||||
log.debug "Places=${result.data}"
|
||||
|
||||
def places = result.data.places
|
||||
state.places = places
|
||||
|
||||
// If there is a place called "Home" use it as the default
|
||||
def defaultPlace = places.find{it.name=="Home"}
|
||||
def defaultPlaceId
|
||||
if (defaultPlace) {
|
||||
defaultPlaceId = defaultPlace.id
|
||||
log.debug "Place = $defaultPlace.name, Id=$defaultPlace.id"
|
||||
}
|
||||
|
||||
dynamicPage(name: "listPlacesPage", title: "Life360 Places", nextPage: null, uninstall: uninstallOption, install:false) {
|
||||
section("Select Life360 Place to Match Current Location:") {
|
||||
paragraph "Please select the ONE Life360 Place that matches your SmartThings location: ${location.name}"
|
||||
input "place", "enum", multiple: false, required:true, title:"Life360 Places: ", options: places.collectEntries{[it.id, it.name]}, defaultValue: defaultPlaceId
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def listUsers () {
|
||||
|
||||
// understand whether to present the Uninstall option
|
||||
def uninstallOption = false
|
||||
if (app.installationState == "COMPLETE")
|
||||
uninstallOption = true
|
||||
|
||||
if (!state?.circle)
|
||||
state.circle = settings.circle
|
||||
|
||||
// call life360 and get list of users (members)
|
||||
|
||||
def url = "https://api.life360.com/v3/circles/${state.circle}/members.json"
|
||||
|
||||
def result = null
|
||||
|
||||
httpGet(uri: url, headers: ["Authorization": "Bearer ${state.life360AccessToken}" ]) {response ->
|
||||
result = response
|
||||
}
|
||||
|
||||
log.debug "Members=${result.data}"
|
||||
|
||||
// save members list for later
|
||||
|
||||
def members = result.data.members
|
||||
|
||||
state.members = members
|
||||
|
||||
// build preferences page
|
||||
|
||||
dynamicPage(name: "listUsersPage", title: "Life360 Users", nextPage: null, uninstall: uninstallOption, install:true) {
|
||||
section("Select Life360 Users to Import into SmartThings:") {
|
||||
input "users", "enum", multiple: true, required:true, title:"Life360 Users: ", options: members.collectEntries{[it.id, it.firstName+" "+it.lastName]}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
|
||||
if (!state?.circle)
|
||||
state.circle = settings.circle
|
||||
|
||||
log.debug "In installed() method."
|
||||
// log.debug "Members: ${state.members}"
|
||||
// log.debug "Users: ${settings.users}"
|
||||
|
||||
settings.users.each {memberId->
|
||||
|
||||
// log.debug "Find by Member Id = ${memberId}"
|
||||
|
||||
def member = state.members.find{it.id==memberId}
|
||||
|
||||
// log.debug "After Find Attempt."
|
||||
|
||||
// log.debug "Member Id = ${member.id}, Name = ${member.firstName} ${member.lastName}, Email Address = ${member.loginEmail}"
|
||||
|
||||
// log.debug "External Id=${app.id}:${member.id}"
|
||||
|
||||
// create the device
|
||||
if (member) {
|
||||
|
||||
def childDevice = addChildDevice("smartthings", "Life360 User", "${app.id}.${member.id}",null,[name:member.firstName, completedSetup: true])
|
||||
|
||||
// save the memberId on the device itself so we can find easily later
|
||||
// childDevice.setMemberId(member.id)
|
||||
|
||||
if (childDevice)
|
||||
{
|
||||
// log.debug "Child Device Successfully Created"
|
||||
generateInitialEvent (member, childDevice)
|
||||
|
||||
// build the icon name form the L360 Avatar URL
|
||||
// URL Format: https://www.life360.com/img/user_images/b4698717-1f2e-4b7a-b0d4-98ccfb4e9730/Maddie_Hagins_51d2eea2019c7.jpeg
|
||||
// SmartThings Icon format is: L360.b4698717-1f2e-4b7a-b0d4-98ccfb4e9730.Maddie_Hagins_51d2eea2019c7
|
||||
try {
|
||||
|
||||
// build the icon name from the avatar URL
|
||||
log.debug "Avatar URL = ${member.avatar}"
|
||||
def urlPathElements = member.avatar.tokenize("/")
|
||||
def fileElements = urlPathElements[5].tokenize(".")
|
||||
// def icon = "st.Lighting.light1"
|
||||
def icon="l360.${urlPathElements[4]}.${fileElements[0]}"
|
||||
log.debug "Icon = ${icon}"
|
||||
|
||||
// set the icon on the device
|
||||
childDevice.setIcon("presence","present",icon)
|
||||
childDevice.setIcon("presence","not present",icon)
|
||||
childDevice.save()
|
||||
}
|
||||
catch (e) { // do nothing
|
||||
log.debug "Error = ${e}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
createCircleSubscription()
|
||||
|
||||
}
|
||||
|
||||
def createCircleSubscription() {
|
||||
|
||||
// delete any existing webhook subscriptions for this circle
|
||||
//
|
||||
// curl -X DELETE https://webhook.qa.life360.com/v3/circles/:circleId/webhook.json
|
||||
|
||||
log.debug "Remove any existing Life360 Webhooks for this Circle."
|
||||
|
||||
def deleteUrl = "https://api.life360.com/v3/circles/${state.circle}/webhook.json"
|
||||
|
||||
try { // ignore any errors - there many not be any existing webhooks
|
||||
|
||||
httpDelete (uri: deleteUrl, headers: ["Authorization": "Bearer ${state.life360AccessToken}" ]) {response ->
|
||||
result = response}
|
||||
}
|
||||
|
||||
catch (e) {
|
||||
|
||||
log.debug (e)
|
||||
}
|
||||
|
||||
// subscribe to the life360 webhook to get push notifications on place events within this circle
|
||||
|
||||
// POST /circles/:circle_id/places/webooks
|
||||
// Params: hook_url
|
||||
|
||||
log.debug "Create a new Life360 Webhooks for this Circle."
|
||||
|
||||
createAccessToken() // create our own OAUTH access token to use in webhook url
|
||||
|
||||
def hookUrl = "${serverUrl}/api/smartapps/installations/${app.id}/placecallback?access_token=${state.accessToken}".encodeAsURL()
|
||||
|
||||
def url = "https://api.life360.com/v3/circles/${state.circle}/webhook.json"
|
||||
|
||||
def postBody = "url=${hookUrl}"
|
||||
|
||||
log.debug "Post Body: ${postBody}"
|
||||
|
||||
def result = null
|
||||
|
||||
try {
|
||||
|
||||
httpPost(uri: url, body: postBody, headers: ["Authorization": "Bearer ${state.life360AccessToken}" ]) {response ->
|
||||
result = response}
|
||||
|
||||
} catch (e) {
|
||||
log.debug (e)
|
||||
}
|
||||
|
||||
// response from this call looks like this:
|
||||
// {"circleId":"41094b6a-32fc-4ef5-a9cd-913f82268836","userId":"0d1db550-9163-471b-8829-80b375e0fa51","clientId":"11",
|
||||
// "hookUrl":"https://testurl.com"}
|
||||
|
||||
log.debug "Response = ${response}"
|
||||
|
||||
if (result.data?.hookUrl) {
|
||||
log.debug "Webhook creation successful. Response = ${result.data}"
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def updated() {
|
||||
|
||||
if (!state?.circle)
|
||||
state.circle = settings.circle
|
||||
|
||||
log.debug "In updated() method."
|
||||
// log.debug "Members: ${state.members}"
|
||||
// log.debug "Users: ${settings.users}"
|
||||
|
||||
// loop through selected users and try to find child device for each
|
||||
|
||||
settings.users.each {memberId->
|
||||
|
||||
def externalId = "${app.id}.${memberId}"
|
||||
|
||||
// find the appropriate child device based on my app id and the device network id
|
||||
|
||||
def deviceWrapper = getChildDevice("${externalId}")
|
||||
|
||||
if (!deviceWrapper) { // device isn't there - so we need to create
|
||||
|
||||
// log.debug "Find by Member Id = ${memberId}"
|
||||
|
||||
def member = state.members.find{it.id==memberId}
|
||||
|
||||
// log.debug "After Find Attempt."
|
||||
|
||||
log.debug "Member Id = ${member.id}, Name = ${member.firstName} ${member.lastName}, Email Address = ${member.loginEmail}"
|
||||
|
||||
// log.debug "External Id=${app.id}:${member.id}"
|
||||
|
||||
// create the device
|
||||
def childDevice = addChildDevice("smartthings", "life360-user", "${app.id}.${member.id}",null,[name:member.firstName, completedSetup: true])
|
||||
// childDevice.setMemberId(member.id)
|
||||
|
||||
if (childDevice)
|
||||
{
|
||||
// log.debug "Child Device Successfully Created"
|
||||
generateInitialEvent (member, childDevice)
|
||||
|
||||
// build the icon name form the L360 Avatar URL
|
||||
// URL Format: https://www.life360.com/img/user_images/b4698717-1f2e-4b7a-b0d4-98ccfb4e9730/Maddie_Hagins_51d2eea2019c7.jpeg
|
||||
// SmartThings Icon format is: L360.b4698717-1f2e-4b7a-b0d4-98ccfb4e9730.Maddie_Hagins_51d2eea2019c7
|
||||
try {
|
||||
|
||||
// build the icon name from the avatar URL
|
||||
log.debug "Avatar URL = ${member.avatar}"
|
||||
def urlPathElements = member.avatar.tokenize("/")
|
||||
def icon="l360.${urlPathElements[4]}.${urlPathElements[5]}"
|
||||
|
||||
// set the icon on the device
|
||||
childDevice.setIcon("presence","present",icon)
|
||||
childDevice.setIcon("presence","not present",icon)
|
||||
childDevice.save()
|
||||
}
|
||||
catch (e) { // do nothing
|
||||
log.debug "Error = ${e}"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
else {
|
||||
|
||||
// log.debug "Find by Member Id = ${memberId}"
|
||||
|
||||
def member = state.members.find{it.id==memberId}
|
||||
|
||||
generateInitialEvent (member, deviceWrapper)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Now remove any existing devices that represent users that are no longer selected
|
||||
|
||||
def childDevices = getAllChildDevices()
|
||||
|
||||
log.debug "Child Devices = ${childDevices}"
|
||||
|
||||
childDevices.each {childDevice->
|
||||
|
||||
log.debug "Child = ${childDevice}, DNI=${childDevice.deviceNetworkId}"
|
||||
|
||||
// def childMemberId = childDevice.getMemberId()
|
||||
|
||||
def splitStrings = childDevice.deviceNetworkId.split("\\.")
|
||||
|
||||
log.debug "Strings = ${splitStrings}"
|
||||
|
||||
def childMemberId = splitStrings[1]
|
||||
|
||||
log.debug "Child Member Id = ${childMemberId}"
|
||||
|
||||
log.debug "Settings.users = ${settings.users}"
|
||||
|
||||
if (!settings.users.find{it==childMemberId}) {
|
||||
deleteChildDevice(childDevice.deviceNetworkId)
|
||||
def member = state.members.find {it.id==memberId}
|
||||
if (member)
|
||||
state.members.remove(member)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
def generateInitialEvent (member, childDevice) {
|
||||
|
||||
// lets figure out if the member is currently "home" (At the place)
|
||||
|
||||
try { // we are going to just ignore any errors
|
||||
|
||||
log.debug "Generate Initial Event for New Device for Member = ${member.id}"
|
||||
|
||||
def place = state.places.find{it.id==settings.place}
|
||||
|
||||
if (place) {
|
||||
|
||||
def memberLatitude = new Float (member.location.latitude)
|
||||
def memberLongitude = new Float (member.location.longitude)
|
||||
def placeLatitude = new Float (place.latitude)
|
||||
def placeLongitude = new Float (place.longitude)
|
||||
def placeRadius = new Float (place.radius)
|
||||
|
||||
// log.debug "Member Location = ${memberLatitude}/${memberLongitude}"
|
||||
// log.debug "Place Location = ${placeLatitude}/${placeLongitude}"
|
||||
// log.debug "Place Radius = ${placeRadius}"
|
||||
|
||||
def distanceAway = haversine(memberLatitude, memberLongitude, placeLatitude, placeLongitude)*1000 // in meters
|
||||
|
||||
// log.debug "Distance Away = ${distanceAway}"
|
||||
|
||||
boolean isPresent = (distanceAway <= placeRadius)
|
||||
|
||||
// log.debug "External Id=${app.id}:${member.id}"
|
||||
|
||||
// def childDevice2 = getChildDevice("${app.id}.${member.id}")
|
||||
|
||||
// log.debug "Child Device = ${childDevice2}"
|
||||
|
||||
childDevice?.generatePresenceEvent(isPresent)
|
||||
|
||||
// log.debug "After generating presence event."
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
catch (e) {
|
||||
// eat it
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
// TODO: subscribe to attributes, devices, locations, etc.
|
||||
}
|
||||
|
||||
def haversine(lat1, lon1, lat2, lon2) {
|
||||
def R = 6372.8
|
||||
// In kilometers
|
||||
def dLat = Math.toRadians(lat2 - lat1)
|
||||
def dLon = Math.toRadians(lon2 - lon1)
|
||||
lat1 = Math.toRadians(lat1)
|
||||
lat2 = Math.toRadians(lat2)
|
||||
|
||||
def a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.sin(dLon / 2) * Math.sin(dLon / 2) * Math.cos(lat1) * Math.cos(lat2)
|
||||
def c = 2 * Math.asin(Math.sqrt(a))
|
||||
def d = R * c
|
||||
return(d)
|
||||
}
|
||||
|
||||
|
||||
def placeEventHandler() {
|
||||
|
||||
log.debug "In placeEventHandler method."
|
||||
|
||||
// the POST to this end-point will look like:
|
||||
// POST http://test.com/webhook?circleId=XXXX&placeId=XXXX&userId=XXXX&direction=arrive
|
||||
|
||||
def circleId = params?.circleId
|
||||
def placeId = params?.placeId
|
||||
def userId = params?.userId
|
||||
def direction = params?.direction
|
||||
def timestamp = params?.timestamp
|
||||
|
||||
log.debug "Life360 Event: Circle: ${circleId}, Place: ${placeId}, User: ${userId}, Direction: ${direction}"
|
||||
|
||||
if (placeId == settings.place) {
|
||||
|
||||
def presenceState = (direction=="in")
|
||||
|
||||
def externalId = "${app.id}.${userId}"
|
||||
|
||||
// find the appropriate child device based on my app id and the device network id
|
||||
|
||||
def deviceWrapper = getChildDevice("${externalId}")
|
||||
|
||||
// invoke the generatePresenceEvent method on the child device
|
||||
|
||||
if (deviceWrapper) {
|
||||
deviceWrapper.generatePresenceEvent(presenceState)
|
||||
log.debug "Event raised on child device: ${externalId}"
|
||||
}
|
||||
else {
|
||||
log.debug "Couldn't find child device associated with inbound Life360 event."
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Light Follows Me
|
||||
*
|
||||
* Author: SmartThings
|
||||
*/
|
||||
|
||||
definition(
|
||||
name: "Light Follows Me",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Turn your lights on when motion is detected and then off again once the motion stops for a set period of time.",
|
||||
category: "Convenience",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/temp_thermo-switch.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/temp_thermo-switch@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("Turn on when there's movement..."){
|
||||
input "motion1", "capability.motionSensor", title: "Where?"
|
||||
}
|
||||
section("And off when there's been no movement for..."){
|
||||
input "minutes1", "number", title: "Minutes?"
|
||||
}
|
||||
section("Turn on/off light(s)..."){
|
||||
input "switches", "capability.switch", multiple: true
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
subscribe(motion1, "motion", motionHandler)
|
||||
}
|
||||
|
||||
def updated() {
|
||||
unsubscribe()
|
||||
subscribe(motion1, "motion", motionHandler)
|
||||
}
|
||||
|
||||
def motionHandler(evt) {
|
||||
log.debug "$evt.name: $evt.value"
|
||||
if (evt.value == "active") {
|
||||
log.debug "turning on lights"
|
||||
switches.on()
|
||||
} else if (evt.value == "inactive") {
|
||||
runIn(minutes1 * 60, scheduleCheck, [overwrite: false])
|
||||
}
|
||||
}
|
||||
|
||||
def scheduleCheck() {
|
||||
log.debug "schedule check"
|
||||
def motionState = motion1.currentState("motion")
|
||||
if (motionState.value == "inactive") {
|
||||
def elapsed = now() - motionState.rawDateCreated.time
|
||||
def threshold = 1000 * 60 * minutes1 - 1000
|
||||
if (elapsed >= threshold) {
|
||||
log.debug "Motion has stayed inactive long enough since last check ($elapsed ms): turning lights off"
|
||||
switches.off()
|
||||
} else {
|
||||
log.debug "Motion has not stayed inactive long enough since last check ($elapsed ms): doing nothing"
|
||||
}
|
||||
} else {
|
||||
log.debug "Motion is active, do nothing and wait for inactive"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Light Up The Night
|
||||
*
|
||||
* Author: SmartThings
|
||||
*/
|
||||
definition(
|
||||
name: "Light Up the Night",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Turn your lights on when it gets dark and off when it becomes light again.",
|
||||
category: "Convenience",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/light_outlet-luminance.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/light_outlet-luminance@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("Monitor the luminosity...") {
|
||||
input "lightSensor", "capability.illuminanceMeasurement"
|
||||
}
|
||||
section("Turn on a light...") {
|
||||
input "lights", "capability.switch", multiple: true
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
subscribe(lightSensor, "illuminance", illuminanceHandler)
|
||||
}
|
||||
|
||||
def updated() {
|
||||
unsubscribe()
|
||||
subscribe(lightSensor, "illuminance", illuminanceHandler)
|
||||
}
|
||||
|
||||
// New aeon implementation
|
||||
def illuminanceHandler(evt) {
|
||||
def lastStatus = state.lastStatus
|
||||
if (lastStatus != "on" && evt.integerValue < 30) {
|
||||
lights.on()
|
||||
state.lastStatus = "on"
|
||||
}
|
||||
else if (lastStatus != "off" && evt.integerValue > 50) {
|
||||
lights.off()
|
||||
state.lastStatus = "off"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Lights Off, When Closed
|
||||
*
|
||||
* Author: SmartThings
|
||||
*/
|
||||
definition(
|
||||
name: "Lights Off, When Closed",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Turn your lights off when an open/close sensor closes.",
|
||||
category: "Convenience",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/light_contact-outlet.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/light_contact-outlet@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section ("When the door closes...") {
|
||||
input "contact1", "capability.contactSensor", title: "Where?"
|
||||
}
|
||||
section ("Turn off a light...") {
|
||||
input "switch1", "capability.switch"
|
||||
}
|
||||
}
|
||||
|
||||
def installed()
|
||||
{
|
||||
subscribe(contact1, "contact.closed", contactClosedHandler)
|
||||
}
|
||||
|
||||
def updated()
|
||||
{
|
||||
unsubscribe()
|
||||
subscribe(contact1, "contact.closed", contactClosedHandler)
|
||||
}
|
||||
|
||||
def contactClosedHandler(evt) {
|
||||
switch1.off()
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Lock It When I Leave
|
||||
*
|
||||
* Author: SmartThings
|
||||
* Date: 2013-02-11
|
||||
*/
|
||||
|
||||
definition(
|
||||
name: "Lock It When I Leave",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Locks a deadbolt or lever lock when a SmartSense Presence tag or smartphone leaves a location.",
|
||||
category: "Safety & Security",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience%402x.png",
|
||||
oauth: true
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("When I leave...") {
|
||||
input "presence1", "capability.presenceSensor", title: "Who?", multiple: true
|
||||
}
|
||||
section("Lock the lock...") {
|
||||
input "lock1","capability.lock", multiple: true
|
||||
input "unlock", "enum", title: "Unlock when presence is detected?", options: ["Yes","No"]
|
||||
input("recipients", "contact", title: "Send notifications to") {
|
||||
input "spam", "enum", title: "Send Me Notifications?", options: ["Yes", "No"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def installed()
|
||||
{
|
||||
subscribe(presence1, "presence", presence)
|
||||
}
|
||||
|
||||
def updated()
|
||||
{
|
||||
unsubscribe()
|
||||
subscribe(presence1, "presence", presence)
|
||||
}
|
||||
|
||||
def presence(evt)
|
||||
{
|
||||
if (evt.value == "present") {
|
||||
if (unlock == "Yes") {
|
||||
def anyLocked = lock1.count{it.currentLock == "unlocked"} != lock1.size()
|
||||
if (anyLocked) {
|
||||
sendMessage("Doors unlocked at arrival of $evt.linkText")
|
||||
}
|
||||
lock1.unlock()
|
||||
}
|
||||
}
|
||||
else {
|
||||
def nobodyHome = presence1.find{it.currentPresence == "present"} == null
|
||||
if (nobodyHome) {
|
||||
def anyUnlocked = lock1.count{it.currentLock == "locked"} != lock1.size()
|
||||
if (anyUnlocked) {
|
||||
sendMessage("Doors locked after everyone departed")
|
||||
}
|
||||
lock1.lock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def sendMessage(msg) {
|
||||
|
||||
if (location.contactBookEnabled) {
|
||||
sendNotificationToContacts(msg, recipients)
|
||||
}
|
||||
else {
|
||||
if (spam == "Yes") {
|
||||
sendPush msg
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,881 @@
|
||||
/**
|
||||
* Harmony (Connect) - https://developer.Harmony.com/documentation
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* Author: SmartThings
|
||||
*
|
||||
* For complete set of capabilities, attributes, and commands see:
|
||||
*
|
||||
* https://graph.api.smartthings.com/ide/doc/capabilities
|
||||
*
|
||||
* ---------------------+-------------------+-----------------------------+------------------------------------
|
||||
* Device Type | Attribute Name | Commands | Attribute Values
|
||||
* ---------------------+-------------------+-----------------------------+------------------------------------
|
||||
* switches | switch | on, off | on, off
|
||||
* motionSensors | motion | | active, inactive
|
||||
* contactSensors | contact | | open, closed
|
||||
* presenceSensors | presence | | present, 'not present'
|
||||
* temperatureSensors | temperature | | <numeric, F or C according to unit>
|
||||
* accelerationSensors | acceleration | | active, inactive
|
||||
* waterSensors | water | | wet, dry
|
||||
* lightSensors | illuminance | | <numeric, lux>
|
||||
* humiditySensors | humidity | | <numeric, percent>
|
||||
* alarms | alarm | strobe, siren, both, off | strobe, siren, both, off
|
||||
* locks | lock | lock, unlock | locked, unlocked
|
||||
* ---------------------+-------------------+-----------------------------+------------------------------------
|
||||
*/
|
||||
|
||||
definition(
|
||||
name: "Logitech Harmony (Connect)",
|
||||
namespace: "smartthings",
|
||||
author: "Juan Pablo Risso",
|
||||
description: "Allows you to integrate your Logitech Harmony account with SmartThings.",
|
||||
category: "SmartThings Labs",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/harmony.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/harmony%402x.png",
|
||||
oauth: [displayName: "Logitech Harmony", displayLink: "http://www.logitech.com/en-us/harmony-remotes"]
|
||||
){
|
||||
appSetting "clientId"
|
||||
appSetting "clientSecret"
|
||||
appSetting "callbackUrl"
|
||||
}
|
||||
|
||||
preferences(oauthPage: "deviceAuthorization") {
|
||||
page(name: "Credentials", title: "Connect to your Logitech Harmony device", content: "authPage", install: false, nextPage: "deviceAuthorization")
|
||||
page(name: "deviceAuthorization", title: "Logitech Harmony device authorization", install: true) {
|
||||
section("Allow Logitech Harmony to control these things...") {
|
||||
input "switches", "capability.switch", title: "Which Switches?", multiple: true, required: false
|
||||
input "motionSensors", "capability.motionSensor", title: "Which Motion Sensors?", multiple: true, required: false
|
||||
input "contactSensors", "capability.contactSensor", title: "Which Contact Sensors?", multiple: true, required: false
|
||||
input "presenceSensors", "capability.presenceSensor", title: "Which Presence Sensors?", multiple: true, required: false
|
||||
input "temperatureSensors", "capability.temperatureMeasurement", title: "Which Temperature Sensors?", multiple: true, required: false
|
||||
input "accelerationSensors", "capability.accelerationSensor", title: "Which Vibration Sensors?", multiple: true, required: false
|
||||
input "waterSensors", "capability.waterSensor", title: "Which Water Sensors?", multiple: true, required: false
|
||||
input "lightSensors", "capability.illuminanceMeasurement", title: "Which Light Sensors?", multiple: true, required: false
|
||||
input "humiditySensors", "capability.relativeHumidityMeasurement", title: "Which Relative Humidity Sensors?", multiple: true, required: false
|
||||
input "alarms", "capability.alarm", title: "Which Sirens?", multiple: true, required: false
|
||||
input "locks", "capability.lock", title: "Which Locks?", multiple: true, required: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mappings {
|
||||
path("/devices") { action: [ GET: "listDevices"] }
|
||||
path("/devices/:id") { action: [ GET: "getDevice", PUT: "updateDevice"] }
|
||||
path("/subscriptions") { action: [ GET: "listSubscriptions", POST: "addSubscription"] }
|
||||
path("/subscriptions/:id") { action: [ DELETE: "removeSubscription"] }
|
||||
path("/phrases") { action: [ GET: "listPhrases"] }
|
||||
path("/phrases/:id") { action: [ PUT: "executePhrase"] }
|
||||
path("/hubs") { action: [ GET: "listHubs" ] }
|
||||
path("/hubs/:id") { action: [ GET: "getHub" ] }
|
||||
path("/activityCallback/:dni") { action: [ POST: "activityCallback" ] }
|
||||
path("/harmony") { action: [ GET: "getHarmony", POST: "harmony" ] }
|
||||
path("/harmony/:mac") { action: [ DELETE: "deleteHarmony" ] }
|
||||
path("/receivedToken") { action: [ POST: "receivedToken", GET: "receivedToken"] }
|
||||
path("/receiveToken") { action: [ POST: "receiveToken", GET: "receiveToken"] }
|
||||
path("/hookCallback") { action: [ POST: "hookEventHandler", GET: "hookEventHandler"] }
|
||||
path("/oauth/callback") { action: [ GET: "callback" ] }
|
||||
path("/oauth/initialize") { action: [ GET: "init"] }
|
||||
}
|
||||
|
||||
def getServerUrl() { return "https://graph.api.smartthings.com" }
|
||||
|
||||
def authPage() {
|
||||
def description = null
|
||||
if (!state.HarmonyAccessToken) {
|
||||
if (!state.accessToken) {
|
||||
log.debug "About to create access token"
|
||||
createAccessToken()
|
||||
}
|
||||
description = "Click to enter Harmony Credentials"
|
||||
def redirectUrl = "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${state.accessToken}"
|
||||
return dynamicPage(name: "Credentials", title: "Harmony", nextPage: null, uninstall: true, install:false) {
|
||||
section { href url:redirectUrl, style:"embedded", required:true, title:"Harmony", description:description }
|
||||
}
|
||||
} else {
|
||||
//device discovery request every 5 //25 seconds
|
||||
int deviceRefreshCount = !state.deviceRefreshCount ? 0 : state.deviceRefreshCount as int
|
||||
state.deviceRefreshCount = deviceRefreshCount + 1
|
||||
def refreshInterval = 3
|
||||
|
||||
def huboptions = state.HarmonyHubs ?: []
|
||||
def actoptions = state.HarmonyActivities ?: []
|
||||
|
||||
def numFoundHub = huboptions.size() ?: 0
|
||||
def numFoundAct = actoptions.size() ?: 0
|
||||
if((deviceRefreshCount % 5) == 0) {
|
||||
discoverDevices()
|
||||
}
|
||||
return dynamicPage(name:"Credentials", title:"Discovery Started!", nextPage:"", refreshInterval:refreshInterval, install:true, uninstall: true) {
|
||||
section("Please wait while we discover your Harmony Hubs and Activities. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") {
|
||||
input "selectedhubs", "enum", required:false, title:"Select Harmony Hubs (${numFoundHub} found)", multiple:true, options:huboptions
|
||||
}
|
||||
if (numFoundHub > 0 && numFoundAct > 0 && false)
|
||||
section("You can also add activities as virtual switches for other convenient integrations") {
|
||||
input "selectedactivities", "enum", required:false, title:"Select Harmony Activities (${numFoundAct} found)", multiple:true, options:actoptions
|
||||
}
|
||||
if (state.resethub)
|
||||
section("Connection to the hub timed out. Please restart the hub and try again.") {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def callback() {
|
||||
def redirectUrl = null
|
||||
if (params.authQueryString) {
|
||||
redirectUrl = URLDecoder.decode(params.authQueryString.replaceAll(".+&redirect_url=", ""))
|
||||
log.debug "redirectUrl: ${redirectUrl}"
|
||||
} else {
|
||||
log.warn "No authQueryString"
|
||||
}
|
||||
|
||||
if (state.HarmonyAccessToken) {
|
||||
log.debug "Access token already exists"
|
||||
discovery()
|
||||
success()
|
||||
} else {
|
||||
def code = params.code
|
||||
if (code) {
|
||||
if (code.size() > 6) {
|
||||
// Harmony code
|
||||
log.debug "Exchanging code for access token"
|
||||
receiveToken(redirectUrl)
|
||||
} else {
|
||||
// Initiate the Harmony OAuth flow.
|
||||
init()
|
||||
}
|
||||
} else {
|
||||
log.debug "This code should be unreachable"
|
||||
success()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def init() {
|
||||
log.debug "Requesting Code"
|
||||
def oauthParams = [client_id: "${appSettings.clientId}", scope: "remote", response_type: "code", redirect_uri: "${appSettings.callbackUrl}" ]
|
||||
redirect(location: "https://home.myharmony.com/oauth2/authorize?${toQueryString(oauthParams)}")
|
||||
}
|
||||
|
||||
def receiveToken(redirectUrl = null) {
|
||||
log.debug "receiveToken"
|
||||
def oauthParams = [ client_id: "${appSettings.clientId}", client_secret: "${appSettings.clientSecret}", grant_type: "authorization_code", code: params.code ]
|
||||
def params = [
|
||||
uri: "https://home.myharmony.com/oauth2/token?${toQueryString(oauthParams)}",
|
||||
]
|
||||
try {
|
||||
httpPost(params) { response ->
|
||||
state.HarmonyAccessToken = response.data.access_token
|
||||
}
|
||||
} catch (java.util.concurrent.TimeoutException e) {
|
||||
fail(e)
|
||||
log.warn "Connection timed out, please try again later."
|
||||
}
|
||||
discovery()
|
||||
if (state.HarmonyAccessToken) {
|
||||
success()
|
||||
} else {
|
||||
fail("")
|
||||
}
|
||||
}
|
||||
|
||||
def success() {
|
||||
def message = """
|
||||
<p>Your Harmony Account is now connected to SmartThings!</p>
|
||||
<p>Click 'Done' to finish setup.</p>
|
||||
"""
|
||||
connectionStatus(message)
|
||||
}
|
||||
|
||||
def fail(msg) {
|
||||
def message = """
|
||||
<p>The connection could not be established!</p>
|
||||
<p>$msg</p>
|
||||
<p>Click 'Done' to return to the menu.</p>
|
||||
"""
|
||||
connectionStatus(message)
|
||||
}
|
||||
|
||||
def receivedToken() {
|
||||
def message = """
|
||||
<p>Your Harmony Account is already connected to SmartThings!</p>
|
||||
<p>Click 'Done' to finish setup.</p>
|
||||
"""
|
||||
connectionStatus(message)
|
||||
}
|
||||
|
||||
def connectionStatus(message, redirectUrl = null) {
|
||||
def redirectHtml = ""
|
||||
if (redirectUrl) {
|
||||
redirectHtml = """
|
||||
<meta http-equiv="refresh" content="3; url=${redirectUrl}" />
|
||||
"""
|
||||
}
|
||||
|
||||
def html = """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=640">
|
||||
<title>SmartThings Connection</title>
|
||||
<style type="text/css">
|
||||
@font-face {
|
||||
font-family: 'Swiss 721 W01 Thin';
|
||||
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot');
|
||||
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot?#iefix') format('embedded-opentype'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.woff') format('woff'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.ttf') format('truetype'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.svg#swis721_th_btthin') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Swiss 721 W01 Light';
|
||||
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot');
|
||||
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot?#iefix') format('embedded-opentype'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.woff') format('woff'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.ttf') format('truetype'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.svg#swis721_lt_btlight') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
.container {
|
||||
width: 560px;
|
||||
padding: 40px;
|
||||
/*background: #eee;*/
|
||||
text-align: center;
|
||||
}
|
||||
img {
|
||||
vertical-align: middle;
|
||||
}
|
||||
img:nth-child(2) {
|
||||
margin: 0 30px;
|
||||
}
|
||||
p {
|
||||
font-size: 2.2em;
|
||||
font-family: 'Swiss 721 W01 Thin';
|
||||
text-align: center;
|
||||
color: #666666;
|
||||
padding: 0 40px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
/*
|
||||
p:last-child {
|
||||
margin-top: 0px;
|
||||
}
|
||||
*/
|
||||
span {
|
||||
font-family: 'Swiss 721 W01 Light';
|
||||
}
|
||||
</style>
|
||||
${redirectHtml}
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<img src="https://s3.amazonaws.com/smartapp-icons/Partner/harmony@2x.png" alt="Harmony icon" />
|
||||
<img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/connected-device-icn%402x.png" alt="connected device icon" />
|
||||
<img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/st-logo%402x.png" alt="SmartThings logo" />
|
||||
${message}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
render contentType: 'text/html', data: html
|
||||
}
|
||||
|
||||
String toQueryString(Map m) {
|
||||
return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&")
|
||||
}
|
||||
|
||||
def buildRedirectUrl(page) {
|
||||
return "${serverUrl}/api/token/${state.accessToken}/smartapps/installations/${app.id}/${page}"
|
||||
}
|
||||
|
||||
def installed() {
|
||||
enableCallback()
|
||||
if (!state.accessToken) {
|
||||
log.debug "About to create access token"
|
||||
createAccessToken()
|
||||
} else {
|
||||
initialize()
|
||||
}
|
||||
}
|
||||
|
||||
def updated() {
|
||||
unsubscribe()
|
||||
unschedule()
|
||||
enableCallback()
|
||||
if (!state.accessToken) {
|
||||
log.debug "About to create access token"
|
||||
createAccessToken()
|
||||
} else {
|
||||
initialize()
|
||||
}
|
||||
}
|
||||
|
||||
def uninstalled() {
|
||||
if (state.HarmonyAccessToken) {
|
||||
try {
|
||||
state.HarmonyAccessToken = ""
|
||||
log.debug "Success disconnecting Harmony from SmartThings"
|
||||
} catch (groovyx.net.http.HttpResponseException e) {
|
||||
log.error "Error disconnecting Harmony from SmartThings: ${e.statusCode}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
state.aux = 0
|
||||
if (selectedhubs || selectedactivities) {
|
||||
addDevice()
|
||||
runEvery5Minutes("discovery")
|
||||
}
|
||||
}
|
||||
|
||||
def getHarmonydevices() {
|
||||
state.Harmonydevices ?: []
|
||||
}
|
||||
|
||||
Map discoverDevices() {
|
||||
log.trace "Discovering devices..."
|
||||
discovery()
|
||||
if (getHarmonydevices() != []) {
|
||||
def devices = state.Harmonydevices.hubs
|
||||
log.trace devices.toString()
|
||||
def activities = [:]
|
||||
def hubs = [:]
|
||||
devices.each {
|
||||
def hubkey = it.key
|
||||
def hubname = getHubName(it.key)
|
||||
def hubvalue = "${hubname}"
|
||||
hubs["harmony-${hubkey}"] = hubvalue
|
||||
it.value.response.data.activities.each {
|
||||
def value = "${it.value.name}"
|
||||
def key = "harmony-${hubkey}-${it.key}"
|
||||
activities["${key}"] = value
|
||||
}
|
||||
}
|
||||
state.HarmonyHubs = hubs
|
||||
state.HarmonyActivities = activities
|
||||
}
|
||||
}
|
||||
|
||||
//CHILD DEVICE METHODS
|
||||
def discovery() {
|
||||
def Params = [auth: state.HarmonyAccessToken]
|
||||
def url = "https://home.myharmony.com/cloudapi/activity/all?${toQueryString(Params)}"
|
||||
try {
|
||||
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
|
||||
if (response.status == 200) {
|
||||
log.debug "valid Token"
|
||||
state.Harmonydevices = response.data
|
||||
state.resethub = false
|
||||
getActivityList()
|
||||
poll()
|
||||
} else {
|
||||
log.debug "Error: $response.status"
|
||||
}
|
||||
}
|
||||
} catch (groovyx.net.http.HttpResponseException e) {
|
||||
if (e.statusCode == 401) { // token is expired
|
||||
state.remove("HarmonyAccessToken")
|
||||
log.warn "Harmony Access token has expired"
|
||||
}
|
||||
} catch (java.net.SocketTimeoutException e) {
|
||||
log.warn "Connection to the hub timed out. Please restart the hub and try again."
|
||||
state.resethub = true
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
def addDevice() {
|
||||
log.trace "Adding Hubs"
|
||||
selectedhubs.each { dni ->
|
||||
def d = getChildDevice(dni)
|
||||
if(!d) {
|
||||
def newAction = state.HarmonyHubs.find { it.key == dni }
|
||||
d = addChildDevice("smartthings", "Logitech Harmony Hub C2C", dni, null, [label:"${newAction.value}"])
|
||||
log.trace "created ${d.displayName} with id $dni"
|
||||
poll()
|
||||
} else {
|
||||
log.trace "found ${d.displayName} with id $dni already exists"
|
||||
}
|
||||
}
|
||||
log.trace "Adding Activities"
|
||||
selectedactivities.each { dni ->
|
||||
def d = getChildDevice(dni)
|
||||
if(!d) {
|
||||
def newAction = state.HarmonyActivities.find { it.key == dni }
|
||||
d = addChildDevice("smartthings", "Harmony Activity", dni, null, [label:"${newAction.value} [Harmony Activity]"])
|
||||
log.trace "created ${d.displayName} with id $dni"
|
||||
poll()
|
||||
} else {
|
||||
log.trace "found ${d.displayName} with id $dni already exists"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def activity(dni,mode) {
|
||||
def Params = [auth: state.HarmonyAccessToken]
|
||||
def msg = "Command failed"
|
||||
def url = ''
|
||||
if (dni == "all") {
|
||||
url = "https://home.myharmony.com/cloudapi/activity/off?${toQueryString(Params)}"
|
||||
} else {
|
||||
def aux = dni.split('-')
|
||||
def hubId = aux[1]
|
||||
if (mode == "hub" || (aux.size() <= 2) || (aux[2] == "off")){
|
||||
url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/off?${toQueryString(Params)}"
|
||||
} else {
|
||||
def activityId = aux[2]
|
||||
url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/${activityId}/${mode}?${toQueryString(Params)}"
|
||||
}
|
||||
}
|
||||
try {
|
||||
httpPostJson(uri: url) { response ->
|
||||
if (response.data.code == 200 || dni == "all") {
|
||||
msg = "Command sent succesfully"
|
||||
state.aux = 0
|
||||
} else {
|
||||
msg = "Command failed. Error: $response.data.code"
|
||||
}
|
||||
}
|
||||
} catch (groovyx.net.http.HttpResponseException ex) {
|
||||
log.error ex
|
||||
if (state.aux == 0) {
|
||||
state.aux = 1
|
||||
activity(dni,mode)
|
||||
} else {
|
||||
msg = ex
|
||||
state.aux = 0
|
||||
}
|
||||
}
|
||||
runIn(10, "poll", [overwrite: true])
|
||||
return msg
|
||||
}
|
||||
|
||||
def poll() {
|
||||
// GET THE LIST OF ACTIVITIES
|
||||
if (state.HarmonyAccessToken) {
|
||||
getActivityList()
|
||||
def Params = [auth: state.HarmonyAccessToken]
|
||||
def url = "https://home.myharmony.com/cloudapi/state?${toQueryString(Params)}"
|
||||
try {
|
||||
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
|
||||
def map = [:]
|
||||
response.data.hubs.each {
|
||||
if (it.value.message == "OK") {
|
||||
map["${it.key}"] = "${it.value.response.data.currentAvActivity},${it.value.response.data.activityStatus}"
|
||||
def hub = getChildDevice("harmony-${it.key}")
|
||||
if (hub) {
|
||||
if (it.value.response.data.currentAvActivity == "-1") {
|
||||
hub.sendEvent(name: "currentActivity", value: "--", descriptionText: "There isn't any activity running", display: false)
|
||||
} else {
|
||||
def currentActivity = getActivityName(it.value.response.data.currentAvActivity,it.key)
|
||||
hub.sendEvent(name: "currentActivity", value: currentActivity, descriptionText: "Current activity is ${currentActivity}", display: false)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.trace it.value.message
|
||||
}
|
||||
}
|
||||
def activities = getChildDevices()
|
||||
def activitynotrunning = true
|
||||
activities.each { activity ->
|
||||
def act = activity.deviceNetworkId.split('-')
|
||||
if (act.size() > 2) {
|
||||
def aux = map.find { it.key == act[1] }
|
||||
if (aux) {
|
||||
def aux2 = aux.value.split(',')
|
||||
def childDevice = getChildDevice(activity.deviceNetworkId)
|
||||
if ((act[2] == aux2[0]) && (aux2[1] == "1" || aux2[1] == "2")) {
|
||||
childDevice?.sendEvent(name: "switch", value: "on")
|
||||
if (aux2[1] == "1")
|
||||
runIn(5, "poll", [overwrite: true])
|
||||
} else {
|
||||
childDevice?.sendEvent(name: "switch", value: "off")
|
||||
if (aux2[1] == "3")
|
||||
runIn(5, "poll", [overwrite: true])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return "Poll completed $map - $state.hubs"
|
||||
}
|
||||
} catch (groovyx.net.http.HttpResponseException e) {
|
||||
if (e.statusCode == 401) { // token is expired
|
||||
state.remove("HarmonyAccessToken")
|
||||
return "Harmony Access token has expired"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def getActivityList() {
|
||||
// GET ACTIVITY'S NAME
|
||||
if (state.HarmonyAccessToken) {
|
||||
def Params = [auth: state.HarmonyAccessToken]
|
||||
def url = "https://home.myharmony.com/cloudapi/activity/all?${toQueryString(Params)}"
|
||||
try {
|
||||
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
|
||||
response.data.hubs.each {
|
||||
def hub = getChildDevice("harmony-${it.key}")
|
||||
if (hub) {
|
||||
def hubname = getHubName("${it.key}")
|
||||
def activities = []
|
||||
def aux = it.value.response.data.activities.size()
|
||||
if (aux >= 1) {
|
||||
activities = it.value.response.data.activities.collect {
|
||||
[id: it.key, name: it.value['name'], type: it.value['type']]
|
||||
}
|
||||
activities += [id: "off", name: "Activity OFF", type: "0"]
|
||||
log.trace activities
|
||||
}
|
||||
hub.sendEvent(name: "activities", value: new groovy.json.JsonBuilder(activities).toString(), descriptionText: "Activities are ${activities.collect { it.name }?.join(', ')}", display: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (groovyx.net.http.HttpResponseException e) {
|
||||
log.trace e
|
||||
} catch (java.net.SocketTimeoutException e) {
|
||||
log.trace e
|
||||
}
|
||||
}
|
||||
return activity
|
||||
}
|
||||
|
||||
def getActivityName(activity,hubId) {
|
||||
// GET ACTIVITY'S NAME
|
||||
def actname = activity
|
||||
if (state.HarmonyAccessToken) {
|
||||
def Params = [auth: state.HarmonyAccessToken]
|
||||
def url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/all?${toQueryString(Params)}"
|
||||
try {
|
||||
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
|
||||
actname = response.data.data.activities[activity].name
|
||||
}
|
||||
} catch (groovyx.net.http.HttpResponseException e) {
|
||||
log.trace e
|
||||
}
|
||||
}
|
||||
return actname
|
||||
}
|
||||
|
||||
def getActivityId(activity,hubId) {
|
||||
// GET ACTIVITY'S NAME
|
||||
def actid = activity
|
||||
if (state.HarmonyAccessToken) {
|
||||
def Params = [auth: state.HarmonyAccessToken]
|
||||
def url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/all?${toQueryString(Params)}"
|
||||
try {
|
||||
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
|
||||
response.data.data.activities.each {
|
||||
if (it.value.name == activity)
|
||||
actid = it.key
|
||||
}
|
||||
}
|
||||
} catch (groovyx.net.http.HttpResponseException e) {
|
||||
log.trace e
|
||||
}
|
||||
}
|
||||
return actid
|
||||
}
|
||||
|
||||
def getHubName(hubId) {
|
||||
// GET HUB'S NAME
|
||||
def hubname = hubId
|
||||
if (state.HarmonyAccessToken) {
|
||||
def Params = [auth: state.HarmonyAccessToken]
|
||||
def url = "https://home.myharmony.com/cloudapi/hub/${hubId}/discover?${toQueryString(Params)}"
|
||||
try {
|
||||
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
|
||||
hubname = response.data.data.name
|
||||
}
|
||||
} catch (groovyx.net.http.HttpResponseException e) {
|
||||
log.trace e
|
||||
}
|
||||
}
|
||||
return hubname
|
||||
}
|
||||
|
||||
def sendNotification(msg) {
|
||||
sendNotification(msg)
|
||||
}
|
||||
|
||||
def hookEventHandler() {
|
||||
// log.debug "In hookEventHandler method."
|
||||
log.debug "request = ${request}"
|
||||
|
||||
def json = request.JSON
|
||||
|
||||
def html = """{"code":200,"message":"OK"}"""
|
||||
render contentType: 'application/json', data: html
|
||||
}
|
||||
|
||||
def listDevices() {
|
||||
log.debug "getDevices, params: ${params}"
|
||||
allDevices.collect {
|
||||
deviceItem(it)
|
||||
}
|
||||
}
|
||||
|
||||
def getDevice() {
|
||||
log.debug "getDevice, params: ${params}"
|
||||
def device = allDevices.find { it.id == params.id }
|
||||
if (!device) {
|
||||
render status: 404, data: '{"msg": "Device not found"}'
|
||||
} else {
|
||||
deviceItem(device)
|
||||
}
|
||||
}
|
||||
|
||||
def updateDevice() {
|
||||
def data = request.JSON
|
||||
def command = data.command
|
||||
def arguments = data.arguments
|
||||
|
||||
log.debug "updateDevice, params: ${params}, request: ${data}"
|
||||
if (!command) {
|
||||
render status: 400, data: '{"msg": "command is required"}'
|
||||
} else {
|
||||
def device = allDevices.find { it.id == params.id }
|
||||
if (device) {
|
||||
if (arguments) {
|
||||
device."$command"(*arguments)
|
||||
} else {
|
||||
device."$command"()
|
||||
}
|
||||
render status: 204, data: "{}"
|
||||
} else {
|
||||
render status: 404, data: '{"msg": "Device not found"}'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def listSubscriptions() {
|
||||
log.debug "listSubscriptions()"
|
||||
app.subscriptions?.findAll { it.device?.device && it.device.id }?.collect {
|
||||
def deviceInfo = state[it.device.id]
|
||||
def response = [
|
||||
id: it.id,
|
||||
deviceId: it.device.id,
|
||||
attributeName: it.data,
|
||||
handler: it.handler
|
||||
]
|
||||
if (!state.harmonyHubs) {
|
||||
response.callbackUrl = deviceInfo?.callbackUrl
|
||||
}
|
||||
response
|
||||
} ?: []
|
||||
}
|
||||
|
||||
def addSubscription() {
|
||||
def data = request.JSON
|
||||
def attribute = data.attributeName
|
||||
def callbackUrl = data.callbackUrl
|
||||
|
||||
log.debug "addSubscription, params: ${params}, request: ${data}"
|
||||
if (!attribute) {
|
||||
render status: 400, data: '{"msg": "attributeName is required"}'
|
||||
} else {
|
||||
def device = allDevices.find { it.id == data.deviceId }
|
||||
if (device) {
|
||||
if (!state.harmonyHubs) {
|
||||
log.debug "Adding callbackUrl: $callbackUrl"
|
||||
state[device.id] = [callbackUrl: callbackUrl]
|
||||
}
|
||||
log.debug "Adding subscription"
|
||||
def subscription = subscribe(device, attribute, deviceHandler)
|
||||
if (!subscription || !subscription.eventSubscription) {
|
||||
subscription = app.subscriptions?.find { it.device?.device && it.device.id == data.deviceId && it.data == attribute && it.handler == 'deviceHandler' }
|
||||
}
|
||||
|
||||
def response = [
|
||||
id: subscription.id,
|
||||
deviceId: subscription.device.id,
|
||||
attributeName: subscription.data,
|
||||
handler: subscription.handler
|
||||
]
|
||||
if (!state.harmonyHubs) {
|
||||
response.callbackUrl = callbackUrl
|
||||
}
|
||||
response
|
||||
} else {
|
||||
render status: 400, data: '{"msg": "Device not found"}'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def removeSubscription() {
|
||||
def subscription = app.subscriptions?.find { it.id == params.id }
|
||||
def device = subscription?.device
|
||||
|
||||
log.debug "removeSubscription, params: ${params}, subscription: ${subscription}, device: ${device}"
|
||||
if (device) {
|
||||
log.debug "Removing subscription for device: ${device.id}"
|
||||
state.remove(device.id)
|
||||
unsubscribe(device)
|
||||
}
|
||||
render status: 204, data: "{}"
|
||||
}
|
||||
|
||||
def listPhrases() {
|
||||
location.helloHome.getPhrases()?.collect {[
|
||||
id: it.id,
|
||||
label: it.label
|
||||
]}
|
||||
}
|
||||
|
||||
def executePhrase() {
|
||||
log.debug "executedPhrase, params: ${params}"
|
||||
location.helloHome.execute(params.id)
|
||||
render status: 204, data: "{}"
|
||||
}
|
||||
|
||||
def deviceHandler(evt) {
|
||||
def deviceInfo = state[evt.deviceId]
|
||||
if (state.harmonyHubs) {
|
||||
state.harmonyHubs.each { harmonyHub ->
|
||||
sendToHarmony(evt, harmonyHub.callbackUrl)
|
||||
}
|
||||
} else if (deviceInfo) {
|
||||
if (deviceInfo.callbackUrl) {
|
||||
sendToHarmony(evt, deviceInfo.callbackUrl)
|
||||
} else {
|
||||
log.warn "No callbackUrl set for device: ${evt.deviceId}"
|
||||
}
|
||||
} else {
|
||||
log.warn "No subscribed device found for device: ${evt.deviceId}"
|
||||
}
|
||||
}
|
||||
|
||||
def sendToHarmony(evt, String callbackUrl) {
|
||||
def callback = new URI(callbackUrl)
|
||||
def host = callback.port != -1 ? "${callback.host}:${callback.port}" : callback.host
|
||||
def path = callback.query ? "${callback.path}?${callback.query}".toString() : callback.path
|
||||
sendHubCommand(new physicalgraph.device.HubAction(
|
||||
method: "POST",
|
||||
path: path,
|
||||
headers: [
|
||||
"Host": host,
|
||||
"Content-Type": "application/json"
|
||||
],
|
||||
body: [evt: [deviceId: evt.deviceId, name: evt.name, value: evt.value]]
|
||||
))
|
||||
}
|
||||
|
||||
def listHubs() {
|
||||
location.hubs?.findAll { it.type.toString() == "PHYSICAL" }?.collect { hubItem(it) }
|
||||
}
|
||||
|
||||
def getHub() {
|
||||
def hub = location.hubs?.findAll { it.type.toString() == "PHYSICAL" }?.find { it.id == params.id }
|
||||
if (!hub) {
|
||||
render status: 404, data: '{"msg": "Hub not found"}'
|
||||
} else {
|
||||
hubItem(hub)
|
||||
}
|
||||
}
|
||||
|
||||
def activityCallback() {
|
||||
def data = request.JSON
|
||||
def device = getChildDevice(params.dni)
|
||||
if (device) {
|
||||
if (data.errorCode == "200") {
|
||||
device.setCurrentActivity(data.currentActivityId)
|
||||
} else {
|
||||
log.warn "Activity callback error: ${data}"
|
||||
}
|
||||
} else {
|
||||
log.warn "Activity callback sent to non-existant dni: ${params.dni}"
|
||||
}
|
||||
render status: 200, data: '{"msg": "Successfully received callbackUrl"}'
|
||||
}
|
||||
|
||||
def getHarmony() {
|
||||
state.harmonyHubs ?: []
|
||||
}
|
||||
|
||||
def harmony() {
|
||||
def data = request.JSON
|
||||
if (data.mac && data.callbackUrl && data.name) {
|
||||
if (!state.harmonyHubs) { state.harmonyHubs = [] }
|
||||
def harmonyHub = state.harmonyHubs.find { it.mac == data.mac }
|
||||
if (harmonyHub) {
|
||||
harmonyHub.mac = data.mac
|
||||
harmonyHub.callbackUrl = data.callbackUrl
|
||||
harmonyHub.name = data.name
|
||||
} else {
|
||||
state.harmonyHubs << [mac: data.mac, callbackUrl: data.callbackUrl, name: data.name]
|
||||
}
|
||||
render status: 200, data: '{"msg": "Successfully received Harmony data"}'
|
||||
} else {
|
||||
if (!data.mac) {
|
||||
render status: 400, data: '{"msg": "mac is required"}'
|
||||
} else if (!data.callbackUrl) {
|
||||
render status: 400, data: '{"msg": "callbackUrl is required"}'
|
||||
} else if (!data.name) {
|
||||
render status: 400, data: '{"msg": "name is required"}'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def deleteHarmony() {
|
||||
log.debug "Trying to delete Harmony hub with mac: ${params.mac}"
|
||||
def harmonyHub = state.harmonyHubs?.find { it.mac == params.mac }
|
||||
if (harmonyHub) {
|
||||
log.debug "Deleting Harmony hub with mac: ${params.mac}"
|
||||
state.harmonyHubs.remove(harmonyHub)
|
||||
} else {
|
||||
log.debug "Couldn't find Harmony hub with mac: ${params.mac}"
|
||||
}
|
||||
render status: 204, data: "{}"
|
||||
}
|
||||
|
||||
private getAllDevices() {
|
||||
([] + switches + motionSensors + contactSensors + presenceSensors + temperatureSensors + accelerationSensors + waterSensors + lightSensors + humiditySensors + alarms + locks)?.findAll()?.unique { it.id }
|
||||
}
|
||||
|
||||
private deviceItem(device) {
|
||||
[
|
||||
id: device.id,
|
||||
label: device.displayName,
|
||||
currentStates: device.currentStates,
|
||||
capabilities: device.capabilities?.collect {[
|
||||
name: it.name
|
||||
]},
|
||||
attributes: device.supportedAttributes?.collect {[
|
||||
name: it.name,
|
||||
dataType: it.dataType,
|
||||
values: it.values
|
||||
]},
|
||||
commands: device.supportedCommands?.collect {[
|
||||
name: it.name,
|
||||
arguments: it.arguments
|
||||
]},
|
||||
type: [
|
||||
name: device.typeName,
|
||||
author: device.typeAuthor
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
private hubItem(hub) {
|
||||
[
|
||||
id: hub.id,
|
||||
name: hub.name,
|
||||
ip: hub.localIP,
|
||||
port: hub.localSrvPortTCP
|
||||
]
|
||||
}
|
||||
77
smartapps/smartthings/mail-arrived.src/mail-arrived.groovy
Normal file
77
smartapps/smartthings/mail-arrived.src/mail-arrived.groovy
Normal file
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Mail Arrived
|
||||
*
|
||||
* Author: SmartThings
|
||||
*/
|
||||
definition(
|
||||
name: "Mail Arrived",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Send a text when mail arrives in your mailbox using a SmartSense Multi on your mailbox door. Note: battery life may be impacted in cold climates.",
|
||||
category: "Convenience",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/mail_contact.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/mail_contact@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("When mail arrives...") {
|
||||
input "accelerationSensor", "capability.accelerationSensor", title: "Where?"
|
||||
}
|
||||
section("Notify me...") {
|
||||
input("recipients", "contact", title: "Send notifications to") {
|
||||
input "pushNotification", "bool", title: "Push notification", required: false, defaultValue: "true"
|
||||
input "phone1", "phone", title: "Phone number", required: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
subscribe(accelerationSensor, "acceleration.active", accelerationActiveHandler)
|
||||
}
|
||||
|
||||
def updated() {
|
||||
unsubscribe()
|
||||
subscribe(accelerationSensor, "acceleration.active", accelerationActiveHandler)
|
||||
}
|
||||
|
||||
def accelerationActiveHandler(evt) {
|
||||
log.trace "$evt.value: $evt, $settings"
|
||||
|
||||
// Don't send a continuous stream of notifications
|
||||
def deltaSeconds = 5
|
||||
def timeAgo = new Date(now() - (1000 * deltaSeconds))
|
||||
def recentEvents = accelerationSensor.eventsSince(timeAgo)
|
||||
log.trace "Found ${recentEvents?.size() ?: 0} events in the last $deltaSeconds seconds"
|
||||
def alreadySentNotifications = recentEvents.count { it.value && it.value == "active" } > 1
|
||||
|
||||
if (alreadySentNotifications) {
|
||||
log.debug "Notifications already sent within the last $deltaSeconds seconds (phone1: $phone1, pushNotification: $pushNotification)"
|
||||
}
|
||||
else {
|
||||
if (location.contactBookEnabled) {
|
||||
log.debug "$accelerationSensor has moved, notifying ${recipients?.size()}"
|
||||
sendNotificationToContacts("Mail has arrived!", recipients)
|
||||
}
|
||||
else {
|
||||
if (phone1 != null && phone1 != "") {
|
||||
log.debug "$accelerationSensor has moved, texting $phone1"
|
||||
sendSms(phone1, "Mail has arrived!")
|
||||
}
|
||||
if (pushNotification) {
|
||||
log.debug "$accelerationSensor has moved, sending push"
|
||||
sendPush("Mail has arrived!")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
137
smartapps/smartthings/make-it-so.src/make-it-so.groovy
Normal file
137
smartapps/smartthings/make-it-so.src/make-it-so.groovy
Normal file
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Make it So
|
||||
*
|
||||
* Author: SmartThings
|
||||
* Date: 2013-03-06
|
||||
*/
|
||||
definition(
|
||||
name: "Make It So",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Saves the states of a specified set switches and thermostat setpoints and restores them at each mode change. To use 1) Set the mode, 2) Change switches and setpoint to where you want them for that mode, and 3) Install or update the app. Changing to that mode or touching the app will set the devices to the saved state.",
|
||||
category: "Convenience",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/light_thermo-switch.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/light_thermo-switch@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("Switches") {
|
||||
input "switches", "capability.switch", multiple: true, required: false
|
||||
}
|
||||
section("Thermostats") {
|
||||
input "thermostats", "capability.thermostat", multiple: true, required: false
|
||||
}
|
||||
section("Locks") {
|
||||
input "locks", "capability.lock", multiple: true, required: false
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
subscribe(location, changedLocationMode)
|
||||
subscribe(app, appTouch)
|
||||
saveState()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
unsubscribe()
|
||||
subscribe(location, changedLocationMode)
|
||||
subscribe(app, appTouch)
|
||||
saveState()
|
||||
}
|
||||
|
||||
def appTouch(evt)
|
||||
{
|
||||
restoreState(currentMode)
|
||||
}
|
||||
|
||||
def changedLocationMode(evt)
|
||||
{
|
||||
restoreState(evt.value)
|
||||
}
|
||||
|
||||
private restoreState(mode)
|
||||
{
|
||||
log.info "restoring state for mode '$mode'"
|
||||
def map = state[mode] ?: [:]
|
||||
switches?.each {
|
||||
def value = map[it.id]
|
||||
if (value?.switch == "on") {
|
||||
def level = value.level
|
||||
if (level) {
|
||||
log.debug "setting $it.label level to $level"
|
||||
it.setLevel(level)
|
||||
}
|
||||
else {
|
||||
log.debug "turning $it.label on"
|
||||
it.on()
|
||||
}
|
||||
}
|
||||
else if (value?.switch == "off") {
|
||||
log.debug "turning $it.label off"
|
||||
it.off()
|
||||
}
|
||||
}
|
||||
|
||||
thermostats?.each {
|
||||
def value = map[it.id]
|
||||
if (value?.coolingSetpoint) {
|
||||
log.debug "coolingSetpoint = $value.coolingSetpoint"
|
||||
it.setCoolingSetpoint(value.coolingSetpoint)
|
||||
}
|
||||
if (value?.heatingSetpoint) {
|
||||
log.debug "heatingSetpoint = $value.heatingSetpoint"
|
||||
it.setHeatingSetpoint(value.heatingSetpoint)
|
||||
}
|
||||
}
|
||||
|
||||
locks?.each {
|
||||
def value = map[it.id]
|
||||
if (value) {
|
||||
if (value?.locked) {
|
||||
it.lock()
|
||||
}
|
||||
else {
|
||||
it.unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private saveState()
|
||||
{
|
||||
def mode = currentMode
|
||||
def map = state[mode] ?: [:]
|
||||
|
||||
switches?.each {
|
||||
map[it.id] = [switch: it.currentSwitch, level: it.currentLevel]
|
||||
}
|
||||
|
||||
thermostats?.each {
|
||||
map[it.id] = [coolingSetpoint: it.currentCoolingSetpoint, heatingSetpoint: it.currentHeatingSetpoint]
|
||||
}
|
||||
|
||||
locks?.each {
|
||||
map[it.id] = [locked: it.currentLock == "locked"]
|
||||
}
|
||||
|
||||
state[mode] = map
|
||||
log.debug "saved state for mode ${mode}: ${state[mode]}"
|
||||
log.debug "state: $state"
|
||||
}
|
||||
|
||||
private getCurrentMode()
|
||||
{
|
||||
location.mode ?: "_none_"
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Medicine Reminder
|
||||
*
|
||||
* Author: SmartThings
|
||||
*/
|
||||
|
||||
definition(
|
||||
name: "Medicine Reminder",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Set up a reminder so that if you forget to take your medicine (determined by whether a cabinet or drawer has been opened) by specified time you get a notification or text message.",
|
||||
category: "Health & Wellness",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/text_contact.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/text_contact@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("Choose your medicine cabinet..."){
|
||||
input "cabinet1", "capability.contactSensor", title: "Where?"
|
||||
}
|
||||
section("Take my medicine at..."){
|
||||
input "time1", "time", title: "Time 1"
|
||||
input "time2", "time", title: "Time 2", required: false
|
||||
input "time3", "time", title: "Time 3", required: false
|
||||
input "time4", "time", title: "Time 4", required: false
|
||||
}
|
||||
section("I forget send me a notification and/or text message..."){
|
||||
input("recipients", "contact", title: "Send notifications to") {
|
||||
input "sendPush", "enum", title: "Push Notification", required: false, options: ["Yes", "No"]
|
||||
input "phone1", "phone", title: "Phone Number", required: false
|
||||
}
|
||||
}
|
||||
section("Time window (optional, defaults to plus or minus 15 minutes") {
|
||||
input "timeWindow", "decimal", title: "Minutes", required: false
|
||||
}
|
||||
}
|
||||
|
||||
def installed()
|
||||
{
|
||||
initialize()
|
||||
}
|
||||
|
||||
def updated()
|
||||
{
|
||||
unschedule()
|
||||
initialize()
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
def window = timeWindowMsec
|
||||
[time1, time2, time3, time4].eachWithIndex {time, index ->
|
||||
if (time != null) {
|
||||
def endTime = new Date(timeToday(time, location?.timeZone).time + window)
|
||||
log.debug "Scheduling check at $endTime"
|
||||
//runDaily(endTime, "scheduleCheck${index}")
|
||||
switch (index) {
|
||||
case 0:
|
||||
schedule(endTime, scheduleCheck0)
|
||||
break
|
||||
case 1:
|
||||
schedule(endTime, scheduleCheck1)
|
||||
break
|
||||
case 2:
|
||||
schedule(endTime, scheduleCheck2)
|
||||
break
|
||||
case 3:
|
||||
schedule(endTime, scheduleCheck3)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def scheduleCheck0() { scheduleCheck() }
|
||||
def scheduleCheck1() { scheduleCheck() }
|
||||
def scheduleCheck2() { scheduleCheck() }
|
||||
def scheduleCheck3() { scheduleCheck() }
|
||||
|
||||
def scheduleCheck()
|
||||
{
|
||||
log.debug "scheduleCheck"
|
||||
def t0 = new Date(now() - (2 * timeWindowMsec))
|
||||
def t1 = new Date()
|
||||
def cabinetOpened = cabinet1.eventsBetween(t0, t1).find{it.name == "contact" && it.value == "open"}
|
||||
log.trace "Looking for events between $t0 and $t1: $cabinetOpened"
|
||||
|
||||
if (cabinetOpened) {
|
||||
log.trace "Medicine cabinet was opened since $midnight, no notification required"
|
||||
} else {
|
||||
log.trace "Medicine cabinet was not opened since $midnight, sending notification"
|
||||
sendMessage()
|
||||
}
|
||||
}
|
||||
|
||||
private sendMessage() {
|
||||
def msg = "Please remember to take your medicine"
|
||||
log.info msg
|
||||
if (location.contactBookEnabled) {
|
||||
sendNotificationToContacts(msg, recipients)
|
||||
}
|
||||
else {
|
||||
if (phone1) {
|
||||
sendSms(phone1, msg)
|
||||
}
|
||||
if (sendPush == "Yes") {
|
||||
sendPush(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def getTimeWindowMsec() {
|
||||
(timeWindow ?: 15) * 60000 as Long
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
/**
|
||||
* Mini Hue Controller
|
||||
*
|
||||
* Copyright 2014 SmartThings
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
definition(
|
||||
name: "Mini Hue Controller",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Control one or more Hue bulbs using an Aeon MiniMote.",
|
||||
category: "SmartThings Labs",
|
||||
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")
|
||||
|
||||
|
||||
preferences {
|
||||
section("Control these lights") {
|
||||
input "bulbs", "capability.colorControl", title: "Hue light bulbs", multiple: true
|
||||
}
|
||||
section("Using this controller") {
|
||||
input "controller", "capability.button", title: "Aeon minimote"
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
state.colorIndex = -1
|
||||
initialize()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
unsubscribe()
|
||||
initialize()
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
subscribe controller, "button", buttonHandler
|
||||
}
|
||||
|
||||
def buttonHandler(evt) {
|
||||
switch(evt.jsonData?.buttonNumber) {
|
||||
case 2:
|
||||
if (evt.value == "held") {
|
||||
bulbs.setLevel(100)
|
||||
}
|
||||
else {
|
||||
levelUp()
|
||||
}
|
||||
break
|
||||
|
||||
case 3:
|
||||
if (evt.value == "held") {
|
||||
def color = [name:"Soft White", hue: 23, saturation: 56]
|
||||
bulbs.setColor(hue: color.hue, saturation: color.saturation)
|
||||
}
|
||||
else {
|
||||
changeColor()
|
||||
}
|
||||
break
|
||||
|
||||
case 4:
|
||||
if (evt.value == "held") {
|
||||
bulbs.setLevel(10)
|
||||
}
|
||||
else {
|
||||
levelDown()
|
||||
}
|
||||
break
|
||||
|
||||
default:
|
||||
toggleState()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private toggleState() {
|
||||
if (currentSwitchState == "on") {
|
||||
log.debug "off"
|
||||
bulbs.off()
|
||||
}
|
||||
else {
|
||||
log.debug "on"
|
||||
bulbs.on()
|
||||
}
|
||||
}
|
||||
|
||||
private levelUp() {
|
||||
def level = Math.min(currentSwitchLevel + 10, 100)
|
||||
log.debug "level = $level"
|
||||
bulbs.setLevel(level)
|
||||
}
|
||||
|
||||
private levelDown() {
|
||||
def level = Math.max(currentSwitchLevel - 10, 10)
|
||||
log.debug "level = $level"
|
||||
bulbs.setLevel(level)
|
||||
}
|
||||
|
||||
private changeColor() {
|
||||
|
||||
final colors = [
|
||||
[name:"Soft White", hue: 23, saturation: 56],
|
||||
[name:"Daylight", hue: 53, saturation: 91],
|
||||
[name:"White", hue: 52, saturation: 19],
|
||||
[name:"Warm White", hue: 20, saturation: 80],
|
||||
[name:"Blue", hue: 70, saturation: 100],
|
||||
[name:"Green", hue: 39, saturation: 100],
|
||||
[name:"Yellow", hue: 25, saturation: 100],
|
||||
[name:"Orange", hue: 10, saturation: 100],
|
||||
[name:"Purple", hue: 75, saturation: 100],
|
||||
[name:"Pink", hue: 83, saturation: 100],
|
||||
[name:"Red", hue: 100, saturation: 100]
|
||||
]
|
||||
|
||||
final maxIndex = colors.size() - 1
|
||||
|
||||
if (state.colorIndex < maxIndex) {
|
||||
state.colorIndex = state.colorIndex + 1
|
||||
}
|
||||
else {
|
||||
state.colorIndex = 0
|
||||
}
|
||||
|
||||
def color = colors[state.colorIndex]
|
||||
bulbs.setColor(hue: color.hue, saturation: color.saturation)
|
||||
}
|
||||
|
||||
private getCurrentSwitchState() {
|
||||
def on = 0
|
||||
def off = 0
|
||||
bulbs.each {
|
||||
if (it.currentValue("switch") == "on") {
|
||||
on++
|
||||
}
|
||||
else {
|
||||
off++
|
||||
}
|
||||
}
|
||||
on > off ? "on" : "off"
|
||||
}
|
||||
|
||||
private getCurrentSwitchLevel() {
|
||||
def level = 0
|
||||
bulbs.each {
|
||||
level = Math.max(it.currentValue("level")?.toInteger() ?: 0, level)
|
||||
}
|
||||
level.toInteger()
|
||||
}
|
||||
344
smartapps/smartthings/mood-cube.src/mood-cube.groovy
Normal file
344
smartapps/smartthings/mood-cube.src/mood-cube.groovy
Normal file
@@ -0,0 +1,344 @@
|
||||
/**
|
||||
* Mood Cube
|
||||
*
|
||||
* Copyright 2014 SmartThings, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
/************
|
||||
* Metadata *
|
||||
************/
|
||||
definition(
|
||||
name: "Mood Cube",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Set your lighting by rotating a cube containing a SmartSense Multi",
|
||||
category: "SmartThings Labs",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/App-LightUpMyWorld.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/App-LightUpMyWorld@2x.png"
|
||||
)
|
||||
|
||||
/**********
|
||||
* Setup *
|
||||
**********/
|
||||
preferences {
|
||||
page(name: "mainPage", title: "", nextPage: "scenesPage", uninstall: true) {
|
||||
section("Use the orientation of this cube") {
|
||||
input "cube", "capability.threeAxis", required: false, title: "SmartSense Multi sensor"
|
||||
}
|
||||
section("To control these lights") {
|
||||
input "lights", "capability.switch", multiple: true, required: false, title: "Lights, switches & dimmers"
|
||||
}
|
||||
section([title: " ", mobileOnly:true]) {
|
||||
label title: "Assign a name", required: false
|
||||
mode title: "Set for specific mode(s)", required: false
|
||||
}
|
||||
}
|
||||
page(name: "scenesPage", title: "Scenes", install: true, uninstall: true)
|
||||
page(name: "scenePage", title: "Scene", install: false, uninstall: false, previousPage: "scenesPage")
|
||||
page(name: "devicePage", install: false, uninstall: false, previousPage: "scenePage")
|
||||
page(name: "saveStatesPage", install: false, uninstall: false, previousPage: "scenePage")
|
||||
}
|
||||
|
||||
|
||||
def scenesPage() {
|
||||
log.debug "scenesPage()"
|
||||
def sceneId = getOrientation()
|
||||
dynamicPage(name:"scenesPage") {
|
||||
section {
|
||||
for (num in 1..6) {
|
||||
href "scenePage", title: "${num}. ${sceneName(num)}${sceneId==num ? ' (current)' : ''}", params: [sceneId:num], description: "", state: sceneIsDefined(num) ? "complete" : "incomplete"
|
||||
}
|
||||
}
|
||||
section {
|
||||
href "scenesPage", title: "Refresh", description: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def scenePage(params=[:]) {
|
||||
log.debug "scenePage($params)"
|
||||
def currentSceneId = getOrientation()
|
||||
def sceneId = params.sceneId as Integer ?: state.lastDisplayedSceneId
|
||||
state.lastDisplayedSceneId = sceneId
|
||||
dynamicPage(name:"scenePage", title: "${sceneId}. ${sceneName(sceneId)}") {
|
||||
section {
|
||||
input "sceneName${sceneId}", "text", title: "Scene Name", required: false
|
||||
}
|
||||
|
||||
section {
|
||||
href "devicePage", title: "Show Device States", params: [sceneId:sceneId], description: "", state: sceneIsDefined(sceneId) ? "complete" : "incomplete"
|
||||
}
|
||||
|
||||
if (sceneId == currentSceneId) {
|
||||
section {
|
||||
href "saveStatesPage", title: "Record Current Device States", params: [sceneId:sceneId], description: ""
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
def devicePage(params) {
|
||||
log.debug "devicePage($params)"
|
||||
|
||||
getDeviceCapabilities()
|
||||
|
||||
def sceneId = params.sceneId as Integer ?: state.lastDisplayedSceneId
|
||||
|
||||
dynamicPage(name:"devicePage", title: "${sceneId}. ${sceneName(sceneId)} Device States") {
|
||||
section("Lights") {
|
||||
lights.each {light ->
|
||||
input "onoff_${sceneId}_${light.id}", "boolean", title: light.displayName
|
||||
}
|
||||
}
|
||||
|
||||
section("Dimmers") {
|
||||
lights.each {light ->
|
||||
if (state.lightCapabilities[light.id] in ["level", "color"]) {
|
||||
input "level_${sceneId}_${light.id}", "enum", title: light.displayName, options: levels, description: "", required: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section("Colors (hue/saturation)") {
|
||||
lights.each {light ->
|
||||
if (state.lightCapabilities[light.id] == "color") {
|
||||
input "color_${sceneId}_${light.id}", "text", title: light.displayName, description: "", required: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def saveStatesPage(params) {
|
||||
saveStates(params)
|
||||
devicePage(params)
|
||||
}
|
||||
|
||||
|
||||
/*************************
|
||||
* Installation & update *
|
||||
*************************/
|
||||
def installed() {
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
|
||||
initialize()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
|
||||
unsubscribe()
|
||||
initialize()
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
subscribe cube, "threeAxis", positionHandler
|
||||
}
|
||||
|
||||
|
||||
/******************
|
||||
* Event handlers *
|
||||
******************/
|
||||
def positionHandler(evt) {
|
||||
|
||||
def sceneId = getOrientation(evt.xyzValue)
|
||||
log.trace "orientation: $sceneId"
|
||||
|
||||
if (sceneId != state.lastActiveSceneId) {
|
||||
restoreStates(sceneId)
|
||||
}
|
||||
else {
|
||||
log.trace "No status change"
|
||||
}
|
||||
state.lastActiveSceneId = sceneId
|
||||
}
|
||||
|
||||
|
||||
/******************
|
||||
* Helper methods *
|
||||
******************/
|
||||
private Boolean sceneIsDefined(sceneId) {
|
||||
def tgt = "onoff_${sceneId}".toString()
|
||||
settings.find{it.key.startsWith(tgt)} != null
|
||||
}
|
||||
|
||||
private updateSetting(name, value) {
|
||||
app.updateSetting(name, value)
|
||||
settings[name] = value
|
||||
}
|
||||
|
||||
private closestLevel(level) {
|
||||
level ? "${Math.round(level/5) * 5}%" : "0%"
|
||||
}
|
||||
|
||||
private saveStates(params) {
|
||||
log.trace "saveStates($params)"
|
||||
def sceneId = params.sceneId as Integer
|
||||
getDeviceCapabilities()
|
||||
|
||||
lights.each {light ->
|
||||
def type = state.lightCapabilities[light.id]
|
||||
|
||||
updateSetting("onoff_${sceneId}_${light.id}", light.currentValue("switch") == "on")
|
||||
|
||||
if (type == "level") {
|
||||
updateSetting("level_${sceneId}_${light.id}", closestLevel(light.currentValue('level')))
|
||||
}
|
||||
else if (type == "color") {
|
||||
updateSetting("level_${sceneId}_${light.id}", closestLevel(light.currentValue('level')))
|
||||
updateSetting("color_${sceneId}_${light.id}", "${light.currentValue("hue")}/${light.currentValue("saturation")}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private restoreStates(sceneId) {
|
||||
log.trace "restoreStates($sceneId)"
|
||||
getDeviceCapabilities()
|
||||
|
||||
lights.each {light ->
|
||||
def type = state.lightCapabilities[light.id]
|
||||
|
||||
def isOn = settings."onoff_${sceneId}_${light.id}" == "true" ? true : false
|
||||
log.debug "${light.displayName} is '$isOn'"
|
||||
if (isOn) {
|
||||
light.on()
|
||||
}
|
||||
else {
|
||||
light.off()
|
||||
}
|
||||
|
||||
if (type != "switch") {
|
||||
def level = switchLevel(sceneId, light)
|
||||
|
||||
if (type == "level") {
|
||||
log.debug "${light.displayName} level is '$level'"
|
||||
if (level != null) {
|
||||
light.setLevel(value)
|
||||
}
|
||||
}
|
||||
else if (type == "color") {
|
||||
def segs = settings."color_${sceneId}_${light.id}"?.split("/")
|
||||
if (segs?.size() == 2) {
|
||||
def hue = segs[0].toInteger()
|
||||
def saturation = segs[1].toInteger()
|
||||
log.debug "${light.displayName} color is level: $level, hue: $hue, sat: $saturation"
|
||||
if (level != null) {
|
||||
light.setColor(level: level, hue: hue, saturation: saturation)
|
||||
}
|
||||
else {
|
||||
light.setColor(hue: hue, saturation: saturation)
|
||||
}
|
||||
}
|
||||
else {
|
||||
log.debug "${light.displayName} level is '$level'"
|
||||
if (level != null) {
|
||||
light.setLevel(level)
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
log.error "Unknown type '$type'"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private switchLevel(sceneId, light) {
|
||||
def percent = settings."level_${sceneId}_${light.id}"
|
||||
if (percent) {
|
||||
percent[0..-2].toInteger()
|
||||
}
|
||||
else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private getDeviceCapabilities() {
|
||||
def caps = [:]
|
||||
lights.each {
|
||||
if (it.hasCapability("Color Control")) {
|
||||
caps[it.id] = "color"
|
||||
}
|
||||
else if (it.hasCapability("Switch Level")) {
|
||||
caps[it.id] = "level"
|
||||
}
|
||||
else {
|
||||
caps[it.id] = "switch"
|
||||
}
|
||||
}
|
||||
state.lightCapabilities = caps
|
||||
}
|
||||
|
||||
private getLevels() {
|
||||
def levels = []
|
||||
for (int i = 0; i <= 100; i += 5) {
|
||||
levels << "$i%"
|
||||
}
|
||||
levels
|
||||
}
|
||||
|
||||
private getOrientation(xyz=null) {
|
||||
final threshold = 250
|
||||
|
||||
def value = xyz ?: cube.currentValue("threeAxis")
|
||||
|
||||
def x = Math.abs(value.x) > threshold ? (value.x > 0 ? 1 : -1) : 0
|
||||
def y = Math.abs(value.y) > threshold ? (value.y > 0 ? 1 : -1) : 0
|
||||
def z = Math.abs(value.z) > threshold ? (value.z > 0 ? 1 : -1) : 0
|
||||
|
||||
def orientation = 0
|
||||
if (z > 0) {
|
||||
if (x == 0 && y == 0) {
|
||||
orientation = 1
|
||||
}
|
||||
}
|
||||
else if (z < 0) {
|
||||
if (x == 0 && y == 0) {
|
||||
orientation = 2
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (x > 0) {
|
||||
if (y == 0) {
|
||||
orientation = 3
|
||||
}
|
||||
}
|
||||
else if (x < 0) {
|
||||
if (y == 0) {
|
||||
orientation = 4
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (y > 0) {
|
||||
orientation = 5
|
||||
}
|
||||
else if (y < 0) {
|
||||
orientation = 6
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
orientation
|
||||
}
|
||||
|
||||
private sceneName(num) {
|
||||
final names = ["UNDEFINED","One","Two","Three","Four","Five","Six"]
|
||||
settings."sceneName${num}" ?: "Scene ${names[num]}"
|
||||
}
|
||||
|
||||
|
||||
|
||||
143
smartapps/smartthings/nfc-tag-toggle.src/nfc-tag-toggle.groovy
Normal file
143
smartapps/smartthings/nfc-tag-toggle.src/nfc-tag-toggle.groovy
Normal file
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* NFC Tag Toggle
|
||||
*
|
||||
* Copyright 2014 SmartThings
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
definition(
|
||||
name: "NFC Tag Toggle",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Allows toggling of a switch, lock, or garage door based on an NFC Tag touch event",
|
||||
category: "SmartThings Internal",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Developers/nfc-tag-executor.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Developers/nfc-tag-executor@2x.png",
|
||||
iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Developers/nfc-tag-executor@2x.png")
|
||||
|
||||
|
||||
preferences {
|
||||
page(name: "pageOne", title: "Device selection", uninstall: true, nextPage: "pageTwo") {
|
||||
section("Select an NFC tag") {
|
||||
input "tag", "capability.touchSensor", title: "NFC Tag"
|
||||
}
|
||||
section("Select devices to control") {
|
||||
input "switch1", "capability.switch", title: "Light or switch", required: false, multiple: true
|
||||
input "lock", "capability.lock", title: "Lock", required: false, multiple: true
|
||||
input "garageDoor", "capability.doorControl", title: "Garage door controller", required: false, multiple: true
|
||||
}
|
||||
}
|
||||
|
||||
page(name: "pageTwo", title: "Master devices", install: true, uninstall: true)
|
||||
}
|
||||
|
||||
def pageTwo() {
|
||||
dynamicPage(name: "pageTwo") {
|
||||
section("If set, the state of these devices will be toggled each time the tag is touched, " +
|
||||
"e.g. a light that's on will be turned off and one that's off will be turned on, " +
|
||||
"other devices of the same type will be set to the same state as their master device. " +
|
||||
"If no master is designated then the majority of devices of the same type will be used " +
|
||||
"to determine whether to turn on or off the devices.") {
|
||||
|
||||
if (switch1 || masterSwitch) {
|
||||
input "masterSwitch", "enum", title: "Master switch", options: switch1.collect{[(it.id): it.displayName]}, required: false
|
||||
}
|
||||
if (lock || masterLock) {
|
||||
input "masterLock", "enum", title: "Master lock", options: lock.collect{[(it.id): it.displayName]}, required: false
|
||||
}
|
||||
if (garageDoor || masterDoor) {
|
||||
input "masterDoor", "enum", title: "Master door", options: garageDoor.collect{[(it.id): it.displayName]}, required: false
|
||||
}
|
||||
}
|
||||
section([mobileOnly:true]) {
|
||||
label title: "Assign a name", required: false
|
||||
mode title: "Set for specific mode(s)", required: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
|
||||
initialize()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
|
||||
unsubscribe()
|
||||
initialize()
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
subscribe tag, "nfcTouch", touchHandler
|
||||
subscribe app, touchHandler
|
||||
}
|
||||
|
||||
private currentStatus(devices, master, attribute) {
|
||||
log.trace "currentStatus($devices, $master, $attribute)"
|
||||
def result = null
|
||||
if (master) {
|
||||
result = devices.find{it.id == master}?.currentValue(attribute)
|
||||
}
|
||||
else {
|
||||
def map = [:]
|
||||
devices.each {
|
||||
def value = it.currentValue(attribute)
|
||||
map[value] = (map[value] ?: 0) + 1
|
||||
log.trace "$it.displayName: $value"
|
||||
}
|
||||
log.trace map
|
||||
result = map.collect{it}.sort{it.value}[-1].key
|
||||
}
|
||||
log.debug "$attribute = $result"
|
||||
result
|
||||
}
|
||||
|
||||
def touchHandler(evt) {
|
||||
log.trace "touchHandler($evt.descriptionText)"
|
||||
if (switch1) {
|
||||
def status = currentStatus(switch1, masterSwitch, "switch")
|
||||
switch1.each {
|
||||
if (status == "on") {
|
||||
it.off()
|
||||
}
|
||||
else {
|
||||
it.on()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (lock) {
|
||||
def status = currentStatus(lock, masterLock, "lock")
|
||||
lock.each {
|
||||
if (status == "locked") {
|
||||
lock.unlock()
|
||||
}
|
||||
else {
|
||||
lock.lock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (garageDoor) {
|
||||
def status = currentStatus(garageDoor, masterDoor, "status")
|
||||
garageDoor.each {
|
||||
if (status == "open") {
|
||||
it.close()
|
||||
}
|
||||
else {
|
||||
it.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Notify Me When It Opens
|
||||
*
|
||||
* Author: SmartThings
|
||||
*/
|
||||
definition(
|
||||
name: "Notify Me When It Opens",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Get a push message sent to your phone when an open/close sensor is opened.",
|
||||
category: "Convenience",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/window_contact.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/window_contact@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("When the door opens..."){
|
||||
input "contact1", "capability.contactSensor", title: "Where?"
|
||||
}
|
||||
}
|
||||
|
||||
def installed()
|
||||
{
|
||||
subscribe(contact1, "contact.open", contactOpenHandler)
|
||||
}
|
||||
|
||||
def updated()
|
||||
{
|
||||
unsubscribe()
|
||||
subscribe(contact1, "contact.open", contactOpenHandler)
|
||||
}
|
||||
|
||||
def contactOpenHandler(evt) {
|
||||
log.trace "$evt.value: $evt, $settings"
|
||||
|
||||
log.debug "$contact1 was opened, sending push message to user"
|
||||
sendPush("Your ${contact1.label ?: contact1.name} was opened")
|
||||
}
|
||||
151
smartapps/smartthings/notify-me-when.src/notify-me-when.groovy
Normal file
151
smartapps/smartthings/notify-me-when.src/notify-me-when.groovy
Normal file
@@ -0,0 +1,151 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Notify Me When
|
||||
*
|
||||
* Author: SmartThings
|
||||
* Date: 2013-03-20
|
||||
*
|
||||
* Change Log:
|
||||
* 1. Todd Wackford
|
||||
* 2014-10-03: Added capability.button device picker and button.pushed event subscription. For Doorbell.
|
||||
*/
|
||||
definition(
|
||||
name: "Notify Me When",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Get a push notification or text message when any of a variety of SmartThings is activated. Supports button push, motion, contact, acceleration, moisture and presence sensors as well as switches.",
|
||||
category: "Convenience",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/window_contact.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/window_contact@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("Choose one or more, when..."){
|
||||
input "button", "capability.button", title: "Button Pushed", required: false, multiple: true //tw
|
||||
input "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true
|
||||
input "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true
|
||||
input "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true
|
||||
input "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true
|
||||
input "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true
|
||||
input "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true
|
||||
input "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true
|
||||
input "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true
|
||||
input "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true
|
||||
input "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true
|
||||
}
|
||||
section("Send this message (optional, sends standard status message if not specified)"){
|
||||
input "messageText", "text", title: "Message Text", required: false
|
||||
}
|
||||
section("Via a push notification and/or an SMS message"){
|
||||
input("recipients", "contact", title: "Send notifications to") {
|
||||
input "phone", "phone", title: "Phone Number (for SMS, optional)", required: false
|
||||
input "pushAndPhone", "enum", title: "Both Push and SMS?", required: false, options: ["Yes", "No"]
|
||||
}
|
||||
}
|
||||
section("Minimum time between messages (optional, defaults to every message)") {
|
||||
input "frequency", "decimal", title: "Minutes", required: false
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
subscribeToEvents()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
unsubscribe()
|
||||
subscribeToEvents()
|
||||
}
|
||||
|
||||
def subscribeToEvents() {
|
||||
subscribe(button, "button.pushed", eventHandler) //tw
|
||||
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)
|
||||
}
|
||||
|
||||
def eventHandler(evt) {
|
||||
log.debug "Notify got evt ${evt}"
|
||||
if (frequency) {
|
||||
def lastTime = state[evt.deviceId]
|
||||
if (lastTime == null || now() - lastTime >= frequency * 60000) {
|
||||
sendMessage(evt)
|
||||
}
|
||||
}
|
||||
else {
|
||||
sendMessage(evt)
|
||||
}
|
||||
}
|
||||
|
||||
private sendMessage(evt) {
|
||||
def msg = messageText ?: defaultText(evt)
|
||||
log.debug "$evt.name:$evt.value, pushAndPhone:$pushAndPhone, '$msg'"
|
||||
|
||||
if (location.contactBookEnabled) {
|
||||
sendNotificationToContacts(msg, recipients)
|
||||
}
|
||||
else {
|
||||
|
||||
if (!phone || pushAndPhone != "No") {
|
||||
log.debug "sending push"
|
||||
sendPush(msg)
|
||||
}
|
||||
if (phone) {
|
||||
log.debug "sending SMS"
|
||||
sendSms(phone, msg)
|
||||
}
|
||||
}
|
||||
if (frequency) {
|
||||
state[evt.deviceId] = now()
|
||||
}
|
||||
}
|
||||
|
||||
private defaultText(evt) {
|
||||
if (evt.name == "presence") {
|
||||
if (evt.value == "present") {
|
||||
if (includeArticle) {
|
||||
"$evt.linkText has arrived at the $location.name"
|
||||
}
|
||||
else {
|
||||
"$evt.linkText has arrived at $location.name"
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (includeArticle) {
|
||||
"$evt.linkText has left the $location.name"
|
||||
}
|
||||
else {
|
||||
"$evt.linkText has left $location.name"
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
evt.descriptionText
|
||||
}
|
||||
}
|
||||
|
||||
private getIncludeArticle() {
|
||||
def name = location.name.toLowerCase()
|
||||
def segs = name.split(" ")
|
||||
!(["work","home"].contains(name) || (segs.size() > 1 && (["the","my","a","an"].contains(segs[0]) || segs[0].endsWith("'s"))))
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Notify Me With Hue
|
||||
*
|
||||
* Author: SmartThings
|
||||
* Date: 2014-01-20
|
||||
*/
|
||||
definition(
|
||||
name: "Notify Me With Hue",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Changes the color and brightness of Philips Hue bulbs when any of a variety of SmartThings is activated. Supports motion, contact, acceleration, moisture and presence sensors as well as switches.",
|
||||
category: "SmartThings Labs",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/hue.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/hue@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
|
||||
section("Control these bulbs...") {
|
||||
input "hues", "capability.colorControl", title: "Which Hue Bulbs?", required:true, multiple:true
|
||||
}
|
||||
|
||||
section("Choose one or more, when..."){
|
||||
input "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true
|
||||
input "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true
|
||||
input "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true
|
||||
input "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true
|
||||
input "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true
|
||||
input "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true
|
||||
input "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true
|
||||
input "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true
|
||||
input "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true
|
||||
input "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true
|
||||
input "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production
|
||||
input "triggerModes", "mode", title: "System Changes Mode", description: "Select mode(s)", required: false, multiple: true
|
||||
input "timeOfDay", "time", title: "At a Scheduled Time", required: false
|
||||
}
|
||||
|
||||
section("Choose light effects...")
|
||||
{
|
||||
input "color", "enum", title: "Hue Color?", required: false, multiple:false, options: ["Red","Green","Blue","Yellow","Orange","Purple","Pink"]
|
||||
input "lightLevel", "enum", title: "Light Level?", required: false, options: [[10:"10%"],[20:"20%"],[30:"30%"],[40:"40%"],[50:"50%"],[60:"60%"],[70:"70%"],[80:"80%"],[90:"90%"],[100:"100%"]]
|
||||
input "duration", "number", title: "Duration Seconds?", required: false
|
||||
//input "turnOn", "enum", title: "Turn On when Off?", required: false, options: ["Yes","No"]
|
||||
}
|
||||
|
||||
section("Minimum time between messages (optional, defaults to every message)") {
|
||||
input "frequency", "decimal", title: "Minutes", required: false
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
subscribeToEvents()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
unsubscribe()
|
||||
unschedule()
|
||||
subscribeToEvents()
|
||||
}
|
||||
|
||||
def 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 (frequency) {
|
||||
def lastTime = state[evt.deviceId]
|
||||
if (lastTime == null || now() - lastTime >= frequency * 60000) {
|
||||
takeAction(evt)
|
||||
}
|
||||
}
|
||||
else {
|
||||
takeAction(evt)
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
if (frequency) {
|
||||
state[evt.deviceId] = now()
|
||||
}
|
||||
|
||||
def hueColor = 0
|
||||
if(color == "Blue")
|
||||
hueColor = 70//60
|
||||
else if(color == "Green")
|
||||
hueColor = 39//30
|
||||
else if(color == "Yellow")
|
||||
hueColor = 25//16
|
||||
else if(color == "Orange")
|
||||
hueColor = 10
|
||||
else if(color == "Purple")
|
||||
hueColor = 75
|
||||
else if(color == "Pink")
|
||||
hueColor = 83
|
||||
|
||||
|
||||
state.previous = [:]
|
||||
|
||||
hues.each {
|
||||
state.previous[it.id] = [
|
||||
"switch": it.currentValue("switch"),
|
||||
"level" : it.currentValue("level"),
|
||||
"hue": it.currentValue("hue"),
|
||||
"saturation": it.currentValue("saturation"),
|
||||
"color": it.currentValue("color")
|
||||
]
|
||||
}
|
||||
|
||||
log.debug "current values = $state.previous"
|
||||
|
||||
def newValue = [hue: hueColor, saturation: 100, level: (lightLevel as Integer) ?: 100]
|
||||
log.debug "new value = $newValue"
|
||||
|
||||
hues*.setColor(newValue)
|
||||
setTimer()
|
||||
}
|
||||
|
||||
def setTimer()
|
||||
{
|
||||
if(!duration) //default to 10 seconds
|
||||
{
|
||||
log.debug "pause 10"
|
||||
pause(10 * 1000)
|
||||
log.debug "reset hue"
|
||||
resetHue()
|
||||
}
|
||||
else if(duration < 10)
|
||||
{
|
||||
log.debug "pause $duration"
|
||||
pause(duration * 1000)
|
||||
log.debug "resetHue"
|
||||
resetHue()
|
||||
}
|
||||
else
|
||||
{
|
||||
log.debug "runIn $duration, resetHue"
|
||||
runIn(duration,"resetHue", [overwrite: false])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def resetHue()
|
||||
{
|
||||
hues.each {
|
||||
it.setColor(state.previous[it.id])
|
||||
}
|
||||
}
|
||||
64
smartapps/smartthings/once-a-day.src/once-a-day.groovy
Normal file
64
smartapps/smartthings/once-a-day.src/once-a-day.groovy
Normal file
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Once a Day
|
||||
*
|
||||
* Author: SmartThings
|
||||
*
|
||||
* Turn on one or more switches at a specified time and turn them off at a later time.
|
||||
*/
|
||||
|
||||
definition(
|
||||
name: "Once a Day",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Turn on one or more switches at a specified time and turn them off at a later time.",
|
||||
category: "Convenience",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/light_outlet.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/light_outlet@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("Select switches to control...") {
|
||||
input name: "switches", type: "capability.switch", multiple: true
|
||||
}
|
||||
section("Turn them all on at...") {
|
||||
input name: "startTime", title: "Turn On Time?", type: "time"
|
||||
}
|
||||
section("And turn them off at...") {
|
||||
input name: "stopTime", title: "Turn Off Time?", type: "time"
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
schedule(startTime, "startTimerCallback")
|
||||
schedule(stopTime, "stopTimerCallback")
|
||||
|
||||
}
|
||||
|
||||
def updated(settings) {
|
||||
unschedule()
|
||||
schedule(startTime, "startTimerCallback")
|
||||
schedule(stopTime, "stopTimerCallback")
|
||||
}
|
||||
|
||||
def startTimerCallback() {
|
||||
log.debug "Turning on switches"
|
||||
switches.on()
|
||||
|
||||
}
|
||||
|
||||
def stopTimerCallback() {
|
||||
log.debug "Turning off switches"
|
||||
switches.off()
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Photo Burst When...
|
||||
*
|
||||
* Author: SmartThings
|
||||
*
|
||||
* Date: 2013-09-30
|
||||
*/
|
||||
|
||||
definition(
|
||||
name: "Photo Burst When...",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Take a burst of photos and send a push notification when...",
|
||||
category: "SmartThings Labs",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/photo-burst-when.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/photo-burst-when@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("Choose one or more, when..."){
|
||||
input "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true
|
||||
input "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true
|
||||
input "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true
|
||||
input "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true
|
||||
input "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true
|
||||
input "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true
|
||||
}
|
||||
section("Take a burst of pictures") {
|
||||
input "camera", "capability.imageCapture"
|
||||
input "burstCount", "number", title: "How many? (default 5)", defaultValue:5
|
||||
}
|
||||
section("Then send this message in a push notification"){
|
||||
input "messageText", "text", title: "Message Text"
|
||||
}
|
||||
section("And as text message to this number (optional)"){
|
||||
input("recipients", "contact", title: "Send notifications to") {
|
||||
input "phone", "phone", title: "Phone Number", required: false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def installed() {
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
subscribeToEvents()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
unsubscribe()
|
||||
subscribeToEvents()
|
||||
}
|
||||
|
||||
def subscribeToEvents() {
|
||||
subscribe(contact, "contact.open", sendMessage)
|
||||
subscribe(acceleration, "acceleration.active", sendMessage)
|
||||
subscribe(motion, "motion.active", sendMessage)
|
||||
subscribe(mySwitch, "switch.on", sendMessage)
|
||||
subscribe(arrivalPresence, "presence.present", sendMessage)
|
||||
subscribe(departurePresence, "presence.not present", sendMessage)
|
||||
}
|
||||
|
||||
def sendMessage(evt) {
|
||||
log.debug "$evt.name: $evt.value, $messageText"
|
||||
|
||||
camera.take()
|
||||
(1..((burstCount ?: 5) - 1)).each {
|
||||
camera.take(delay: (500 * it))
|
||||
}
|
||||
|
||||
if (location.contactBookEnabled) {
|
||||
sendNotificationToContacts(messageText, recipients)
|
||||
}
|
||||
else {
|
||||
sendPush(messageText)
|
||||
if (phone) {
|
||||
sendSms(phone, messageText)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Power Allowance
|
||||
*
|
||||
* Author: SmartThings
|
||||
*/
|
||||
definition(
|
||||
name: "Power Allowance",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Save energy or restrict total time an appliance (like a curling iron or TV) can be in use. When a switch turns on, automatically turn it back off after a set number of minutes you specify.",
|
||||
category: "Green Living",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/light_outlet.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/light_outlet@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("When a switch turns on...") {
|
||||
input "theSwitch", "capability.switch"
|
||||
}
|
||||
section("Turn it off how many minutes later?") {
|
||||
input "minutesLater", "number", title: "When?"
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
subscribe(theSwitch, "switch.on", switchOnHandler, [filterEvents: false])
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
|
||||
unsubscribe()
|
||||
subscribe(theSwitch, "switch.on", switchOnHandler, [filterEvents: false])
|
||||
}
|
||||
|
||||
def switchOnHandler(evt) {
|
||||
log.debug "Switch ${theSwitch} turned: ${evt.value}"
|
||||
def delay = minutesLater * 60
|
||||
log.debug "Turning off in ${minutesLater} minutes (${delay}seconds)"
|
||||
runIn(delay, turnOffSwitch)
|
||||
}
|
||||
|
||||
def turnOffSwitch() {
|
||||
theSwitch.off()
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Presence Change Push
|
||||
*
|
||||
* Author: SmartThings
|
||||
*/
|
||||
definition(
|
||||
name: "Presence Change Push",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Get a push notification when a SmartSense Presence tag or smartphone arrives at or departs from a location.",
|
||||
category: "Safety & Security",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/text_presence.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/text_presence@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("When a presence sensor arrives or departs this location..") {
|
||||
input "presence", "capability.presenceSensor", title: "Which sensor?"
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
subscribe(presence, "presence", presenceHandler)
|
||||
}
|
||||
|
||||
def updated() {
|
||||
unsubscribe()
|
||||
subscribe(presence, "presence", presenceHandler)
|
||||
}
|
||||
|
||||
def presenceHandler(evt) {
|
||||
if (evt.value == "present") {
|
||||
log.debug "${presence.label ?: presence.name} has arrived at the ${location}"
|
||||
sendPush("${presence.label ?: presence.name} has arrived at the ${location}")
|
||||
} else if (evt.value == "not present") {
|
||||
log.debug "${presence.label ?: presence.name} has left the ${location}"
|
||||
sendPush("${presence.label ?: presence.name} has left the ${location}")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Presence Change Text
|
||||
*
|
||||
* Author: SmartThings
|
||||
*/
|
||||
definition(
|
||||
name: "Presence Change Text",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Send me a text message when my presence status changes.",
|
||||
category: "Safety & Security",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/text_presence.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/text_presence@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("When a presence sensor arrives or departs this location..") {
|
||||
input "presence", "capability.presenceSensor", title: "Which sensor?"
|
||||
}
|
||||
section("Send a text message to...") {
|
||||
input("recipients", "contact", title: "Send notifications to") {
|
||||
input "phone1", "phone", title: "Phone number?"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def installed() {
|
||||
subscribe(presence, "presence", presenceHandler)
|
||||
}
|
||||
|
||||
def updated() {
|
||||
unsubscribe()
|
||||
subscribe(presence, "presence", presenceHandler)
|
||||
}
|
||||
|
||||
def presenceHandler(evt) {
|
||||
if (evt.value == "present") {
|
||||
log.debug "${presence.label ?: presence.name} has arrived at the ${location}"
|
||||
|
||||
if (location.contactBookEnabled) {
|
||||
sendNotificationToContacts("${presence.label ?: presence.name} has arrived at the ${location}", recipients)
|
||||
}
|
||||
else {
|
||||
sendSms(phone1, "${presence.label ?: presence.name} has arrived at the ${location}")
|
||||
}
|
||||
} else if (evt.value == "not present") {
|
||||
log.debug "${presence.label ?: presence.name} has left the ${location}"
|
||||
|
||||
if (location.contactBookEnabled) {
|
||||
sendNotificationToContacts("${presence.label ?: presence.name} has left the ${location}", recipients)
|
||||
}
|
||||
else {
|
||||
sendSms(phone1, "${presence.label ?: presence.name} has left the ${location}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Ridiculously Automated Garage Door
|
||||
*
|
||||
* Author: SmartThings
|
||||
* Date: 2013-03-10
|
||||
*
|
||||
* Monitors arrival and departure of car(s) and
|
||||
*
|
||||
* 1) opens door when car arrives,
|
||||
* 2) closes door after car has departed (for N minutes),
|
||||
* 3) opens door when car door motion is detected,
|
||||
* 4) closes door when door was opened due to arrival and interior door is closed.
|
||||
*/
|
||||
|
||||
definition(
|
||||
name: "Ridiculously Automated Garage Door",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Monitors arrival and departure of car(s) and 1) opens door when car arrives, 2) closes door after car has departed (for N minutes), 3) opens door when car door motion is detected, 4) closes door when door was opened due to arrival and interior door is closed.",
|
||||
category: "Convenience",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/garage_contact.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/garage_contact@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
|
||||
section("Garage door") {
|
||||
input "doorSensor", "capability.contactSensor", title: "Which sensor?"
|
||||
input "doorSwitch", "capability.momentary", title: "Which switch?"
|
||||
input "openThreshold", "number", title: "Warn when open longer than (optional)",description: "Number of minutes", required: false
|
||||
input("recipients", "contact", title: "Send notifications to") {
|
||||
input "phone", "phone", title: "Warn with text message (optional)", description: "Phone Number", required: false
|
||||
}
|
||||
}
|
||||
section("Car(s) using this garage door") {
|
||||
input "cars", "capability.presenceSensor", title: "Presence sensor", description: "Which car(s)?", multiple: true, required: false
|
||||
input "carDoorSensors", "capability.accelerationSensor", title: "Car door sensor(s)", description: "Which car(s)?", multiple: true, required: false
|
||||
}
|
||||
section("Interior door (optional)") {
|
||||
input "interiorDoorSensor", "capability.contactSensor", title: "Contact sensor?", required: false
|
||||
}
|
||||
section("False alarm threshold (defaults to 10 min)") {
|
||||
input "falseAlarmThreshold", "number", title: "Number of minutes", required: false
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
log.trace "installed()"
|
||||
subscribe()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.trace "updated()"
|
||||
unsubscribe()
|
||||
subscribe()
|
||||
}
|
||||
|
||||
def subscribe() {
|
||||
log.debug "present: ${cars.collect{it.displayName + ': ' + it.currentPresence}}"
|
||||
subscribe(doorSensor, "contact", garageDoorContact)
|
||||
|
||||
subscribe(cars, "presence", carPresence)
|
||||
subscribe(carDoorSensors, "acceleration", accelerationActive)
|
||||
|
||||
if (interiorDoorSensor) {
|
||||
subscribe(interiorDoorSensor, "contact.closed", interiorDoorClosed)
|
||||
}
|
||||
}
|
||||
|
||||
def doorOpenCheck()
|
||||
{
|
||||
final thresholdMinutes = openThreshold
|
||||
if (thresholdMinutes) {
|
||||
def currentState = doorSensor.contactState
|
||||
log.debug "doorOpenCheck"
|
||||
if (currentState?.value == "open") {
|
||||
log.debug "open for ${now() - currentState.date.time}, openDoorNotificationSent: ${state.openDoorNotificationSent}"
|
||||
if (!state.openDoorNotificationSent && now() - currentState.date.time > thresholdMinutes * 60 *1000) {
|
||||
def msg = "${doorSwitch.displayName} was been open for ${thresholdMinutes} minutes"
|
||||
log.info msg
|
||||
|
||||
if (location.contactBookEnabled) {
|
||||
sendNotificationToContacts(msg, recipients)
|
||||
}
|
||||
else {
|
||||
sendPush msg
|
||||
if (phone) {
|
||||
sendSms phone, msg
|
||||
}
|
||||
}
|
||||
state.openDoorNotificationSent = true
|
||||
}
|
||||
}
|
||||
else {
|
||||
state.openDoorNotificationSent = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def carPresence(evt)
|
||||
{
|
||||
log.info "$evt.name: $evt.value"
|
||||
// time in which there must be no "not present" events in order to open the door
|
||||
final openDoorAwayInterval = falseAlarmThreshold ? falseAlarmThreshold * 60 : 600
|
||||
|
||||
if (evt.value == "present") {
|
||||
// A car comes home
|
||||
|
||||
def car = getCar(evt)
|
||||
def t0 = new Date(now() - (openDoorAwayInterval * 1000))
|
||||
def states = car.statesSince("presence", t0)
|
||||
def recentNotPresentState = states.find{it.value == "not present"}
|
||||
|
||||
if (recentNotPresentState) {
|
||||
log.debug "Not opening ${doorSwitch.displayName} since car was not present at ${recentNotPresentState.date}, less than ${openDoorAwayInterval} sec ago"
|
||||
}
|
||||
else {
|
||||
if (doorSensor.currentContact == "closed") {
|
||||
openDoor()
|
||||
sendPush "Opening garage door due to arrival of ${car.displayName}"
|
||||
state.appOpenedDoor = now()
|
||||
}
|
||||
else {
|
||||
log.debug "door already open"
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
// A car departs
|
||||
if (doorSensor.currentContact == "open") {
|
||||
closeDoor()
|
||||
log.debug "Closing ${doorSwitch.displayName} after departure"
|
||||
sendPush("Closing ${doorSwitch.displayName} after departure")
|
||||
|
||||
}
|
||||
else {
|
||||
log.debug "Not closing ${doorSwitch.displayName} because its already closed"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def garageDoorContact(evt)
|
||||
{
|
||||
log.info "garageDoorContact, $evt.name: $evt.value"
|
||||
if (evt.value == "open") {
|
||||
schedule("0 * * * * ?", "doorOpenCheck")
|
||||
}
|
||||
else {
|
||||
unschedule("doorOpenCheck")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def interiorDoorClosed(evt)
|
||||
{
|
||||
log.info "interiorContact, $evt.name: $evt.value"
|
||||
|
||||
// time during which closing the interior door will shut the garage door, if the app opened it
|
||||
final threshold = 15 * 60 * 1000
|
||||
if (state.appOpenedDoor && now() - state.appOpenedDoor < threshold) {
|
||||
state.appOpenedDoor = 0
|
||||
closeDoor()
|
||||
}
|
||||
else {
|
||||
log.debug "app didn't open door"
|
||||
}
|
||||
}
|
||||
|
||||
def accelerationActive(evt)
|
||||
{
|
||||
log.info "$evt.name: $evt.value"
|
||||
|
||||
if (doorSensor.currentContact == "closed") {
|
||||
log.debug "opening door when car door opened"
|
||||
openDoor()
|
||||
}
|
||||
}
|
||||
|
||||
private openDoor()
|
||||
{
|
||||
if (doorSensor.currentContact == "closed") {
|
||||
log.debug "opening door"
|
||||
doorSwitch.push()
|
||||
}
|
||||
}
|
||||
|
||||
private closeDoor()
|
||||
{
|
||||
if (doorSensor.currentContact == "open") {
|
||||
log.debug "closing door"
|
||||
doorSwitch.push()
|
||||
}
|
||||
}
|
||||
|
||||
private getCar(evt)
|
||||
{
|
||||
cars.find{it.id == evt.deviceId}
|
||||
}
|
||||
129
smartapps/smartthings/rise-and-shine.src/rise-and-shine.groovy
Normal file
129
smartapps/smartthings/rise-and-shine.src/rise-and-shine.groovy
Normal file
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* Copyright 2015 SmartThings
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
* Rise and Shine
|
||||
*
|
||||
* Author: SmartThings
|
||||
* Date: 2013-03-07
|
||||
*/
|
||||
|
||||
definition(
|
||||
name: "Rise and Shine",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Changes mode when someone wakes up after a set time in the morning.",
|
||||
category: "Mode Magic",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/ModeMagic/rise-and-shine.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/ModeMagic/rise-and-shine@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("When there's motion on any of these sensors") {
|
||||
input "motionSensors", "capability.motionSensor", multiple: true
|
||||
}
|
||||
section("During this time window (default End Time is 4:00 PM)") {
|
||||
input "timeOfDay", "time", title: "Start Time?"
|
||||
input "endTime", "time", title: "End Time?", required: false
|
||||
}
|
||||
section("Change to this mode") {
|
||||
input "newMode", "mode", title: "Mode?"
|
||||
}
|
||||
section("And (optionally) turn on these appliances") {
|
||||
input "switches", "capability.switch", multiple: true, required: false
|
||||
}
|
||||
section( "Notifications" ) {
|
||||
input("recipients", "contact", title: "Send notifications to") {
|
||||
input "sendPushMessage", "enum", title: "Send a push notification?", options: ["Yes", "No"], required: false
|
||||
input "phoneNumber", "phone", title: "Send a Text Message?", required: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
log.debug "installed, current mode = ${location.mode}, state.actionTakenOn = ${state.actionTakenOn}"
|
||||
initialize()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "updated, current mode = ${location.mode}, state.actionTakenOn = ${state.actionTakenOn}"
|
||||
unsubscribe()
|
||||
initialize()
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
log.trace "timeOfDay: $timeOfDay, endTime: $endTime"
|
||||
subscribe(motionSensors, "motion.active", motionActiveHandler)
|
||||
subscribe(location, modeChangeHandler)
|
||||
if (state.modeStartTime == null) {
|
||||
state.modeStartTime = 0
|
||||
}
|
||||
}
|
||||
|
||||
def modeChangeHandler(evt) {
|
||||
state.modeStartTime = now()
|
||||
}
|
||||
|
||||
def motionActiveHandler(evt)
|
||||
{
|
||||
// for backward compatibility
|
||||
if (state.modeStartTime == null) {
|
||||
subscribe(location, modeChangeHandler)
|
||||
state.modeStartTime = 0
|
||||
}
|
||||
|
||||
def t0 = now()
|
||||
def modeStartTime = new Date(state.modeStartTime)
|
||||
def timeZone = location.timeZone ?: timeZone(timeOfDay)
|
||||
def startTime = timeTodayAfter(modeStartTime, timeOfDay, timeZone)
|
||||
def endTime = timeTodayAfter(startTime, endTime ?: "16:00", timeZone)
|
||||
log.debug "startTime: $startTime, endTime: $endTime, t0: ${new Date(t0)}, modeStartTime: ${modeStartTime}, actionTakenOn: $state.actionTakenOn, currentMode: $location.mode, newMode: $newMode "
|
||||
|
||||
if (t0 >= startTime.time && t0 <= endTime.time && location.mode != newMode) {
|
||||
def message = "Good morning! SmartThings changed the mode to '$newMode'"
|
||||
send(message)
|
||||
setLocationMode(newMode)
|
||||
log.debug message
|
||||
|
||||
def dateString = new Date().format("yyyy-MM-dd")
|
||||
log.debug "last turned on switches on ${state.actionTakenOn}, today is ${dateString}"
|
||||
if (state.actionTakenOn != dateString) {
|
||||
log.debug "turning on switches"
|
||||
state.actionTakenOn = dateString
|
||||
switches?.on()
|
||||
}
|
||||
|
||||
}
|
||||
else {
|
||||
log.debug "not in time window, or mode is already set, currentMode = ${location.mode}, newMode = $newMode"
|
||||
}
|
||||
}
|
||||
|
||||
private send(msg) {
|
||||
|
||||
if (location.contactBookEnabled) {
|
||||
log.debug("sending notifications to: ${recipients?.size()}")
|
||||
sendNotificationToContacts(msg, recipients)
|
||||
}
|
||||
else {
|
||||
if (sendPushMessage != "No") {
|
||||
log.debug("sending push message")
|
||||
sendPush(msg)
|
||||
}
|
||||
|
||||
if (phoneNumber) {
|
||||
log.debug("sending text message")
|
||||
sendSms(phoneNumber, msg)
|
||||
}
|
||||
}
|
||||
|
||||
log.debug msg
|
||||
}
|
||||
@@ -0,0 +1,420 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Samsung TV Service Manager
|
||||
*
|
||||
* Author: SmartThings (Juan Risso)
|
||||
*/
|
||||
|
||||
definition(
|
||||
name: "Samsung TV (Connect)",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Allows you to control your Samsung TV from the SmartThings app. Perform basic functions like power Off, source, volume, channels and other remote control functions.",
|
||||
category: "SmartThings Labs",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Samsung/samsung-remote%402x.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Samsung/samsung-remote%403x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
page(name:"samsungDiscovery", title:"Samsung TV Setup", content:"samsungDiscovery", refreshTimeout:5)
|
||||
}
|
||||
|
||||
def getDeviceType() {
|
||||
return "urn:samsung.com:device:RemoteControlReceiver:1"
|
||||
}
|
||||
|
||||
//PAGES
|
||||
def samsungDiscovery()
|
||||
{
|
||||
if(canInstallLabs())
|
||||
{
|
||||
int samsungRefreshCount = !state.samsungRefreshCount ? 0 : state.samsungRefreshCount as int
|
||||
state.samsungRefreshCount = samsungRefreshCount + 1
|
||||
def refreshInterval = 3
|
||||
|
||||
def options = samsungesDiscovered() ?: []
|
||||
|
||||
def numFound = options.size() ?: 0
|
||||
|
||||
if(!state.subscribe) {
|
||||
log.trace "subscribe to location"
|
||||
subscribe(location, null, locationHandler, [filterEvents:false])
|
||||
state.subscribe = true
|
||||
}
|
||||
|
||||
//samsung discovery request every 5 //25 seconds
|
||||
if((samsungRefreshCount % 5) == 0) {
|
||||
log.trace "Discovering..."
|
||||
discoversamsunges()
|
||||
}
|
||||
|
||||
//setup.xml request every 3 seconds except on discoveries
|
||||
if(((samsungRefreshCount % 1) == 0) && ((samsungRefreshCount % 8) != 0)) {
|
||||
log.trace "Verifing..."
|
||||
verifysamsungPlayer()
|
||||
}
|
||||
|
||||
return dynamicPage(name:"samsungDiscovery", title:"Discovery Started!", nextPage:"", refreshInterval:refreshInterval, install:true, uninstall: true) {
|
||||
section("Please wait while we discover your Samsung TV. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") {
|
||||
input "selectedsamsung", "enum", required:false, title:"Select Samsung TV (${numFound} found)", multiple:true, options:options
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
def upgradeNeeded = """To use SmartThings Labs, your Hub should be completely up to date.
|
||||
|
||||
To update your Hub, access Location Settings in the Main Menu (tap the gear next to your location name), select your Hub, and choose "Update Hub"."""
|
||||
|
||||
return dynamicPage(name:"samsungDiscovery", title:"Upgrade needed!", nextPage:"", install:true, uninstall: true) {
|
||||
section("Upgrade") {
|
||||
paragraph "$upgradeNeeded"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
log.trace "Installed with settings: ${settings}"
|
||||
initialize()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.trace "Updated with settings: ${settings}"
|
||||
unschedule()
|
||||
initialize()
|
||||
}
|
||||
|
||||
def uninstalled() {
|
||||
def devices = getChildDevices()
|
||||
log.trace "deleting ${devices.size()} samsung"
|
||||
devices.each {
|
||||
deleteChildDevice(it.deviceNetworkId)
|
||||
}
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
// remove location subscription afterwards
|
||||
if (selectedsamsung) {
|
||||
addsamsung()
|
||||
}
|
||||
//Check every 5 minutes for IP change
|
||||
runEvery5Minutes("discoversamsunges")
|
||||
}
|
||||
|
||||
//CHILD DEVICE METHODS
|
||||
def addsamsung() {
|
||||
def players = getVerifiedsamsungPlayer()
|
||||
log.trace "Adding childs"
|
||||
selectedsamsung.each { dni ->
|
||||
def d = getChildDevice(dni)
|
||||
if(!d) {
|
||||
def newPlayer = players.find { (it.value.ip + ":" + it.value.port) == dni }
|
||||
log.trace "newPlayer = $newPlayer"
|
||||
log.trace "dni = $dni"
|
||||
d = addChildDevice("smartthings", "Samsung Smart TV", dni, newPlayer?.value.hub, [label:"${newPlayer?.value.name}"])
|
||||
log.trace "created ${d.displayName} with id $dni"
|
||||
|
||||
d.setModel(newPlayer?.value.model)
|
||||
log.trace "setModel to ${newPlayer?.value.model}"
|
||||
} else {
|
||||
log.trace "found ${d.displayName} with id $dni already exists"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private tvAction(key,deviceNetworkId) {
|
||||
log.debug "Executing ${tvCommand}"
|
||||
|
||||
def tvs = getVerifiedsamsungPlayer()
|
||||
def thetv = tvs.find { (it.value.ip + ":" + it.value.port) == deviceNetworkId }
|
||||
|
||||
// Standard Connection Data
|
||||
def appString = "iphone..iapp.samsung"
|
||||
def appStringLength = appString.getBytes().size()
|
||||
|
||||
def tvAppString = "iphone.UN60ES8000.iapp.samsung"
|
||||
def tvAppStringLength = tvAppString.getBytes().size()
|
||||
|
||||
def remoteName = "SmartThings".encodeAsBase64().toString()
|
||||
def remoteNameLength = remoteName.getBytes().size()
|
||||
|
||||
// Device Connection Data
|
||||
def ipAddress = convertHexToIP(thetv?.value.ip).encodeAsBase64().toString()
|
||||
def ipAddressHex = deviceNetworkId.substring(0,8)
|
||||
def ipAddressLength = ipAddress.getBytes().size()
|
||||
|
||||
def macAddress = thetv?.value.mac.encodeAsBase64().toString()
|
||||
def macAddressLength = macAddress.getBytes().size()
|
||||
|
||||
// The Authentication Message
|
||||
def authenticationMessage = "${(char)0x64}${(char)0x00}${(char)ipAddressLength}${(char)0x00}${ipAddress}${(char)macAddressLength}${(char)0x00}${macAddress}${(char)remoteNameLength}${(char)0x00}${remoteName}"
|
||||
def authenticationMessageLength = authenticationMessage.getBytes().size()
|
||||
|
||||
def authenticationPacket = "${(char)0x00}${(char)appStringLength}${(char)0x00}${appString}${(char)authenticationMessageLength}${(char)0x00}${authenticationMessage}"
|
||||
|
||||
// If our initial run, just send the authentication packet so the prompt appears on screen
|
||||
if (key == "AUTHENTICATE") {
|
||||
sendHubCommand(new physicalgraph.device.HubAction(authenticationPacket, physicalgraph.device.Protocol.LAN, "${ipAddressHex}:D6D8"))
|
||||
} else {
|
||||
// Build the command we will send to the Samsung TV
|
||||
def command = "KEY_${key}".encodeAsBase64().toString()
|
||||
def commandLength = command.getBytes().size()
|
||||
|
||||
def actionMessage = "${(char)0x00}${(char)0x00}${(char)0x00}${(char)commandLength}${(char)0x00}${command}"
|
||||
def actionMessageLength = actionMessage.getBytes().size()
|
||||
|
||||
def actionPacket = "${(char)0x00}${(char)tvAppStringLength}${(char)0x00}${tvAppString}${(char)actionMessageLength}${(char)0x00}${actionMessage}"
|
||||
|
||||
// Send both the authentication and action at the same time
|
||||
sendHubCommand(new physicalgraph.device.HubAction(authenticationPacket + actionPacket, physicalgraph.device.Protocol.LAN, "${ipAddressHex}:D6D8"))
|
||||
}
|
||||
}
|
||||
|
||||
private discoversamsunges()
|
||||
{
|
||||
sendHubCommand(new physicalgraph.device.HubAction("lan discovery ${getDeviceType()}", physicalgraph.device.Protocol.LAN))
|
||||
}
|
||||
|
||||
|
||||
private verifysamsungPlayer() {
|
||||
def devices = getsamsungPlayer().findAll { it?.value?.verified != true }
|
||||
|
||||
if(devices) {
|
||||
log.warn "UNVERIFIED PLAYERS!: $devices"
|
||||
}
|
||||
|
||||
devices.each {
|
||||
verifysamsung((it?.value?.ip + ":" + it?.value?.port), it?.value?.ssdpPath)
|
||||
}
|
||||
}
|
||||
|
||||
private verifysamsung(String deviceNetworkId, String devicessdpPath) {
|
||||
log.trace "dni: $deviceNetworkId, ssdpPath: $devicessdpPath"
|
||||
String ip = getHostAddress(deviceNetworkId)
|
||||
log.trace "ip:" + ip
|
||||
sendHubCommand(new physicalgraph.device.HubAction("""GET ${devicessdpPath} HTTP/1.1\r\nHOST: $ip\r\n\r\n""", physicalgraph.device.Protocol.LAN, "${deviceNetworkId}"))
|
||||
}
|
||||
|
||||
Map samsungesDiscovered() {
|
||||
def vsamsunges = getVerifiedsamsungPlayer()
|
||||
def map = [:]
|
||||
vsamsunges.each {
|
||||
def value = "${it.value.name}"
|
||||
def key = it.value.ip + ":" + it.value.port
|
||||
map["${key}"] = value
|
||||
}
|
||||
log.trace "Devices discovered $map"
|
||||
map
|
||||
}
|
||||
|
||||
def getsamsungPlayer()
|
||||
{
|
||||
state.samsunges = state.samsunges ?: [:]
|
||||
}
|
||||
|
||||
def getVerifiedsamsungPlayer()
|
||||
{
|
||||
getsamsungPlayer().findAll{ it?.value?.verified == true }
|
||||
}
|
||||
|
||||
def locationHandler(evt) {
|
||||
def description = evt.description
|
||||
def hub = evt?.hubId
|
||||
def parsedEvent = parseEventMessage(description)
|
||||
parsedEvent << ["hub":hub]
|
||||
log.trace "${parsedEvent}"
|
||||
log.trace "${getDeviceType()} - ${parsedEvent.ssdpTerm}"
|
||||
if (parsedEvent?.ssdpTerm?.contains(getDeviceType()))
|
||||
{ //SSDP DISCOVERY EVENTS
|
||||
|
||||
log.trace "TV found"
|
||||
def samsunges = getsamsungPlayer()
|
||||
|
||||
if (!(samsunges."${parsedEvent.ssdpUSN.toString()}"))
|
||||
{ //samsung does not exist
|
||||
log.trace "Adding Device to state..."
|
||||
samsunges << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent]
|
||||
}
|
||||
else
|
||||
{ // update the values
|
||||
|
||||
log.trace "Device was already found in state..."
|
||||
|
||||
def d = samsunges."${parsedEvent.ssdpUSN.toString()}"
|
||||
boolean deviceChangedValues = false
|
||||
|
||||
if(d.ip != parsedEvent.ip || d.port != parsedEvent.port) {
|
||||
d.ip = parsedEvent.ip
|
||||
d.port = parsedEvent.port
|
||||
deviceChangedValues = true
|
||||
log.trace "Device's port or ip changed..."
|
||||
}
|
||||
|
||||
if (deviceChangedValues) {
|
||||
def children = getChildDevices()
|
||||
children.each {
|
||||
if (it.getDeviceDataByName("mac") == parsedEvent.mac) {
|
||||
log.trace "updating dni for device ${it} with mac ${parsedEvent.mac}"
|
||||
it.setDeviceNetworkId((parsedEvent.ip + ":" + parsedEvent.port)) //could error if device with same dni already exists
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (parsedEvent.headers && parsedEvent.body)
|
||||
{ // samsung RESPONSES
|
||||
def deviceHeaders = parseLanMessage(description, false)
|
||||
def type = deviceHeaders.headers."content-type"
|
||||
def body
|
||||
log.trace "REPONSE TYPE: $type"
|
||||
if (type?.contains("xml"))
|
||||
{ // description.xml response (application/xml)
|
||||
body = new XmlSlurper().parseText(deviceHeaders.body)
|
||||
log.debug body.device.deviceType.text()
|
||||
if (body?.device?.deviceType?.text().contains(getDeviceType()))
|
||||
{
|
||||
def samsunges = getsamsungPlayer()
|
||||
def player = samsunges.find {it?.key?.contains(body?.device?.UDN?.text())}
|
||||
if (player)
|
||||
{
|
||||
player.value << [name:body?.device?.friendlyName?.text(),model:body?.device?.modelName?.text(), serialNumber:body?.device?.serialNum?.text(), verified: true]
|
||||
}
|
||||
else
|
||||
{
|
||||
log.error "The xml file returned a device that didn't exist"
|
||||
}
|
||||
}
|
||||
}
|
||||
else if(type?.contains("json"))
|
||||
{ //(application/json)
|
||||
body = new groovy.json.JsonSlurper().parseText(bodyString)
|
||||
log.trace "GOT JSON $body"
|
||||
}
|
||||
|
||||
}
|
||||
else {
|
||||
log.trace "TV not found..."
|
||||
//log.trace description
|
||||
}
|
||||
}
|
||||
|
||||
private def parseEventMessage(String description) {
|
||||
def event = [:]
|
||||
def parts = description.split(',')
|
||||
parts.each { part ->
|
||||
part = part.trim()
|
||||
if (part.startsWith('devicetype:')) {
|
||||
def valueString = part.split(":")[1].trim()
|
||||
event.devicetype = valueString
|
||||
}
|
||||
else if (part.startsWith('mac:')) {
|
||||
def valueString = part.split(":")[1].trim()
|
||||
if (valueString) {
|
||||
event.mac = valueString
|
||||
}
|
||||
}
|
||||
else if (part.startsWith('networkAddress:')) {
|
||||
def valueString = part.split(":")[1].trim()
|
||||
if (valueString) {
|
||||
event.ip = valueString
|
||||
}
|
||||
}
|
||||
else if (part.startsWith('deviceAddress:')) {
|
||||
def valueString = part.split(":")[1].trim()
|
||||
if (valueString) {
|
||||
event.port = valueString
|
||||
}
|
||||
}
|
||||
else if (part.startsWith('ssdpPath:')) {
|
||||
def valueString = part.split(":")[1].trim()
|
||||
if (valueString) {
|
||||
event.ssdpPath = valueString
|
||||
}
|
||||
}
|
||||
else if (part.startsWith('ssdpUSN:')) {
|
||||
part -= "ssdpUSN:"
|
||||
def valueString = part.trim()
|
||||
if (valueString) {
|
||||
event.ssdpUSN = valueString
|
||||
}
|
||||
}
|
||||
else if (part.startsWith('ssdpTerm:')) {
|
||||
part -= "ssdpTerm:"
|
||||
def valueString = part.trim()
|
||||
if (valueString) {
|
||||
event.ssdpTerm = valueString
|
||||
}
|
||||
}
|
||||
else if (part.startsWith('headers')) {
|
||||
part -= "headers:"
|
||||
def valueString = part.trim()
|
||||
if (valueString) {
|
||||
event.headers = valueString
|
||||
}
|
||||
}
|
||||
else if (part.startsWith('body')) {
|
||||
part -= "body:"
|
||||
def valueString = part.trim()
|
||||
if (valueString) {
|
||||
event.body = valueString
|
||||
}
|
||||
}
|
||||
}
|
||||
event
|
||||
}
|
||||
|
||||
def parse(childDevice, description) {
|
||||
def parsedEvent = parseEventMessage(description)
|
||||
|
||||
if (parsedEvent.headers && parsedEvent.body) {
|
||||
def headerString = new String(parsedEvent.headers.decodeBase64())
|
||||
def bodyString = new String(parsedEvent.body.decodeBase64())
|
||||
log.trace "parse() - ${bodyString}"
|
||||
|
||||
def body = new groovy.json.JsonSlurper().parseText(bodyString)
|
||||
} else {
|
||||
log.trace "parse - got something other than headers,body..."
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
private Integer convertHexToInt(hex) {
|
||||
Integer.parseInt(hex,16)
|
||||
}
|
||||
|
||||
private String convertHexToIP(hex) {
|
||||
[convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
|
||||
}
|
||||
|
||||
private getHostAddress(d) {
|
||||
def parts = d.split(":")
|
||||
def ip = convertHexToIP(parts[0])
|
||||
def port = convertHexToInt(parts[1])
|
||||
return ip + ":" + port
|
||||
}
|
||||
|
||||
private Boolean canInstallLabs()
|
||||
{
|
||||
return hasAllHubsOver("000.011.00603")
|
||||
}
|
||||
|
||||
private Boolean hasAllHubsOver(String desiredFirmware)
|
||||
{
|
||||
return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware }
|
||||
}
|
||||
|
||||
private List getRealHubFirmwareVersions()
|
||||
{
|
||||
return location.hubs*.firmwareVersionString.findAll { it }
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Scheduled Mode Change - Presence Optional
|
||||
*
|
||||
* Author: SmartThings
|
||||
|
||||
*
|
||||
*/
|
||||
|
||||
definition(
|
||||
name: "Scheduled Mode Change",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Changes mode at a specific time of day.",
|
||||
category: "Mode Magic",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/App-LightUpMyWorld.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/App-LightUpMyWorld@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("At this time every day") {
|
||||
input "time", "time", title: "Time of Day"
|
||||
}
|
||||
section("Change to this mode") {
|
||||
input "newMode", "mode", title: "Mode?"
|
||||
}
|
||||
section( "Notifications" ) {
|
||||
input("recipients", "contact", title: "Send notifications to") {
|
||||
input "sendPushMessage", "enum", title: "Send a push notification?", options: ["Yes", "No"], required: false
|
||||
input "phoneNumber", "phone", title: "Send a text message?", required: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
initialize()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
unschedule()
|
||||
initialize()
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
schedule(time, changeMode)
|
||||
}
|
||||
|
||||
def changeMode() {
|
||||
log.debug "changeMode, location.mode = $location.mode, newMode = $newMode, location.modes = $location.modes"
|
||||
if (location.mode != newMode) {
|
||||
if (location.modes?.find{it.name == newMode}) {
|
||||
setLocationMode(newMode)
|
||||
send "${label} has changed the mode to '${newMode}'"
|
||||
}
|
||||
else {
|
||||
send "${label} tried to change to undefined mode '${newMode}'"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private send(msg) {
|
||||
|
||||
if (location.contactBookEnabled) {
|
||||
log.debug("sending notifications to: ${recipients?.size()}")
|
||||
sendNotificationToContacts(msg, recipients)
|
||||
}
|
||||
else {
|
||||
if (sendPushMessage == "Yes") {
|
||||
log.debug("sending push message")
|
||||
sendPush(msg)
|
||||
}
|
||||
|
||||
if (phoneNumber) {
|
||||
log.debug("sending text message")
|
||||
sendSms(phoneNumber, msg)
|
||||
}
|
||||
}
|
||||
|
||||
log.debug msg
|
||||
}
|
||||
|
||||
private getLabel() {
|
||||
app.label ?: "SmartThings"
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Send HAM Bridge Command When…
|
||||
*
|
||||
* For more information about HAM Bridge please visit http://solutionsetcetera.com/HAMBridge/
|
||||
*
|
||||
* Copyright 2014 Scottin Pollock
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
definition(
|
||||
name: "Send HAM Bridge Command When…",
|
||||
namespace: "soletc.com",
|
||||
author: "Scottin Pollock",
|
||||
description: "Sends a command to your HAM Bridge server when SmartThings are activated.",
|
||||
category: "Convenience",
|
||||
iconUrl: "http://solutionsetcetera.com/stuff/STIcons/HB.png",
|
||||
iconX2Url: "http://solutionsetcetera.com/stuff/STIcons/HB@2x.png"
|
||||
)
|
||||
|
||||
|
||||
preferences {
|
||||
section("Choose one or more, when..."){
|
||||
input "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true
|
||||
input "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true
|
||||
input "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true
|
||||
input "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true
|
||||
input "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true
|
||||
input "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true
|
||||
input "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true
|
||||
input "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true
|
||||
input "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true
|
||||
input "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true
|
||||
}
|
||||
section("Send this command to HAM Bridge"){
|
||||
input "HAMBcommand", "text", title: "Command to send", required: true
|
||||
}
|
||||
section("Server address and port number"){
|
||||
input "server", "text", title: "Server IP", description: "Your HAM Bridger Server IP", required: true
|
||||
input "port", "number", title: "Port", description: "Port Number", required: true
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
subscribeToEvents()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
unsubscribe()
|
||||
subscribeToEvents()
|
||||
}
|
||||
|
||||
def subscribeToEvents() {
|
||||
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)
|
||||
}
|
||||
|
||||
def eventHandler(evt) {
|
||||
sendHttp()
|
||||
}
|
||||
|
||||
def sendHttp() {
|
||||
def ip = "${settings.server}:${settings.port}"
|
||||
def deviceNetworkId = "1234"
|
||||
sendHubCommand(new physicalgraph.device.HubAction("""GET /?${settings.HAMBcommand} HTTP/1.1\r\nHOST: $ip\r\n\r\n""", physicalgraph.device.Protocol.LAN, "${deviceNetworkId}"))
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Severe Weather Alert
|
||||
*
|
||||
* Author: SmartThings
|
||||
* Date: 2013-03-04
|
||||
*/
|
||||
definition(
|
||||
name: "Severe Weather Alert",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Get a push notification when severe weather is in your area.",
|
||||
category: "Safety & Security",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/SafetyAndSecurity/App-SevereWeather.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/SafetyAndSecurity/App-SevereWeather@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section ("In addition to push notifications, send text alerts to...") {
|
||||
input("recipients", "contact", title: "Send notifications to") {
|
||||
input "phone1", "phone", title: "Phone Number 1", required: false
|
||||
input "phone2", "phone", title: "Phone Number 2", required: false
|
||||
input "phone3", "phone", title: "Phone Number 3", required: false
|
||||
}
|
||||
}
|
||||
|
||||
section ("Zip code (optional, defaults to location coordinates)...") {
|
||||
input "zipcode", "text", title: "Zip Code", required: false
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
scheduleJob()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
unschedule()
|
||||
scheduleJob()
|
||||
}
|
||||
|
||||
def scheduleJob() {
|
||||
def sec = Math.round(Math.floor(Math.random() * 60))
|
||||
def min = Math.round(Math.floor(Math.random() * 60))
|
||||
def cron = "$sec $min * * * ?"
|
||||
schedule(cron, "checkForSevereWeather")
|
||||
}
|
||||
|
||||
def checkForSevereWeather() {
|
||||
def alerts
|
||||
if(locationIsDefined()) {
|
||||
if(zipcodeIsValid()) {
|
||||
alerts = getWeatherFeature("alerts", zipcode)?.alerts
|
||||
} else {
|
||||
log.warn "Severe Weather Alert: Invalid zipcode entered, defaulting to location's zipcode"
|
||||
alerts = getWeatherFeature("alerts")?.alerts
|
||||
}
|
||||
} else {
|
||||
log.warn "Severe Weather Alert: Location is not defined"
|
||||
}
|
||||
|
||||
def newKeys = alerts?.collect{it.type + it.date_epoch} ?: []
|
||||
log.debug "Severe Weather Alert: newKeys: $newKeys"
|
||||
|
||||
def oldKeys = state.alertKeys ?: []
|
||||
log.debug "Severe Weather Alert: oldKeys: $oldKeys"
|
||||
|
||||
if (newKeys != oldKeys) {
|
||||
|
||||
state.alertKeys = newKeys
|
||||
|
||||
alerts.each {alert ->
|
||||
if (!oldKeys.contains(alert.type + alert.date_epoch) && descriptionFilter(alert.description)) {
|
||||
def msg = "Weather Alert! ${alert.description} from ${alert.date} until ${alert.expires}"
|
||||
send(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def descriptionFilter(String description) {
|
||||
def filterList = ["special", "statement", "test"]
|
||||
def passesFilter = true
|
||||
filterList.each() { word ->
|
||||
if(description.toLowerCase().contains(word)) { passesFilter = false }
|
||||
}
|
||||
passesFilter
|
||||
}
|
||||
|
||||
def locationIsDefined() {
|
||||
zipcodeIsValid() || location.zipCode || ( location.latitude && location.longitude )
|
||||
}
|
||||
|
||||
def zipcodeIsValid() {
|
||||
zipcode && zipcode.isNumber() && zipcode.size() == 5
|
||||
}
|
||||
|
||||
private send(message) {
|
||||
if (location.contactBookEnabled) {
|
||||
log.debug("sending notifications to: ${recipients?.size()}")
|
||||
sendNotificationToContacts(msg, recipients)
|
||||
}
|
||||
else {
|
||||
sendPush message
|
||||
if (settings.phone1) {
|
||||
sendSms phone1, message
|
||||
}
|
||||
if (settings.phone2) {
|
||||
sendSms phone2, message
|
||||
}
|
||||
if (settings.phone3) {
|
||||
sendSms phone3, message
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
/**
|
||||
* Button Controller
|
||||
*
|
||||
* Copyright 2014 SmartThings
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
definition(name: "Single Button Controller",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Use your Aeon Panic Button to setup events when the button is used",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
|
||||
category: "Reviewers")
|
||||
|
||||
preferences {
|
||||
page(name: "selectButton")
|
||||
}
|
||||
|
||||
def selectButton() {
|
||||
dynamicPage(name: "selectButton", title: "First, select your button device", install: true, uninstall: configured()) {
|
||||
section {
|
||||
input "buttonDevice", "device.aeonKeyFob", title: "Button", multiple: false, required: true
|
||||
}
|
||||
section("Lights") {
|
||||
input "lights_1_pushed", "capability.switch", title: "Pushed", multiple: true, required: false
|
||||
input "lights_1_held", "capability.switch", title: "Held", multiple: true, required: false
|
||||
}
|
||||
section("Locks") {
|
||||
input "locks_1_pushed", "capability.lock", title: "Pushed", multiple: true, required: false
|
||||
input "locks_1_held", "capability.lock", title: "Held", multiple: true, required: false
|
||||
}
|
||||
section("Sonos") {
|
||||
input "sonos_1_pushed", "capability.musicPlayer", title: "Pushed", multiple: true, required: false
|
||||
input "sonos_1_held", "capability.musicPlayer", title: "Held", multiple: true, required: false
|
||||
}
|
||||
section("Modes") {
|
||||
input "mode_1_pushed", "mode", title: "Pushed", required: false
|
||||
input "mode_1_held", "mode", title: "Held", required: false
|
||||
}
|
||||
def phrases = location.helloHome?.getPhrases()*.label
|
||||
if (phrases) {
|
||||
section("Hello Home Actions") {
|
||||
log.trace phrases
|
||||
input "phrase_1_pushed", "enum", title: "Pushed", required: false, options: phrases
|
||||
input "phrase_1_held", "enum", title: "Held", required: false, options: phrases
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
initialize()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
unsubscribe()
|
||||
initialize()
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
subscribe(buttonDevice, "button", buttonEvent)
|
||||
}
|
||||
|
||||
def configured() {
|
||||
return buttonDevice || buttonConfigured(1)
|
||||
}
|
||||
|
||||
def buttonConfigured(idx) {
|
||||
return settings["lights_$idx_pushed"] ||
|
||||
settings["locks_$idx_pushed"] ||
|
||||
settings["sonos_$idx_pushed"] ||
|
||||
settings["mode_$idx_pushed"]
|
||||
}
|
||||
|
||||
def buttonEvent(evt){
|
||||
def buttonNumber = evt.data // why doesn't jsonData work? always returning [:]
|
||||
def value = evt.value
|
||||
log.debug "buttonEvent: $evt.name = $evt.value ($evt.data)"
|
||||
log.debug "button: $buttonNumber, value: $value"
|
||||
|
||||
def recentEvents = buttonDevice.eventsSince(new Date(now() - 3000)).findAll{it.value == evt.value}
|
||||
log.debug "Found ${recentEvents.size()?:0} events in past 3 seconds"
|
||||
|
||||
executeHandlers(1, value)
|
||||
}
|
||||
|
||||
def executeHandlers(buttonNumber, value) {
|
||||
log.debug "executeHandlers: $buttonNumber - $value"
|
||||
|
||||
def lights = find('lights', buttonNumber, value)
|
||||
if (lights != null) toggle(lights)
|
||||
|
||||
def locks = find('locks', buttonNumber, value)
|
||||
if (locks != null) toggle(locks)
|
||||
|
||||
def sonos = find('sonos', buttonNumber, value)
|
||||
if (sonos != null) toggle(sonos)
|
||||
|
||||
def mode = find('mode', buttonNumber, value)
|
||||
if (mode != null) changeMode(mode)
|
||||
|
||||
def phrase = find('phrase', buttonNumber, value)
|
||||
if (phrase != null) location.helloHome.execute(phrase)
|
||||
}
|
||||
|
||||
def find(type, buttonNumber, value) {
|
||||
def preferenceName = type + "_" + buttonNumber + "_" + value
|
||||
def pref = settings[preferenceName]
|
||||
if(pref != null) {
|
||||
log.debug "Found: $pref for $preferenceName"
|
||||
}
|
||||
|
||||
return pref
|
||||
}
|
||||
|
||||
def toggle(devices) {
|
||||
log.debug "toggle: $devices = ${devices*.currentValue('switch')}"
|
||||
|
||||
if (devices*.currentValue('switch').contains('on')) {
|
||||
devices.off()
|
||||
}
|
||||
else if (devices*.currentValue('switch').contains('off')) {
|
||||
devices.on()
|
||||
}
|
||||
else if (devices*.currentValue('lock').contains('locked')) {
|
||||
devices.unlock()
|
||||
}
|
||||
else if (devices*.currentValue('lock').contains('unlocked')) {
|
||||
devices.lock()
|
||||
}
|
||||
else {
|
||||
devices.on()
|
||||
}
|
||||
}
|
||||
|
||||
def changeMode(mode) {
|
||||
log.debug "changeMode: $mode, location.mode = $location.mode, location.modes = $location.modes"
|
||||
|
||||
if (location.mode != mode && location.modes?.find { it.name == mode }) {
|
||||
setLocationMode(mode)
|
||||
}
|
||||
}
|
||||
89
smartapps/smartthings/sleepy-time.src/sleepy-time.groovy
Normal file
89
smartapps/smartthings/sleepy-time.src/sleepy-time.groovy
Normal file
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* Sleepy Time
|
||||
*
|
||||
* Copyright 2014 Physical Graph Corporation
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
definition(
|
||||
name: "Sleepy Time",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Use Jawbone sleep mode events to automatically execute Hello, Home phrases. Automatially put the house to bed or wake it up in the morning by pushing the button on your UP.",
|
||||
category: "SmartThings Labs",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/jawbone-up.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/jawbone-up@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
page(name: "selectPhrases")
|
||||
}
|
||||
|
||||
def selectPhrases() {
|
||||
dynamicPage(name: "selectPhrases", title: "Configure Your Jawbone Phrases.", install: true, uninstall: true) {
|
||||
section("Select your Jawbone UP") {
|
||||
input "jawbone", "device.jawboneUser", title: "Jawbone UP", required: true, multiple: false, submitOnChange:true
|
||||
}
|
||||
|
||||
def phrases = location.helloHome?.getPhrases()*.label
|
||||
if (phrases) {
|
||||
phrases.sort()
|
||||
section("Hello Home Actions") {
|
||||
log.trace phrases
|
||||
input "sleepPhrase", "enum", title: "Enter Sleep Mode (Bedtime) Phrase", required: false, options: phrases, submitOnChange:true
|
||||
input "wakePhrase", "enum", title: "Exit Sleep Mode (Waking Up) Phrase", required: false, options: phrases, submitOnChange:true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
|
||||
initialize()
|
||||
|
||||
log.debug "Subscribing to sleeping events."
|
||||
|
||||
subscribe (jawbone, "sleeping", jawboneHandler)
|
||||
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
|
||||
unsubscribe()
|
||||
|
||||
log.debug "Subscribing to sleeping events."
|
||||
|
||||
subscribe (jawbone, "sleeping", jawboneHandler)
|
||||
|
||||
initialize()
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
// TODO: subscribe to attributes, devices, locations, etc.
|
||||
}
|
||||
|
||||
def jawboneHandler(evt) {
|
||||
log.debug "In Jawbone Event Handler, Event Name = ${evt.name}, Value = ${evt.value}"
|
||||
if (evt.value == "sleeping" && sleepPhrase) {
|
||||
log.debug "Sleeping"
|
||||
sendNotificationEvent("Sleepy Time performing \"${sleepPhrase}\" for you as requested.")
|
||||
location.helloHome.execute(settings.sleepPhrase)
|
||||
}
|
||||
else if (evt.value == "not sleeping" && wakePhrase) {
|
||||
log.debug "Awake"
|
||||
sendNotificationEvent("Sleepy Time performing \"${wakePhrase}\" for you as requested.")
|
||||
location.helloHome.execute(settings.wakePhrase)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Smart Nightlight
|
||||
*
|
||||
* Author: SmartThings
|
||||
*
|
||||
*/
|
||||
definition(
|
||||
name: "Smart Nightlight",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Turns on lights when it's dark and motion is detected. Turns lights off when it becomes light or some time after motion ceases.",
|
||||
category: "Convenience",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/light_motion-outlet-luminance.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/light_motion-outlet-luminance@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("Control these lights..."){
|
||||
input "lights", "capability.switch", multiple: true
|
||||
}
|
||||
section("Turning on when it's dark and there's movement..."){
|
||||
input "motionSensor", "capability.motionSensor", title: "Where?"
|
||||
}
|
||||
section("And then off when it's light or there's been no movement for..."){
|
||||
input "delayMinutes", "number", title: "Minutes?"
|
||||
}
|
||||
section("Using either on this light sensor (optional) or the local sunrise and sunset"){
|
||||
input "lightSensor", "capability.illuminanceMeasurement", required: false
|
||||
}
|
||||
section ("Sunrise offset (optional)...") {
|
||||
input "sunriseOffsetValue", "text", title: "HH:MM", required: false
|
||||
input "sunriseOffsetDir", "enum", title: "Before or After", required: false, options: ["Before","After"]
|
||||
}
|
||||
section ("Sunset offset (optional)...") {
|
||||
input "sunsetOffsetValue", "text", title: "HH:MM", required: false
|
||||
input "sunsetOffsetDir", "enum", title: "Before or After", required: false, options: ["Before","After"]
|
||||
}
|
||||
section ("Zip code (optional, defaults to location coordinates when location services are enabled)...") {
|
||||
input "zipCode", "text", title: "Zip code", required: false
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
initialize()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
unsubscribe()
|
||||
unschedule()
|
||||
initialize()
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
subscribe(motionSensor, "motion", motionHandler)
|
||||
if (lightSensor) {
|
||||
subscribe(lightSensor, "illuminance", illuminanceHandler, [filterEvents: false])
|
||||
}
|
||||
else {
|
||||
subscribe(location, "position", locationPositionChange)
|
||||
subscribe(location, "sunriseTime", sunriseSunsetTimeHandler)
|
||||
subscribe(location, "sunsetTime", sunriseSunsetTimeHandler)
|
||||
astroCheck()
|
||||
}
|
||||
}
|
||||
|
||||
def locationPositionChange(evt) {
|
||||
log.trace "locationChange()"
|
||||
astroCheck()
|
||||
}
|
||||
|
||||
def sunriseSunsetTimeHandler(evt) {
|
||||
state.lastSunriseSunsetEvent = now()
|
||||
log.debug "SmartNightlight.sunriseSunsetTimeHandler($app.id)"
|
||||
astroCheck()
|
||||
}
|
||||
|
||||
def motionHandler(evt) {
|
||||
log.debug "$evt.name: $evt.value"
|
||||
if (evt.value == "active") {
|
||||
if (enabled()) {
|
||||
log.debug "turning on lights due to motion"
|
||||
lights.on()
|
||||
state.lastStatus = "on"
|
||||
}
|
||||
state.motionStopTime = null
|
||||
}
|
||||
else {
|
||||
state.motionStopTime = now()
|
||||
if(delayMinutes) {
|
||||
runIn(delayMinutes*60, turnOffMotionAfterDelay, [overwrite: false])
|
||||
} else {
|
||||
turnOffMotionAfterDelay()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def illuminanceHandler(evt) {
|
||||
log.debug "$evt.name: $evt.value, lastStatus: $state.lastStatus, motionStopTime: $state.motionStopTime"
|
||||
def lastStatus = state.lastStatus
|
||||
if (lastStatus != "off" && evt.integerValue > 50) {
|
||||
lights.off()
|
||||
state.lastStatus = "off"
|
||||
}
|
||||
else if (state.motionStopTime) {
|
||||
if (lastStatus != "off") {
|
||||
def elapsed = now() - state.motionStopTime
|
||||
if (elapsed >= ((delayMinutes ?: 0) * 60000L) - 2000) {
|
||||
lights.off()
|
||||
state.lastStatus = "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (lastStatus != "on" && evt.integerValue < 30){
|
||||
lights.on()
|
||||
state.lastStatus = "on"
|
||||
}
|
||||
}
|
||||
|
||||
def turnOffMotionAfterDelay() {
|
||||
log.trace "In turnOffMotionAfterDelay, state.motionStopTime = $state.motionStopTime, state.lastStatus = $state.lastStatus"
|
||||
if (state.motionStopTime && state.lastStatus != "off") {
|
||||
def elapsed = now() - state.motionStopTime
|
||||
log.trace "elapsed = $elapsed"
|
||||
if (elapsed >= ((delayMinutes ?: 0) * 60000L) - 2000) {
|
||||
log.debug "Turning off lights"
|
||||
lights.off()
|
||||
state.lastStatus = "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def scheduleCheck() {
|
||||
log.debug "In scheduleCheck - skipping"
|
||||
//turnOffMotionAfterDelay()
|
||||
}
|
||||
|
||||
def astroCheck() {
|
||||
def s = getSunriseAndSunset(zipCode: zipCode, sunriseOffset: sunriseOffset, sunsetOffset: sunsetOffset)
|
||||
state.riseTime = s.sunrise.time
|
||||
state.setTime = s.sunset.time
|
||||
log.debug "rise: ${new Date(state.riseTime)}($state.riseTime), set: ${new Date(state.setTime)}($state.setTime)"
|
||||
}
|
||||
|
||||
private enabled() {
|
||||
def result
|
||||
if (lightSensor) {
|
||||
result = lightSensor.currentIlluminance?.toInteger() < 30
|
||||
}
|
||||
else {
|
||||
def t = now()
|
||||
result = t < state.riseTime || t > state.setTime
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
private getSunriseOffset() {
|
||||
sunriseOffsetValue ? (sunriseOffsetDir == "Before" ? "-$sunriseOffsetValue" : sunriseOffsetValue) : null
|
||||
}
|
||||
|
||||
private getSunsetOffset() {
|
||||
sunsetOffsetValue ? (sunsetOffsetDir == "Before" ? "-$sunsetOffsetValue" : sunsetOffsetValue) : null
|
||||
}
|
||||
|
||||
317
smartapps/smartthings/smart-security.src/smart-security.groovy
Normal file
317
smartapps/smartthings/smart-security.src/smart-security.groovy
Normal file
@@ -0,0 +1,317 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Smart Security
|
||||
*
|
||||
* Author: SmartThings
|
||||
* Date: 2013-03-07
|
||||
*/
|
||||
definition(
|
||||
name: "Smart Security",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Alerts you when there are intruders but not when you just got up for a glass of water in the middle of the night",
|
||||
category: "Safety & Security",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/SafetyAndSecurity/App-IsItSafe.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/SafetyAndSecurity/App-IsItSafe@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("Sensors detecting an intruder") {
|
||||
input "intrusionMotions", "capability.motionSensor", title: "Motion Sensors", multiple: true, required: false
|
||||
input "intrusionContacts", "capability.contactSensor", title: "Contact Sensors", multiple: true, required: false
|
||||
}
|
||||
section("Sensors detecting residents") {
|
||||
input "residentMotions", "capability.motionSensor", title: "Motion Sensors", multiple: true, required: false
|
||||
}
|
||||
section("Alarm settings and actions") {
|
||||
input "alarms", "capability.alarm", title: "Which Alarm(s)", multiple: true, required: false
|
||||
input "silent", "enum", options: ["Yes","No"], title: "Silent alarm only (Yes/No)"
|
||||
input "seconds", "number", title: "Delay in seconds before siren sounds"
|
||||
input "lights", "capability.switch", title: "Flash these lights (optional)", multiple: true, required: false
|
||||
input "newMode", "mode", title: "Change to this mode (optional)", required: false
|
||||
}
|
||||
section("Notify others (optional)") {
|
||||
input "textMessage", "text", title: "Send this message", multiple: false, required: false
|
||||
input("recipients", "contact", title: "Send notifications to") {
|
||||
input "phone", "phone", title: "To this phone", multiple: false, required: false
|
||||
}
|
||||
}
|
||||
section("Arm system when residents quiet for (default 3 minutes)") {
|
||||
input "residentsQuietThreshold", "number", title: "Time in minutes", required: false
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
log.debug "INSTALLED"
|
||||
subscribeToEvents()
|
||||
state.alarmActive = null
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "UPDATED"
|
||||
unsubscribe()
|
||||
subscribeToEvents()
|
||||
unschedule()
|
||||
state.alarmActive = null
|
||||
state.residentsAreUp = null
|
||||
state.lastIntruderMotion = null
|
||||
alarms?.off()
|
||||
}
|
||||
|
||||
private subscribeToEvents()
|
||||
{
|
||||
subscribe intrusionMotions, "motion", intruderMotion
|
||||
subscribe residentMotions, "motion", residentMotion
|
||||
subscribe intrusionContacts, "contact", contact
|
||||
subscribe alarms, "alarm", alarm
|
||||
subscribe(app, appTouch)
|
||||
}
|
||||
|
||||
private residentsHaveBeenQuiet()
|
||||
{
|
||||
def threshold = ((residentsQuietThreshold != null && residentsQuietThreshold != "") ? residentsQuietThreshold : 3) * 60 * 1000
|
||||
def result = true
|
||||
def t0 = new Date(now() - threshold)
|
||||
for (sensor in residentMotions) {
|
||||
def recentStates = sensor.statesSince("motion", t0)
|
||||
if (recentStates.find{it.value == "active"}) {
|
||||
result = false
|
||||
break
|
||||
}
|
||||
}
|
||||
log.debug "residentsHaveBeenQuiet: $result"
|
||||
result
|
||||
}
|
||||
|
||||
private intruderMotionInactive()
|
||||
{
|
||||
def result = true
|
||||
for (sensor in intrusionMotions) {
|
||||
if (sensor.currentMotion == "active") {
|
||||
result = false
|
||||
break
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
private isResidentMotionSensor(evt)
|
||||
{
|
||||
residentMotions?.find{it.id == evt.deviceId} != null
|
||||
}
|
||||
|
||||
def appTouch(evt)
|
||||
{
|
||||
alarms?.off()
|
||||
state.alarmActive = false
|
||||
}
|
||||
|
||||
// Here to handle old subscriptions
|
||||
def motion(evt)
|
||||
{
|
||||
if (isResidentMotionSensor(evt)) {
|
||||
log.debug "resident motion, $evt.name: $evt.value"
|
||||
residentMotion(evt)
|
||||
}
|
||||
else {
|
||||
log.debug "intruder motion, $evt.name: $evt.value"
|
||||
intruderMotion(evt)
|
||||
}
|
||||
}
|
||||
|
||||
def intruderMotion(evt)
|
||||
{
|
||||
if (evt.value == "active") {
|
||||
log.debug "motion by potential intruder, residentsAreUp: $state.residentsAreUp"
|
||||
if (!state.residentsAreUp) {
|
||||
log.trace "checking if residents have been quiet"
|
||||
if (residentsHaveBeenQuiet()) {
|
||||
log.trace "calling startAlarmSequence"
|
||||
startAlarmSequence()
|
||||
}
|
||||
else {
|
||||
log.trace "calling disarmIntrusionDetection"
|
||||
disarmIntrusionDetection()
|
||||
}
|
||||
}
|
||||
}
|
||||
state.lastIntruderMotion = now()
|
||||
}
|
||||
|
||||
def residentMotion(evt)
|
||||
{
|
||||
// Don't think we need this any more
|
||||
//if (evt.value == "inactive") {
|
||||
// if (state.residentsAreUp) {
|
||||
// startReArmSequence()
|
||||
// }
|
||||
//}
|
||||
}
|
||||
|
||||
def contact(evt)
|
||||
{
|
||||
if (evt.value == "open") {
|
||||
// TODO - check for residents being up?
|
||||
if (!state.residentsAreUp) {
|
||||
if (residentsHaveBeenQuiet()) {
|
||||
startAlarmSequence()
|
||||
}
|
||||
else {
|
||||
disarmIntrusionDetection()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def alarm(evt)
|
||||
{
|
||||
log.debug "$evt.name: $evt.value"
|
||||
if (evt.value == "off") {
|
||||
alarms?.off()
|
||||
state.alarmActive = false
|
||||
}
|
||||
}
|
||||
|
||||
private disarmIntrusionDetection()
|
||||
{
|
||||
log.debug "residents are up, disarming intrusion detection"
|
||||
state.residentsAreUp = true
|
||||
scheduleReArmCheck()
|
||||
}
|
||||
|
||||
private scheduleReArmCheck()
|
||||
{
|
||||
def cron = "0 * * * * ?"
|
||||
schedule(cron, "checkForReArm")
|
||||
log.debug "Starting re-arm check, cron: $cron"
|
||||
}
|
||||
|
||||
def checkForReArm()
|
||||
{
|
||||
def threshold = ((residentsQuietThreshold != null && residentsQuietThreshold != "") ? residentsQuietThreshold : 3) * 60 * 1000
|
||||
log.debug "checkForReArm: threshold is $threshold"
|
||||
// check last intruder motion
|
||||
def lastIntruderMotion = state.lastIntruderMotion
|
||||
log.debug "checkForReArm: lastIntruderMotion=$lastIntruderMotion"
|
||||
if (lastIntruderMotion != null)
|
||||
{
|
||||
log.debug "checkForReArm, time since last intruder motion: ${now() - lastIntruderMotion}"
|
||||
if (now() - lastIntruderMotion > threshold) {
|
||||
log.debug "re-arming intrusion detection"
|
||||
state.residentsAreUp = false
|
||||
unschedule()
|
||||
}
|
||||
}
|
||||
else {
|
||||
log.warn "checkForReArm: lastIntruderMotion was null, unable to check for re-arming intrusion detection"
|
||||
}
|
||||
}
|
||||
|
||||
private startAlarmSequence()
|
||||
{
|
||||
if (state.alarmActive) {
|
||||
log.debug "alarm already active"
|
||||
}
|
||||
else {
|
||||
state.alarmActive = true
|
||||
log.debug "starting alarm sequence"
|
||||
|
||||
sendPush("Potential intruder detected!")
|
||||
|
||||
if (newMode) {
|
||||
setLocationMode(newMode)
|
||||
}
|
||||
|
||||
if (silentAlarm()) {
|
||||
log.debug "Silent alarm only"
|
||||
alarms?.strobe()
|
||||
if (location.contactBookEnabled) {
|
||||
sendNotificationToContacts(textMessage ?: "Potential intruder detected", recipients)
|
||||
}
|
||||
else {
|
||||
if (phone) {
|
||||
sendSms(phone, textMessage ?: "Potential intruder detected")
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
def delayTime = seconds
|
||||
if (delayTime) {
|
||||
alarms?.strobe()
|
||||
runIn(delayTime, "soundSiren")
|
||||
log.debug "Sounding siren in $delayTime seconds"
|
||||
}
|
||||
else {
|
||||
soundSiren()
|
||||
}
|
||||
}
|
||||
|
||||
if (lights) {
|
||||
flashLights(Math.min((seconds/2) as Integer, 10))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def soundSiren()
|
||||
{
|
||||
if (state.alarmActive) {
|
||||
log.debug "Sounding siren"
|
||||
if (location.contactBookEnabled) {
|
||||
sendNotificationToContacts(textMessage ?: "Potential intruder detected", recipients)
|
||||
}
|
||||
else {
|
||||
if (phone) {
|
||||
sendSms(phone, textMessage ?: "Potential intruder detected")
|
||||
}
|
||||
}
|
||||
alarms?.both()
|
||||
if (lights) {
|
||||
log.debug "continue flashing lights"
|
||||
continueFlashing()
|
||||
}
|
||||
}
|
||||
else {
|
||||
log.debug "alarm activation aborted"
|
||||
}
|
||||
unschedule("soundSiren") // Temporary work-around to scheduling bug
|
||||
}
|
||||
|
||||
def continueFlashing()
|
||||
{
|
||||
unschedule()
|
||||
if (state.alarmActive) {
|
||||
flashLights(10)
|
||||
schedule(util.cronExpression(now() + 10000), "continueFlashing")
|
||||
}
|
||||
}
|
||||
|
||||
private flashLights(numFlashes) {
|
||||
def onFor = 1000
|
||||
def offFor = 1000
|
||||
|
||||
log.debug "FLASHING $numFlashes times"
|
||||
def delay = 1L
|
||||
numFlashes.times {
|
||||
log.trace "Switch on after $delay msec"
|
||||
lights?.on(delay: delay)
|
||||
delay += onFor
|
||||
log.trace "Switch off after $delay msec"
|
||||
lights?.off(delay: delay)
|
||||
delay += offFor
|
||||
}
|
||||
}
|
||||
|
||||
private silentAlarm()
|
||||
{
|
||||
silent?.toLowerCase() in ["yes","true","y"]
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Weather Station Controller
|
||||
*
|
||||
* Copyright 2014 SmartThings
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
definition(
|
||||
name: "SmartWeather Station Controller",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Updates SmartWeather Station Tile devices every hour.",
|
||||
category: "Convenience",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/SafetyAndSecurity/App-MindYourHome.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/SafetyAndSecurity/App-MindYourHome@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section {
|
||||
input "weatherDevices", "device.smartweatherStationTile"
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
|
||||
initialize()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
|
||||
unschedule()
|
||||
initialize()
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
scheduledEvent()
|
||||
}
|
||||
|
||||
def scheduledEvent() {
|
||||
log.info "SmartWeather Station Controller / scheduledEvent terminated due to deprecation" // device handles this itself now -- Bob
|
||||
/*
|
||||
log.trace "scheduledEvent()"
|
||||
|
||||
def delayTimeSecs = 60 * 60 // reschedule every 60 minutes
|
||||
def runAgainWindowMS = 58 * 60 * 1000 // can run at most every 58 minutes
|
||||
def timeSinceLastRunMS = state.lastRunTime ? now() - state.lastRunTime : null //how long since it last ran?
|
||||
|
||||
if(!timeSinceLastRunMS || timeSinceLastRunMS > runAgainWindowMS){
|
||||
runIn(delayTimeSecs, scheduledEvent, [overwrite: false])
|
||||
state.lastRunTime = now()
|
||||
weatherDevices.refresh()
|
||||
} else {
|
||||
log.trace "Trying to run smartweather-station-controller too soon. Has only been ${timeSinceLastRunMS} ms but needs to be at least ${runAgainWindowMS} ms"
|
||||
}
|
||||
*/
|
||||
}
|
||||
317
smartapps/smartthings/sonos-control.src/sonos-control.groovy
Normal file
317
smartapps/smartthings/sonos-control.src/sonos-control.groovy
Normal file
@@ -0,0 +1,317 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Sonos Control
|
||||
*
|
||||
* Author: SmartThings
|
||||
*
|
||||
* Date: 2013-12-10
|
||||
*/
|
||||
definition(
|
||||
name: "Sonos Control",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Play or pause your Sonos when certain actions take place in your home.",
|
||||
category: "SmartThings Labs",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
page(name: "mainPage", title: "Control your Sonos 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: [
|
||||
"Play",
|
||||
"Stop Playing",
|
||||
"Toggle Play/Pause",
|
||||
"Skip to Next Track",
|
||||
"Play Previous Track"
|
||||
]
|
||||
}
|
||||
section {
|
||||
input "sonos", "capability.musicPlayer", title: "Sonos 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) {
|
||||
sonos.setLevel(volume as Integer)
|
||||
options.delay = 1000
|
||||
}
|
||||
|
||||
switch (actionType) {
|
||||
case "Play":
|
||||
options ? sonos.on(options) : sonos.on()
|
||||
break
|
||||
case "Stop Playing":
|
||||
options ? sonos.off(options) : sonos.off()
|
||||
break
|
||||
case "Toggle Play/Pause":
|
||||
def currentStatus = sonos.currentValue("status")
|
||||
if (currentStatus == "playing") {
|
||||
options ? sonos.pause(options) : sonos.pause()
|
||||
}
|
||||
else {
|
||||
options ? sonos.play(options) : sonos.play()
|
||||
}
|
||||
break
|
||||
case "Skip to Next Track":
|
||||
options ? sonos.nextTrack(options) : sonos.nextTrack()
|
||||
break
|
||||
case "Play Previous Track":
|
||||
options ? sonos.previousTrack(options) : sonos.previousTrack()
|
||||
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
|
||||
|
||||
@@ -0,0 +1,339 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Sonos Mood Music
|
||||
*
|
||||
* Author: SmartThings
|
||||
* Date: 2014-02-12
|
||||
*/
|
||||
|
||||
|
||||
private songOptions() {
|
||||
|
||||
// Make sure current selection is in the set
|
||||
|
||||
def options = new LinkedHashSet()
|
||||
if (state.selectedSong?.station) {
|
||||
options << state.selectedSong.station
|
||||
}
|
||||
else if (state.selectedSong?.description) {
|
||||
// TODO - Remove eventually? 'description' for backward compatibility
|
||||
options << state.selectedSong.description
|
||||
}
|
||||
|
||||
// Query for recent tracks
|
||||
def states = sonos.statesSince("trackData", new Date(0), [max:30])
|
||||
def dataMaps = states.collect{it.jsonValue}
|
||||
options.addAll(dataMaps.collect{it.station})
|
||||
|
||||
log.trace "${options.size()} songs in list"
|
||||
options.take(20) as List
|
||||
}
|
||||
|
||||
private saveSelectedSong() {
|
||||
try {
|
||||
def thisSong = song
|
||||
log.info "Looking for $thisSong"
|
||||
def songs = sonos.statesSince("trackData", new Date(0), [max:30]).collect{it.jsonValue}
|
||||
log.info "Searching ${songs.size()} records"
|
||||
|
||||
def data = songs.find {s -> s.station == thisSong}
|
||||
log.info "Found ${data?.station}"
|
||||
if (data) {
|
||||
state.selectedSong = data
|
||||
log.debug "Selected song = $state.selectedSong"
|
||||
}
|
||||
else if (song == state.selectedSong?.station) {
|
||||
log.debug "Selected existing entry '$song', which is no longer in the last 20 list"
|
||||
}
|
||||
else {
|
||||
log.warn "Selected song '$song' not found"
|
||||
}
|
||||
}
|
||||
catch (Throwable t) {
|
||||
log.error t
|
||||
}
|
||||
}
|
||||
|
||||
definition(
|
||||
name: "Sonos Mood Music",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Plays a selected song or station.",
|
||||
category: "SmartThings Labs",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
page(name: "mainPage", title: "Play a selected song or station on your Sonos when something happens", nextPage: "chooseTrack", uninstall: true)
|
||||
page(name: "chooseTrack", title: "Select a song", install: 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("Play music 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
|
||||
}
|
||||
}
|
||||
|
||||
def hideable = anythingSet || app.installationState == "COMPLETE"
|
||||
def sectionTitle = anythingSet ? "Select additional triggers" : "Play music when..."
|
||||
|
||||
section(sectionTitle, hideable: hideable, 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 {
|
||||
input "sonos", "capability.musicPlayer", title: "On this Sonos player", required: true
|
||||
}
|
||||
section("More options", hideable: true, hidden: true) {
|
||||
input "volume", "number", title: "Set the 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def chooseTrack() {
|
||||
dynamicPage(name: "chooseTrack") {
|
||||
section{
|
||||
input "song","enum",title:"Play this track", required:true, multiple: false, options: songOptions()
|
||||
}
|
||||
section([mobileOnly:true]) {
|
||||
label title: "Assign a name", required: false
|
||||
mode title: "Set for specific mode(s)", required: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private anythingSet() {
|
||||
for (name in ["motion","contact","contactClosed","acceleration","mySwitch","mySwitchOff","arrivalPresence","departurePresence","smoke","water","button1","timeOfDay","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()"
|
||||
saveSelectedSong()
|
||||
|
||||
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) {
|
||||
if (frequency) {
|
||||
def lastTime = state[frequencyKey(evt)]
|
||||
if (lastTime == null || now() - lastTime >= frequency * 60000) {
|
||||
takeAction(evt)
|
||||
}
|
||||
}
|
||||
else {
|
||||
takeAction(evt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.info "Playing '$state.selectedSong"
|
||||
|
||||
if (volume != null) {
|
||||
sonos.stop()
|
||||
pause(500)
|
||||
sonos.setLevel(volume)
|
||||
pause(500)
|
||||
}
|
||||
|
||||
sonos.playTrack(state.selectedSong)
|
||||
|
||||
if (frequency || oncePerDay) {
|
||||
state[frequencyKey(evt)] = now()
|
||||
}
|
||||
log.trace "Exiting takeAction()"
|
||||
}
|
||||
|
||||
private frequencyKey(evt) {
|
||||
"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 = 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
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Sonos Mood Music
|
||||
*
|
||||
* Author: SmartThings
|
||||
* Date: 2014-02-12
|
||||
*/
|
||||
|
||||
|
||||
private songOptions() {
|
||||
|
||||
// Make sure current selection is in the set
|
||||
|
||||
def options = new LinkedHashSet()
|
||||
options << "STOP PLAYING"
|
||||
if (state.selectedSong?.station) {
|
||||
options << state.selectedSong.station
|
||||
}
|
||||
else if (state.selectedSong?.description) {
|
||||
// TODO - Remove eventually? 'description' for backward compatibility
|
||||
options << state.selectedSong.description
|
||||
}
|
||||
|
||||
// Query for recent tracks
|
||||
def states = sonos.statesSince("trackData", new Date(0), [max:30])
|
||||
def dataMaps = states.collect{it.jsonValue}
|
||||
options.addAll(dataMaps.collect{it.station})
|
||||
|
||||
log.trace "${options.size()} songs in list"
|
||||
options.take(20) as List
|
||||
}
|
||||
|
||||
private saveSelectedSongs() {
|
||||
try {
|
||||
def songs = sonos.statesSince("trackData", new Date(0), [max:30]).collect{it.jsonValue}
|
||||
log.info "Searching ${songs.size()} records"
|
||||
|
||||
if (!state.selectedSongs) {
|
||||
state.selectedSongs = [:]
|
||||
}
|
||||
|
||||
settings.each {name, thisSong ->
|
||||
if (thisSong == "STOP PLAYING") {
|
||||
state.selectedSongs."$name" = "PAUSE"
|
||||
}
|
||||
if (name.startsWith("mode_")) {
|
||||
log.info "Looking for $thisSong"
|
||||
|
||||
def data = songs.find {s -> s.station == thisSong}
|
||||
log.info "Found ${data?.station}"
|
||||
if (data) {
|
||||
state.selectedSongs."$name" = data
|
||||
log.debug "Selected song = $data.station"
|
||||
}
|
||||
else if (song == state.selectedSongs."$name"?.station) {
|
||||
log.debug "Selected existing entry '$thisSong', which is no longer in the last 20 list"
|
||||
}
|
||||
else {
|
||||
log.warn "Selected song '$thisSong' not found"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Throwable t) {
|
||||
log.error t
|
||||
}
|
||||
}
|
||||
|
||||
definition(
|
||||
name: "Sonos Music Modes",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Plays a different selected song or station for each mode.",
|
||||
category: "SmartThings Internal",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
page(name: "mainPage", title: "Play a message on your Sonos when something happens", nextPage: "chooseTrack", uninstall: true)
|
||||
page(name: "chooseTrack", title: "Select a song", install: 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") {
|
||||
|
||||
section {
|
||||
input "sonos", "capability.musicPlayer", title: "Sonos player", required: true
|
||||
}
|
||||
section("More options", hideable: true, hidden: true) {
|
||||
input "volume", "number", title: "Set the 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def chooseTrack() {
|
||||
dynamicPage(name: "chooseTrack") {
|
||||
section("Play a different song for each mode in which you want music") {
|
||||
def options = songOptions()
|
||||
location.modes.each {mode ->
|
||||
input "mode_$mode.name", "enum", title: mode.name, options: options, required: false
|
||||
}
|
||||
}
|
||||
section([mobileOnly:true]) {
|
||||
label title: "Assign a name", required: false
|
||||
mode title: "Set for specific mode(s)", required: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
subscribeToEvents()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
unsubscribe()
|
||||
subscribeToEvents()
|
||||
}
|
||||
|
||||
def subscribeToEvents() {
|
||||
log.trace "subscribeToEvents()"
|
||||
saveSelectedSongs()
|
||||
|
||||
subscribe(location, modeChangeHandler)
|
||||
}
|
||||
|
||||
def modeChangeHandler(evt) {
|
||||
log.trace "modeChangeHandler($evt.name: $evt.value)"
|
||||
if (allOk) {
|
||||
if (frequency) {
|
||||
def lastTime = state[frequencyKey(evt)]
|
||||
if (lastTime == null || now() - lastTime >= frequency * 60000) {
|
||||
takeAction(evt)
|
||||
}
|
||||
}
|
||||
else {
|
||||
takeAction(evt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private takeAction(evt) {
|
||||
|
||||
def name = "mode_$evt.value".toString()
|
||||
def selectedSong = state.selectedSongs."$name"
|
||||
|
||||
if (selectedSong == "PAUSE") {
|
||||
sonos.stop()
|
||||
}
|
||||
else {
|
||||
log.info "Playing '$selectedSong"
|
||||
|
||||
if (volume != null) {
|
||||
sonos.stop()
|
||||
pause(500)
|
||||
sonos.setLevel(volume)
|
||||
pause(500)
|
||||
}
|
||||
|
||||
sonos.playTrack(selectedSong)
|
||||
}
|
||||
|
||||
if (frequency || oncePerDay) {
|
||||
state[frequencyKey(evt)] = now()
|
||||
}
|
||||
log.trace "Exiting takeAction()"
|
||||
}
|
||||
|
||||
private frequencyKey(evt) {
|
||||
"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 = 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
|
||||
|
||||
@@ -0,0 +1,420 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Sonos Custom Message
|
||||
*
|
||||
* Author: SmartThings
|
||||
* Date: 2014-1-29
|
||||
*/
|
||||
definition(
|
||||
name: "Sonos Notify with Sound",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Play a sound or custom message through your Sonos when the mode changes or other events occur.",
|
||||
category: "SmartThings Labs",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
page(name: "mainPage", title: "Play a message on your Sonos when something happens", install: true, uninstall: true)
|
||||
page(name: "chooseTrack", title: "Select a song or station")
|
||||
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("Play message 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
|
||||
}
|
||||
}
|
||||
def hideable = anythingSet || app.installationState == "COMPLETE"
|
||||
def sectionTitle = anythingSet ? "Select additional triggers" : "Play message when..."
|
||||
|
||||
section(sectionTitle, hideable: hideable, 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", description: "Select mode(s)", required: false, multiple: true
|
||||
ifUnset "timeOfDay", "time", title: "At a Scheduled Time", required: false
|
||||
}
|
||||
section{
|
||||
input "actionType", "enum", title: "Action?", required: true, defaultValue: "Custom Message", options: [
|
||||
"Custom Message",
|
||||
"Bell 1",
|
||||
"Bell 2",
|
||||
"Dogs Barking",
|
||||
"Fire Alarm",
|
||||
"The mail has arrived",
|
||||
"A door opened",
|
||||
"There is motion",
|
||||
"Smartthings detected a flood",
|
||||
"Smartthings detected smoke",
|
||||
"Someone is arriving",
|
||||
"Piano",
|
||||
"Lightsaber"]
|
||||
input "message","text",title:"Play this message", required:false, multiple: false
|
||||
}
|
||||
section {
|
||||
input "sonos", "capability.musicPlayer", title: "On this Sonos player", required: true
|
||||
}
|
||||
section("More options", hideable: true, hidden: true) {
|
||||
input "resumePlaying", "bool", title: "Resume currently playing music after notification", required: false, defaultValue: true
|
||||
href "chooseTrack", title: "Or play this music or radio station", description: song ? state.selectedSong?.station : "Tap to set", state: song ? "complete" : "incomplete"
|
||||
|
||||
input "volume", "number", title: "Temporarily change 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)", required: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def chooseTrack() {
|
||||
dynamicPage(name: "chooseTrack") {
|
||||
section{
|
||||
input "song","enum",title:"Play this track", required:true, multiple: false, options: songOptions()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private songOptions() {
|
||||
|
||||
// Make sure current selection is in the set
|
||||
|
||||
def options = new LinkedHashSet()
|
||||
if (state.selectedSong?.station) {
|
||||
options << state.selectedSong.station
|
||||
}
|
||||
else if (state.selectedSong?.description) {
|
||||
// TODO - Remove eventually? 'description' for backward compatibility
|
||||
options << state.selectedSong.description
|
||||
}
|
||||
|
||||
// Query for recent tracks
|
||||
def states = sonos.statesSince("trackData", new Date(0), [max:30])
|
||||
def dataMaps = states.collect{it.jsonValue}
|
||||
options.addAll(dataMaps.collect{it.station})
|
||||
|
||||
log.trace "${options.size()} songs in list"
|
||||
options.take(20) as List
|
||||
}
|
||||
|
||||
private saveSelectedSong() {
|
||||
try {
|
||||
def thisSong = song
|
||||
log.info "Looking for $thisSong"
|
||||
def songs = sonos.statesSince("trackData", new Date(0), [max:30]).collect{it.jsonValue}
|
||||
log.info "Searching ${songs.size()} records"
|
||||
|
||||
def data = songs.find {s -> s.station == thisSong}
|
||||
log.info "Found ${data?.station}"
|
||||
if (data) {
|
||||
state.selectedSong = data
|
||||
log.debug "Selected song = $state.selectedSong"
|
||||
}
|
||||
else if (song == state.selectedSong?.station) {
|
||||
log.debug "Selected existing entry '$song', which is no longer in the last 20 list"
|
||||
}
|
||||
else {
|
||||
log.warn "Selected song '$song' not found"
|
||||
}
|
||||
}
|
||||
catch (Throwable t) {
|
||||
log.error t
|
||||
}
|
||||
}
|
||||
|
||||
private anythingSet() {
|
||||
for (name in ["motion","contact","contactClosed","acceleration","mySwitch","mySwitchOff","arrivalPresence","departurePresence","smoke","water","button1","timeOfDay","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() {
|
||||
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)
|
||||
}
|
||||
|
||||
if (song) {
|
||||
saveSelectedSong()
|
||||
}
|
||||
|
||||
loadText()
|
||||
}
|
||||
|
||||
def eventHandler(evt) {
|
||||
log.trace "eventHandler($evt?.name: $evt?.value)"
|
||||
if (allOk) {
|
||||
log.trace "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.trace "takeAction()"
|
||||
|
||||
if (song) {
|
||||
sonos.playSoundAndTrack(state.sound.uri, state.sound.duration, state.selectedSong, volume)
|
||||
}
|
||||
else if (resumePlaying){
|
||||
sonos.playTrackAndResume(state.sound.uri, state.sound.duration, volume)
|
||||
}
|
||||
else {
|
||||
sonos.playTrackAndRestore(state.sound.uri, state.sound.duration, volume)
|
||||
}
|
||||
|
||||
if (frequency || oncePerDay) {
|
||||
state[frequencyKey(evt)] = now()
|
||||
}
|
||||
log.trace "Exiting takeAction()"
|
||||
}
|
||||
|
||||
private frequencyKey(evt) {
|
||||
"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 getTimeLabel()
|
||||
{
|
||||
(starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z") : ""
|
||||
}
|
||||
// TODO - End Centralize
|
||||
|
||||
private loadText() {
|
||||
switch ( actionType) {
|
||||
case "Bell 1":
|
||||
state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/bell1.mp3", duration: "10"]
|
||||
break;
|
||||
case "Bell 2":
|
||||
state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/bell2.mp3", duration: "10"]
|
||||
break;
|
||||
case "Dogs Barking":
|
||||
state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/dogs.mp3", duration: "10"]
|
||||
break;
|
||||
case "Fire Alarm":
|
||||
state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/alarm.mp3", duration: "17"]
|
||||
break;
|
||||
case "The mail has arrived":
|
||||
state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/the+mail+has+arrived.mp3", duration: "1"]
|
||||
break;
|
||||
case "A door opened":
|
||||
state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/a+door+opened.mp3", duration: "1"]
|
||||
break;
|
||||
case "There is motion":
|
||||
state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/there+is+motion.mp3", duration: "1"]
|
||||
break;
|
||||
case "Smartthings detected a flood":
|
||||
state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/smartthings+detected+a+flood.mp3", duration: "2"]
|
||||
break;
|
||||
case "Smartthings detected smoke":
|
||||
state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/smartthings+detected+smoke.mp3", duration: "1"]
|
||||
break;
|
||||
case "Someone is arriving":
|
||||
state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/someone+is+arriving.mp3", duration: "1"]
|
||||
break;
|
||||
case "Piano":
|
||||
state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/piano2.mp3", duration: "10"]
|
||||
break;
|
||||
case "Lightsaber":
|
||||
state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/lightsaber.mp3", duration: "10"]
|
||||
break;
|
||||
default:
|
||||
if (message) {
|
||||
state.sound = textToSpeech(message instanceof List ? message[0] : message) // not sure why this is (sometimes) needed)
|
||||
}
|
||||
else {
|
||||
state.sound = textToSpeech("You selected the custom message option but did not enter a message in the $app.label Smart App")
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Sonos Remote Control
|
||||
*
|
||||
* Author: Matt Nohr
|
||||
* Date: 2014-04-14
|
||||
*/
|
||||
|
||||
/**
|
||||
* Buttons:
|
||||
* 1 2
|
||||
* 3 4
|
||||
*
|
||||
* Pushed:
|
||||
* 1: Play/Pause
|
||||
* 2: Volume Up
|
||||
* 3: Next Track
|
||||
* 4: Volume Down
|
||||
*
|
||||
* Held:
|
||||
* 1:
|
||||
* 2: Volume Up (2x)
|
||||
* 3: Previous Track
|
||||
* 4: Volume Down (2x)
|
||||
*/
|
||||
|
||||
definition(
|
||||
name: "Sonos Remote Control",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Control your Sonos system with an Aeon Minimote",
|
||||
category: "SmartThings Internal",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience%402x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("Select your devices") {
|
||||
input "buttonDevice", "capability.button", title: "Minimote", multiple: false, required: true
|
||||
input "sonos", "capability.musicPlayer", title: "Sonos", multiple: false, required: true
|
||||
}
|
||||
section("Options") {
|
||||
input "volumeOffset", "number", title: "Adjust Volume by this amount", required: false, description: "optional - 5% default"
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
|
||||
initialize()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
|
||||
unsubscribe()
|
||||
initialize()
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
subscribe(buttonDevice, "button", buttonEvent)
|
||||
}
|
||||
|
||||
def buttonEvent(evt){
|
||||
def buttonNumber = evt.data
|
||||
def value = evt.value
|
||||
log.debug "buttonEvent: $evt.name = $evt.value ($evt.data)"
|
||||
log.debug "button: $buttonNumber, value: $value"
|
||||
|
||||
def recentEvents = buttonDevice.eventsSince(new Date(now() - 2000)).findAll{it.value == evt.value && it.data == evt.data}
|
||||
log.debug "Found ${recentEvents.size()?:0} events in past 2 seconds"
|
||||
|
||||
if(recentEvents.size <= 1){
|
||||
handleButton(extractButtonNumber(buttonNumber), value)
|
||||
} else {
|
||||
log.debug "Found recent button press events for $buttonNumber with value $value"
|
||||
}
|
||||
}
|
||||
|
||||
def extractButtonNumber(data) {
|
||||
def buttonNumber
|
||||
//TODO must be a better way to do this. Data is like {buttonNumber:1}
|
||||
switch(data) {
|
||||
case ~/.*1.*/:
|
||||
buttonNumber = 1
|
||||
break
|
||||
case ~/.*2.*/:
|
||||
buttonNumber = 2
|
||||
break
|
||||
case ~/.*3.*/:
|
||||
buttonNumber = 3
|
||||
break
|
||||
case ~/.*4.*/:
|
||||
buttonNumber = 4
|
||||
break
|
||||
}
|
||||
return buttonNumber
|
||||
}
|
||||
|
||||
def handleButton(buttonNumber, value) {
|
||||
switch([number: buttonNumber, value: value]) {
|
||||
case{it.number == 1 && it.value == 'pushed'}:
|
||||
log.debug "Button 1 pushed - Play/Pause"
|
||||
togglePlayPause()
|
||||
break
|
||||
case{it.number == 2 && it.value == 'pushed'}:
|
||||
log.debug "Button 2 pushed - Volume Up"
|
||||
adjustVolume(true, false)
|
||||
break
|
||||
case{it.number == 3 && it.value == 'pushed'}:
|
||||
log.debug "Button 3 pushed - Next Track"
|
||||
sonos.nextTrack()
|
||||
break
|
||||
case{it.number == 4 && it.value == 'pushed'}:
|
||||
log.debug "Button 4 pushed - Volume Down"
|
||||
adjustVolume(false, false)
|
||||
break
|
||||
case{it.number == 2 && it.value == 'held'}:
|
||||
log.debug "Button 2 held - Volume Up 2x"
|
||||
adjustVolume(true, true)
|
||||
break
|
||||
case{it.number == 3 && it.value == 'held'}:
|
||||
log.debug "Button 3 held - Previous Track"
|
||||
sonos.previousTrack()
|
||||
break
|
||||
case{it.number == 4 && it.value == 'held'}:
|
||||
log.debug "Button 4 held - Volume Down 2x"
|
||||
adjustVolume(false, true)
|
||||
break
|
||||
default:
|
||||
log.debug "Unhandled command: $buttonNumber $value"
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
def togglePlayPause() {
|
||||
def currentStatus = sonos.currentValue("status")
|
||||
if (currentStatus == "playing") {
|
||||
options ? sonos.pause(options) : sonos.pause()
|
||||
}
|
||||
else {
|
||||
options ? sonos.play(options) : sonos.play()
|
||||
}
|
||||
}
|
||||
|
||||
def adjustVolume(boolean up, boolean doubleAmount) {
|
||||
def changeAmount = (volumeOffset ?: 5) * (doubleAmount ? 2 : 1)
|
||||
def currentVolume = sonos.currentValue("level")
|
||||
|
||||
if(up) {
|
||||
sonos.setLevel(currentVolume + changeAmount)
|
||||
} else {
|
||||
sonos.setLevel(currentVolume - changeAmount)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,432 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Sonos Weather Forecast
|
||||
*
|
||||
* Author: SmartThings
|
||||
* Date: 2014-1-29
|
||||
*/
|
||||
definition(
|
||||
name: "Sonos Weather Forecast",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Play a weather report through your Sonos when the mode changes or other events occur",
|
||||
category: "SmartThings Labs",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
page(name: "mainPage", title: "Play the weather report on your sonos", install: true, uninstall: true)
|
||||
page(name: "chooseTrack", title: "Select a song or station")
|
||||
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("Play weather report 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
|
||||
}
|
||||
}
|
||||
def hideable = anythingSet || app.installationState == "COMPLETE"
|
||||
def sectionTitle = anythingSet ? "Select additional triggers" : "Play weather report when..."
|
||||
|
||||
section(sectionTitle, hideable: hideable, 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 {
|
||||
input("forecastOptions", "enum", defaultValue: "0", title: "Weather report options", description: "Select one or more", multiple: true,
|
||||
options: [
|
||||
["0": "Current Conditions"],
|
||||
["1": "Today's Forecast"],
|
||||
["2": "Tonight's Forecast"],
|
||||
["3": "Tomorrow's Forecast"],
|
||||
]
|
||||
)
|
||||
}
|
||||
section {
|
||||
input "sonos", "capability.musicPlayer", title: "On this Sonos player", required: true
|
||||
}
|
||||
section("More options", hideable: true, hidden: true) {
|
||||
input "resumePlaying", "bool", title: "Resume currently playing music after weather report finishes", required: false, defaultValue: true
|
||||
href "chooseTrack", title: "Or play this music or radio station", description: song ? state.selectedSong?.station : "Tap to set", state: song ? "complete" : "incomplete"
|
||||
|
||||
input "zipCode", "text", title: "Zip Code", required: false
|
||||
input "volume", "number", title: "Temporarily change 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)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def chooseTrack() {
|
||||
dynamicPage(name: "chooseTrack") {
|
||||
section{
|
||||
input "song","enum",title:"Play this track", required:true, multiple: false, options: songOptions()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private anythingSet() {
|
||||
for (name in ["motion","contact","contactClosed","acceleration","mySwitch","mySwitchOff","arrivalPresence","departurePresence","smoke","water","button1","timeOfDay","triggerModes"]) {
|
||||
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() {
|
||||
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)
|
||||
}
|
||||
|
||||
if (song) {
|
||||
saveSelectedSong()
|
||||
}
|
||||
}
|
||||
|
||||
def eventHandler(evt) {
|
||||
log.trace "eventHandler($evt?.name: $evt?.value)"
|
||||
if (allOk) {
|
||||
log.trace "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) {
|
||||
|
||||
loadText()
|
||||
|
||||
if (song) {
|
||||
sonos.playSoundAndTrack(state.sound.uri, state.sound.duration, state.selectedSong, volume)
|
||||
}
|
||||
else if (resumePlaying){
|
||||
sonos.playTrackAndResume(state.sound.uri, state.sound.duration, volume)
|
||||
}
|
||||
else if (volume) {
|
||||
sonos.playTrackAtVolume(state.sound.uri, volume)
|
||||
}
|
||||
else {
|
||||
sonos.playTrack(state.sound.uri)
|
||||
}
|
||||
|
||||
if (frequency || oncePerDay) {
|
||||
state[frequencyKey(evt)] = now()
|
||||
}
|
||||
}
|
||||
|
||||
private songOptions() {
|
||||
|
||||
// Make sure current selection is in the set
|
||||
|
||||
def options = new LinkedHashSet()
|
||||
if (state.selectedSong?.station) {
|
||||
options << state.selectedSong.station
|
||||
}
|
||||
else if (state.selectedSong?.description) {
|
||||
// TODO - Remove eventually? 'description' for backward compatibility
|
||||
options << state.selectedSong.description
|
||||
}
|
||||
|
||||
// Query for recent tracks
|
||||
def states = sonos.statesSince("trackData", new Date(0), [max:30])
|
||||
def dataMaps = states.collect{it.jsonValue}
|
||||
options.addAll(dataMaps.collect{it.station})
|
||||
|
||||
log.trace "${options.size()} songs in list"
|
||||
options.take(20) as List
|
||||
}
|
||||
|
||||
private saveSelectedSong() {
|
||||
try {
|
||||
def thisSong = song
|
||||
log.info "Looking for $thisSong"
|
||||
def songs = sonos.statesSince("trackData", new Date(0), [max:30]).collect{it.jsonValue}
|
||||
log.info "Searching ${songs.size()} records"
|
||||
|
||||
def data = songs.find {s -> s.station == thisSong}
|
||||
log.info "Found ${data?.station}"
|
||||
if (data) {
|
||||
state.selectedSong = data
|
||||
log.debug "Selected song = $state.selectedSong"
|
||||
}
|
||||
else if (song == state.selectedSong?.station) {
|
||||
log.debug "Selected existing entry '$song', which is no longer in the last 20 list"
|
||||
}
|
||||
else {
|
||||
log.warn "Selected song '$song' not found"
|
||||
}
|
||||
}
|
||||
catch (Throwable t) {
|
||||
log.error t
|
||||
}
|
||||
}
|
||||
|
||||
private frequencyKey(evt) {
|
||||
"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 getTimeLabel()
|
||||
{
|
||||
(starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z") : ""
|
||||
}
|
||||
// TODO - End Centralize
|
||||
|
||||
private loadText() {
|
||||
if (location.timeZone || zipCode) {
|
||||
def weather = getWeatherFeature("forecast", zipCode)
|
||||
def current = getWeatherFeature("conditions", zipCode)
|
||||
def isMetric = location.temperatureScale == "C"
|
||||
def delim = ""
|
||||
def sb = new StringBuilder()
|
||||
list(forecastOptions).sort().each {opt ->
|
||||
if (opt == "0") {
|
||||
if (isMetric) {
|
||||
sb << "The current temperature is ${Math.round(current.current_observation.temp_c)} degrees."
|
||||
}
|
||||
else {
|
||||
sb << "The current temperature is ${Math.round(current.current_observation.temp_f)} degrees."
|
||||
}
|
||||
delim = " "
|
||||
}
|
||||
else if (opt == "1") {
|
||||
sb << delim
|
||||
sb << "Today's forecast is "
|
||||
if (isMetric) {
|
||||
sb << weather.forecast.txt_forecast.forecastday[0].fcttext_metric
|
||||
}
|
||||
else {
|
||||
sb << weather.forecast.txt_forecast.forecastday[0].fcttext
|
||||
}
|
||||
}
|
||||
else if (opt == "2") {
|
||||
sb << delim
|
||||
sb << "Tonight will be "
|
||||
if (isMetric) {
|
||||
sb << weather.forecast.txt_forecast.forecastday[1].fcttext_metric
|
||||
}
|
||||
else {
|
||||
sb << weather.forecast.txt_forecast.forecastday[1].fcttext
|
||||
}
|
||||
}
|
||||
else if (opt == "3") {
|
||||
sb << delim
|
||||
sb << "Tomorrow will be "
|
||||
if (isMetric) {
|
||||
sb << weather.forecast.txt_forecast.forecastday[2].fcttext_metric
|
||||
}
|
||||
else {
|
||||
sb << weather.forecast.txt_forecast.forecastday[2].fcttext
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def msg = sb.toString()
|
||||
msg = msg.replaceAll(/([0-9]+)C/,'$1 degrees') // TODO - remove after next release
|
||||
log.debug "msg = ${msg}"
|
||||
state.sound = textToSpeech(msg, true)
|
||||
}
|
||||
else {
|
||||
state.sound = textToSpeech("Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive weather forecasts.")
|
||||
}
|
||||
}
|
||||
|
||||
private list(String s) {
|
||||
[s]
|
||||
}
|
||||
private list(l) {
|
||||
l
|
||||
}
|
||||
366
smartapps/smartthings/step-notifier.src/step-notifier.groovy
Normal file
366
smartapps/smartthings/step-notifier.src/step-notifier.groovy
Normal file
@@ -0,0 +1,366 @@
|
||||
/**
|
||||
* Step Notifier
|
||||
*
|
||||
* Copyright 2014 Jeff's Account
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
definition(
|
||||
name: "Step Notifier",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Use a step tracker device to track daily step goals and trigger various device actions when your goals are met!",
|
||||
category: "SmartThings Labs",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/jawbone-up.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/jawbone-up@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
page(name: "setupNotifications")
|
||||
page(name: "chooseTrack", title: "Select a song or station")
|
||||
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 setupNotifications() {
|
||||
|
||||
dynamicPage(name: "setupNotifications", title: "Configure Your Goal Notifications.", install: true, uninstall: true) {
|
||||
|
||||
section("Select your Jawbone UP") {
|
||||
input "jawbone", "device.jawboneUser", title: "Jawbone UP", required: true, multiple: false
|
||||
}
|
||||
|
||||
section("Notify Me When"){
|
||||
input "thresholdType", "enum", title: "Select When to Notify", required: false, defaultValue: "Goal Reached", options: [["Goal":"Goal Reached"],["Threshold":"Specific Number of Steps"]], submitOnChange:true
|
||||
if (settings.thresholdType) {
|
||||
if (settings.thresholdType == "Threshold") {
|
||||
input "threshold", "number", title: "Enter Step Threshold", description: "Number", required: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section("Via a push notification and/or an SMS message"){
|
||||
input("recipients", "contact", title: "Send notifications to") {
|
||||
input "phone", "phone", title: "Phone Number (for SMS, optional)", required: false
|
||||
input "notificationType", "enum", title: "Select Notification", required: false, defaultValue: "None", options: ["None", "Push", "SMS", "Both"]
|
||||
}
|
||||
}
|
||||
|
||||
section("Flash the Lights") {
|
||||
input "lights", "capability.switch", title: "Which Lights?", required: false, multiple: true
|
||||
input "flashCount", "number", title: "How Many Times?", defaultValue: 5, required: false
|
||||
}
|
||||
|
||||
section("Change the Color of the Lights") {
|
||||
input "hues", "capability.colorControl", title: "Which Hue Bulbs?", required:false, multiple:true
|
||||
input "color", "enum", title: "Hue Color?", required: false, multiple:false, options: ["Red","Green","Blue","Yellow","Orange","Purple","Pink"]
|
||||
input "lightLevel", "enum", title: "Light Level?", required: false, options: [[10:"10%"],[20:"20%"],[30:"30%"],[40:"40%"],[50:"50%"],[60:"60%"],[70:"70%"],[80:"80%"],[90:"90%"],[100:"100%"]]
|
||||
input "duration", "number", title: "Duration in Seconds?", defaultValue: 30, required: false
|
||||
}
|
||||
|
||||
section("Play a song on the Sonos") {
|
||||
input "sonos", "capability.musicPlayer", title: "On this Sonos player", required: false, submitOnChange:true
|
||||
if (settings.sonos) {
|
||||
input "song","enum",title:"Play this track or radio station", required:true, multiple: false, options: songOptions()
|
||||
input "resumePlaying", "bool", title: "Resume currently playing music after notification", required: false, defaultValue: true
|
||||
input "volume", "number", title: "Temporarily change volume", description: "0-100%", required: false
|
||||
input "songDuration", "number", title: "Play for this many seconds", defaultValue: 60, description: "0-100%", required: true
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def chooseTrack() {
|
||||
dynamicPage(name: "chooseTrack") {
|
||||
section{
|
||||
input "song","enum",title:"Play this track", required:true, multiple: false, options: songOptions()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private songOptions() {
|
||||
|
||||
// Make sure current selection is in the set
|
||||
|
||||
def options = new LinkedHashSet()
|
||||
if (state.selectedSong?.station) {
|
||||
options << state.selectedSong.station
|
||||
}
|
||||
else if (state.selectedSong?.description) {
|
||||
// TODO - Remove eventually? 'description' for backward compatibility
|
||||
options << state.selectedSong.description
|
||||
}
|
||||
|
||||
// Query for recent tracks
|
||||
def states = sonos.statesSince("trackData", new Date(0), [max:30])
|
||||
def dataMaps = states.collect{it.jsonValue}
|
||||
options.addAll(dataMaps.collect{it.station})
|
||||
|
||||
log.trace "${options.size()} songs in list"
|
||||
options.take(20) as List
|
||||
}
|
||||
|
||||
private saveSelectedSong() {
|
||||
try {
|
||||
def thisSong = song
|
||||
log.info "Looking for $thisSong"
|
||||
def songs = sonos.statesSince("trackData", new Date(0), [max:30]).collect{it.jsonValue}
|
||||
log.info "Searching ${songs.size()} records"
|
||||
|
||||
def data = songs.find {s -> s.station == thisSong}
|
||||
log.info "Found ${data?.station}"
|
||||
if (data) {
|
||||
state.selectedSong = data
|
||||
log.debug "Selected song = $state.selectedSong"
|
||||
}
|
||||
else if (song == state.selectedSong?.station) {
|
||||
log.debug "Selected existing entry '$song', which is no longer in the last 20 list"
|
||||
}
|
||||
else {
|
||||
log.warn "Selected song '$song' not found"
|
||||
}
|
||||
}
|
||||
catch (Throwable t) {
|
||||
log.error t
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
|
||||
initialize()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
|
||||
unsubscribe()
|
||||
initialize()
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
|
||||
log.trace "Entering initialize()"
|
||||
|
||||
state.lastSteps = 0
|
||||
state.steps = jawbone.currentValue("steps").toInteger()
|
||||
state.goal = jawbone.currentValue("goal").toInteger()
|
||||
|
||||
subscribe (jawbone,"goal",goalHandler)
|
||||
subscribe (jawbone,"steps",stepHandler)
|
||||
|
||||
if (song) {
|
||||
saveSelectedSong()
|
||||
}
|
||||
|
||||
log.trace "Exiting initialize()"
|
||||
}
|
||||
|
||||
def goalHandler(evt) {
|
||||
|
||||
log.trace "Entering goalHandler()"
|
||||
|
||||
def goal = evt.value.toInteger()
|
||||
|
||||
state.goal = goal
|
||||
|
||||
log.trace "Exiting goalHandler()"
|
||||
}
|
||||
|
||||
def stepHandler(evt) {
|
||||
|
||||
log.trace "Entering stepHandler()"
|
||||
|
||||
log.debug "Event Value ${evt.value}"
|
||||
log.debug "state.steps = ${state.steps}"
|
||||
log.debug "state.goal = ${state.goal}"
|
||||
|
||||
def steps = evt.value.toInteger()
|
||||
|
||||
state.lastSteps = state.steps
|
||||
state.steps = steps
|
||||
|
||||
def stepGoal
|
||||
if (settings.thresholdType == "Goal")
|
||||
stepGoal = state.goal
|
||||
else
|
||||
stepGoal = settings.threshold
|
||||
|
||||
if ((state.lastSteps < stepGoal) && (state.steps >= stepGoal)) { // only trigger when crossing through the goal threshold
|
||||
|
||||
// goal achieved for the day! Yay! Lets tell someone!
|
||||
|
||||
if (settings.notificationType != "None") { // Push or SMS Notification requested
|
||||
|
||||
if (location.contactBookEnabled) {
|
||||
sendNotificationToContacts(stepMessage, recipients)
|
||||
}
|
||||
else {
|
||||
|
||||
def options = [
|
||||
method: settings.notificationType.toLowerCase(),
|
||||
phone: settings.phone
|
||||
]
|
||||
|
||||
sendNotification(stepMessage, options)
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.sonos) { // play a song on the Sonos as requested
|
||||
|
||||
// runIn(1, sonosNotification, [overwrite: false])
|
||||
sonosNotification()
|
||||
|
||||
}
|
||||
|
||||
if (settings.hues) { // change the color of hue bulbs ras equested
|
||||
|
||||
// runIn(1, hueNotification, [overwrite: false])
|
||||
hueNotification()
|
||||
|
||||
}
|
||||
|
||||
if (settings.lights) { // flash the lights as requested
|
||||
|
||||
// runIn(1, lightsNotification, [overwrite: false])
|
||||
lightsNotification()
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
log.trace "Exiting stepHandler()"
|
||||
|
||||
}
|
||||
|
||||
|
||||
def lightsNotification() {
|
||||
|
||||
// save the current state of the lights
|
||||
|
||||
log.trace "Save current state of lights"
|
||||
|
||||
state.previousLights = [:]
|
||||
|
||||
lights.each {
|
||||
state.previousLights[it.id] = it.currentValue("switch")
|
||||
}
|
||||
|
||||
// Flash the light on and off 5 times for now - this could be configurable
|
||||
|
||||
log.trace "Now flash the lights"
|
||||
|
||||
for (i in 1..flashCount) {
|
||||
|
||||
lights.on()
|
||||
pause(500)
|
||||
lights.off()
|
||||
|
||||
}
|
||||
|
||||
// restore the original state
|
||||
|
||||
log.trace "Now restore the original state of lights"
|
||||
|
||||
lights.each {
|
||||
it."${state.previousLights[it.id]}"()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
def hueNotification() {
|
||||
|
||||
log.trace "Entering hueNotification()"
|
||||
|
||||
def hueColor = 0
|
||||
if(color == "Blue")
|
||||
hueColor = 70//60
|
||||
else if(color == "Green")
|
||||
hueColor = 39//30
|
||||
else if(color == "Yellow")
|
||||
hueColor = 25//16
|
||||
else if(color == "Orange")
|
||||
hueColor = 10
|
||||
else if(color == "Purple")
|
||||
hueColor = 75
|
||||
else if(color == "Pink")
|
||||
hueColor = 83
|
||||
|
||||
|
||||
state.previousHue = [:]
|
||||
|
||||
hues.each {
|
||||
state.previousHue[it.id] = [
|
||||
"switch": it.currentValue("switch"),
|
||||
"level" : it.currentValue("level"),
|
||||
"hue": it.currentValue("hue"),
|
||||
"saturation": it.currentValue("saturation")
|
||||
]
|
||||
}
|
||||
|
||||
log.debug "current values = ${state.previousHue}"
|
||||
|
||||
def newValue = [hue: hueColor, saturation: 100, level: (lightLevel as Integer) ?: 100]
|
||||
log.debug "new value = $newValue"
|
||||
|
||||
hues*.setColor(newValue)
|
||||
setTimer()
|
||||
|
||||
log.trace "Exiting hueNotification()"
|
||||
|
||||
}
|
||||
|
||||
def setTimer()
|
||||
{
|
||||
log.debug "runIn ${duration}, resetHue"
|
||||
runIn(duration, resetHue, [overwrite: false])
|
||||
}
|
||||
|
||||
|
||||
def resetHue()
|
||||
{
|
||||
log.trace "Entering resetHue()"
|
||||
settings.hues.each {
|
||||
it.setColor(state.previousHue[it.id])
|
||||
}
|
||||
log.trace "Exiting resetHue()"
|
||||
}
|
||||
|
||||
def sonosNotification() {
|
||||
|
||||
log.trace "sonosNotification()"
|
||||
|
||||
if (settings.song) {
|
||||
|
||||
if (settings.resumePlaying) {
|
||||
if (settings.volume)
|
||||
sonos.playTrackAndResume(state.selectedSong, settings.songDuration, settings.volume)
|
||||
else
|
||||
sonos.playTrackAndResume(state.selectedSong, settings.songDuration)
|
||||
} else {
|
||||
if (settings.volume)
|
||||
sonos.playTrackAtVolume(state.selectedSong, settings.volume)
|
||||
else
|
||||
sonos.playTrack(state.selectedSong)
|
||||
}
|
||||
|
||||
sonos.on() // make sure it is playing
|
||||
|
||||
}
|
||||
|
||||
log.trace "Exiting sonosNotification()"
|
||||
}
|
||||
189
smartapps/smartthings/sunrise-sunset.src/sunrise-sunset.groovy
Normal file
189
smartapps/smartthings/sunrise-sunset.src/sunrise-sunset.groovy
Normal file
@@ -0,0 +1,189 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Sunrise, Sunset
|
||||
*
|
||||
* Author: SmartThings
|
||||
*
|
||||
* Date: 2013-04-30
|
||||
*/
|
||||
definition(
|
||||
name: "Sunrise/Sunset",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Changes mode and controls lights based on local sunrise and sunset times.",
|
||||
category: "Mode Magic",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/ModeMagic/rise-and-shine.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/ModeMagic/rise-and-shine@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section ("At sunrise...") {
|
||||
input "sunriseMode", "mode", title: "Change mode to?", required: false
|
||||
input "sunriseOn", "capability.switch", title: "Turn on?", required: false, multiple: true
|
||||
input "sunriseOff", "capability.switch", title: "Turn off?", required: false, multiple: true
|
||||
}
|
||||
section ("At sunset...") {
|
||||
input "sunsetMode", "mode", title: "Change mode to?", required: false
|
||||
input "sunsetOn", "capability.switch", title: "Turn on?", required: false, multiple: true
|
||||
input "sunsetOff", "capability.switch", title: "Turn off?", required: false, multiple: true
|
||||
}
|
||||
section ("Sunrise offset (optional)...") {
|
||||
input "sunriseOffsetValue", "text", title: "HH:MM", required: false
|
||||
input "sunriseOffsetDir", "enum", title: "Before or After", required: false, options: ["Before","After"]
|
||||
}
|
||||
section ("Sunset offset (optional)...") {
|
||||
input "sunsetOffsetValue", "text", title: "HH:MM", required: false
|
||||
input "sunsetOffsetDir", "enum", title: "Before or After", required: false, options: ["Before","After"]
|
||||
}
|
||||
section ("Zip code (optional, defaults to location coordinates)...") {
|
||||
input "zipCode", "text", required: false
|
||||
}
|
||||
section( "Notifications" ) {
|
||||
input("recipients", "contact", title: "Send notifications to") {
|
||||
input "sendPushMessage", "enum", title: "Send a push notification?", options: ["Yes", "No"], required: false
|
||||
input "phoneNumber", "phone", title: "Send a text message?", required: false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def installed() {
|
||||
initialize()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
unsubscribe()
|
||||
//unschedule handled in astroCheck method
|
||||
initialize()
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
subscribe(location, "position", locationPositionChange)
|
||||
subscribe(location, "sunriseTime", sunriseSunsetTimeHandler)
|
||||
subscribe(location, "sunsetTime", sunriseSunsetTimeHandler)
|
||||
|
||||
astroCheck()
|
||||
}
|
||||
|
||||
def locationPositionChange(evt) {
|
||||
log.trace "locationChange()"
|
||||
astroCheck()
|
||||
}
|
||||
|
||||
def sunriseSunsetTimeHandler(evt) {
|
||||
log.trace "sunriseSunsetTimeHandler()"
|
||||
astroCheck()
|
||||
}
|
||||
|
||||
def astroCheck() {
|
||||
def s = getSunriseAndSunset(zipCode: zipCode, sunriseOffset: sunriseOffset, sunsetOffset: sunsetOffset)
|
||||
|
||||
def now = new Date()
|
||||
def riseTime = s.sunrise
|
||||
def setTime = s.sunset
|
||||
log.debug "riseTime: $riseTime"
|
||||
log.debug "setTime: $setTime"
|
||||
|
||||
if (state.riseTime != riseTime.time) {
|
||||
unschedule("sunriseHandler")
|
||||
|
||||
if(riseTime.before(now)) {
|
||||
riseTime = riseTime.next()
|
||||
}
|
||||
|
||||
state.riseTime = riseTime.time
|
||||
|
||||
log.info "scheduling sunrise handler for $riseTime"
|
||||
schedule(riseTime, sunriseHandler)
|
||||
}
|
||||
|
||||
if (state.setTime != setTime.time) {
|
||||
unschedule("sunsetHandler")
|
||||
|
||||
if(setTime.before(now)) {
|
||||
setTime = setTime.next()
|
||||
}
|
||||
|
||||
state.setTime = setTime.time
|
||||
|
||||
log.info "scheduling sunset handler for $setTime"
|
||||
schedule(setTime, sunsetHandler)
|
||||
}
|
||||
}
|
||||
|
||||
def sunriseHandler() {
|
||||
log.info "Executing sunrise handler"
|
||||
if (sunriseOn) {
|
||||
sunriseOn.on()
|
||||
}
|
||||
if (sunriseOff) {
|
||||
sunriseOff.off()
|
||||
}
|
||||
changeMode(sunriseMode)
|
||||
}
|
||||
|
||||
def sunsetHandler() {
|
||||
log.info "Executing sunset handler"
|
||||
if (sunsetOn) {
|
||||
sunsetOn.on()
|
||||
}
|
||||
if (sunsetOff) {
|
||||
sunsetOff.off()
|
||||
}
|
||||
changeMode(sunsetMode)
|
||||
}
|
||||
|
||||
def changeMode(newMode) {
|
||||
if (newMode && location.mode != newMode) {
|
||||
if (location.modes?.find{it.name == newMode}) {
|
||||
setLocationMode(newMode)
|
||||
send "${label} has changed the mode to '${newMode}'"
|
||||
}
|
||||
else {
|
||||
send "${label} tried to change to undefined mode '${newMode}'"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private send(msg) {
|
||||
if (location.contactBookEnabled) {
|
||||
log.debug("sending notifications to: ${recipients?.size()}")
|
||||
sendNotificationToContacts(msg, recipients)
|
||||
}
|
||||
else {
|
||||
if (sendPushMessage != "No") {
|
||||
log.debug("sending push message")
|
||||
sendPush(msg)
|
||||
}
|
||||
|
||||
if (phoneNumber) {
|
||||
log.debug("sending text message")
|
||||
sendSms(phoneNumber, msg)
|
||||
}
|
||||
}
|
||||
|
||||
log.debug msg
|
||||
}
|
||||
|
||||
private getLabel() {
|
||||
app.label ?: "SmartThings"
|
||||
}
|
||||
|
||||
private getSunriseOffset() {
|
||||
sunriseOffsetValue ? (sunriseOffsetDir == "Before" ? "-$sunriseOffsetValue" : sunriseOffsetValue) : null
|
||||
}
|
||||
|
||||
private getSunsetOffset() {
|
||||
sunsetOffsetValue ? (sunsetOffsetDir == "Before" ? "-$sunsetOffsetValue" : sunsetOffsetValue) : null
|
||||
}
|
||||
|
||||
419
smartapps/smartthings/tesla-connect.src/tesla-connect.groovy
Normal file
419
smartapps/smartthings/tesla-connect.src/tesla-connect.groovy
Normal file
@@ -0,0 +1,419 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Tesla Service Manager
|
||||
*
|
||||
* Author: juano23@gmail.com
|
||||
* Date: 2013-08-15
|
||||
*/
|
||||
|
||||
definition(
|
||||
name: "Tesla (Connect)",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Integrate your Tesla car with SmartThings.",
|
||||
category: "SmartThings Labs",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/tesla-app%402x.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/tesla-app%403x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
page(name: "loginToTesla", title: "Tesla")
|
||||
page(name: "selectCars", title: "Tesla")
|
||||
}
|
||||
|
||||
def loginToTesla() {
|
||||
def showUninstall = username != null && password != null
|
||||
return dynamicPage(name: "loginToTesla", title: "Connect your Tesla", nextPage:"selectCars", uninstall:showUninstall) {
|
||||
section("Log in to your Tesla account:") {
|
||||
input "username", "text", title: "Username", required: true, autoCorrect:false
|
||||
input "password", "password", title: "Password", required: true, autoCorrect:false
|
||||
}
|
||||
section("To use Tesla, SmartThings encrypts and securely stores your Tesla credentials.") {}
|
||||
}
|
||||
}
|
||||
|
||||
def selectCars() {
|
||||
def loginResult = forceLogin()
|
||||
|
||||
if(loginResult.success)
|
||||
{
|
||||
def options = carsDiscovered() ?: []
|
||||
|
||||
return dynamicPage(name: "selectCars", title: "Tesla", install:true, uninstall:true) {
|
||||
section("Select which Tesla to connect"){
|
||||
input(name: "selectedCars", type: "enum", required:false, multiple:true, options:options)
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
log.error "login result false"
|
||||
return dynamicPage(name: "selectCars", title: "Tesla", install:false, uninstall:true, nextPage:"") {
|
||||
section("") {
|
||||
paragraph "Please check your username and password"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def installed() {
|
||||
log.debug "Installed"
|
||||
initialize()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "Updated"
|
||||
|
||||
unsubscribe()
|
||||
initialize()
|
||||
}
|
||||
|
||||
def uninstalled() {
|
||||
removeChildDevices(getChildDevices())
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
|
||||
if (selectCars) {
|
||||
addDevice()
|
||||
}
|
||||
|
||||
// Delete any that are no longer in settings
|
||||
def delete = getChildDevices().findAll { !selectedCars }
|
||||
log.info delete
|
||||
//removeChildDevices(delete)
|
||||
}
|
||||
|
||||
//CHILD DEVICE METHODS
|
||||
def addDevice() {
|
||||
def devices = getcarList()
|
||||
log.trace "Adding childs $devices - $selectedCars"
|
||||
selectedCars.each { dni ->
|
||||
def d = getChildDevice(dni)
|
||||
if(!d) {
|
||||
def newCar = devices.find { (it.dni) == dni }
|
||||
d = addChildDevice("smartthings", "Tesla", dni, null, [name:"Tesla", label:"Tesla"])
|
||||
log.trace "created ${d.name} with id $dni"
|
||||
} else {
|
||||
log.trace "found ${d.name} with id $key already exists"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private removeChildDevices(delete)
|
||||
{
|
||||
log.debug "deleting ${delete.size()} Teslas"
|
||||
delete.each {
|
||||
state.suppressDelete[it.deviceNetworkId] = true
|
||||
deleteChildDevice(it.deviceNetworkId)
|
||||
state.suppressDelete.remove(it.deviceNetworkId)
|
||||
}
|
||||
}
|
||||
|
||||
def getcarList() {
|
||||
def devices = []
|
||||
|
||||
def carListParams = [
|
||||
uri: "https://portal.vn.teslamotors.com/",
|
||||
path: "/vehicles",
|
||||
headers: [Cookie: getCookieValue(), 'User-Agent': validUserAgent()]
|
||||
]
|
||||
|
||||
httpGet(carListParams) { resp ->
|
||||
log.debug "Getting car list"
|
||||
if(resp.status == 200) {
|
||||
def vehicleId = resp.data.id.value[0].toString()
|
||||
def vehicleVIN = resp.data.vin[0]
|
||||
def dni = vehicleVIN + ":" + vehicleId
|
||||
def name = "Tesla [${vehicleId}]"
|
||||
// CHECK HERE IF MOBILE IS ENABLE
|
||||
// path: "/vehicles/${vehicleId}/mobile_enabled",
|
||||
// if (enable)
|
||||
devices += ["name" : "${name}", "dni" : "${dni}"]
|
||||
// else return [errorMessage:"Mobile communication isn't enable on all of your vehicles."]
|
||||
} else if(resp.status == 302) {
|
||||
// Token expired or incorrect
|
||||
singleUrl = resp.headers.Location.value
|
||||
} else {
|
||||
// ERROR
|
||||
log.error "car list: unknown response"
|
||||
}
|
||||
}
|
||||
return devices
|
||||
}
|
||||
|
||||
Map carsDiscovered() {
|
||||
def devices = getcarList()
|
||||
log.trace "Map $devices"
|
||||
def map = [:]
|
||||
if (devices instanceof java.util.Map) {
|
||||
devices.each {
|
||||
def value = "${it?.name}"
|
||||
def key = it?.dni
|
||||
map["${key}"] = value
|
||||
}
|
||||
} else { //backwards compatable
|
||||
devices.each {
|
||||
def value = "${it?.name}"
|
||||
def key = it?.dni
|
||||
map["${key}"] = value
|
||||
}
|
||||
}
|
||||
map
|
||||
}
|
||||
|
||||
def removeChildFromSettings(child) {
|
||||
def device = child.device
|
||||
def dni = device.deviceNetworkId
|
||||
log.debug "removing child device $device with dni ${dni}"
|
||||
if(!state?.suppressDelete?.get(dni))
|
||||
{
|
||||
def newSettings = settings.cars?.findAll { it != dni } ?: []
|
||||
app.updateSetting("cars", newSettings)
|
||||
}
|
||||
}
|
||||
|
||||
private forceLogin() {
|
||||
updateCookie(null)
|
||||
login()
|
||||
}
|
||||
|
||||
|
||||
private login() {
|
||||
if(getCookieValueIsValid()) {
|
||||
return [success:true]
|
||||
}
|
||||
return doLogin()
|
||||
}
|
||||
|
||||
private doLogin() {
|
||||
def loginParams = [
|
||||
uri: "https://portal.vn.teslamotors.com",
|
||||
path: "/login",
|
||||
contentType: "application/x-www-form-urlencoded",
|
||||
body: "user_session%5Bemail%5D=${username}&user_session%5Bpassword%5D=${password}"
|
||||
]
|
||||
|
||||
def result = [success:false]
|
||||
|
||||
try {
|
||||
httpPost(loginParams) { resp ->
|
||||
if (resp.status == 302) {
|
||||
log.debug "login 302 json headers: " + resp.headers.collect { "${it.name}:${it.value}" }
|
||||
def cookie = resp?.headers?.'Set-Cookie'?.split(";")?.getAt(0)
|
||||
if (cookie) {
|
||||
log.debug "login setting cookie to $cookie"
|
||||
updateCookie(cookie)
|
||||
result.success = true
|
||||
} else {
|
||||
// ERROR: any more information we can give?
|
||||
result.reason = "Bad login"
|
||||
}
|
||||
} else {
|
||||
// ERROR: any more information we can give?
|
||||
result.reason = "Bad login"
|
||||
}
|
||||
}
|
||||
} catch (groovyx.net.http.HttpResponseException e) {
|
||||
result.reason = "Bad login"
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private command(String dni, String command, String value = '') {
|
||||
def id = getVehicleId(dni)
|
||||
def commandPath
|
||||
switch (command) {
|
||||
case "flash":
|
||||
commandPath = "/vehicles/${id}/command/flash_lights"
|
||||
break;
|
||||
case "honk":
|
||||
commandPath = "/vehicles/${id}/command/honk_horn"
|
||||
break;
|
||||
case "doorlock":
|
||||
commandPath = "/vehicles/${id}/command/door_lock"
|
||||
break;
|
||||
case "doorunlock":
|
||||
commandPath = "/vehicles/${id}/command/door_unlock"
|
||||
break;
|
||||
case "climaon":
|
||||
commandPath = "/vehicles/${id}/command/auto_conditioning_start"
|
||||
break;
|
||||
case "climaoff":
|
||||
commandPath = "/vehicles/${id}/command/auto_conditioning_stop"
|
||||
break;
|
||||
case "roof":
|
||||
commandPath = "/vehicles/${id}/command/sun_roof_control?state=${value}"
|
||||
break;
|
||||
case "temp":
|
||||
commandPath = "/vehicles/${id}/command/set_temps?driver_temp=${value}&passenger_temp=${value}"
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
def commandParams = [
|
||||
uri: "https://portal.vn.teslamotors.com",
|
||||
path: commandPath,
|
||||
headers: [Cookie: getCookieValue(), 'User-Agent': validUserAgent()]
|
||||
]
|
||||
|
||||
def loginRequired = false
|
||||
|
||||
httpGet(commandParams) { resp ->
|
||||
|
||||
if(resp.status == 403) {
|
||||
loginRequired = true
|
||||
} else if (resp.status == 200) {
|
||||
def data = resp.data
|
||||
sendNotification(data.toString())
|
||||
} else {
|
||||
log.error "unknown response: ${resp.status} - ${resp.headers.'Content-Type'}"
|
||||
}
|
||||
}
|
||||
if(loginRequired) { throw new Exception("Login Required") }
|
||||
}
|
||||
|
||||
private honk(String dni) {
|
||||
def id = getVehicleId(dni)
|
||||
def honkParams = [
|
||||
uri: "https://portal.vn.teslamotors.com",
|
||||
path: "/vehicles/${id}/command/honk_horn",
|
||||
headers: [Cookie: getCookieValue(), 'User-Agent': validUserAgent()]
|
||||
]
|
||||
|
||||
def loginRequired = false
|
||||
|
||||
httpGet(honkParams) { resp ->
|
||||
|
||||
if(resp.status == 403) {
|
||||
loginRequired = true
|
||||
} else if (resp.status == 200) {
|
||||
def data = resp.data
|
||||
} else {
|
||||
log.error "unknown response: ${resp.status} - ${resp.headers.'Content-Type'}"
|
||||
}
|
||||
}
|
||||
|
||||
if(loginRequired) {
|
||||
throw new Exception("Login Required")
|
||||
}
|
||||
}
|
||||
|
||||
private poll(String dni) {
|
||||
def id = getVehicleId(dni)
|
||||
def pollParams1 = [
|
||||
uri: "https://portal.vn.teslamotors.com",
|
||||
path: "/vehicles/${id}/command/climate_state",
|
||||
headers: [Cookie: getCookieValue(), 'User-Agent': validUserAgent()]
|
||||
]
|
||||
|
||||
def childDevice = getChildDevice(dni)
|
||||
|
||||
def loginRequired = false
|
||||
|
||||
httpGet(pollParams1) { resp ->
|
||||
|
||||
if(resp.status == 403) {
|
||||
loginRequired = true
|
||||
} else if (resp.status == 200) {
|
||||
def data = resp.data
|
||||
childDevice?.sendEvent(name: 'temperature', value: cToF(data.inside_temp).toString())
|
||||
if (data.is_auto_conditioning_on)
|
||||
childDevice?.sendEvent(name: 'clima', value: 'on')
|
||||
else
|
||||
childDevice?.sendEvent(name: 'clima', value: 'off')
|
||||
childDevice?.sendEvent(name: 'thermostatSetpoint', value: cToF(data.driver_temp_setting).toString())
|
||||
} else {
|
||||
log.error "unknown response: ${resp.status} - ${resp.headers.'Content-Type'}"
|
||||
}
|
||||
}
|
||||
|
||||
def pollParams2 = [
|
||||
uri: "https://portal.vn.teslamotors.com",
|
||||
path: "/vehicles/${id}/command/vehicle_state",
|
||||
headers: [Cookie: getCookieValue(), 'User-Agent': validUserAgent()]
|
||||
]
|
||||
|
||||
httpGet(pollParams2) { resp ->
|
||||
if(resp.status == 403) {
|
||||
loginRequired = true
|
||||
} else if (resp.status == 200) {
|
||||
def data = resp.data
|
||||
if (data.sun_roof_percent_open == 0)
|
||||
childDevice?.sendEvent(name: 'roof', value: 'close')
|
||||
else if (data.sun_roof_percent_open > 0 && data.sun_roof_percent_open < 70)
|
||||
childDevice?.sendEvent(name: 'roof', value: 'vent')
|
||||
else if (data.sun_roof_percent_open >= 70 && data.sun_roof_percent_open <= 80)
|
||||
childDevice?.sendEvent(name: 'roof', value: 'comfort')
|
||||
else if (data.sun_roof_percent_open > 80 && data.sun_roof_percent_open <= 100)
|
||||
childDevice?.sendEvent(name: 'roof', value: 'open')
|
||||
if (data.locked)
|
||||
childDevice?.sendEvent(name: 'door', value: 'lock')
|
||||
else
|
||||
childDevice?.sendEvent(name: 'door', value: 'unlock')
|
||||
} else {
|
||||
log.error "unknown response: ${resp.status} - ${resp.headers.'Content-Type'}"
|
||||
}
|
||||
}
|
||||
|
||||
def pollParams3 = [
|
||||
uri: "https://portal.vn.teslamotors.com",
|
||||
path: "/vehicles/${id}/command/charge_state",
|
||||
headers: [Cookie: getCookieValue(), 'User-Agent': validUserAgent()]
|
||||
]
|
||||
|
||||
httpGet(pollParams3) { resp ->
|
||||
if(resp.status == 403) {
|
||||
loginRequired = true
|
||||
} else if (resp.status == 200) {
|
||||
def data = resp.data
|
||||
childDevice?.sendEvent(name: 'connected', value: data.charging_state.toString())
|
||||
childDevice?.sendEvent(name: 'miles', value: data.battery_range.toString())
|
||||
childDevice?.sendEvent(name: 'battery', value: data.battery_level.toString())
|
||||
} else {
|
||||
log.error "unknown response: ${resp.status} - ${resp.headers.'Content-Type'}"
|
||||
}
|
||||
}
|
||||
|
||||
if(loginRequired) {
|
||||
throw new Exception("Login Required")
|
||||
}
|
||||
}
|
||||
|
||||
private getVehicleId(String dni) {
|
||||
return dni.split(":").last()
|
||||
}
|
||||
|
||||
private Boolean getCookieValueIsValid()
|
||||
{
|
||||
// TODO: make a call with the cookie to verify that it works
|
||||
return getCookieValue()
|
||||
}
|
||||
|
||||
private updateCookie(String cookie) {
|
||||
state.cookie = cookie
|
||||
}
|
||||
|
||||
private getCookieValue() {
|
||||
state.cookie
|
||||
}
|
||||
|
||||
def cToF(temp) {
|
||||
return temp * 1.8 + 32
|
||||
}
|
||||
|
||||
private validUserAgent() {
|
||||
"curl/7.24.0 (x86_64-apple-darwin12.0) libcurl/7.24.0 OpenSSL/0.9.8x zlib/1.2.5"
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Text Me When It Opens
|
||||
*
|
||||
* Author: SmartThings
|
||||
*/
|
||||
definition(
|
||||
name: "Text Me When It Opens",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Get a text message sent to your phone when an open/close sensor is opened.",
|
||||
category: "Convenience",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/window_contact.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/window_contact@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("When the door opens...") {
|
||||
input "contact1", "capability.contactSensor", title: "Where?"
|
||||
}
|
||||
section("Text me at...") {
|
||||
input("recipients", "contact", title: "Send notifications to") {
|
||||
input "phone1", "phone", title: "Phone number?"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def installed()
|
||||
{
|
||||
subscribe(contact1, "contact.open", contactOpenHandler)
|
||||
}
|
||||
|
||||
def updated()
|
||||
{
|
||||
unsubscribe()
|
||||
subscribe(contact1, "contact.open", contactOpenHandler)
|
||||
}
|
||||
|
||||
def contactOpenHandler(evt) {
|
||||
log.trace "$evt.value: $evt, $settings"
|
||||
log.debug "$contact1 was opened, texting $phone1"
|
||||
if (location.contactBookEnabled) {
|
||||
sendNotificationToContacts("Your ${contact1.label ?: contact1.name} was opened", recipients)
|
||||
}
|
||||
else {
|
||||
sendSms(phone1, "Your ${contact1.label ?: contact1.name} was opened")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Text Me When There's Motion and I'm Not Here
|
||||
*
|
||||
* Author: SmartThings
|
||||
*/
|
||||
|
||||
definition(
|
||||
name: "Text Me When There's Motion and I'm Not Here",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Send a text message when there is motion while you are away.",
|
||||
category: "Convenience",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/intruder_motion-presence.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/intruder_motion-presence@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("When there's movement...") {
|
||||
input "motion1", "capability.motionSensor", title: "Where?"
|
||||
}
|
||||
section("While I'm out...") {
|
||||
input "presence1", "capability.presenceSensor", title: "Who?"
|
||||
}
|
||||
section("Text me at...") {
|
||||
input("recipients", "contact", title: "Send notifications to") {
|
||||
input "phone1", "phone", title: "Phone number?"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
subscribe(motion1, "motion.active", motionActiveHandler)
|
||||
}
|
||||
|
||||
def updated() {
|
||||
unsubscribe()
|
||||
subscribe(motion1, "motion.active", motionActiveHandler)
|
||||
}
|
||||
|
||||
def motionActiveHandler(evt) {
|
||||
log.trace "$evt.value: $evt, $settings"
|
||||
|
||||
if (presence1.latestValue("presence") == "not present") {
|
||||
// Don't send a continuous stream of text messages
|
||||
def deltaSeconds = 10
|
||||
def timeAgo = new Date(now() - (1000 * deltaSeconds))
|
||||
def recentEvents = motion1.eventsSince(timeAgo)
|
||||
log.debug "Found ${recentEvents?.size() ?: 0} events in the last $deltaSeconds seconds"
|
||||
def alreadySentSms = recentEvents.count { it.value && it.value == "active" } > 1
|
||||
|
||||
if (alreadySentSms) {
|
||||
log.debug "SMS already sent to $phone1 within the last $deltaSeconds seconds"
|
||||
} else {
|
||||
if (location.contactBookEnabled) {
|
||||
log.debug "$motion1 has moved while you were out, sending notifications to: ${recipients?.size()}"
|
||||
sendNotificationToContacts("${motion1.label} ${motion1.name} moved while you were out", recipients)
|
||||
}
|
||||
else {
|
||||
log.debug "$motion1 has moved while you were out, texting $phone1"
|
||||
sendSms(phone1, "${motion1.label} ${motion1.name} moved while you were out")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.debug "Motion detected, but presence sensor indicates you are present"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* The Big Switch
|
||||
*
|
||||
* Author: SmartThings
|
||||
*
|
||||
* Date: 2013-05-01
|
||||
*/
|
||||
definition(
|
||||
name: "The Big Switch",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Turns on, off and dim a collection of lights based on the state of a specific switch.",
|
||||
category: "Convenience",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/light_outlet.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/light_outlet@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("When this switch is turned on, off or dimmed") {
|
||||
input "master", "capability.switch", title: "Where?"
|
||||
}
|
||||
section("Turn on or off all of these switches as well") {
|
||||
input "switches", "capability.switch", multiple: true, required: false
|
||||
}
|
||||
section("And turn off but not on all of these switches") {
|
||||
input "offSwitches", "capability.switch", multiple: true, required: false
|
||||
}
|
||||
section("And turn on but not off all of these switches") {
|
||||
input "onSwitches", "capability.switch", multiple: true, required: false
|
||||
}
|
||||
section("And Dim these switches") {
|
||||
input "dimSwitches", "capability.switchLevel", multiple: true, required: false
|
||||
}
|
||||
}
|
||||
|
||||
def installed()
|
||||
{
|
||||
subscribe(master, "switch.on", onHandler)
|
||||
subscribe(master, "switch.off", offHandler)
|
||||
subscribe(master, "level", dimHandler)
|
||||
}
|
||||
|
||||
def updated()
|
||||
{
|
||||
unsubscribe()
|
||||
subscribe(master, "switch.on", onHandler)
|
||||
subscribe(master, "switch.off", offHandler)
|
||||
subscribe(master, "level", dimHandler)
|
||||
}
|
||||
|
||||
def logHandler(evt) {
|
||||
log.debug evt.value
|
||||
}
|
||||
|
||||
def onHandler(evt) {
|
||||
log.debug evt.value
|
||||
log.debug onSwitches()
|
||||
onSwitches()?.on()
|
||||
}
|
||||
|
||||
def offHandler(evt) {
|
||||
log.debug evt.value
|
||||
log.debug offSwitches()
|
||||
offSwitches()?.off()
|
||||
}
|
||||
|
||||
def dimHandler(evt) {
|
||||
log.debug "Dim level: $evt.value"
|
||||
dimSwitches?.setLevel(evt.value)
|
||||
}
|
||||
|
||||
private onSwitches() {
|
||||
if(switches && onSwitches) { switches + onSwitches }
|
||||
else if(switches) { switches }
|
||||
else { onSwitches }
|
||||
}
|
||||
|
||||
private offSwitches() {
|
||||
if(switches && offSwitches) { switches + offSwitches }
|
||||
else if(switches) { switches }
|
||||
else { offSwitches }
|
||||
}
|
||||
150
smartapps/smartthings/the-flasher.src/the-flasher.groovy
Normal file
150
smartapps/smartthings/the-flasher.src/the-flasher.groovy
Normal file
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* The Flasher
|
||||
*
|
||||
* Author: bob
|
||||
* Date: 2013-02-06
|
||||
*/
|
||||
definition(
|
||||
name: "The Flasher",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Flashes a set of lights in response to motion, an open/close event, or a switch.",
|
||||
category: "Convenience",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/light_motion-outlet-contact.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/light_motion-outlet-contact@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("When any of the following devices trigger..."){
|
||||
input "motion", "capability.motionSensor", title: "Motion Sensor?", required: false
|
||||
input "contact", "capability.contactSensor", title: "Contact Sensor?", required: false
|
||||
input "acceleration", "capability.accelerationSensor", title: "Acceleration Sensor?", required: false
|
||||
input "mySwitch", "capability.switch", title: "Switch?", required: false
|
||||
input "myPresence", "capability.presenceSensor", title: "Presence Sensor?", required: false
|
||||
}
|
||||
section("Then flash..."){
|
||||
input "switches", "capability.switch", title: "These lights", multiple: true
|
||||
input "numFlashes", "number", title: "This number of times (default 3)", required: false
|
||||
}
|
||||
section("Time settings in milliseconds (optional)..."){
|
||||
input "onFor", "number", title: "On for (default 1000)", required: false
|
||||
input "offFor", "number", title: "Off for (default 1000)", required: false
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
|
||||
subscribe()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
|
||||
unsubscribe()
|
||||
subscribe()
|
||||
}
|
||||
|
||||
def subscribe() {
|
||||
if (contact) {
|
||||
subscribe(contact, "contact.open", contactOpenHandler)
|
||||
}
|
||||
if (acceleration) {
|
||||
subscribe(acceleration, "acceleration.active", accelerationActiveHandler)
|
||||
}
|
||||
if (motion) {
|
||||
subscribe(motion, "motion.active", motionActiveHandler)
|
||||
}
|
||||
if (mySwitch) {
|
||||
subscribe(mySwitch, "switch.on", switchOnHandler)
|
||||
}
|
||||
if (myPresence) {
|
||||
subscribe(myPresence, "presence", presenceHandler)
|
||||
}
|
||||
}
|
||||
|
||||
def motionActiveHandler(evt) {
|
||||
log.debug "motion $evt.value"
|
||||
flashLights()
|
||||
}
|
||||
|
||||
def contactOpenHandler(evt) {
|
||||
log.debug "contact $evt.value"
|
||||
flashLights()
|
||||
}
|
||||
|
||||
def accelerationActiveHandler(evt) {
|
||||
log.debug "acceleration $evt.value"
|
||||
flashLights()
|
||||
}
|
||||
|
||||
def switchOnHandler(evt) {
|
||||
log.debug "switch $evt.value"
|
||||
flashLights()
|
||||
}
|
||||
|
||||
def presenceHandler(evt) {
|
||||
log.debug "presence $evt.value"
|
||||
if (evt.value == "present") {
|
||||
flashLights()
|
||||
} else if (evt.value == "not present") {
|
||||
flashLights()
|
||||
}
|
||||
}
|
||||
|
||||
private flashLights() {
|
||||
def doFlash = true
|
||||
def onFor = onFor ?: 1000
|
||||
def offFor = offFor ?: 1000
|
||||
def numFlashes = numFlashes ?: 3
|
||||
|
||||
log.debug "LAST ACTIVATED IS: ${state.lastActivated}"
|
||||
if (state.lastActivated) {
|
||||
def elapsed = now() - state.lastActivated
|
||||
def sequenceTime = (numFlashes + 1) * (onFor + offFor)
|
||||
doFlash = elapsed > sequenceTime
|
||||
log.debug "DO FLASH: $doFlash, ELAPSED: $elapsed, LAST ACTIVATED: ${state.lastActivated}"
|
||||
}
|
||||
|
||||
if (doFlash) {
|
||||
log.debug "FLASHING $numFlashes times"
|
||||
state.lastActivated = now()
|
||||
log.debug "LAST ACTIVATED SET TO: ${state.lastActivated}"
|
||||
def initialActionOn = switches.collect{it.currentSwitch != "on"}
|
||||
def delay = 0L
|
||||
numFlashes.times {
|
||||
log.trace "Switch on after $delay msec"
|
||||
switches.eachWithIndex {s, i ->
|
||||
if (initialActionOn[i]) {
|
||||
s.on(delay: delay)
|
||||
}
|
||||
else {
|
||||
s.off(delay:delay)
|
||||
}
|
||||
}
|
||||
delay += onFor
|
||||
log.trace "Switch off after $delay msec"
|
||||
switches.eachWithIndex {s, i ->
|
||||
if (initialActionOn[i]) {
|
||||
s.off(delay: delay)
|
||||
}
|
||||
else {
|
||||
s.on(delay:delay)
|
||||
}
|
||||
}
|
||||
delay += offFor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* The Gun Case Moved
|
||||
*
|
||||
* Author: SmartThings
|
||||
*/
|
||||
definition(
|
||||
name: "The Gun Case Moved",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Send a text when your gun case moves",
|
||||
category: "Safety & Security",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/text_accelerometer.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/text_accelerometer@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("When the gun case moves..."){
|
||||
input "accelerationSensor", "capability.accelerationSensor", title: "Where?"
|
||||
}
|
||||
section("Text me at..."){
|
||||
input("recipients", "contact", title: "Send notifications to") {
|
||||
input "phone1", "phone", title: "Phone number?"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
subscribe(accelerationSensor, "acceleration.active", accelerationActiveHandler)
|
||||
}
|
||||
|
||||
def updated() {
|
||||
unsubscribe()
|
||||
subscribe(accelerationSensor, "acceleration.active", accelerationActiveHandler)
|
||||
}
|
||||
|
||||
def accelerationActiveHandler(evt) {
|
||||
// Don't send a continuous stream of text messages
|
||||
def deltaSeconds = 5
|
||||
def timeAgo = new Date(now() - (1000 * deltaSeconds))
|
||||
def recentEvents = accelerationSensor.eventsSince(timeAgo)
|
||||
log.trace "Found ${recentEvents?.size() ?: 0} events in the last $deltaSeconds seconds"
|
||||
def alreadySentSms = recentEvents.count { it.value && it.value == "active" } > 1
|
||||
|
||||
if (alreadySentSms) {
|
||||
log.debug "SMS already sent to $phone1 within the last $deltaSeconds seconds"
|
||||
} else {
|
||||
if (location.contactBookEnabled) {
|
||||
sendNotificationToContacts("Gun case has moved!", recipients)
|
||||
}
|
||||
else {
|
||||
log.debug "$accelerationSensor has moved, texting $phone1"
|
||||
sendSms(phone1, "Gun case has moved!")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Turn It On For 5 Minutes
|
||||
* Turn on a switch when a contact sensor opens and then turn it back off 5 minutes later.
|
||||
*
|
||||
* Author: SmartThings
|
||||
*/
|
||||
definition(
|
||||
name: "Turn It On For 5 Minutes",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "When a SmartSense Multi is opened, a switch will be turned on, and then turned off after 5 minutes.",
|
||||
category: "Safety & Security",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/light_contact-outlet.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/light_contact-outlet@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("When it opens..."){
|
||||
input "contact1", "capability.contactSensor"
|
||||
}
|
||||
section("Turn on a switch for 5 minutes..."){
|
||||
input "switch1", "capability.switch"
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
subscribe(contact1, "contact.open", contactOpenHandler)
|
||||
}
|
||||
|
||||
def updated(settings) {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
unsubscribe()
|
||||
subscribe(contact1, "contact.open", contactOpenHandler)
|
||||
}
|
||||
|
||||
def contactOpenHandler(evt) {
|
||||
switch1.on()
|
||||
def fiveMinuteDelay = 60 * 5
|
||||
runIn(fiveMinuteDelay, turnOffSwitch)
|
||||
}
|
||||
|
||||
def turnOffSwitch() {
|
||||
switch1.off()
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Turn It On When I'm Here
|
||||
*
|
||||
* Author: SmartThings
|
||||
*/
|
||||
definition(
|
||||
name: "Turn It On When I'm Here",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Turn something on when you arrive and back off when you leave.",
|
||||
category: "Convenience",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/light_presence-outlet.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/light_presence-outlet@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("When I arrive and leave..."){
|
||||
input "presence1", "capability.presenceSensor", title: "Who?", multiple: true
|
||||
}
|
||||
section("Turn on/off a light..."){
|
||||
input "switch1", "capability.switch", multiple: true
|
||||
}
|
||||
}
|
||||
|
||||
def installed()
|
||||
{
|
||||
subscribe(presence1, "presence", presenceHandler)
|
||||
}
|
||||
|
||||
def updated()
|
||||
{
|
||||
unsubscribe()
|
||||
subscribe(presence1, "presence", presenceHandler)
|
||||
}
|
||||
|
||||
def presenceHandler(evt)
|
||||
{
|
||||
log.debug "presenceHandler $evt.name: $evt.value"
|
||||
def current = presence1.currentValue("presence")
|
||||
log.debug current
|
||||
def presenceValue = presence1.find{it.currentPresence == "present"}
|
||||
log.debug presenceValue
|
||||
if(presenceValue){
|
||||
switch1.on()
|
||||
log.debug "Someone's home!"
|
||||
}
|
||||
else{
|
||||
switch1.off()
|
||||
log.debug "Everyone's away."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Turn It On When It Opens
|
||||
*
|
||||
* Author: SmartThings
|
||||
*/
|
||||
definition(
|
||||
name: "Turn It On When It Opens",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Turn something on when an open/close sensor opens.",
|
||||
category: "Convenience",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/light_contact-outlet.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/light_contact-outlet@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("When the door opens..."){
|
||||
input "contact1", "capability.contactSensor", title: "Where?"
|
||||
}
|
||||
section("Turn on a light..."){
|
||||
input "switches", "capability.switch", multiple: true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def installed()
|
||||
{
|
||||
subscribe(contact1, "contact.open", contactOpenHandler)
|
||||
}
|
||||
|
||||
def updated()
|
||||
{
|
||||
unsubscribe()
|
||||
subscribe(contact1, "contact.open", contactOpenHandler)
|
||||
}
|
||||
|
||||
def contactOpenHandler(evt) {
|
||||
log.debug "$evt.value: $evt, $settings"
|
||||
log.trace "Turning on switches: $switches"
|
||||
switches.on()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Turn On Only If I Arrive After Sunset
|
||||
*
|
||||
* Author: Danny De Leo
|
||||
*/
|
||||
definition(
|
||||
name: "Turn On Only If I Arrive After Sunset",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Turn something on only if you arrive after sunset and back off anytime you leave.",
|
||||
category: "Convenience",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/light_presence-outlet.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/light_presence-outlet@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("When I arrive and leave..."){
|
||||
input "presence1", "capability.presenceSensor", title: "Who?", multiple: true
|
||||
}
|
||||
section("Turn on/off a light..."){
|
||||
input "switch1", "capability.switch", multiple: true
|
||||
}
|
||||
}
|
||||
|
||||
def installed()
|
||||
{
|
||||
subscribe(presence1, "presence", presenceHandler)
|
||||
}
|
||||
|
||||
def updated()
|
||||
{
|
||||
unsubscribe()
|
||||
subscribe(presence1, "presence", presenceHandler)
|
||||
}
|
||||
|
||||
def presenceHandler(evt)
|
||||
{
|
||||
def now = new Date()
|
||||
def sunTime = getSunriseAndSunset();
|
||||
|
||||
log.debug "nowTime: $now"
|
||||
log.debug "riseTime: $sunTime.sunrise"
|
||||
log.debug "setTime: $sunTime.sunset"
|
||||
log.debug "presenceHandler $evt.name: $evt.value"
|
||||
|
||||
def current = presence1.currentValue("presence")
|
||||
log.debug current
|
||||
def presenceValue = presence1.find{it.currentPresence == "present"}
|
||||
log.debug presenceValue
|
||||
if(presenceValue && (now > sunTime.sunset)) {
|
||||
switch1.on()
|
||||
log.debug "Welcome home at night!"
|
||||
}
|
||||
else if(presenceValue && (now < sunTime.sunset)) {
|
||||
log.debug "Welcome home at daytime!"
|
||||
}
|
||||
else {
|
||||
switch1.off()
|
||||
log.debug "Everyone's away."
|
||||
}
|
||||
}
|
||||
|
||||
532
smartapps/smartthings/ubi.src/ubi.groovy
Normal file
532
smartapps/smartthings/ubi.src/ubi.groovy
Normal file
@@ -0,0 +1,532 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Ubi
|
||||
*
|
||||
* Author: SmartThings
|
||||
*/
|
||||
|
||||
definition(
|
||||
name: "Ubi",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Add your Ubi device to your SmartThings Account",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/ubi-app-icn.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/ubi-app-icn@2x.png",
|
||||
oauth: [displayName: "Ubi", displayLink: ""]
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("Allow a web application to control these things...") {
|
||||
input name: "switches", type: "capability.switch", title: "Which Switches?", multiple: true, required: false
|
||||
input name: "motions", type: "capability.motionSensor", title: "Which Motion Sensors?", multiple: true, required: false
|
||||
input name: "locks", type: "capability.lock", title: "Which Locks?", multiple: true, required: false
|
||||
input name: "contactSensors", type: "capability.contactSensor", title: "Which Contact Sensors?", multiple: true, required: false
|
||||
input name: "presenceSensors", type: "capability.presenceSensor", title: "Which Presence Sensors?", multiple: true, required: false
|
||||
}
|
||||
}
|
||||
|
||||
mappings {
|
||||
path("/list") {
|
||||
action: [
|
||||
GET: "listAll"
|
||||
]
|
||||
}
|
||||
|
||||
path("/events/:id") {
|
||||
action: [
|
||||
GET: "showEvents"
|
||||
]
|
||||
}
|
||||
|
||||
path("/switches") {
|
||||
action: [
|
||||
GET: "listSwitches",
|
||||
PUT: "updateSwitches",
|
||||
POST: "updateSwitches"
|
||||
]
|
||||
}
|
||||
path("/switches/:id") {
|
||||
action: [
|
||||
GET: "showSwitch",
|
||||
PUT: "updateSwitch",
|
||||
POST: "updateSwitch"
|
||||
]
|
||||
}
|
||||
path("/switches/subscriptions") {
|
||||
log.debug "switches added"
|
||||
action: [
|
||||
POST: "addSwitchSubscription"
|
||||
]
|
||||
}
|
||||
path("/switches/subscriptions/:id") {
|
||||
action: [
|
||||
DELETE: "removeSwitchSubscription",
|
||||
GET: "removeSwitchSubscription"
|
||||
]
|
||||
}
|
||||
|
||||
path("/motionSensors") {
|
||||
action: [
|
||||
GET: "listMotions",
|
||||
PUT: "updateMotions",
|
||||
POST: "updateMotions"
|
||||
|
||||
]
|
||||
}
|
||||
path("/motionSensors/:id") {
|
||||
action: [
|
||||
GET: "showMotion",
|
||||
PUT: "updateMotion",
|
||||
POST: "updateMotion"
|
||||
]
|
||||
}
|
||||
path("/motionSensors/subscriptions") {
|
||||
log.debug "motionSensors added"
|
||||
action: [
|
||||
POST: "addMotionSubscription"
|
||||
]
|
||||
}
|
||||
path("/motionSensors/subscriptions/:id") {
|
||||
log.debug "motionSensors Deleted"
|
||||
action: [
|
||||
DELETE: "removeMotionSubscription",
|
||||
GET: "removeMotionSubscription"
|
||||
]
|
||||
}
|
||||
|
||||
path("/locks") {
|
||||
action: [
|
||||
GET: "listLocks",
|
||||
PUT: "updateLock",
|
||||
POST: "updateLock"
|
||||
]
|
||||
}
|
||||
path("/locks/:id") {
|
||||
action: [
|
||||
GET: "showLock",
|
||||
PUT: "updateLock",
|
||||
POST: "updateLock"
|
||||
]
|
||||
}
|
||||
path("/locks/subscriptions") {
|
||||
action: [
|
||||
POST: "addLockSubscription"
|
||||
]
|
||||
}
|
||||
path("/locks/subscriptions/:id") {
|
||||
action: [
|
||||
DELETE: "removeLockSubscription",
|
||||
GET: "removeLockSubscription"
|
||||
]
|
||||
}
|
||||
|
||||
path("/contactSensors") {
|
||||
action: [
|
||||
GET: "listContactSensors",
|
||||
PUT: "updateContactSensor",
|
||||
POST: "updateContactSensor"
|
||||
]
|
||||
}
|
||||
path("/contactSensors/:id") {
|
||||
action: [
|
||||
GET: "showContactSensor",
|
||||
PUT: "updateContactSensor",
|
||||
POST: "updateContactSensor"
|
||||
]
|
||||
}
|
||||
path("/contactSensors/subscriptions") {
|
||||
log.debug "contactSensors/subscriptions"
|
||||
action: [
|
||||
POST: "addContactSubscription"
|
||||
]
|
||||
}
|
||||
path("/contactSensors/subscriptions/:id") {
|
||||
action: [
|
||||
DELETE: "removeContactSensorSubscription",
|
||||
GET: "removeContactSensorSubscription"
|
||||
]
|
||||
}
|
||||
|
||||
path("/presenceSensors") {
|
||||
action: [
|
||||
GET: "listPresenceSensors",
|
||||
PUT: "updatePresenceSensor",
|
||||
POST: "updatePresenceSensor"
|
||||
]
|
||||
}
|
||||
path("/presenceSensors/:id") {
|
||||
action: [
|
||||
GET: "showPresenceSensor",
|
||||
PUT: "updatePresenceSensor",
|
||||
POST: "updatePresenceSensor"
|
||||
]
|
||||
}
|
||||
path("/presenceSensors/subscriptions") {
|
||||
log.debug "PresenceSensors/subscriptions"
|
||||
action: [
|
||||
POST: "addPresenceSubscription"
|
||||
]
|
||||
}
|
||||
path("/presenceSensors/subscriptions/:id") {
|
||||
action: [
|
||||
DELETE: "removePresenceSensorSubscription",
|
||||
GET: "removePresenceSensorSubscription"
|
||||
]
|
||||
}
|
||||
|
||||
path("/state") {
|
||||
action: [
|
||||
GET: "currentState"
|
||||
]
|
||||
}
|
||||
|
||||
path("/phrases") {
|
||||
action: [
|
||||
GET: "listPhrases"
|
||||
]
|
||||
}
|
||||
path("/phrases/:phraseName") {
|
||||
action: [
|
||||
GET: "executePhrase",
|
||||
POST: "executePhrase",
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def installed() {
|
||||
// subscribe(motions, "motion.active", motionOpenHandler)
|
||||
|
||||
// subscribe(contactSensors, "contact.open", contactOpenHandler)
|
||||
// log.trace "contactSensors Installed"
|
||||
|
||||
}
|
||||
|
||||
def updated() {
|
||||
|
||||
// unsubscribe()
|
||||
// subscribe(motions, "motion.active", motionOpenHandler)
|
||||
// subscribe(contactSensors, "contact.open", contactOpenHandler)
|
||||
|
||||
//log.trace "contactSensors Updated"
|
||||
|
||||
}
|
||||
|
||||
def listAll() {
|
||||
listSwitches() + listMotions() + listLocks() + listContactSensors() + listPresenceSensors() + listPhrasesWithType()
|
||||
}
|
||||
|
||||
def listContactSensors() {
|
||||
contactSensors.collect { device(it, "contactSensor") }
|
||||
}
|
||||
|
||||
|
||||
void updateContactSensors() {
|
||||
updateAll(contactSensors)
|
||||
}
|
||||
|
||||
def showContactSensor() {
|
||||
show(contactSensors, "contact")
|
||||
}
|
||||
|
||||
void updateContactSensor() {
|
||||
update(contactSensors)
|
||||
}
|
||||
|
||||
def addContactSubscription() {
|
||||
log.debug "addContactSensorSubscription, params: ${params}"
|
||||
addSubscription(contactSensors, "contact")
|
||||
}
|
||||
|
||||
def removeContactSensorSubscription() {
|
||||
removeSubscription(contactSensors)
|
||||
}
|
||||
|
||||
|
||||
def listPresenceSensors() {
|
||||
presenceSensors.collect { device(it, "presenceSensor") }
|
||||
}
|
||||
|
||||
|
||||
void updatePresenceSensors() {
|
||||
updateAll(presenceSensors)
|
||||
}
|
||||
|
||||
def showPresenceSensor() {
|
||||
show(presenceSensors, "presence")
|
||||
}
|
||||
|
||||
void updatePresenceSensor() {
|
||||
update(presenceSensors)
|
||||
}
|
||||
|
||||
def addPresenceSubscription() {
|
||||
log.debug "addPresenceSensorSubscription, params: ${params}"
|
||||
addSubscription(presenceSensors, "presence")
|
||||
}
|
||||
|
||||
def removePresenceSensorSubscription() {
|
||||
removeSubscription(presenceSensors)
|
||||
}
|
||||
|
||||
|
||||
def listSwitches() {
|
||||
switches.collect { device(it, "switch") }
|
||||
}
|
||||
|
||||
void updateSwitches() {
|
||||
updateAll(switches)
|
||||
}
|
||||
|
||||
def showSwitch() {
|
||||
show(switches, "switch")
|
||||
}
|
||||
|
||||
void updateSwitch() {
|
||||
update(switches)
|
||||
}
|
||||
|
||||
def addSwitchSubscription() {
|
||||
log.debug "addSwitchSubscription, params: ${params}"
|
||||
addSubscription(switches, "switch")
|
||||
}
|
||||
|
||||
def removeSwitchSubscription() {
|
||||
removeSubscription(switches)
|
||||
}
|
||||
|
||||
def listMotions() {
|
||||
motions.collect { device(it, "motionSensor") }
|
||||
}
|
||||
|
||||
void updateMotions() {
|
||||
updateAll(motions)
|
||||
}
|
||||
|
||||
def showMotion() {
|
||||
show(motions, "motion")
|
||||
}
|
||||
|
||||
void updateMotion() {
|
||||
update(motions)
|
||||
}
|
||||
|
||||
def addMotionSubscription() {
|
||||
|
||||
addSubscription(motions, "motion")
|
||||
}
|
||||
|
||||
def removeMotionSubscription() {
|
||||
removeSubscription(motions)
|
||||
}
|
||||
|
||||
def listLocks() {
|
||||
locks.collect { device(it, "lock") }
|
||||
}
|
||||
|
||||
void updateLocks() {
|
||||
updateAll(locks)
|
||||
}
|
||||
|
||||
def showLock() {
|
||||
show(locks, "lock")
|
||||
}
|
||||
|
||||
void updateLock() {
|
||||
update(locks)
|
||||
}
|
||||
|
||||
def addLockSubscription() {
|
||||
addSubscription(locks, "lock")
|
||||
}
|
||||
|
||||
def removeLockSubscription() {
|
||||
removeSubscription(locks)
|
||||
}
|
||||
|
||||
/*
|
||||
def motionOpenHandler(evt) {
|
||||
//log.trace "$evt.value: $evt, $settings"
|
||||
|
||||
log.debug "$motions was active, sending push message to user"
|
||||
//sendPush("Your ${contact1.label ?: contact1.name} was opened")
|
||||
|
||||
|
||||
httpPostJson(uri: "http://automatesolutions.ca/test.php", path: '', body: [evt: [value: "motionSensor Active"]]) {
|
||||
log.debug "Event data successfully posted"
|
||||
}
|
||||
|
||||
}
|
||||
def contactOpenHandler(evt) {
|
||||
//log.trace "$evt.value: $evt, $settings"
|
||||
|
||||
log.debug "$contactSensors was opened, sending push message to user"
|
||||
//sendPush("Your ${contact1.label ?: contact1.name} was opened")
|
||||
|
||||
|
||||
httpPostJson(uri: "http://automatesolutions.ca/test.php", path: '', body: [evt: [value: "ContactSensor Opened"]]) {
|
||||
log.debug "Event data successfully posted"
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
def deviceHandler(evt) {
|
||||
log.debug "~~~~~TEST~~~~~~"
|
||||
def deviceInfo = state[evt.deviceId]
|
||||
if (deviceInfo)
|
||||
{
|
||||
httpPostJson(uri: deviceInfo.callbackUrl, path: '', body: [evt: [value: evt.value]]) {
|
||||
log.debug "Event data successfully posted"
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
log.debug "No subscribed device found"
|
||||
}
|
||||
}
|
||||
|
||||
def currentState() {
|
||||
state
|
||||
}
|
||||
|
||||
def showStates() {
|
||||
def device = (switches + motions + locks).find { it.id == params.id }
|
||||
if (!device)
|
||||
{
|
||||
httpError(404, "Switch not found")
|
||||
}
|
||||
else
|
||||
{
|
||||
device.events(params)
|
||||
}
|
||||
}
|
||||
|
||||
def listPhrasesWithType() {
|
||||
location.helloHome.getPhrases().collect {
|
||||
[
|
||||
"id" : it.id,
|
||||
"label": it.label,
|
||||
"type" : "phrase"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
def listPhrases() {
|
||||
location.helloHome.getPhrases().label
|
||||
}
|
||||
|
||||
def executePhrase() {
|
||||
def phraseName = params.phraseName
|
||||
if (phraseName)
|
||||
{
|
||||
location.helloHome.execute(phraseName)
|
||||
log.debug "executed phrase: $phraseName"
|
||||
}
|
||||
else
|
||||
{
|
||||
httpError(404, "Phrase not found")
|
||||
}
|
||||
}
|
||||
|
||||
private void updateAll(devices) {
|
||||
def command = request.JSON?.command
|
||||
if (command)
|
||||
{
|
||||
command = command.toLowerCase()
|
||||
devices."$command"()
|
||||
}
|
||||
}
|
||||
|
||||
private void update(devices) {
|
||||
log.debug "update, request: ${request.JSON}, params: ${params}, devices: $devices.id"
|
||||
//def command = request.JSON?.command
|
||||
def command = params.command
|
||||
if (command)
|
||||
{
|
||||
command = command.toLowerCase()
|
||||
def device = devices.find { it.id == params.id }
|
||||
if (!device)
|
||||
{
|
||||
httpError(404, "Device not found")
|
||||
}
|
||||
else
|
||||
{
|
||||
device."$command"()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private show(devices, type) {
|
||||
def device = devices.find { it.id == params.id }
|
||||
if (!device)
|
||||
{
|
||||
httpError(404, "Device not found")
|
||||
}
|
||||
else
|
||||
{
|
||||
def attributeName = type
|
||||
|
||||
def s = device.currentState(attributeName)
|
||||
[id: device.id, label: device.displayName, value: s?.value, unitTime: s?.date?.time, type: type]
|
||||
}
|
||||
}
|
||||
|
||||
private addSubscription(devices, attribute) {
|
||||
//def deviceId = request.JSON?.deviceId
|
||||
//def callbackUrl = request.JSON?.callbackUrl
|
||||
|
||||
log.debug "addSubscription, params: ${params}"
|
||||
|
||||
def deviceId = params.deviceId
|
||||
def callbackUrl = params.callbackUrl
|
||||
|
||||
def myDevice = devices.find { it.id == deviceId }
|
||||
if (myDevice)
|
||||
{
|
||||
log.debug "Adding switch subscription" + callbackUrl
|
||||
state[deviceId] = [callbackUrl: callbackUrl]
|
||||
log.debug "Added state: $state"
|
||||
def subscription = subscribe(myDevice, attribute, deviceHandler)
|
||||
if (subscription && subscription.eventSubscription) {
|
||||
log.debug "Subscription is newly created"
|
||||
} else {
|
||||
log.debug "Subscription already exists, returning existing subscription"
|
||||
subscription = app.subscriptions?.find { it.deviceId == deviceId && it.data == attribute && it.handler == 'deviceHandler' }
|
||||
}
|
||||
[
|
||||
id: subscription.id,
|
||||
deviceId: subscription.deviceId,
|
||||
data: subscription.data,
|
||||
handler: subscription.handler,
|
||||
callbackUrl: callbackUrl
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
private removeSubscription(devices) {
|
||||
def deviceId = params.id
|
||||
def device = devices.find { it.id == deviceId }
|
||||
if (device)
|
||||
{
|
||||
log.debug "Removing $device.displayName subscription"
|
||||
state.remove(device.id)
|
||||
unsubscribe(device)
|
||||
}
|
||||
}
|
||||
|
||||
private device(it, type) {
|
||||
it ? [id: it.id, label: it.displayName, type: type] : null
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* The simplest Undead Early Warning system that could possibly work. ;)
|
||||
*
|
||||
* Author: SmartThings
|
||||
*/
|
||||
definition(
|
||||
name: "Undead Early Warning",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Undead Early Warning",
|
||||
category: "Safety & Security",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/SafetyAndSecurity/App-UndeadEarlyWarning.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/SafetyAndSecurity/App-UndeadEarlyWarning@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("When the door opens...") {
|
||||
input "contacts", "capability.contactSensor", multiple: true, title: "Where could they come from?"
|
||||
}
|
||||
section("Turn on the lights!") {
|
||||
input "switches", "capability.switch", multiple: true
|
||||
}
|
||||
}
|
||||
|
||||
def installed()
|
||||
{
|
||||
subscribe(contacts, "contact.open", contactOpenHandler)
|
||||
}
|
||||
|
||||
def updated()
|
||||
{
|
||||
unsubscribe()
|
||||
subscribe(contacts, "contact.open", contactOpenHandler)
|
||||
}
|
||||
|
||||
def contactOpenHandler(evt) {
|
||||
log.debug "$evt.value: $evt, $settings"
|
||||
log.trace "The Undead are coming! Turning on the lights: $switches"
|
||||
switches.on()
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Unlock It When I Arrive
|
||||
*
|
||||
* Author: SmartThings
|
||||
* Date: 2013-02-11
|
||||
*/
|
||||
|
||||
definition(
|
||||
name: "Unlock It When I Arrive",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Unlocks the door when you arrive at your location.",
|
||||
category: "Safety & Security",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience%402x.png",
|
||||
oauth: true
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("When I arrive..."){
|
||||
input "presence1", "capability.presenceSensor", title: "Who?", multiple: true
|
||||
}
|
||||
section("Unlock the lock..."){
|
||||
input "lock1", "capability.lock", multiple: true
|
||||
}
|
||||
}
|
||||
|
||||
def installed()
|
||||
{
|
||||
subscribe(presence1, "presence.present", presence)
|
||||
}
|
||||
|
||||
def updated()
|
||||
{
|
||||
unsubscribe()
|
||||
subscribe(presence1, "presence.present", presence)
|
||||
}
|
||||
|
||||
def presence(evt)
|
||||
{
|
||||
def anyLocked = lock1.count{it.currentLock == "unlocked"} != lock1.size()
|
||||
if (anyLocked) {
|
||||
sendPush "Unlocked door due to arrival of $evt.displayName"
|
||||
lock1.unlock()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Virtual Thermostat
|
||||
*
|
||||
* Author: SmartThings
|
||||
*/
|
||||
definition(
|
||||
name: "Virtual Thermostat",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Control a space heater or window air conditioner in conjunction with any temperature sensor, like a SmartSense Multi.",
|
||||
category: "Green Living",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/temp_thermo-switch.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/temp_thermo-switch@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("Choose a temperature sensor... "){
|
||||
input "sensor", "capability.temperatureMeasurement", title: "Sensor"
|
||||
}
|
||||
section("Select the heater or air conditioner outlet(s)... "){
|
||||
input "outlets", "capability.switch", title: "Outlets", multiple: true
|
||||
}
|
||||
section("Set the desired temperature..."){
|
||||
input "setpoint", "decimal", title: "Set Temp"
|
||||
}
|
||||
section("When there's been movement from (optional, leave blank to not require motion)..."){
|
||||
input "motion", "capability.motionSensor", title: "Motion", required: false
|
||||
}
|
||||
section("Within this number of minutes..."){
|
||||
input "minutes", "number", title: "Minutes", required: false
|
||||
}
|
||||
section("But never go below (or above if A/C) this value with or without motion..."){
|
||||
input "emergencySetpoint", "decimal", title: "Emer Temp", required: false
|
||||
}
|
||||
section("Select 'heat' for a heater and 'cool' for an air conditioner..."){
|
||||
input "mode", "enum", title: "Heating or cooling?", options: ["heat","cool"]
|
||||
}
|
||||
}
|
||||
|
||||
def installed()
|
||||
{
|
||||
subscribe(sensor, "temperature", temperatureHandler)
|
||||
if (motion) {
|
||||
subscribe(motion, "motion", motionHandler)
|
||||
}
|
||||
}
|
||||
|
||||
def updated()
|
||||
{
|
||||
unsubscribe()
|
||||
subscribe(sensor, "temperature", temperatureHandler)
|
||||
if (motion) {
|
||||
subscribe(motion, "motion", motionHandler)
|
||||
}
|
||||
}
|
||||
|
||||
def temperatureHandler(evt)
|
||||
{
|
||||
def isActive = hasBeenRecentMotion()
|
||||
if (isActive || emergencySetpoint) {
|
||||
evaluate(evt.doubleValue, isActive ? setpoint : emergencySetpoint)
|
||||
}
|
||||
else {
|
||||
outlets.off()
|
||||
}
|
||||
}
|
||||
|
||||
def motionHandler(evt)
|
||||
{
|
||||
if (evt.value == "active") {
|
||||
def lastTemp = sensor.currentTemperature
|
||||
if (lastTemp != null) {
|
||||
evaluate(lastTemp, setpoint)
|
||||
}
|
||||
} else if (evt.value == "inactive") {
|
||||
def isActive = hasBeenRecentMotion()
|
||||
log.debug "INACTIVE($isActive)"
|
||||
if (isActive || emergencySetpoint) {
|
||||
def lastTemp = sensor.currentTemperature
|
||||
if (lastTemp != null) {
|
||||
evaluate(lastTemp, isActive ? setpoint : emergencySetpoint)
|
||||
}
|
||||
}
|
||||
else {
|
||||
outlets.off()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private evaluate(currentTemp, desiredTemp)
|
||||
{
|
||||
log.debug "EVALUATE($currentTemp, $desiredTemp)"
|
||||
def threshold = 1.0
|
||||
if (mode == "cool") {
|
||||
// air conditioner
|
||||
if (currentTemp - desiredTemp >= threshold) {
|
||||
outlets.on()
|
||||
}
|
||||
else if (desiredTemp - currentTemp >= threshold) {
|
||||
outlets.off()
|
||||
}
|
||||
}
|
||||
else {
|
||||
// heater
|
||||
if (desiredTemp - currentTemp >= threshold) {
|
||||
outlets.on()
|
||||
}
|
||||
else if (currentTemp - desiredTemp >= threshold) {
|
||||
outlets.off()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private hasBeenRecentMotion()
|
||||
{
|
||||
def isActive = false
|
||||
if (motion && minutes) {
|
||||
def deltaMinutes = minutes as Long
|
||||
if (deltaMinutes) {
|
||||
def motionEvents = motion.eventsSince(new Date(now() - (60000 * deltaMinutes)))
|
||||
log.trace "Found ${motionEvents?.size() ?: 0} events in the last $deltaMinutes minutes"
|
||||
if (motionEvents.find { it.value == "active" }) {
|
||||
isActive = true
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
isActive = true
|
||||
}
|
||||
isActive
|
||||
}
|
||||
|
||||
@@ -0,0 +1,481 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Wattvision Manager
|
||||
*
|
||||
* Author: steve
|
||||
* Date: 2014-02-13
|
||||
*/
|
||||
|
||||
// Automatically generated. Make future change here.
|
||||
definition(
|
||||
name: "Wattvision Manager",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Monitor your whole-house energy use by connecting to your Wattvision account",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/wattvision.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/wattvision%402x.png",
|
||||
oauth: [displayName: "Wattvision", displayLink: "https://www.wattvision.com/"]
|
||||
)
|
||||
|
||||
preferences {
|
||||
page(name: "rootPage")
|
||||
}
|
||||
|
||||
def rootPage() {
|
||||
def sensors = state.sensors
|
||||
def hrefState = sensors ? "complete" : ""
|
||||
def hrefDescription = ""
|
||||
sensors.each { sensorId, sensorName ->
|
||||
hrefDescription += "${sensorName}\n"
|
||||
}
|
||||
|
||||
dynamicPage(name: "rootPage", install: sensors ? true : false, uninstall: true) {
|
||||
section {
|
||||
href(url: loginURL(), title: "Connect Wattvision Sensors", style: "embedded", description: hrefDescription, state: hrefState)
|
||||
}
|
||||
section {
|
||||
href(url: "https://www.wattvision.com", title: "Learn More About Wattvision", style: "external", description: null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mappings {
|
||||
path("/access") {
|
||||
actions:
|
||||
[
|
||||
POST : "setApiAccess",
|
||||
DELETE: "revokeApiAccess"
|
||||
]
|
||||
}
|
||||
path("/devices") {
|
||||
actions:
|
||||
[
|
||||
GET: "listDevices"
|
||||
]
|
||||
}
|
||||
path("/device/:sensorId") {
|
||||
actions:
|
||||
[
|
||||
GET : "getDevice",
|
||||
PUT : "updateDevice",
|
||||
POST : "createDevice",
|
||||
DELETE: "deleteDevice"
|
||||
]
|
||||
}
|
||||
path("/${loginCallbackPath()}") {
|
||||
actions:
|
||||
[
|
||||
GET: "loginCallback"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
|
||||
initialize()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
|
||||
unsubscribe()
|
||||
unschedule()
|
||||
initialize()
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
getDataFromWattvision()
|
||||
scheduleDataCollection()
|
||||
}
|
||||
|
||||
def getDataFromWattvision() {
|
||||
|
||||
log.trace "Getting data from Wattvision"
|
||||
|
||||
def children = getChildDevices()
|
||||
if (!children) {
|
||||
log.warn "No children. Not collecting data from Wattviwion"
|
||||
// currently only support one child
|
||||
return
|
||||
}
|
||||
|
||||
def endDate = new Date()
|
||||
def startDate
|
||||
|
||||
if (!state.lastUpdated) {
|
||||
// log.debug "no state.lastUpdated"
|
||||
startDate = new Date(hours: endDate.hours - 3)
|
||||
} else {
|
||||
// log.debug "parsing state.lastUpdated"
|
||||
startDate = new Date().parse(smartThingsDateFormat(), state.lastUpdated)
|
||||
}
|
||||
|
||||
state.lastUpdated = endDate.format(smartThingsDateFormat())
|
||||
|
||||
children.each { child ->
|
||||
getDataForChild(child, startDate, endDate)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def getDataForChild(child, startDate, endDate) {
|
||||
if (!child) {
|
||||
return
|
||||
}
|
||||
|
||||
def wattvisionURL = wattvisionURL(child.deviceNetworkId, startDate, endDate)
|
||||
if (wattvisionURL) {
|
||||
httpGet(uri: wattvisionURL) { response ->
|
||||
def json = new org.json.JSONObject(response.data.toString())
|
||||
child.addWattvisionData(json)
|
||||
return "success"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def wattvisionURL(senorId, startDate, endDate) {
|
||||
|
||||
log.trace "getting wattvisionURL"
|
||||
|
||||
def wattvisionApiAccess = state.wattvisionApiAccess
|
||||
if (!wattvisionApiAccess.id || !wattvisionApiAccess.key) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!endDate) {
|
||||
endDate = new Date()
|
||||
}
|
||||
if (!startDate) {
|
||||
startDate = new Date(hours: endDate.hours - 3)
|
||||
}
|
||||
|
||||
def diff = endDate.getTime() - startDate.getTime()
|
||||
if (diff > 259200000) { // 3 days in milliseconds
|
||||
// Wattvision only allows pulling 3 hours of data at a time
|
||||
startDate = new Date(hours: endDate.hours - 3)
|
||||
}
|
||||
|
||||
|
||||
def params = [
|
||||
"sensor_id" : senorId,
|
||||
"api_id" : wattvisionApiAccess.id,
|
||||
"api_key" : wattvisionApiAccess.key,
|
||||
"type" : wattvisionDataType ?: "rate",
|
||||
"start_time": startDate.format(wattvisionDateFormat()),
|
||||
"end_time" : endDate.format(wattvisionDateFormat())
|
||||
]
|
||||
|
||||
def parameterString = params.collect { key, value -> "${key.encodeAsURL()}=${value.encodeAsURL()}" }.join("&")
|
||||
def accessURL = wattvisionApiAccess.url ?: "https://www.wattvision.com/api/v0.2/elec"
|
||||
def url = "${accessURL}?${parameterString}"
|
||||
|
||||
// log.debug "wattvisionURL: ${url}"
|
||||
return url
|
||||
}
|
||||
|
||||
def getData() {
|
||||
state.lastUpdated = new Date().format(smartThingsDateFormat())
|
||||
}
|
||||
|
||||
public smartThingsDateFormat() { "yyyy-MM-dd'T'HH:mm:ss.SSSZ" }
|
||||
|
||||
public wattvisionDateFormat() { "yyyy-MM-dd'T'HH:mm:ss" }
|
||||
|
||||
def childMarshaller(child) {
|
||||
return [
|
||||
name : child.name,
|
||||
label : child.label,
|
||||
sensor_id: child.deviceNetworkId,
|
||||
location : child.location.name
|
||||
]
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
// ENDPOINTS
|
||||
// ========================================================
|
||||
|
||||
def listDevices() {
|
||||
getChildDevices().collect { childMarshaller(it) }
|
||||
}
|
||||
|
||||
def getDevice() {
|
||||
|
||||
log.trace "Getting device"
|
||||
|
||||
def child = getChildDevice(params.sensorId)
|
||||
|
||||
if (!child) {
|
||||
httpError(404, "Device not found")
|
||||
}
|
||||
|
||||
return childMarshaller(child)
|
||||
}
|
||||
|
||||
def updateDevice() {
|
||||
|
||||
log.trace "Updating Device with data from Wattvision"
|
||||
|
||||
def body = request.JSON
|
||||
|
||||
def child = getChildDevice(params.sensorId)
|
||||
|
||||
if (!child) {
|
||||
httpError(404, "Device not found")
|
||||
}
|
||||
|
||||
child.addWattvisionData(body)
|
||||
|
||||
render([status: 204, data: " "])
|
||||
}
|
||||
|
||||
def createDevice() {
|
||||
|
||||
log.trace "Creating Wattvision device"
|
||||
|
||||
if (getChildDevice(params.sensorId)) {
|
||||
httpError(403, "Device already exists")
|
||||
}
|
||||
|
||||
def child = addChildDevice("smartthings", "Wattvision", params.sensorId, null, [name: "Wattvision", label: request.JSON.label])
|
||||
|
||||
child.setGraphUrl(getGraphUrl(params.sensorId));
|
||||
|
||||
getDataForChild(child, null, null)
|
||||
|
||||
return childMarshaller(child)
|
||||
}
|
||||
|
||||
def deleteDevice() {
|
||||
|
||||
log.trace "Deleting Wattvision device"
|
||||
|
||||
deleteChildDevice(params.sensorId)
|
||||
render([status: 204, data: " "])
|
||||
}
|
||||
|
||||
def setApiAccess() {
|
||||
|
||||
log.trace "Granting access to Wattvision API"
|
||||
|
||||
def body = request.JSON
|
||||
|
||||
state.wattvisionApiAccess = [
|
||||
url: body.url,
|
||||
id : body.id,
|
||||
key: body.key
|
||||
]
|
||||
|
||||
scheduleDataCollection()
|
||||
|
||||
render([status: 204, data: " "])
|
||||
}
|
||||
|
||||
def scheduleDataCollection() {
|
||||
schedule("* /1 * * * ?", "getDataFromWattvision") // every 1 minute
|
||||
}
|
||||
|
||||
def revokeApiAccess() {
|
||||
|
||||
log.trace "Revoking access to Wattvision API"
|
||||
|
||||
state.wattvisionApiAccess = [:]
|
||||
render([status: 204, data: " "])
|
||||
}
|
||||
|
||||
public getGraphUrl(sensorId) {
|
||||
|
||||
log.trace "Collecting URL for Wattvision graph"
|
||||
|
||||
def apiId = state.wattvisionApiAccess.id
|
||||
def apiKey = state.wattvisionApiAccess.key
|
||||
|
||||
// TODO: allow the changing of type?
|
||||
"http://www.wattvision.com/partners/smartthings/charts?s=${sensorId}&api_id=${apiId}&api_key=${apiKey}&type=w"
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
// SmartThings initiated setup
|
||||
// ========================================================
|
||||
|
||||
/* Debug info for Steve / Andrew
|
||||
|
||||
this page: /partners/smartthings/whatswv
|
||||
- linked from within smartthings, will tell you how to get a wattvision sensor, etc.
|
||||
- pass the debug flag (?debug=1) to show this text.
|
||||
|
||||
login page: /partners/smartthings/login?callback_url=CALLBACKURL
|
||||
- open this page, which will require login.
|
||||
- once login is complete, we call you back at callback_url with:
|
||||
<callback_url>?id=<wattvision_api_id>&key=<wattvision_api_key>
|
||||
question: will you know which user this is on your end?
|
||||
|
||||
sensor json: /partners/smartthings/sensor_list?api_id=...&api_key=...
|
||||
- returns a list of sensors and their associated house names, as a json object
|
||||
- example return value with one sensor id 2, associated with house 'Test's House'
|
||||
- content type is application/json
|
||||
- {"2": "Test's House"}
|
||||
|
||||
*/
|
||||
|
||||
def loginCallback() {
|
||||
log.trace "loginCallback"
|
||||
|
||||
state.wattvisionApiAccess = [
|
||||
id : params.id,
|
||||
key: params.key
|
||||
]
|
||||
|
||||
getSensorJSON(params.id, params.key)
|
||||
|
||||
connectionSuccessful("Wattvision", "https://s3.amazonaws.com/smartapp-icons/Partner/wattvision@2x.png")
|
||||
}
|
||||
|
||||
private getSensorJSON(id, key) {
|
||||
log.trace "getSensorJSON"
|
||||
|
||||
def sensorUrl = "${wattvisionBaseURL()}/partners/smartthings/sensor_list?api_id=${id}&api_key=${key}"
|
||||
|
||||
httpGet(uri: sensorUrl) { response ->
|
||||
|
||||
def json = new org.json.JSONObject(response.data)
|
||||
|
||||
state.sensors = json
|
||||
|
||||
json.each { sensorId, sensorName ->
|
||||
createChild(sensorId, sensorName)
|
||||
}
|
||||
|
||||
return "success"
|
||||
}
|
||||
}
|
||||
|
||||
def createChild(sensorId, sensorName) {
|
||||
log.trace "creating Wattvision Child"
|
||||
|
||||
def child = getChildDevice(sensorId)
|
||||
|
||||
if (child) {
|
||||
log.warn "Device already exists"
|
||||
} else {
|
||||
child = addChildDevice("smartthings", "Wattvision", sensorId, null, [name: "Wattvision", label: sensorName])
|
||||
}
|
||||
|
||||
child.setGraphUrl(getGraphUrl(sensorId));
|
||||
|
||||
getDataForChild(child, null, null)
|
||||
|
||||
scheduleDataCollection()
|
||||
|
||||
return childMarshaller(child)
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
// URL HELPERS
|
||||
// ========================================================
|
||||
|
||||
private loginURL() { "${wattvisionBaseURL()}${loginPath()}" }
|
||||
|
||||
private wattvisionBaseURL() { "https://www.wattvision.com" }
|
||||
|
||||
private loginPath() { "/partners/smartthings/login?callback_url=${loginCallbackURL().encodeAsURL()}" }
|
||||
|
||||
private loginCallbackURL() {
|
||||
if (!atomicState.accessToken) { createAccessToken() }
|
||||
buildActionUrl(loginCallbackPath())
|
||||
}
|
||||
private loginCallbackPath() { "login/callback" }
|
||||
|
||||
// ========================================================
|
||||
// Access Token
|
||||
// ========================================================
|
||||
|
||||
private getMyAccessToken() { return atomicState.accessToken ?: createAccessToken() }
|
||||
|
||||
// ========================================================
|
||||
// CONNECTED HTML
|
||||
// ========================================================
|
||||
|
||||
def connectionSuccessful(deviceName, iconSrc) {
|
||||
def html = """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=640">
|
||||
<title>Withings Connection</title>
|
||||
<style type="text/css">
|
||||
@font-face {
|
||||
font-family: 'Swiss 721 W01 Thin';
|
||||
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot');
|
||||
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot?#iefix') format('embedded-opentype'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.woff') format('woff'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.ttf') format('truetype'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.svg#swis721_th_btthin') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Swiss 721 W01 Light';
|
||||
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot');
|
||||
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot?#iefix') format('embedded-opentype'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.woff') format('woff'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.ttf') format('truetype'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.svg#swis721_lt_btlight') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
.container {
|
||||
width: 560px;
|
||||
padding: 40px;
|
||||
/*background: #eee;*/
|
||||
text-align: center;
|
||||
}
|
||||
img {
|
||||
vertical-align: middle;
|
||||
}
|
||||
img:nth-child(2) {
|
||||
margin: 0 30px;
|
||||
}
|
||||
p {
|
||||
font-size: 2.2em;
|
||||
font-family: 'Swiss 721 W01 Thin';
|
||||
text-align: center;
|
||||
color: #666666;
|
||||
padding: 0 40px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
/*
|
||||
p:last-child {
|
||||
margin-top: 0px;
|
||||
}
|
||||
*/
|
||||
span {
|
||||
font-family: 'Swiss 721 W01 Light';
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<img src="${iconSrc}" alt="${deviceName} icon" />
|
||||
<img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/connected-device-icn%402x.png" alt="connected device icon" />
|
||||
<img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/st-logo%402x.png" alt="SmartThings logo" />
|
||||
<p>Your ${deviceName} is now connected to SmartThings!</p>
|
||||
<p>Click 'Done' to finish setup.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
render contentType: 'text/html', data: html
|
||||
}
|
||||
|
||||
652
smartapps/smartthings/wemo-connect.src/wemo-connect.groovy
Normal file
652
smartapps/smartthings/wemo-connect.src/wemo-connect.groovy
Normal file
@@ -0,0 +1,652 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Wemo Service Manager
|
||||
*
|
||||
* Author: superuser
|
||||
* Date: 2013-09-06
|
||||
*/
|
||||
definition(
|
||||
name: "Wemo (Connect)",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Allows you to integrate your WeMo Switch and Wemo Motion sensor with SmartThings.",
|
||||
category: "SmartThings Labs",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/wemo.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/wemo@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
page(name:"firstPage", title:"Wemo Device Setup", content:"firstPage")
|
||||
}
|
||||
|
||||
private discoverAllWemoTypes()
|
||||
{
|
||||
sendHubCommand(new physicalgraph.device.HubAction("lan discovery urn:Belkin:device:insight:1/urn:Belkin:device:controllee:1/urn:Belkin:device:sensor:1/urn:Belkin:device:lightswitch:1", physicalgraph.device.Protocol.LAN))
|
||||
}
|
||||
|
||||
private getFriendlyName(String deviceNetworkId) {
|
||||
sendHubCommand(new physicalgraph.device.HubAction("""GET /setup.xml HTTP/1.1
|
||||
HOST: ${deviceNetworkId}
|
||||
|
||||
""", physicalgraph.device.Protocol.LAN, "${deviceNetworkId}"))
|
||||
}
|
||||
|
||||
private verifyDevices() {
|
||||
def switches = getWemoSwitches().findAll { it?.value?.verified != true }
|
||||
def motions = getWemoMotions().findAll { it?.value?.verified != true }
|
||||
def lightSwitches = getWemoLightSwitches().findAll { it?.value?.verified != true }
|
||||
def devices = switches + motions + lightSwitches
|
||||
devices.each {
|
||||
getFriendlyName((it.value.ip + ":" + it.value.port))
|
||||
}
|
||||
}
|
||||
|
||||
def firstPage()
|
||||
{
|
||||
if(canInstallLabs())
|
||||
{
|
||||
int refreshCount = !state.refreshCount ? 0 : state.refreshCount as int
|
||||
state.refreshCount = refreshCount + 1
|
||||
def refreshInterval = 5
|
||||
|
||||
log.debug "REFRESH COUNT :: ${refreshCount}"
|
||||
|
||||
if(!state.subscribe) {
|
||||
subscribe(location, null, locationHandler, [filterEvents:false])
|
||||
state.subscribe = true
|
||||
}
|
||||
|
||||
//ssdp request every 25 seconds
|
||||
if((refreshCount % 5) == 0) {
|
||||
discoverAllWemoTypes()
|
||||
}
|
||||
|
||||
//setup.xml request every 5 seconds except on discoveries
|
||||
if(((refreshCount % 1) == 0) && ((refreshCount % 5) != 0)) {
|
||||
verifyDevices()
|
||||
}
|
||||
|
||||
def switchesDiscovered = switchesDiscovered()
|
||||
def motionsDiscovered = motionsDiscovered()
|
||||
def lightSwitchesDiscovered = lightSwitchesDiscovered()
|
||||
|
||||
return dynamicPage(name:"firstPage", title:"Discovery Started!", nextPage:"", refreshInterval: refreshInterval, install:true, uninstall: selectedSwitches != null || selectedMotions != null || selectedLightSwitches != null) {
|
||||
section("Select a device...") {
|
||||
input "selectedSwitches", "enum", required:false, title:"Select Wemo Switches \n(${switchesDiscovered.size() ?: 0} found)", multiple:true, options:switchesDiscovered
|
||||
input "selectedMotions", "enum", required:false, title:"Select Wemo Motions \n(${motionsDiscovered.size() ?: 0} found)", multiple:true, options:motionsDiscovered
|
||||
input "selectedLightSwitches", "enum", required:false, title:"Select Wemo Light Switches \n(${lightSwitchesDiscovered.size() ?: 0} found)", multiple:true, options:lightSwitchesDiscovered
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
def upgradeNeeded = """To use SmartThings Labs, your Hub should be completely up to date.
|
||||
|
||||
To update your Hub, access Location Settings in the Main Menu (tap the gear next to your location name), select your Hub, and choose "Update Hub"."""
|
||||
|
||||
return dynamicPage(name:"firstPage", title:"Upgrade needed!", nextPage:"", install:false, uninstall: true) {
|
||||
section("Upgrade") {
|
||||
paragraph "$upgradeNeeded"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def devicesDiscovered() {
|
||||
def switches = getWemoSwitches()
|
||||
def motions = getWemoMotions()
|
||||
def lightSwitches = getWemoLightSwitches()
|
||||
def devices = switches + motions + lightSwitches
|
||||
def list = []
|
||||
|
||||
list = devices?.collect{ [app.id, it.ssdpUSN].join('.') }
|
||||
}
|
||||
|
||||
def switchesDiscovered() {
|
||||
def switches = getWemoSwitches().findAll { it?.value?.verified == true }
|
||||
def map = [:]
|
||||
switches.each {
|
||||
def value = it.value.name ?: "WeMo Switch ${it.value.ssdpUSN.split(':')[1][-3..-1]}"
|
||||
def key = it.value.mac
|
||||
map["${key}"] = value
|
||||
}
|
||||
map
|
||||
}
|
||||
|
||||
def motionsDiscovered() {
|
||||
def motions = getWemoMotions().findAll { it?.value?.verified == true }
|
||||
def map = [:]
|
||||
motions.each {
|
||||
def value = it.value.name ?: "WeMo Motion ${it.value.ssdpUSN.split(':')[1][-3..-1]}"
|
||||
def key = it.value.mac
|
||||
map["${key}"] = value
|
||||
}
|
||||
map
|
||||
}
|
||||
|
||||
def lightSwitchesDiscovered() {
|
||||
//def vmotions = switches.findAll { it?.verified == true }
|
||||
//log.trace "MOTIONS HERE: ${vmotions}"
|
||||
def lightSwitches = getWemoLightSwitches().findAll { it?.value?.verified == true }
|
||||
def map = [:]
|
||||
lightSwitches.each {
|
||||
def value = it.value.name ?: "WeMo Light Switch ${it.value.ssdpUSN.split(':')[1][-3..-1]}"
|
||||
def key = it.value.mac
|
||||
map["${key}"] = value
|
||||
}
|
||||
map
|
||||
}
|
||||
|
||||
def getWemoSwitches()
|
||||
{
|
||||
if (!state.switches) { state.switches = [:] }
|
||||
state.switches
|
||||
}
|
||||
|
||||
def getWemoMotions()
|
||||
{
|
||||
if (!state.motions) { state.motions = [:] }
|
||||
state.motions
|
||||
}
|
||||
|
||||
def getWemoLightSwitches()
|
||||
{
|
||||
if (!state.lightSwitches) { state.lightSwitches = [:] }
|
||||
state.lightSwitches
|
||||
}
|
||||
|
||||
def installed() {
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
initialize()
|
||||
|
||||
runIn(5, "subscribeToDevices") //initial subscriptions delayed by 5 seconds
|
||||
runIn(10, "refreshDevices") //refresh devices, delayed by 10 seconds
|
||||
runIn(900, "doDeviceSync" , [overwrite: false]) //setup ip:port syncing every 15 minutes
|
||||
|
||||
// SUBSCRIBE responses come back with TIMEOUT-1801 (30 minutes), so we refresh things a bit before they expire (29 minutes)
|
||||
runIn(1740, "refresh", [overwrite: false])
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
initialize()
|
||||
|
||||
runIn(5, "subscribeToDevices") //subscribe again to new/old devices wait 5 seconds
|
||||
runIn(10, "refreshDevices") //refresh devices again, delayed by 10 seconds
|
||||
}
|
||||
|
||||
def resubscribe() {
|
||||
log.debug "Resubscribe called, delegating to refresh()"
|
||||
refresh()
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
log.debug "refresh() called"
|
||||
//reschedule the refreshes
|
||||
runIn(1740, "refresh", [overwrite: false])
|
||||
refreshDevices()
|
||||
}
|
||||
|
||||
def refreshDevices() {
|
||||
log.debug "refreshDevices() called"
|
||||
def devices = getAllChildDevices()
|
||||
devices.each { d ->
|
||||
log.debug "Calling refresh() on device: ${d.id}"
|
||||
d.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
def subscribeToDevices() {
|
||||
log.debug "subscribeToDevices() called"
|
||||
def devices = getAllChildDevices()
|
||||
devices.each { d ->
|
||||
d.subscribe()
|
||||
}
|
||||
}
|
||||
|
||||
def addSwitches() {
|
||||
def switches = getWemoSwitches()
|
||||
|
||||
selectedSwitches.each { dni ->
|
||||
def selectedSwitch = switches.find { it.value.mac == dni } ?: switches.find { "${it.value.ip}:${it.value.port}" == dni }
|
||||
def d
|
||||
if (selectedSwitch) {
|
||||
d = getChildDevices()?.find {
|
||||
it.dni == selectedSwitch.value.mac || it.device.getDataValue("mac") == selectedSwitch.value.mac
|
||||
}
|
||||
}
|
||||
|
||||
if (!d) {
|
||||
log.debug "Creating WeMo Switch with dni: ${selectedSwitch.value.mac}"
|
||||
d = addChildDevice("smartthings", "Wemo Switch", selectedSwitch.value.mac, selectedSwitch?.value.hub, [
|
||||
"label": selectedSwitch?.value?.name ?: "Wemo Switch",
|
||||
"data": [
|
||||
"mac": selectedSwitch.value.mac,
|
||||
"ip": selectedSwitch.value.ip,
|
||||
"port": selectedSwitch.value.port
|
||||
]
|
||||
])
|
||||
|
||||
log.debug "Created ${d.displayName} with id: ${d.id}, dni: ${d.deviceNetworkId}"
|
||||
} else {
|
||||
log.debug "found ${d.displayName} with id $dni already exists"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def addMotions() {
|
||||
def motions = getWemoMotions()
|
||||
|
||||
selectedMotions.each { dni ->
|
||||
def selectedMotion = motions.find { it.value.mac == dni } ?: motions.find { "${it.value.ip}:${it.value.port}" == dni }
|
||||
def d
|
||||
if (selectedMotion) {
|
||||
d = getChildDevices()?.find {
|
||||
it.dni == selectedMotion.value.mac || it.device.getDataValue("mac") == selectedMotion.value.mac
|
||||
}
|
||||
}
|
||||
|
||||
if (!d) {
|
||||
log.debug "Creating WeMo Motion with dni: ${selectedMotion.value.mac}"
|
||||
d = addChildDevice("smartthings", "Wemo Motion", selectedMotion.value.mac, selectedMotion?.value.hub, [
|
||||
"label": selectedMotion?.value?.name ?: "Wemo Motion",
|
||||
"data": [
|
||||
"mac": selectedMotion.value.mac,
|
||||
"ip": selectedMotion.value.ip,
|
||||
"port": selectedMotion.value.port
|
||||
]
|
||||
])
|
||||
|
||||
log.debug "Created ${d.displayName} with id: ${d.id}, dni: ${d.deviceNetworkId}"
|
||||
} else {
|
||||
log.debug "found ${d.displayName} with id $dni already exists"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def addLightSwitches() {
|
||||
def lightSwitches = getWemoLightSwitches()
|
||||
|
||||
selectedLightSwitches.each { dni ->
|
||||
def selectedLightSwitch = lightSwitches.find { it.value.mac == dni } ?: lightSwitches.find { "${it.value.ip}:${it.value.port}" == dni }
|
||||
def d
|
||||
if (selectedLightSwitch) {
|
||||
d = getChildDevices()?.find {
|
||||
it.dni == selectedLightSwitch.value.mac || it.device.getDataValue("mac") == selectedLightSwitch.value.mac
|
||||
}
|
||||
}
|
||||
|
||||
if (!d) {
|
||||
log.debug "Creating WeMo Light Switch with dni: ${selectedLightSwitch.value.mac}"
|
||||
d = addChildDevice("smartthings", "Wemo Light Switch", selectedLightSwitch.value.mac, selectedLightSwitch?.value.hub, [
|
||||
"label": selectedLightSwitch?.value?.name ?: "Wemo Light Switch",
|
||||
"data": [
|
||||
"mac": selectedLightSwitch.value.mac,
|
||||
"ip": selectedLightSwitch.value.ip,
|
||||
"port": selectedLightSwitch.value.port
|
||||
]
|
||||
])
|
||||
|
||||
log.debug "created ${d.displayName} with id $dni"
|
||||
} else {
|
||||
log.debug "found ${d.displayName} with id $dni already exists"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
// remove location subscription afterwards
|
||||
unsubscribe()
|
||||
state.subscribe = false
|
||||
|
||||
if (selectedSwitches)
|
||||
{
|
||||
addSwitches()
|
||||
}
|
||||
|
||||
if (selectedMotions)
|
||||
{
|
||||
addMotions()
|
||||
}
|
||||
|
||||
if (selectedLightSwitches)
|
||||
{
|
||||
addLightSwitches()
|
||||
}
|
||||
}
|
||||
|
||||
def locationHandler(evt) {
|
||||
def description = evt.description
|
||||
def hub = evt?.hubId
|
||||
def parsedEvent = parseDiscoveryMessage(description)
|
||||
parsedEvent << ["hub":hub]
|
||||
log.debug parsedEvent
|
||||
|
||||
if (parsedEvent?.ssdpTerm?.contains("Belkin:device:controllee") || parsedEvent?.ssdpTerm?.contains("Belkin:device:insight")) {
|
||||
|
||||
def switches = getWemoSwitches()
|
||||
|
||||
if (!(switches."${parsedEvent.ssdpUSN.toString()}"))
|
||||
{ //if it doesn't already exist
|
||||
switches << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent]
|
||||
}
|
||||
else
|
||||
{ // just update the values
|
||||
|
||||
log.debug "Device was already found in state..."
|
||||
|
||||
def d = switches."${parsedEvent.ssdpUSN.toString()}"
|
||||
boolean deviceChangedValues = false
|
||||
|
||||
if(d.ip != parsedEvent.ip || d.port != parsedEvent.port) {
|
||||
d.ip = parsedEvent.ip
|
||||
d.port = parsedEvent.port
|
||||
deviceChangedValues = true
|
||||
log.debug "Device's port or ip changed..."
|
||||
}
|
||||
|
||||
if (deviceChangedValues) {
|
||||
def children = getChildDevices()
|
||||
log.debug "Found children ${children}"
|
||||
children.each {
|
||||
if (it.getDeviceDataByName("mac") == parsedEvent.mac) {
|
||||
log.debug "updating ip and port, and resubscribing, for device ${it} with mac ${parsedEvent.mac}"
|
||||
it.subscribe(parsedEvent.ip, parsedEvent.port)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
else if (parsedEvent?.ssdpTerm?.contains("Belkin:device:sensor")) {
|
||||
|
||||
def motions = getWemoMotions()
|
||||
|
||||
if (!(motions."${parsedEvent.ssdpUSN.toString()}"))
|
||||
{ //if it doesn't already exist
|
||||
motions << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent]
|
||||
}
|
||||
else
|
||||
{ // just update the values
|
||||
|
||||
log.debug "Device was already found in state..."
|
||||
|
||||
def d = motions."${parsedEvent.ssdpUSN.toString()}"
|
||||
boolean deviceChangedValues = false
|
||||
|
||||
if(d.ip != parsedEvent.ip || d.port != parsedEvent.port) {
|
||||
d.ip = parsedEvent.ip
|
||||
d.port = parsedEvent.port
|
||||
deviceChangedValues = true
|
||||
log.debug "Device's port or ip changed..."
|
||||
}
|
||||
|
||||
if (deviceChangedValues) {
|
||||
def children = getChildDevices()
|
||||
log.debug "Found children ${children}"
|
||||
children.each {
|
||||
if (it.getDeviceDataByName("mac") == parsedEvent.mac) {
|
||||
log.debug "updating ip and port, and resubscribing, for device ${it} with mac ${parsedEvent.mac}"
|
||||
it.subscribe(parsedEvent.ip, parsedEvent.port)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
else if (parsedEvent?.ssdpTerm?.contains("Belkin:device:lightswitch")) {
|
||||
|
||||
def lightSwitches = getWemoLightSwitches()
|
||||
|
||||
if (!(lightSwitches."${parsedEvent.ssdpUSN.toString()}"))
|
||||
{ //if it doesn't already exist
|
||||
lightSwitches << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent]
|
||||
}
|
||||
else
|
||||
{ // just update the values
|
||||
|
||||
log.debug "Device was already found in state..."
|
||||
|
||||
def d = lightSwitches."${parsedEvent.ssdpUSN.toString()}"
|
||||
boolean deviceChangedValues = false
|
||||
|
||||
if(d.ip != parsedEvent.ip || d.port != parsedEvent.port) {
|
||||
d.ip = parsedEvent.ip
|
||||
d.port = parsedEvent.port
|
||||
deviceChangedValues = true
|
||||
log.debug "Device's port or ip changed..."
|
||||
}
|
||||
|
||||
if (deviceChangedValues) {
|
||||
def children = getChildDevices()
|
||||
log.debug "Found children ${children}"
|
||||
children.each {
|
||||
if (it.getDeviceDataByName("mac") == parsedEvent.mac) {
|
||||
log.debug "updating ip and port, and resubscribing, for device ${it} with mac ${parsedEvent.mac}"
|
||||
it.subscribe(parsedEvent.ip, parsedEvent.port)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
else if (parsedEvent.headers && parsedEvent.body) {
|
||||
String headerString = new String(parsedEvent.headers.decodeBase64())?.toLowerCase()
|
||||
if (headerString != null && (headerString.contains('text/xml') || headerString.contains('application/xml'))) {
|
||||
def body = parseXmlBody(parsedEvent.body)
|
||||
if (body?.device?.deviceType?.text().startsWith("urn:Belkin:device:controllee:1"))
|
||||
{
|
||||
def switches = getWemoSwitches()
|
||||
def wemoSwitch = switches.find {it?.key?.contains(body?.device?.UDN?.text())}
|
||||
if (wemoSwitch)
|
||||
{
|
||||
wemoSwitch.value << [name:body?.device?.friendlyName?.text(), verified: true]
|
||||
}
|
||||
else
|
||||
{
|
||||
log.error "/setup.xml returned a wemo device that didn't exist"
|
||||
}
|
||||
}
|
||||
|
||||
if (body?.device?.deviceType?.text().startsWith("urn:Belkin:device:insight:1"))
|
||||
{
|
||||
def switches = getWemoSwitches()
|
||||
def wemoSwitch = switches.find {it?.key?.contains(body?.device?.UDN?.text())}
|
||||
if (wemoSwitch)
|
||||
{
|
||||
wemoSwitch.value << [name:body?.device?.friendlyName?.text(), verified: true]
|
||||
}
|
||||
else
|
||||
{
|
||||
log.error "/setup.xml returned a wemo device that didn't exist"
|
||||
}
|
||||
}
|
||||
|
||||
if (body?.device?.deviceType?.text().startsWith("urn:Belkin:device:sensor")) //?:1
|
||||
{
|
||||
def motions = getWemoMotions()
|
||||
def wemoMotion = motions.find {it?.key?.contains(body?.device?.UDN?.text())}
|
||||
if (wemoMotion)
|
||||
{
|
||||
wemoMotion.value << [name:body?.device?.friendlyName?.text(), verified: true]
|
||||
}
|
||||
else
|
||||
{
|
||||
log.error "/setup.xml returned a wemo device that didn't exist"
|
||||
}
|
||||
}
|
||||
|
||||
if (body?.device?.deviceType?.text().startsWith("urn:Belkin:device:lightswitch")) //?:1
|
||||
{
|
||||
def lightSwitches = getWemoLightSwitches()
|
||||
def wemoLightSwitch = lightSwitches.find {it?.key?.contains(body?.device?.UDN?.text())}
|
||||
if (wemoLightSwitch)
|
||||
{
|
||||
wemoLightSwitch.value << [name:body?.device?.friendlyName?.text(), verified: true]
|
||||
}
|
||||
else
|
||||
{
|
||||
log.error "/setup.xml returned a wemo device that didn't exist"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def parseXmlBody(def body) {
|
||||
def decodedBytes = body.decodeBase64()
|
||||
def bodyString
|
||||
try {
|
||||
bodyString = new String(decodedBytes)
|
||||
} catch (Exception e) {
|
||||
// Keep this log for debugging StringIndexOutOfBoundsException issue
|
||||
log.error("Exception decoding bytes in sonos connect: ${decodedBytes}")
|
||||
throw e
|
||||
}
|
||||
return new XmlSlurper().parseText(bodyString)
|
||||
}
|
||||
|
||||
private def parseDiscoveryMessage(String description) {
|
||||
def device = [:]
|
||||
def parts = description.split(',')
|
||||
parts.each { part ->
|
||||
part = part.trim()
|
||||
if (part.startsWith('devicetype:')) {
|
||||
def valueString = part.split(":")[1].trim()
|
||||
device.devicetype = valueString
|
||||
}
|
||||
else if (part.startsWith('mac:')) {
|
||||
def valueString = part.split(":")[1].trim()
|
||||
if (valueString) {
|
||||
device.mac = valueString
|
||||
}
|
||||
}
|
||||
else if (part.startsWith('networkAddress:')) {
|
||||
def valueString = part.split(":")[1].trim()
|
||||
if (valueString) {
|
||||
device.ip = valueString
|
||||
}
|
||||
}
|
||||
else if (part.startsWith('deviceAddress:')) {
|
||||
def valueString = part.split(":")[1].trim()
|
||||
if (valueString) {
|
||||
device.port = valueString
|
||||
}
|
||||
}
|
||||
else if (part.startsWith('ssdpPath:')) {
|
||||
def valueString = part.split(":")[1].trim()
|
||||
if (valueString) {
|
||||
device.ssdpPath = valueString
|
||||
}
|
||||
}
|
||||
else if (part.startsWith('ssdpUSN:')) {
|
||||
part -= "ssdpUSN:"
|
||||
def valueString = part.trim()
|
||||
if (valueString) {
|
||||
device.ssdpUSN = valueString
|
||||
}
|
||||
}
|
||||
else if (part.startsWith('ssdpTerm:')) {
|
||||
part -= "ssdpTerm:"
|
||||
def valueString = part.trim()
|
||||
if (valueString) {
|
||||
device.ssdpTerm = valueString
|
||||
}
|
||||
}
|
||||
else if (part.startsWith('headers')) {
|
||||
part -= "headers:"
|
||||
def valueString = part.trim()
|
||||
if (valueString) {
|
||||
device.headers = valueString
|
||||
}
|
||||
}
|
||||
else if (part.startsWith('body')) {
|
||||
part -= "body:"
|
||||
def valueString = part.trim()
|
||||
if (valueString) {
|
||||
device.body = valueString
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
device
|
||||
}
|
||||
|
||||
def doDeviceSync(){
|
||||
log.debug "Doing Device Sync!"
|
||||
runIn(900, "doDeviceSync" , [overwrite: false]) //schedule to run again in 15 minutes
|
||||
|
||||
if(!state.subscribe) {
|
||||
subscribe(location, null, locationHandler, [filterEvents:false])
|
||||
state.subscribe = true
|
||||
}
|
||||
|
||||
discoverAllWemoTypes()
|
||||
}
|
||||
|
||||
def pollChildren() {
|
||||
def devices = getAllChildDevices()
|
||||
devices.each { d ->
|
||||
//only poll switches?
|
||||
d.poll()
|
||||
}
|
||||
}
|
||||
|
||||
def delayPoll() {
|
||||
log.debug "Executing 'delayPoll'"
|
||||
|
||||
runIn(5, "pollChildren")
|
||||
}
|
||||
|
||||
/*def poll() {
|
||||
log.debug "Executing 'poll'"
|
||||
runIn(600, "poll", [overwrite: false]) //schedule to run again in 10 minutes
|
||||
|
||||
def lastPoll = getLastPollTime()
|
||||
def currentTime = now()
|
||||
def lastPollDiff = currentTime - lastPoll
|
||||
log.debug "lastPoll: $lastPoll, currentTime: $currentTime, lastPollDiff: $lastPollDiff"
|
||||
setLastPollTime(currentTime)
|
||||
|
||||
doDeviceSync()
|
||||
}
|
||||
|
||||
|
||||
def setLastPollTime(currentTime) {
|
||||
state.lastpoll = currentTime
|
||||
}
|
||||
|
||||
def getLastPollTime() {
|
||||
state.lastpoll ?: now()
|
||||
}
|
||||
|
||||
def now() {
|
||||
new Date().getTime()
|
||||
}*/
|
||||
|
||||
private Boolean canInstallLabs()
|
||||
{
|
||||
return hasAllHubsOver("000.011.00603")
|
||||
}
|
||||
|
||||
private Boolean hasAllHubsOver(String desiredFirmware)
|
||||
{
|
||||
return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware }
|
||||
}
|
||||
|
||||
private List getRealHubFirmwareVersions()
|
||||
{
|
||||
return location.hubs*.firmwareVersionString.findAll { it }
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* When It's Going To Rain
|
||||
*
|
||||
* Author: SmartThings
|
||||
*/
|
||||
definition(
|
||||
name: "When It's Going to Rain",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Is your shed closed? Are your windows shut? Is the grill covered? Are your dogs indoors? Will the lawn and plants need to be watered tomorrow?",
|
||||
category: "Convenience",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/text.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/text@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("Zip code..."){
|
||||
input "zipcode", "text", title: "Zipcode?"
|
||||
}
|
||||
// TODO: would be nice to cron this so we could check every hour or so
|
||||
section("Check at..."){
|
||||
input "time", "time", title: "When?"
|
||||
}
|
||||
section("Things to check..."){
|
||||
input "sensors", "capability.contactSensor", multiple: true
|
||||
}
|
||||
section("Text me if I anything is open..."){
|
||||
input("recipients", "contact", title: "Send notifications to") {
|
||||
input "phone", "phone", title: "Phone number?"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def installed() {
|
||||
log.debug "Installed: $settings"
|
||||
schedule(time, "scheduleCheck")
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "Updated: $settings"
|
||||
unschedule()
|
||||
schedule(time, "scheduleCheck")
|
||||
}
|
||||
|
||||
def scheduleCheck() {
|
||||
def response = getWeatherFeature("forecast", zipcode)
|
||||
if (isStormy(response)) {
|
||||
def open = sensors.findAll { it?.latestValue("contact") == 'open' }
|
||||
if (open) {
|
||||
if (location.contactBookEnabled) {
|
||||
sendNotificationToContacts("A storm is a coming and the following things are open: ${open.join(', ')}", recipients)
|
||||
}
|
||||
else {
|
||||
sendSms(phone, "A storm is a coming and the following things are open: ${open.join(', ')}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private isStormy(json) {
|
||||
def STORMY = ['rain', 'snow', 'showers', 'sprinkles', 'precipitation']
|
||||
|
||||
def forecast = json?.forecast?.txt_forecast?.forecastday?.first()
|
||||
if (forecast) {
|
||||
def text = forecast?.fcttext?.toLowerCase()
|
||||
if (text) {
|
||||
def result = false
|
||||
for (int i = 0; i < STORMY.size() && !result; i++) {
|
||||
result = text.contains(STORMY[i])
|
||||
}
|
||||
return result
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
log.warn "Did not get a forecast: $json"
|
||||
return false
|
||||
}
|
||||
}
|
||||
578
smartapps/smartthings/withings.src/withings.groovy
Normal file
578
smartapps/smartthings/withings.src/withings.groovy
Normal file
@@ -0,0 +1,578 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Withings Service Manager
|
||||
*
|
||||
* Author: SmartThings
|
||||
* Date: 2013-09-26
|
||||
*/
|
||||
|
||||
definition(
|
||||
name: "Withings",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Connect your Withings scale to SmartThings.",
|
||||
category: "Connections",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/withings.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/withings%402x.png",
|
||||
oauth: true
|
||||
) {
|
||||
appSetting "clientId"
|
||||
appSetting "clientSecret"
|
||||
appSetting "serverUrl"
|
||||
}
|
||||
|
||||
preferences {
|
||||
page(name: "auth", title: "Withings", content:"authPage")
|
||||
}
|
||||
|
||||
mappings {
|
||||
path("/exchange") {
|
||||
action: [
|
||||
GET: "exchangeToken"
|
||||
]
|
||||
}
|
||||
path("/load") {
|
||||
action: [
|
||||
GET: "load"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
def authPage() {
|
||||
log.debug "authPage()"
|
||||
dynamicPage(name: "auth", title: "Withings", install:false, uninstall:true) {
|
||||
section {
|
||||
paragraph "This version is no longer supported. Please uninstall it."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def oauthInitUrl() {
|
||||
def token = getToken()
|
||||
log.debug "initiateOauth got token: $token"
|
||||
|
||||
// store these for validate after the user takes the oauth journey
|
||||
state.oauth_request_token = token.oauth_token
|
||||
state.oauth_request_token_secret = token.oauth_token_secret
|
||||
|
||||
return buildOauthUrlWithToken(token.oauth_token, token.oauth_token_secret)
|
||||
}
|
||||
|
||||
def getToken() {
|
||||
def callback = getServerUrl() + "/api/smartapps/installations/${app.id}/exchange?access_token=${state.accessToken}"
|
||||
def params = [
|
||||
oauth_callback:URLEncoder.encode(callback),
|
||||
]
|
||||
def requestTokenBaseUrl = "https://oauth.withings.com/account/request_token"
|
||||
def url = buildSignedUrl(requestTokenBaseUrl, params)
|
||||
log.debug "getToken - url: $url"
|
||||
|
||||
return getJsonFromUrl(url)
|
||||
}
|
||||
|
||||
def buildOauthUrlWithToken(String token, String tokenSecret) {
|
||||
def callback = getServerUrl() + "/api/smartapps/installations/${app.id}/exchange?access_token=${state.accessToken}"
|
||||
def params = [
|
||||
oauth_callback:URLEncoder.encode(callback),
|
||||
oauth_token:token
|
||||
]
|
||||
def authorizeBaseUrl = "https://oauth.withings.com/account/authorize"
|
||||
|
||||
return buildSignedUrl(authorizeBaseUrl, params, tokenSecret)
|
||||
}
|
||||
|
||||
/////////////////////////////////////////
|
||||
/////////////////////////////////////////
|
||||
// vvv vvv OAuth 1.0 vvv vvv //
|
||||
/////////////////////////////////////////
|
||||
/////////////////////////////////////////
|
||||
String buildSignedUrl(String baseUrl, Map urlParams, String tokenSecret="") {
|
||||
def params = [
|
||||
oauth_consumer_key: smartThingsConsumerKey,
|
||||
oauth_nonce: nonce(),
|
||||
oauth_signature_method: "HMAC-SHA1",
|
||||
oauth_timestamp: timestampInSeconds(),
|
||||
oauth_version: 1.0
|
||||
] + urlParams
|
||||
def signatureBaseString = ["GET", baseUrl, toQueryString(params)].collect { URLEncoder.encode(it) }.join("&")
|
||||
|
||||
params.oauth_signature = hmac(signatureBaseString, getSmartThingsConsumerSecret(), tokenSecret)
|
||||
|
||||
// query string is different from what is used in generating the signature above b/c it includes "oauth_signature"
|
||||
def url = [baseUrl, toQueryString(params)].join('?')
|
||||
return url
|
||||
}
|
||||
|
||||
String nonce() {
|
||||
return UUID.randomUUID().toString().replaceAll("-", "")
|
||||
}
|
||||
|
||||
Integer timestampInSeconds() {
|
||||
return (int)(new Date().time/1000)
|
||||
}
|
||||
|
||||
String toQueryString(Map m) {
|
||||
return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&")
|
||||
}
|
||||
|
||||
String hmac(String dataString, String consumerSecret, String tokenSecret="") throws java.security.SignatureException {
|
||||
String result
|
||||
|
||||
def key = [consumerSecret, tokenSecret].join('&')
|
||||
|
||||
// get an hmac_sha1 key from the raw key bytes
|
||||
def signingKey = new javax.crypto.spec.SecretKeySpec(key.getBytes(), "HmacSHA1")
|
||||
|
||||
// get an hmac_sha1 Mac instance and initialize with the signing key
|
||||
def mac = javax.crypto.Mac.getInstance("HmacSHA1")
|
||||
mac.init(signingKey)
|
||||
|
||||
// compute the hmac on input data bytes
|
||||
byte[] rawHmac = mac.doFinal(dataString.getBytes())
|
||||
|
||||
result = org.apache.commons.codec.binary.Base64.encodeBase64String(rawHmac)
|
||||
|
||||
return result
|
||||
}
|
||||
/////////////////////////////////////////
|
||||
/////////////////////////////////////////
|
||||
// ^^^ ^^^ OAuth 1.0 ^^^ ^^^ //
|
||||
/////////////////////////////////////////
|
||||
/////////////////////////////////////////
|
||||
|
||||
/////////////////////////////////////////
|
||||
/////////////////////////////////////////
|
||||
// vvv vvv rest vvv vvv //
|
||||
/////////////////////////////////////////
|
||||
/////////////////////////////////////////
|
||||
|
||||
protected rest(Map params) {
|
||||
new physicalgraph.device.RestAction(params)
|
||||
}
|
||||
|
||||
/////////////////////////////////////////
|
||||
/////////////////////////////////////////
|
||||
// ^^^ ^^^ rest ^^^ ^^^ //
|
||||
/////////////////////////////////////////
|
||||
/////////////////////////////////////////
|
||||
|
||||
def exchangeToken() {
|
||||
// oauth_token=abcd
|
||||
// &userid=123
|
||||
|
||||
def newToken = params.oauth_token
|
||||
def userid = params.userid
|
||||
def tokenSecret = state.oauth_request_token_secret
|
||||
|
||||
def params = [
|
||||
oauth_token: newToken,
|
||||
userid: userid
|
||||
]
|
||||
|
||||
def requestTokenBaseUrl = "https://oauth.withings.com/account/access_token"
|
||||
def url = buildSignedUrl(requestTokenBaseUrl, params, tokenSecret)
|
||||
log.debug "signed url: $url with secret $tokenSecret"
|
||||
|
||||
def token = getJsonFromUrl(url)
|
||||
|
||||
state.userid = userid
|
||||
state.oauth_token = token.oauth_token
|
||||
state.oauth_token_secret = token.oauth_token_secret
|
||||
|
||||
log.debug "swapped token"
|
||||
|
||||
def location = getServerUrl() + "/api/smartapps/installations/${app.id}/load?access_token=${state.accessToken}"
|
||||
redirect(location:location)
|
||||
}
|
||||
|
||||
def load() {
|
||||
def json = get(getMeasurement(new Date() - 30))
|
||||
|
||||
log.debug "swapped, then received: $json"
|
||||
parse(data:json)
|
||||
|
||||
def html = """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=640">
|
||||
<title>Withings Connection</title>
|
||||
<style type="text/css">
|
||||
@font-face {
|
||||
font-family: 'Swiss 721 W01 Thin';
|
||||
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot');
|
||||
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot?#iefix') format('embedded-opentype'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.woff') format('woff'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.ttf') format('truetype'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.svg#swis721_th_btthin') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Swiss 721 W01 Light';
|
||||
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot');
|
||||
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot?#iefix') format('embedded-opentype'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.woff') format('woff'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.ttf') format('truetype'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.svg#swis721_lt_btlight') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
.container {
|
||||
width: 560px;
|
||||
padding: 40px;
|
||||
/*background: #eee;*/
|
||||
text-align: center;
|
||||
}
|
||||
img {
|
||||
vertical-align: middle;
|
||||
}
|
||||
img:nth-child(2) {
|
||||
margin: 0 30px;
|
||||
}
|
||||
p {
|
||||
font-size: 2.2em;
|
||||
font-family: 'Swiss 721 W01 Thin';
|
||||
text-align: center;
|
||||
color: #666666;
|
||||
padding: 0 40px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
/*
|
||||
p:last-child {
|
||||
margin-top: 0px;
|
||||
}
|
||||
*/
|
||||
span {
|
||||
font-family: 'Swiss 721 W01 Light';
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<img src="https://s3.amazonaws.com/smartapp-icons/Partner/withings@2x.png" alt="withings icon" />
|
||||
<img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/connected-device-icn%402x.png" alt="connected device icon" />
|
||||
<img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/st-logo%402x.png" alt="SmartThings logo" />
|
||||
<p>Your Withings scale is now connected to SmartThings!</p>
|
||||
<p>Click 'Done' to finish setup.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
render contentType: 'text/html', data: html
|
||||
}
|
||||
|
||||
Map getJsonFromUrl(String url) {
|
||||
return [:] // stop making requests to Withings API. This entire SmartApp will be replaced with a fix
|
||||
|
||||
def jsonString
|
||||
httpGet(uri: url) { resp ->
|
||||
jsonString = resp.data.toString()
|
||||
}
|
||||
|
||||
return getJsonFromText(jsonString)
|
||||
}
|
||||
|
||||
Map getJsonFromText(String jsonString) {
|
||||
def jsonMap = jsonString.split("&").inject([:]) { c, it ->
|
||||
def parts = it.split('=')
|
||||
def k = parts[0]
|
||||
def v = parts[1]
|
||||
c[k] = v
|
||||
return c
|
||||
}
|
||||
|
||||
return jsonMap
|
||||
}
|
||||
|
||||
def getMeasurement(Date since=null) {
|
||||
return null // stop making requests to Withings API. This entire SmartApp will be replaced with a fix
|
||||
|
||||
// TODO: add startdate and enddate ... esp. when in response to notify
|
||||
def params = [
|
||||
action:"getmeas",
|
||||
oauth_consumer_key:getSmartThingsConsumerKey(),
|
||||
oauth_nonce:nonce(),
|
||||
oauth_signature_method:"HMAC-SHA1",
|
||||
oauth_timestamp:timestampInSeconds(),
|
||||
oauth_token:state.oauth_token,
|
||||
oauth_version:1.0,
|
||||
userid: state.userid
|
||||
]
|
||||
|
||||
if(since)
|
||||
{
|
||||
params.startdate = dateToSeconds(since)
|
||||
}
|
||||
|
||||
def requestTokenBaseUrl = "http://wbsapi.withings.net/measure"
|
||||
def signatureBaseString = ["GET", requestTokenBaseUrl, toQueryString(params)].collect { URLEncoder.encode(it) }.join("&")
|
||||
|
||||
params.oauth_signature = hmac(signatureBaseString, getSmartThingsConsumerSecret(), state.oauth_token_secret)
|
||||
|
||||
return rest(
|
||||
method: 'GET',
|
||||
endpoint: "http://wbsapi.withings.net",
|
||||
path: "/measure",
|
||||
query: params,
|
||||
synchronous: true
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
String get(measurementRestAction) {
|
||||
return "" // stop making requests to Withings API. This entire SmartApp will be replaced with a fix
|
||||
|
||||
def httpGetParams = [
|
||||
uri: measurementRestAction.endpoint,
|
||||
path: measurementRestAction.path,
|
||||
query: measurementRestAction.query
|
||||
]
|
||||
|
||||
String json
|
||||
httpGet(httpGetParams) {resp ->
|
||||
json = resp.data.text.toString()
|
||||
}
|
||||
|
||||
return json
|
||||
}
|
||||
|
||||
def parse(Map response) {
|
||||
def json = new org.codehaus.groovy.grails.web.json.JSONObject(response.data)
|
||||
parseJson(json)
|
||||
}
|
||||
|
||||
def parseJson(json) {
|
||||
log.debug "parseJson: $json"
|
||||
|
||||
def lastDataPointMillis = (state.lastDataPointMillis ?: 0).toLong()
|
||||
def i = 0
|
||||
|
||||
if(json.status == 0)
|
||||
{
|
||||
log.debug "parseJson measure group size: ${json.body.measuregrps.size()}"
|
||||
|
||||
state.errorCount = 0
|
||||
|
||||
def childDni = getWithingsDevice(json.body.measuregrps).deviceNetworkId
|
||||
|
||||
def latestMillis = lastDataPointMillis
|
||||
json.body.measuregrps.sort { it.date }.each { group ->
|
||||
|
||||
def measurementDateSeconds = group.date
|
||||
def dataPointMillis = measurementDateSeconds * 1000L
|
||||
|
||||
if(dataPointMillis > lastDataPointMillis)
|
||||
{
|
||||
group.measures.each { measure ->
|
||||
i++
|
||||
saveMeasurement(childDni, measure, measurementDateSeconds)
|
||||
}
|
||||
}
|
||||
|
||||
if(dataPointMillis > latestMillis)
|
||||
{
|
||||
latestMillis = dataPointMillis
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if(latestMillis > lastDataPointMillis)
|
||||
{
|
||||
state.lastDataPointMillis = latestMillis
|
||||
}
|
||||
|
||||
def weightData = state.findAll { it.key.startsWith("measure.") }
|
||||
|
||||
// remove old data
|
||||
def old = "measure." + (new Date() - 30).format('yyyy-MM-dd')
|
||||
state.findAll { it.key.startsWith("measure.") && it.key < old }.collect { it.key }.each { state.remove(it) }
|
||||
}
|
||||
else
|
||||
{
|
||||
def errorCount = (state.errorCount ?: 0).toInteger()
|
||||
state.errorCount = errorCount + 1
|
||||
|
||||
// TODO: If we poll, consider waiting for a couple failures before showing an error
|
||||
// But if we are only notified, then we need to raise the error right away
|
||||
measurementError(json.status)
|
||||
}
|
||||
|
||||
log.debug "Done adding $i measurements"
|
||||
return
|
||||
}
|
||||
|
||||
def measurementError(status) {
|
||||
log.error "received api response status ${status}"
|
||||
sendEvent(state.childDni, [name: "connection", value:"Connection error: ${status}", isStateChange:true, displayed:true])
|
||||
}
|
||||
|
||||
def saveMeasurement(childDni, measure, measurementDateSeconds) {
|
||||
def dateString = secondsToDate(measurementDateSeconds).format('yyyy-MM-dd')
|
||||
|
||||
def measurement = withingsEvent(measure)
|
||||
sendEvent(state.childDni, measurement + [date:dateString], [dateCreated:secondsToDate(measurementDateSeconds)])
|
||||
|
||||
log.debug "sm: ${measure.type} (${measure.type == 1})"
|
||||
|
||||
if(measure.type == 6)
|
||||
{
|
||||
sendEvent(state.childDni, [name: "leanRatio", value:(100-measurement.value), date:dateString, isStateChange:true, display:true], [dateCreated:secondsToDate(measurementDateSeconds)])
|
||||
}
|
||||
else if(measure.type == 1)
|
||||
{
|
||||
state["measure." + dateString] = measurement.value
|
||||
}
|
||||
}
|
||||
|
||||
def eventValue(measure, roundDigits=1) {
|
||||
def value = measure.value * 10.power(measure.unit)
|
||||
|
||||
if(roundDigits != null)
|
||||
{
|
||||
def significantDigits = 10.power(roundDigits)
|
||||
value = (value * significantDigits).toInteger() / significantDigits
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
def withingsEvent(measure) {
|
||||
def withingsTypes = [
|
||||
(1):"weight",
|
||||
(4):"height",
|
||||
(5):"leanMass",
|
||||
(6):"fatRatio",
|
||||
(8):"fatMass",
|
||||
(11):"pulse"
|
||||
]
|
||||
|
||||
def value = eventValue(measure, (measure.type == 4 ? null : 1))
|
||||
|
||||
if(measure.type == 1) {
|
||||
value *= 2.20462
|
||||
} else if(measure.type == 4) {
|
||||
value *= 39.3701
|
||||
}
|
||||
|
||||
log.debug "m:${measure.type}, v:${value}"
|
||||
|
||||
return [
|
||||
name: withingsTypes[measure.type],
|
||||
value: value
|
||||
]
|
||||
}
|
||||
|
||||
Integer dateToSeconds(Date d) {
|
||||
return d.time / 1000
|
||||
}
|
||||
|
||||
Date secondsToDate(Number seconds) {
|
||||
return new Date(seconds * 1000L)
|
||||
}
|
||||
|
||||
def getWithingsDevice(measuregrps=null) {
|
||||
// unfortunately, Withings doesn't seem to give us enough information to know which device(s) they have,
|
||||
// ... so we have to guess and create a single device
|
||||
|
||||
if(state.childDni)
|
||||
{
|
||||
return getChildDevice(state.childDni)
|
||||
}
|
||||
else
|
||||
{
|
||||
def children = getChildDevices()
|
||||
if(children.size() > 0)
|
||||
{
|
||||
return children[0]
|
||||
}
|
||||
else
|
||||
{
|
||||
// no child yet, create one
|
||||
def dni = [app.id, UUID.randomUUID().toString()].join('.')
|
||||
state.childDni = dni
|
||||
|
||||
def childDeviceType = getBodyAnalyzerChildName()
|
||||
|
||||
if(measuregrps)
|
||||
{
|
||||
def hasNoHeartRate = measuregrps.find { grp -> grp.measures.find { it.type == 11 } } == null
|
||||
if(hasNoHeartRate)
|
||||
{
|
||||
childDeviceType = getScaleChildName()
|
||||
}
|
||||
}
|
||||
|
||||
def child = addChildDevice(getChildNamespace(), childDeviceType, dni, null, [label:"Withings"])
|
||||
state.childId = child.id
|
||||
return child
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
initialize()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
unsubscribe()
|
||||
initialize()
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
// TODO: subscribe to attributes, devices, locations, etc.
|
||||
}
|
||||
|
||||
def poll() {
|
||||
if(shouldPoll())
|
||||
{
|
||||
return getMeasurement()
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
def shouldPoll() {
|
||||
def lastPollString = state.lastPollMillisString
|
||||
def lastPoll = lastPollString?.isNumber() ? lastPollString.toLong() : 0
|
||||
def ONE_HOUR = 60 * 60 * 1000
|
||||
|
||||
def time = new Date().time
|
||||
|
||||
if(time > (lastPoll + ONE_HOUR))
|
||||
{
|
||||
log.debug "Executing poll b/c (now > last + 1hr): ${time} > ${lastPoll + ONE_HOUR} (last: ${lastPollString})"
|
||||
state.lastPollMillisString = time
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
log.debug "skipping poll b/c !(now > last + 1hr): ${time} > ${lastPoll + ONE_HOUR} (last: ${lastPollString})"
|
||||
return false
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
log.debug "Executing 'refresh'"
|
||||
return getMeasurement()
|
||||
}
|
||||
|
||||
def getChildNamespace() { "smartthings" }
|
||||
def getScaleChildName() { "Wireless Scale" }
|
||||
def getBodyAnalyzerChildName() { "Smart Body Analyzer" }
|
||||
|
||||
def getServerUrl() { appSettings.serverUrl }
|
||||
def getSmartThingsConsumerKey() { appSettings.clientId }
|
||||
def getSmartThingsConsumerSecret() { appSettings.clientSecret }
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user