Initial commit

This commit is contained in:
bflorian
2015-08-04 15:49:03 -07:00
commit 6ad3c4fd7a
322 changed files with 67201 additions and 0 deletions

View File

@@ -0,0 +1,762 @@
definition(
name: "Bright When Dark And/Or Bright After Sunset",
namespace: "Arno",
author: "Arnaud",
description: "Turn ON light(s) and/or dimmer(s) when there's movement and the room is dark with illuminance threshold and/or between sunset and sunrise. Then turn OFF after X minute(s) when the brightness of the room is above the illuminance threshold or turn OFF after X minute(s) when there is no movement.",
category: "Convenience",
iconUrl: "http://neiloseman.com/wp-content/uploads/2013/08/stockvault-bulb128619.jpg",
iconX2Url: "http://neiloseman.com/wp-content/uploads/2013/08/stockvault-bulb128619.jpg"
)
preferences
{
page(name: "configurations")
page(name: "options")
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 configurations()
{
dynamicPage(name: "configurations", title: "Configurations...", uninstall: true, nextPage: "options")
{
section(title: "Turn ON lights on movement when...")
{
input "dark", "bool", title: "It is dark?", required: true
input "sun", "bool", title: "Between sunset and surise?", 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
}
section ("Assign a name")
{
label title: "Assign a name", required: false
}
}
}
def options()
{
if (dark == true && sun == true)
{
dynamicPage(name: "options", title: "Lights will turn ON on movement when it is dark and between sunset and sunrise...", install: true, uninstall: true)
{
section("Control these light(s)...")
{
input "lights", "capability.switch", title: "Light(s)?", multiple: true, required: false
}
section("Control these dimmer(s)...")
{
input "dimmers", "capability.switchLevel", title: "Dimmer(s)?", multiple: true, required:false
input "level", "number", title: "How bright?", required:false, description: "0% to 100%"
}
section("Turning ON when it's dark and there's movement...")
{
input "motionSensor", "capability.motionSensor", title: "Where?", multiple: true, required: true
}
section("And then OFF when it's light or there's been no movement for...")
{
input "delayMinutes", "number", title: "Minutes?", required: false
}
section("Using this light sensor...")
{
input "lightSensor", "capability.illuminanceMeasurement",title: "Light Sensor?", multiple: false, required: true
input "luxLevel", "number", title: "Illuminance threshold? (default 50 lux)",defaultValue: "50", required: false
}
section ("And between sunset and sunrise...")
{
input "sunriseOffsetValue", "text", title: "Sunrise offset", required: false, description: "00:00"
input "sunriseOffsetDir", "enum", title: "Before or After", required: false, metadata: [values: ["Before","After"]]
input "sunsetOffsetValue", "text", title: "Sunset offset", required: false, description: "00:00"
input "sunsetOffsetDir", "enum", title: "Before or After", required: false, metadata: [values: ["Before","After"]]
}
section ("Zip code (optional, defaults to location coordinates when location services are enabled)...")
{
input "zipCode", "text", title: "Zip Code?", required: false, description: "Local Zip Code"
}
}
}
else if (dark == true && sun == false)
{
dynamicPage(name: "options", title: "Lights will turn ON on movement when it is dark...", install: true, uninstall: true)
{
section("Control these light(s)...")
{
input "lights", "capability.switch", title: "Light(s)?", multiple: true, required: false
}
section("Control these dimmer(s)...")
{
input "dimmers", "capability.switchLevel", title: "Dimmer(s)?", multiple: true, required:false
input "level", "number", title: "How bright?", required:false, description: "0% to 100%"
}
section("Turning ON when it's dark and there's movement...")
{
input "motionSensor", "capability.motionSensor", title: "Where?", multiple: true, required: true
}
section("And then OFF when it's light or there's been no movement for...")
{
input "delayMinutes", "number", title: "Minutes?", required: false
}
section("Using this light sensor...")
{
input "lightSensor", "capability.illuminanceMeasurement",title: "Light Sensor?", multiple: false, required: true
input "luxLevel", "number", title: "Illuminance threshold? (default 50 lux)",defaultValue: "50", required: false
}
}
}
else if (sun == true && dark == false)
{
dynamicPage(name: "options", title: "Lights will turn ON on movement between sunset and sunrise...", install: true, uninstall: true)
{
section("Control these light(s)...")
{
input "lights", "capability.switch", title: "Light(s)?", multiple: true, required: false
}
section("Control these dimmer(s)...")
{
input "dimmers", "capability.switchLevel", title: "Dimmer(s)?", multiple: true, required:false
input "level", "number", title: "How bright?", required:false, description: "0% to 100%"
}
section("Turning ON there's movement...")
{
input "motionSensor", "capability.motionSensor", title: "Where?", multiple: true, required: true
}
section("And then OFF there's been no movement for...")
{
input "delayMinutes", "number", title: "Minutes?", required: false
}
section ("Between sunset and sunrise...")
{
input "sunriseOffsetValue", "text", title: "Sunrise offset", required: false, description: "00:00"
input "sunriseOffsetDir", "enum", title: "Before or After", required: false, metadata: [values: ["Before","After"]]
input "sunsetOffsetValue", "text", title: "Sunset offset", required: false, description: "00:00"
input "sunsetOffsetDir", "enum", title: "Before or After", required: false, metadata: [values: ["Before","After"]]
}
section ("Zip code (optional, defaults to location coordinates when location services are enabled)...")
{
input "zipCode", "text", title: "Zip Code?", required: false, description: "Local Zip Code"
}
}
}
else
{
dynamicPage(name: "options", title: "Lights will turn ON on movement...", install: true, uninstall: true)
{
section("Control these light(s)...")
{
input "lights", "capability.switch", title: "Light(s)?", multiple: true, required: false
}
section("Control these dimmer(s)...")
{
input "dimmers", "capability.switchLevel", title: "Dimmer(s)?", multiple: true, required:false
input "level", "number", title: "How bright?", required:false, description: "0% to 100%"
}
section("Turning ON when there's movement...")
{
input "motionSensor", "capability.motionSensor", title: "Where?", multiple: true, required: true
}
section("And then OFF when there's been no movement for...")
{
input "delayMinutes", "number", title: "Minutes?", required: false
}
}
}
}
def installed()
{
log.debug "Installed with settings: ${settings}."
initialize()
}
def updated()
{
log.debug "Updated with settings: ${settings}."
unsubscribe()
unschedule()
initialize()
}
def initialize()
{
subscribe(motionSensor, "motion", motionHandler)
if (lights != null && lights != "" && dimmers != null && dimmers != "")
{
log.debug "$lights subscribing..."
subscribe(lights, "switch", lightsHandler)
log.debug "$dimmers subscribing..."
subscribe(dimmers, "switch", dimmersHandler)
if (dark == true && lightSensor != null && lightSensor != "")
{
log.debug "$lights and $dimmers will turn ON when movement detected and when it is dark..."
subscribe(lightSensor, "illuminance", illuminanceHandler, [filterEvents: false])
}
if (sun == true)
{
log.debug "$lights and $dimmers will turn ON when movement detected between sunset and sunrise..."
astroCheck()
subscribe(location, "position", locationPositionChange)
subscribe(location, "sunriseTime", sunriseSunsetTimeHandler)
subscribe(location, "sunsetTime", sunriseSunsetTimeHandler)
}
else if (dark != true && sun != true)
{
log.debug "$lights and $dimmers will turn ON when movement detected..."
}
}
else if (lights != null && lights != "")
{
log.debug "$lights subscribing..."
subscribe(lights, "switch", lightsHandler)
if (dark == true && lightSensor != null && lightSensor != "")
{
log.debug "$lights will turn ON when movement detected and when it is dark..."
subscribe(lightSensor, "illuminance", illuminanceHandler, [filterEvents: false])
}
if (sun == true)
{
log.debug "$lights will turn ON when movement detected between sunset and sunrise..."
astroCheck()
subscribe(location, "position", locationPositionChange)
subscribe(location, "sunriseTime", sunriseSunsetTimeHandler)
subscribe(location, "sunsetTime", sunriseSunsetTimeHandler)
}
else if (dark != true && sun != true)
{
log.debug "$lights will turn ON when movement detected..."
}
}
else if (dimmers != null && dimmers != "")
{
log.debug "$dimmers subscribing..."
subscribe(dimmers, "switch", dimmersHandler)
if (dark == true && lightSensor != null && lightSensor != "")
{
log.debug "$dimmers will turn ON when movement detected and when it is dark..."
subscribe(lightSensor, "illuminance", illuminanceHandler, [filterEvents: false])
}
if (sun == true)
{
log.debug "$dimmers will turn ON when movement detected between sunset and sunrise..."
astroCheck()
subscribe(location, "position", locationPositionChange)
subscribe(location, "sunriseTime", sunriseSunsetTimeHandler)
subscribe(location, "sunsetTime", sunriseSunsetTimeHandler)
}
else if (dark != true && sun != true)
{
log.debug "$dimmers will turn ON when movement detected..."
}
}
log.debug "Determinating lights and dimmers current value..."
if (lights != null && lights != "")
{
if (lights.currentValue("switch").toString().contains("on"))
{
state.lightsState = "on"
log.debug "Lights $state.lightsState."
}
else if (lights.currentValue("switch").toString().contains("off"))
{
state.lightsState = "off"
log.debug "Lights $state.lightsState."
}
else
{
log.debug "ERROR!"
}
}
if (dimmers != null && dimmers != "")
{
if (dimmers.currentValue("switch").toString().contains("on"))
{
state.dimmersState = "on"
log.debug "Dimmers $state.dimmersState."
}
else if (dimmers.currentValue("switch").toString().contains("off"))
{
state.dimmersState = "off"
log.debug "Dimmers $state.dimmersState."
}
else
{
log.debug "ERROR!"
}
}
}
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")
{
unschedule(turnOffLights)
unschedule(turnOffDimmers)
if (dark == true && sun == true)
{
if (darkOk == true && sunOk == true)
{
log.debug "Lights and Dimmers will turn ON because $motionSensor detected motion and $lightSensor was dark or because $motionSensor detected motion between sunset and sunrise..."
if (lights != null && lights != "")
{
log.debug "Lights: $lights will turn ON..."
turnOnLights()
}
if (dimmers != null && dimmers != "")
{
log.debug "Dimmers: $dimmers will turn ON..."
turnOnDimmers()
}
}
else if (darkOk == true && sunOk != true)
{
log.debug "Lights and Dimmers will turn ON because $motionSensor detected motion and $lightSensor was dark..."
if (lights != null && lights != "")
{
log.debug "Lights: $lights will turn ON..."
turnOnLights()
}
if (dimmers != null && dimmers != "")
{
log.debug "Dimmers: $dimmers will turn ON..."
turnOnDimmers()
}
}
else if (darkOk != true && sunOk == true)
{
log.debug "Lights and dimmers will turn ON because $motionSensor detected motion between sunset and sunrise..."
if (lights != null && lights != "")
{
log.debug "Lights: $lights will turn ON..."
turnOnLights()
}
if (dimmers != null && dimmers != "")
{
log.debug "Dimmers: $dimmers will turn ON..."
turnOnDimmers()
}
}
else
{
log.debug "Lights and dimmers will not turn ON because $lightSensor is too bright or because time not between sunset and surise."
}
}
else if (dark == true && sun != true)
{
if (darkOk == true)
{
log.debug "Lights and dimmers will turn ON because $motionSensor detected motion and $lightSensor was dark..."
if (lights != null && lights != "")
{
log.debug "Lights: $lights will turn ON..."
turnOnLights()
}
if (dimmers != null && dimmers != "")
{
log.debug "Dimmers: $dimmers will turn ON..."
turnOnDimmers()
}
}
else
{
log.debug "Lights and dimmers will not turn ON because $lightSensor is too bright."
}
}
else if (dark != true && sun == true)
{
if (sunOk == true)
{
log.debug "Lights and dimmers will turn ON because $motionSensor detected motion between sunset and sunrise..."
if (lights != null && lights != "")
{
log.debug "Lights: $lights will turn ON..."
turnOnLights()
}
if (dimmers != null && dimmers != "")
{
log.debug "Dimmers: $dimmers will turn ON..."
turnOnDimmers()
}
}
else
{
log.debug "Lights and dimmers will not turn ON because time not between sunset and surise."
}
}
else if (dark != true && sun != true)
{
log.debug "Lights and dimmers will turn ON because $motionSensor detected motion..."
if (lights != null && lights != "")
{
log.debug "Lights: $lights will turn ON..."
turnOnLights()
}
if (dimmers != null && dimmers != "")
{
log.debug "Dimmers: $dimmers will turn ON..."
turnOnDimmers()
}
}
}
else if (evt.value == "inactive")
{
unschedule(turnOffLights)
unschedule(turnOffDimmers)
if (state.lightsState != "off" || state.dimmersState != "off")
{
log.debug "Lights and/or dimmers are not OFF."
if (delayMinutes)
{
def delay = delayMinutes * 60
if (dark == true && sun == true)
{
log.debug "Lights and dimmers will turn OFF in $delayMinutes minute(s) after turning ON when dark or between sunset and sunrise..."
if (lights != null && lights != "")
{
log.debug "Lights: $lights will turn OFF in $delayMinutes minute(s)..."
runIn(delay, turnOffLights)
}
if (dimmers != null && dimmers != "")
{
log.debug "Dimmers: $dimmers will turn OFF in $delayMinutes minute(s)..."
runIn(delay, turnOffDimmers)
}
}
else if (dark == true && sun != true)
{
log.debug "Lights and dimmers will turn OFF in $delayMinutes minute(s) after turning ON when dark..."
if (lights != null && lights != "")
{
log.debug "Lights: $lights will turn OFF in $delayMinutes minute(s)..."
runIn(delay, turnOffLights)
}
if (dimmers != null && dimmers != "")
{
log.debug "Dimmers: $dimmers will turn OFF in $delayMinutes minute(s)..."
runIn(delay, turnOffDimmers)
}
}
else if (dark != true && sun == true)
{
log.debug "Lights and dimmers will turn OFF in $delayMinutes minute(s) between sunset and sunrise..."
if (lights != null && lights != "")
{
log.debug "Lights: $lights will turn OFF in $delayMinutes minute(s)..."
runIn(delay, turnOffLights)
}
if (dimmers != null && dimmers != "")
{
log.debug "Dimmers: $dimmers will turn OFF in $delayMinutes minute(s)..."
runIn(delay, turnOffDimmers)
}
}
else if (dark != true && sun != true)
{
log.debug "Lights and dimmers will turn OFF in $delayMinutes minute(s)..."
if (lights != null && lights != "")
{
log.debug "Lights: $lights will turn OFF in $delayMinutes minute(s)..."
runIn(delay, turnOffLights)
}
if (dimmers != null && dimmers != "")
{
log.debug "Dimmers: $dimmers will turn OFF in $delayMinutes minute(s)..."
runIn(delay, turnOffDimmers)
}
}
}
else
{
log.debug "Lights and dimmers will stay ON because no turn OFF delay was set..."
}
}
else if (state.lightsState == "off" && state.dimmersState == "off")
{
log.debug "Lights and dimmers are already OFF and will not turn OFF in $delayMinutes minute(s)."
}
}
}
def lightsHandler(evt)
{
log.debug "Lights Handler $evt.name: $evt.value"
if (evt.value == "on")
{
log.debug "Lights: $lights now ON."
unschedule(turnOffLights)
state.lightsState = "on"
}
else if (evt.value == "off")
{
log.debug "Lights: $lights now OFF."
unschedule(turnOffLights)
state.lightsState = "off"
}
}
def dimmersHandler(evt)
{
log.debug "Dimmer Handler $evt.name: $evt.value"
if (evt.value == "on")
{
log.debug "Dimmers: $dimmers now ON."
unschedule(turnOffDimmers)
state.dimmersState = "on"
}
else if (evt.value == "off")
{
log.debug "Dimmers: $dimmers now OFF."
unschedule(turnOffDimmers)
state.dimmersState = "off"
}
}
def illuminanceHandler(evt)
{
log.debug "$evt.name: $evt.value, lastStatus lights: $state.lightsState, lastStatus dimmers: $state.dimmersState, motionStopTime: $state.motionStopTime"
unschedule(turnOffLights)
unschedule(turnOffDimmers)
if (evt.integerValue > 999)
{
log.debug "Lights and dimmers will turn OFF because illuminance is superior to 999 lux..."
if (lights != null && lights != "")
{
log.debug "Lights: $lights will turn OFF..."
turnOffLights()
}
if (dimmers != null && dimmers != "")
{
log.debug "Dimmers: $dimmers will turn OFF..."
turnOffDimmers()
}
}
else if (evt.integerValue > ((luxLevel != null && luxLevel != "") ? luxLevel : 50))
{
log.debug "Lights and dimmers will turn OFF because illuminance is superior to $luxLevel lux..."
if (lights != null && lights != "")
{
log.debug "Lights: $lights will turn OFF..."
turnOffLights()
}
if (dimmers != null && dimmers != "")
{
log.debug "Dimmers: $dimmers will turn OFF..."
turnOffDimmers()
}
}
}
def turnOnLights()
{
if (allOk)
{
if (state.lightsState != "on")
{
log.debug "Turning ON lights: $lights..."
lights?.on()
state.lightsState = "on"
}
else
{
log.debug "Lights: $lights already ON."
}
}
else
{
log.debug "Time, days of the week or mode out of range! $lights will not turn ON."
}
}
def turnOnDimmers()
{
if (allOk)
{
if (state.dimmersState != "on")
{
log.debug "Turning ON dimmers: $dimmers..."
settings.dimmers?.setLevel(level)
state.dimmersState = "on"
}
else
{
log.debug "Dimmers: $dimmers already ON."
}
}
else
{
log.debug "Time, days of the week or mode out of range! $dimmers will not turn ON."
}
}
def turnOffLights()
{
if (allOk)
{
if (state.lightsState != "off")
{
log.debug "Turning OFF lights: $lights..."
lights?.off()
state.lightsState = "on"
}
else
{
log.debug "Lights: $lights already OFF."
}
}
else
{
log.debug "Time, day of the week or mode out of range! $lights will not turn OFF."
}
}
def turnOffDimmers()
{
if (allOk)
{
if (state.dimmersState != "off")
{
log.debug "Turning OFF dimmers: $dimmers..."
dimmers?.off()
state.dimmersState = "off"
}
else
{
log.debug "Dimmers: $dimmers already OFF."
}
}
else
{
log.debug "Time, day of the week or mode out of range! $dimmers will not turn OFF."
}
}
def astroCheck()
{
def s = getSunriseAndSunset(zipCode: zipCode, sunriseOffset: sunriseOffset, sunsetOffset: sunsetOffset)
state.riseTime = s.sunrise.time
state.setTime = s.sunset.time
log.debug "Sunrise: ${new Date(state.riseTime)}($state.riseTime), Sunset: ${new Date(state.setTime)}($state.setTime)"
}
private getDarkOk()
{
def result
if (dark == true && lightSensor != null && lightSensor != "")
{
result = lightSensor.currentIlluminance < ((luxLevel != null && luxLevel != "") ? luxLevel : 50)
}
log.trace "darkOk = $result"
result
}
private getSunOk()
{
def result
if (sun == true)
{
def t = now()
result = t < state.riseTime || t > state.setTime
}
log.trace "sunOk = $result"
result
}
private getSunriseOffset()
{
sunriseOffsetValue ? (sunriseOffsetDir == "Before" ? "-$sunriseOffsetValue" : sunriseOffsetValue) : null
}
private getSunsetOffset()
{
sunsetOffsetValue ? (sunsetOffsetDir == "Before" ? "-$sunsetOffsetValue" : sunsetOffsetValue) : null
}
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") : ""
}

View File

@@ -0,0 +1,80 @@
/**
* Good Night House
*
* Copyright 2014 Joseph Charette
*
* 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: "Good Night House",
namespace: "charette.joseph@gmail.com",
author: "Joseph Charette",
description: "Some on, some off with delay for bedtime, Lock The Doors",
category: "Convenience",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png"
/**
* Borrowed code from
* Walk Gentle Into That Good Night
*
* Author: oneaccttorulethehouse@gmail.com
* Date: 2014-02-01
*/
)
preferences {
section("When I touch the app turn these lights off…"){
input "switchesoff", "capability.switch", multiple: true, required:true
}
section("When I touch the app turn these lights on…"){
input "switcheson", "capability.switch", multiple: true, required:false
}
section("Lock theses locks...") {
input "lock1","capability.lock", multiple: true
}
section("And change to this mode...") {
input "newMode", "mode", title: "Mode?"
}
section("After so many seconds (optional)"){
input "waitfor", "number", title: "Off after (default 120)", required: true
}
}
def installed()
{
log.debug "Installed with settings: ${settings}"
log.debug "Current mode = ${location.mode}"
subscribe(app, appTouch)
}
def updated()
{
log.debug "Updated with settings: ${settings}"
log.debug "Current mode = ${location.mode}"
unsubscribe()
subscribe(app, appTouch)
}
def appTouch(evt) {
log.debug "changeMode, location.mode = $location.mode, newMode = $newMode, location.modes = $location.modes"
if (location.mode != newMode) {
setLocationMode(newMode)
log.debug "Changed the mode to '${newMode}'"
} else {
log.debug "New mode is the same as the old mode, leaving it be"
}
log.debug "appTouch: $evt"
lock1.lock()
switcheson.on()
def delay = (waitfor != null && waitfor != "") ? waitfor * 1000 : 120000
switchesoff.off(delay: delay)
}

View File

@@ -0,0 +1,117 @@
/**
* Goodnight Ubi
*
* Copyright 2014 Christopher Boerma
*
* 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: "Goodnight Ubi",
namespace: "chrisb",
author: "chrisb",
description: "An app to coordinate bedtime activities between Ubi and SmartThings. This app will activate when a Virtual Tile is triggers (Setup custom behavior in Ubi to turn on this tile when you say goodnight to ubi). This app will then turn off selected lights after a specified number of minutes. It will also check if any doors or windows are open. If they are, Ubi will tell you which ones are open. Finally, the app will say goodnight to hello home if requested.",
category: "Safety & Security",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png")
preferences {
section("Enter Ubi information:") {
input "behaviorToken", "text", title: "What is the Ubi Token?", required: true, autoCorrect:false
// Get token from the Ubi Portal. Select HTTP request as trigger and token will be displayed.
input "trigger", "capability.switch", title: "Which virtual tile is the trigger?", required: true
// Create a Virtual on/off button tile for this.
}
section("Which doors and windows should I check?"){
input "doors", "capability.contactSensor", multiple: true
}
section("Which light switches will I be turning off?") {
input "theSwitches", "capability.switch", Title: "Which?", multiple: true, required: false
input "minutes", "number", Title: "After how many minutes?", required: true
}
section("Should I say 'Goodnight' to Hello Home?") {
input "sayPhrase", "enum", metadata:[values:["Yes","No"]]
}
}
def installed() {
log.debug "Installed with settings: ${settings}"
initialize()
}
def updated() {
log.debug "Updated with settings: ${settings}"
unsubscribe()
initialize()
}
def initialize() {
subscribe(trigger, "switch.on", switchOnHandler) // User should set up on Ubi that when the choosen
} // trigger is said, Ubi turns on this virtual switch.
def switchOnHandler(evt) {
log.debug "trigger turned on!"
def timeDelay = minutes * 60 // convert minutes to seconds.
runIn (timeDelay, lightsOut) // schedule the lights out procedure
def phrase = "" // Make sure Phrase is empty at the start of each run.
doors.each { doorOpen -> // cycles through all contact sensor devices selected
if (doorOpen.currentContact == "open") { // if the current selected device is open, then:
log.debug "$doorOpen.displayName" // echo to the simulator the device's name
def toReplace = doorOpen.displayName // make variable 'toReplace' = the devices name.
def replaced = toReplace.replaceAll(' ', '%20') // make variable 'replaced' = 'toReplace' with all the space changed to %20
log.debug replaced // echo to the simulator the new name.
phrase = phrase.replaceAll('%20And%20', '%20') // Remove any previously added "and's" to make it sound natural.
if (phrase == "") { // If Phrase is empty (ie, this is the first name to be added)...
phrase = "The%20" + replaced // ...then add "The%20" plus the device name.
} else { // If Phrase isn't empty...
phrase = phrase + ",%20And%20The%20" + replaced // ...then add ",%20And%20The%20".
}
log.debug phrase // Echo the current version of 'Phrase'
} // Closes the IF statement.
} // Closes the doors.each cycle
if (phrase == "") {
phrase = "The%20house%20is%20ready%20for%20night."
}
else {
phrase = "You%20have%20left%20" + phrase + "open"
}
httpGet("https://portal.theubi.com/webapi/behaviour?access_token=${behaviorToken}&variable=${phrase}")
// send the http request and push the device name (replaced) as the variable.
// On the Ubi side you need to setup a custom behavior (which you've already done to get the token)
// and have say something like: "Hold on! The ${variable} is open!" Ubi will then take 'replaced'
// from this http request and insert it into the phrase that it says.
if (sayPhrase == "Yes") { // If the user selected to say Goodnight...
location.helloHome.execute("Good Night!") // ...say goodnight to Hello Home.
}
} // Close the switchOnHandler Process
def lightsOut() {
log.debug "Turning off trigger"
trigger.off() // Turn off the trigger tile button for next run
if (theSwitches == "") {} else { // If the user didn't enter any light to turn off, do nothing...
log.debug "Turning off switches" // ...but if the user did enter lights, then turn them
theSwitches.off() // off here.
}
}

View File

@@ -0,0 +1,131 @@
/*
* Copyright (C) 2014 Andrew Reitz
*
* 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.
*/
/**
* Jenkins Notifier
*
* Checks a Jenkins server at a specific time, if the build fails it will turn on a light. If the build goes from
* failing back to succeeding the light will turn off. Hues can also be used in place of the light in order to create
* colors for build statuses
*/
// Automatically generated. Make future change here.
definition(
name: "Jenkins Notifier",
namespace: "com.andrewreitz",
author: "aj.reitz@gmail.com",
description: "Turn off and on devices based on the state that your Jenkins Build is in.",
category: "Fun & Social",
iconUrl: "http://i.imgur.com/tyIp8wQ.jpg",
iconX2Url: "http://i.imgur.com/tyIp8wQ.jpg"
)
preferences {
section("The URL to your Jenkins, including the job you want to monitor. Ex. https://jenkins.example.com/job/myproject/") {
input "jenkinsUrl", "text", title: "Jenkins URL"
}
section("Jenkins Username") {
input "jenkinsUsername", "text", title: "Jenkins Username"
}
section("Jenkins Password") {
input "jenkinsPassword", "password", title: "Jenkins Password"
}
section("On Failed Build Turn On...") {
input "switches", "capability.switch", multiple: true, required: false
}
section("Or Change These Bulbs...") {
input "hues", "capability.colorControl", title: "Which Hue Bulbs?", required: false, multiple: true
input "colorSuccess", "enum", title: "Hue Color On Success?", required: false, multiple: false, options: getHueColors().keySet() as String[]
input "colorFail", "enum", title: "Hue Color On Fail?", required: false, multiple: false, options: getHueColors().keySet() as String[]
input "lightLevelSuccess", "number", title: "Light Level On Success?", required: false
input "lightLevelFail", "number", title: "Light Level On Fail?", required: false
}
section("Additional settings", hideable: true, hidden: true) {
paragraph("Default check time is 15 Minutes")
input "refreshInterval", "decimal", title: "Check Server... (minutes)",
description: "Enter time in minutes", defaultValue: 15, required: false
}
}
def installed() {
log.debug "Installed with settings: ${settings}"
initialize()
}
def updated() {
log.debug "Updated with settings: ${settings}"
unsubscribe()
initialize()
}
/** Constants for Hue Colors */
Map getHueColors() {
return [Red: 0, Green: 39, Blue: 70, Yellow: 25, Orange: 10, Purple: 75, Pink: 83]
}
/** Constant for Saturation */
int getSaturation() {
return 100;
}
/** Constant for Level */
int getMaxLevel() {
return 100;
}
def initialize() {
def successColor = [switch: "on", hue: getHueColors()[colorSuccess], saturation: getSaturation(), level: lightLevelSuccess ?: getMaxLevel()]
def failColor = [switch: "on", hue: getHueColors()[colorFail], saturation: getSaturation(), level: lightLevelFail ?: getMaxLevel()]
state.successColor = successColor
state.failColor = failColor
log.debug "successColor: ${successColor}, failColor: ${failColor}"
checkServer()
def cron = "* */${refreshInterval ?: 15} * * * ?"
schedule(cron, checkServer)
}
def checkServer() {
log.debug "Checking Server Now"
def successColor = state.successColor
def failColor = state.failColor
def basicCredentials = "${jenkinsUsername}:${jenkinsPassword}"
def encodedCredentials = basicCredentials.encodeAsBase64().toString()
def basicAuth = "Basic ${encodedCredentials}"
def head = ["Authorization": basicAuth]
log.debug "Auth ${head}"
def host = jenkinsUrl.contains("lastBuild/api/json") ? jenkinsUrl : "${jenkinsUrl}/lastBuild/api/json"
httpGet(uri: host, headers: ["Authorization": "${basicAuth}"]) { resp ->
def buildError = (resp.data.result == "FAILURE")
def buildSuccess = (resp.data.result == "SUCCESS")
log.debug "Build Success? ${buildSuccess}"
if (buildError) {
switches?.on()
hues?.setColor(failColor)
} else if (buildSuccess) {
switches?.off()
hues?.setColor(successColor)
} // else in some other state, probably building, do nothing.
}
}

View File

@@ -0,0 +1,76 @@
/**
* ObyThing Music SmartApp
*
* Copyright 2014 obycode
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
* for the specific language governing permissions and limitations under the License.
*
*/
definition(
name: "ObyThing Music (Connect)",
namespace: "com.obycode",
author: "obycode",
description: "Use this free SmartApp in conjunction with the ObyThing Music app for your Mac to control and automate music and more with iTunes and SmartThings.",
category: "SmartThings Labs",
iconUrl: "http://obycode.com/obything/ObyThingSTLogo.png",
iconX2Url: "http://obycode.com/obything/ObyThingSTLogo@2x.png")
preferences {
section("Get the IP address and port for your Mac computer using the ObyThing App (http://obything.obycode.com) and set up the SmartApp below:") {
input "theAddr", "string", title: "IP:port (click icon in status bar)", multiple: false, required: true
}
section("on this hub...") {
input "theHub", "hub", multiple: false, required: true
}
}
def installed() {
log.debug "Installed ${app.label} with address '${settings.theAddr}' on hub '${settings.theHub.name}'"
initialize()
}
def updated() {
/*
log.debug "Updated ${app.label} with address '${settings.theAddr}' on hub '${settings.theHub.name}'"
def current = getChildDevices()
log.debug "children: $current"
if (app.label != current.label) {
log.debug "CHANGING name from ${current.label} to ${app.label}"
log.debug "label props: ${current.label.getProperties()}"
current.label[0] = app.label
}
*/
}
def initialize() {
def parts = theAddr.split(":")
def iphex = convertIPtoHex(parts[0])
def porthex = convertPortToHex(parts[1])
def dni = "$iphex:$porthex"
def hubNames = location.hubs*.name.findAll { it }
def d = addChildDevice("com.obycode", "ObyThing Music", dni, theHub.id, [label:"${app.label}", name:"ObyThing"])
log.trace "created ObyThing '${d.displayName}' with id $dni"
}
private String convertIPtoHex(ipAddress) {
String hex = ipAddress.tokenize( '.' ).collect { String.format( '%02X', it.toInteger() ) }.join()
return hex
}
private String convertPortToHex(port) {
String hexport = port.toString().format( '%04X', port.toInteger() )
return hexport
}

View File

@@ -0,0 +1,125 @@
/**
* Working From Home
*
* Copyright 2014 George Sudarkoff
*
* 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: "Working From Home",
namespace: "com.sudarkoff",
author: "George Sudarkoff",
description: "If after a particular time of day a certain person is still at home, trigger a 'Working From Home' action.",
category: "Mode Magic",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/ModeMagic/Cat-ModeMagic.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/ModeMagic/Cat-ModeMagic@2x.png"
)
preferences {
page (name:"configActions")
}
def configActions() {
dynamicPage(name: "configActions", title: "Configure Actions", uninstall: true, install: true) {
section ("When this person") {
input "person", "capability.presenceSensor", title: "Who?", multiple: false, required: true
}
section ("Still at home past") {
input "timeOfDay", "time", title: "What time?", required: true
}
def phrases = location.helloHome?.getPhrases()*.label
if (phrases) {
phrases.sort()
section("Perform this action") {
input "wfhPhrase", "enum", title: "\"Hello, Home\" action", required: true, options: phrases
}
}
section (title: "More options", hidden: hideOptions(), hideable: true) {
input "sendPushMessage", "bool", title: "Send a push notification?"
input "phone", "phone", title: "Send a Text Message?", required: false
input "days", "enum", title: "Set for specific day(s) of the week", multiple: true, required: false,
options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
}
section([mobileOnly:true]) {
label title: "Assign a name", required: false
mode title: "Set for specific mode(s)", required: false
}
}
}
def installed() {
initialize()
}
def updated() {
unschedule()
initialize()
}
def initialize() {
schedule(timeToday(timeOfDay, location?.timeZone), "checkPresence")
if (customName) {
app.setTitle(customName)
}
}
def checkPresence() {
if (daysOk && modeOk) {
if (person.latestValue("presence") == "present") {
log.debug "${person} is present, triggering WFH action."
location.helloHome.execute(settings.wfhPhrase)
def message = "${location.name} executed '${settings.wfhPhrase}' because ${person} is home."
send(message)
}
}
}
private send(msg) {
if (sendPushMessage != "No") {
sendPush(msg)
}
if (phone) {
sendSms(phone, msg)
}
log.debug msg
}
private getModeOk() {
def result = !modes || modes.contains(location.mode)
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/Los_Angeles"))
}
def day = df.format(new Date())
result = days.contains(day)
}
result
}
private hideOptions() {
(days || modes)? false: true
}

View File

@@ -0,0 +1,100 @@
/**
* Curb Control
*
* Author: Curb
*/
definition(
name: "Curb Control",
namespace: "Curb",
author: "Curb",
description: "This SmartApp allows you to interact with the switches in your physical graph through Curb.",
category: "Convenience",
iconUrl: "http://energycurb.com/images/logo.png",
iconX2Url: "http://energycurb.com/images/logo.png",
oauth: [displayName: "SmartThings Curb Control", displayLink: "energycurb.com"]
)
preferences {
section("Allow Curb to Control These Things...") {
input "switches", "capability.switch", title: "Which Switches?", multiple: true, required: false
}
}
mappings {
path("/") {
action: [
GET: "index"
]
}
path("/switches") {
action: [
GET: "listSwitches",
PUT: "updateSwitches"
]
}
path("/switches/:id") {
action: [
GET: "showSwitch",
PUT: "updateSwitch"
]
}
}
def installed() {}
def updated() {}
def index(){
[[url: "/switches"]]
}
def listSwitches() {
switches.collect { device(it,"switch") }
}
void updateSwitches() {
updateAll(switches)
}
def showSwitch() {
show(switches, "switch")
}
void updateSwitch() {
update(switches)
}
private void updateAll(devices) {
def command = request.JSON?.command
if (command) {
devices."$command"()
}
}
private void update(devices) {
log.debug "update, request: ${request.JSON}, params: ${params}, devices: $devices.id"
def command = request.JSON?.command
if (command) {
def device = devices.find { it.id == params.id }
if (!device) {
httpError(404, "Device not found")
} else {
device."$command"()
}
}
}
private show(devices, name) {
def d = devices.find { it.id == params.id }
if (!d) {
httpError(404, "Device not found")
}
else {
device(d, name)
}
}
private device(it, name){
if(it) {
def s = it.currentState(name)
[id: it.id, label: it.displayName, name: it.displayName, state: s]
}
}

View File

@@ -0,0 +1,597 @@
/**
* Netatmo Connect
*/
import java.text.DecimalFormat
import groovy.json.JsonSlurper
private apiUrl() { "https://api.netatmo.com" }
private getVendorName() { "netatmo" }
private getVendorAuthPath() { "https://api.netatmo.com/oauth2/authorize?" }
private getVendorTokenPath(){ "https://api.netatmo.com/oauth2/token" }
private getVendorIcon() { "https://s3.amazonaws.com/smartapp-icons/Partner/netamo-icon-1%402x.png" }
private getClientId() { appSettings.clientId }
private getClientSecret() { appSettings.clientSecret }
private getServerUrl() { "https://graph.api.smartthings.com" }
// Automatically generated. Make future change here.
definition(
name: "Netatmo (Connect)",
namespace: "dianoga",
author: "Brian Steere",
description: "Netatmo Integration",
category: "SmartThings Labs",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/netamo-icon-1.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/netamo-icon-1%402x.png",
oauth: true
){
appSetting "clientId"
appSetting "clientSecret"
}
preferences {
page(name: "Credentials", title: "Fetch OAuth2 Credentials", content: "authPage", install: false)
page(name: "listDevices", title: "Netatmo Devices", content: "listDevices", install: false)
}
mappings {
path("/receivedToken"){action: [POST: "receivedToken", GET: "receivedToken"]}
path("/receiveToken"){action: [POST: "receiveToken", GET: "receiveToken"]}
path("/auth"){action: [GET: "auth"]}
}
def authPage() {
log.debug "In authPage"
if(canInstallLabs()) {
def description = null
if (state.vendorAccessToken == null) {
log.debug "About to create access token."
createAccessToken()
description = "Tap to enter Credentials."
return dynamicPage(name: "Credentials", title: "Authorize Connection", nextPage:"listDevices", uninstall: true, install:false) {
section { href url:buildRedirectUrl("auth"), style:"embedded", required:false, title:"Connect to ${getVendorName()}:", description:description }
}
} else {
description = "Tap 'Next' to proceed"
return dynamicPage(name: "Credentials", title: "Credentials Accepted!", nextPage:"listDevices", uninstall: true, install:false) {
section { href url: buildRedirectUrl("receivedToken"), style:"embedded", required:false, title:"${getVendorName()} is now connected to SmartThings!", description:description }
}
}
}
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:"Credentials", title:"Upgrade needed!", nextPage:"", install:false, uninstall: true) {
section {
paragraph "$upgradeNeeded"
}
}
}
}
def auth() {
redirect location: oauthInitUrl()
}
def oauthInitUrl() {
log.debug "In oauthInitUrl"
/* OAuth Step 1: Request access code with our client ID */
state.oauthInitState = UUID.randomUUID().toString()
def oauthParams = [ response_type: "code",
client_id: getClientId(),
state: state.oauthInitState,
redirect_uri: buildRedirectUrl("receiveToken") ,
scope: "read_station"
]
return getVendorAuthPath() + toQueryString(oauthParams)
}
def buildRedirectUrl(endPoint) {
log.debug "In buildRedirectUrl"
return getServerUrl() + "/api/token/${state.accessToken}/smartapps/installations/${app.id}/${endPoint}"
}
def receiveToken() {
log.debug "In receiveToken"
def oauthParams = [
client_secret: getClientSecret(),
client_id: getClientId(),
grant_type: "authorization_code",
redirect_uri: buildRedirectUrl('receiveToken'),
code: params.code,
scope: "read_station"
]
def tokenUrl = getVendorTokenPath()
def params = [
uri: tokenUrl,
contentType: 'application/x-www-form-urlencoded',
body: oauthParams,
]
log.debug params
/* OAuth Step 2: Request access token with our client Secret and OAuth "Code" */
try {
httpPost(params) { response ->
log.debug response.data
def slurper = new JsonSlurper();
response.data.each {key, value ->
def data = slurper.parseText(key);
log.debug "Data: $data"
state.vendorRefreshToken = data.refresh_token
state.vendorAccessToken = data.access_token
state.vendorTokenExpires = now() + (data.expires_in * 1000)
return
}
}
} catch (Exception e) {
log.debug "Error: $e"
}
log.debug "State: $state"
if ( !state.vendorAccessToken ) { //We didn't get an access token, bail on install
return
}
/* OAuth Step 3: Use the access token to call into the vendor API throughout your code using state.vendorAccessToken. */
def html = """
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${getVendorName()} Connection</title>
<style type="text/css">
* { box-sizing: border-box; }
@font-face {
font-family: 'Swiss 721 W01 Thin';
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot');
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot?#iefix') format('embedded-opentype'),
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.woff') format('woff'),
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.ttf') format('truetype'),
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.svg#swis721_th_btthin') format('svg');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'Swiss 721 W01 Light';
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot');
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot?#iefix') format('embedded-opentype'),
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.woff') format('woff'),
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.ttf') format('truetype'),
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.svg#swis721_lt_btlight') format('svg');
font-weight: normal;
font-style: normal;
}
.container {
width: 100%;
padding: 40px;
/*background: #eee;*/
text-align: center;
}
img {
vertical-align: middle;
}
img:nth-child(2) {
margin: 0 30px;
}
p {
font-size: 2.2em;
font-family: 'Swiss 721 W01 Thin';
text-align: center;
color: #666666;
margin-bottom: 0;
}
/*
p:last-child {
margin-top: 0px;
}
*/
span {
font-family: 'Swiss 721 W01 Light';
}
</style>
</head>
<body>
<div class="container">
<img src=""" + getVendorIcon() + """ alt="Vendor icon" />
<img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/connected-device-icn%402x.png" alt="connected device icon" />
<img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/st-logo%402x.png" alt="SmartThings logo" />
<p>We have located your """ + getVendorName() + """ account.</p>
<p>Tap 'Done' to process your credentials.</p>
</div>
</body>
</html>
"""
render contentType: 'text/html', data: html
}
def receivedToken() {
log.debug "In receivedToken"
def html = """
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Withings Connection</title>
<style type="text/css">
* { box-sizing: border-box; }
@font-face {
font-family: 'Swiss 721 W01 Thin';
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot');
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot?#iefix') format('embedded-opentype'),
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.woff') format('woff'),
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.ttf') format('truetype'),
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.svg#swis721_th_btthin') format('svg');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'Swiss 721 W01 Light';
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot');
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot?#iefix') format('embedded-opentype'),
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.woff') format('woff'),
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.ttf') format('truetype'),
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.svg#swis721_lt_btlight') format('svg');
font-weight: normal;
font-style: normal;
}
.container {
width: 560px;
padding: 40px;
/*background: #eee;*/
text-align: center;
}
img {
vertical-align: middle;
}
img:nth-child(2) {
margin: 0 30px;
}
p {
font-size: 2.2em;
font-family: 'Swiss 721 W01 Thin';
text-align: center;
color: #666666;
margin-bottom: 0;
}
/*
p:last-child {
margin-top: 0px;
}
*/
span {
font-family: 'Swiss 721 W01 Light';
}
</style>
</head>
<body>
<div class="container">
<img src=""" + getVendorIcon() + """ alt="Vendor icon" />
<img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/connected-device-icn%402x.png" alt="connected device icon" />
<img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/st-logo%402x.png" alt="SmartThings logo" />
<p>Tap 'Done' to continue to Devices.</p>
</div>
</body>
</html>
"""
render contentType: 'text/html', data: html
}
// "
def refreshToken() {
log.debug "In refreshToken"
def oauthParams = [
client_secret: getClientSecret(),
client_id: getClientId(),
grant_type: "refresh_token",
refresh_token: state.vendorRefreshToken
]
def tokenUrl = getVendorTokenPath()
def params = [
uri: tokenUrl,
contentType: 'application/x-www-form-urlencoded',
body: oauthParams,
]
/* OAuth Step 2: Request access token with our client Secret and OAuth "Code" */
try {
httpPost(params) { response ->
def slurper = new JsonSlurper();
response.data.each {key, value ->
def data = slurper.parseText(key);
log.debug "Data: $data"
state.vendorRefreshToken = data.refresh_token
state.vendorAccessToken = data.access_token
state.vendorTokenExpires = now() + (data.expires_in * 1000)
return true
}
}
} catch (Exception e) {
log.debug "Error: $e"
}
log.debug "State: $state"
if ( !state.vendorAccessToken ) { //We didn't get an access token
return false
}
}
String toQueryString(Map m) {
return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&")
}
def installed() {
log.debug "Installed with settings: ${settings}"
initialize()
}
def updated() {
log.debug "Updated with settings: ${settings}"
unsubscribe()
unschedule()
initialize()
}
def initialize() {
log.debug "Initialized with settings: ${settings}"
// Pull the latest device info into state
getDeviceList();
settings.devices.each {
def deviceId = it
def detail = state.deviceDetail[deviceId]
try {
switch(detail.type) {
case 'NAMain':
log.debug "Base station"
createChildDevice("Netatmo Basestation", deviceId, "${detail.type}.${deviceId}", detail.module_name)
break
case 'NAModule1':
log.debug "Outdoor module"
createChildDevice("Netatmo Outdoor Module", deviceId, "${detail.type}.${deviceId}", detail.module_name)
break
case 'NAModule3':
log.debug "Rain Gauge"
createChildDevice("Netatmo Rain", deviceId, "${detail.type}.${deviceId}", detail.module_name)
break
case 'NAModule4':
log.debug "Additional module"
createChildDevice("Netatmo Additional Module", deviceId, "${detail.type}.${deviceId}", detail.module_name)
break
}
} catch (Exception e) {
log.error "Error creating device: ${e}"
}
}
// Cleanup any other devices that need to go away
def delete = getChildDevices().findAll { !settings.devices.contains(it.deviceNetworkId) }
log.debug "Delete: $delete"
delete.each { deleteChildDevice(it.deviceNetworkId) }
// Do the initial poll
poll()
// Schedule it to run every 5 minutes
runEvery5Minutes("poll")
}
def uninstalled() {
log.debug "In uninstalled"
removeChildDevices(getChildDevices())
}
def getDeviceList() {
log.debug "In getDeviceList"
def deviceList = [:]
state.deviceDetail = [:]
state.deviceState = [:]
apiGet("/api/devicelist") { response ->
response.data.body.devices.each { value ->
def key = value._id
deviceList[key] = "${value.station_name}: ${value.module_name}"
state.deviceDetail[key] = value
state.deviceState[key] = value.dashboard_data
}
response.data.body.modules.each { value ->
def key = value._id
deviceList[key] = "${state.deviceDetail[value.main_device].station_name}: ${value.module_name}"
state.deviceDetail[key] = value
state.deviceState[key] = value.dashboard_data
}
}
return deviceList.sort() { it.value.toLowerCase() }
}
private removeChildDevices(delete) {
log.debug "In removeChildDevices"
log.debug "deleting ${delete.size()} devices"
delete.each {
deleteChildDevice(it.deviceNetworkId)
}
}
def createChildDevice(deviceFile, dni, name, label) {
log.debug "In createChildDevice"
try {
def existingDevice = getChildDevice(dni)
if(!existingDevice) {
log.debug "Creating child"
def childDevice = addChildDevice("dianoga", deviceFile, dni, null, [name: name, label: label, completedSetup: true])
} else {
log.debug "Device $dni already exists"
}
} catch (e) {
log.error "Error creating device: ${e}"
}
}
def listDevices() {
log.debug "In listDevices"
def devices = getDeviceList()
dynamicPage(name: "listDevices", title: "Choose devices", install: true) {
section("Devices") {
input "devices", "enum", title: "Select Device(s)", required: false, multiple: true, options: devices
}
section("Preferences") {
input "rainUnits", "enum", title: "Rain Units", description: "Millimeters (mm) or Inches (in)", required: true, options: [mm:'Millimeters', in:'Inches']
}
}
}
def apiGet(String path, Map query, Closure callback) {
if(now() >= state.vendorTokenExpires) {
refreshToken();
}
query['access_token'] = state.vendorAccessToken
def params = [
uri: apiUrl(),
path: path,
'query': query
]
// log.debug "API Get: $params"
try {
httpGet(params) { response ->
callback.call(response)
}
} catch (Exception e) {
// This is most likely due to an invalid token. Try to refresh it and try again.
log.debug "apiGet: Call failed $e"
if(refreshToken()) {
log.debug "apiGet: Trying again after refreshing token"
httpGet(params) { response ->
callback.call(response)
}
}
}
}
def apiGet(String path, Closure callback) {
apiGet(path, [:], callback);
}
def poll() {
log.debug "In Poll"
getDeviceList();
def children = getChildDevices()
log.debug "State: ${state.deviceState}"
settings.devices.each { deviceId ->
def detail = state.deviceDetail[deviceId]
def data = state.deviceState[deviceId]
def child = children.find { it.deviceNetworkId == deviceId }
log.debug "Update: $child";
switch(detail.type) {
case 'NAMain':
log.debug "Updating NAMain $data"
child?.sendEvent(name: 'temperature', value: cToPref(data['Temperature']) as float, unit: getTemperatureScale())
child?.sendEvent(name: 'carbonDioxide', value: data['CO2'])
child?.sendEvent(name: 'humidity', value: data['Humidity'])
child?.sendEvent(name: 'pressure', value: data['Pressure'])
child?.sendEvent(name: 'noise', value: data['Noise'])
break;
case 'NAModule1':
log.debug "Updating NAModule1 $data"
child?.sendEvent(name: 'temperature', value: cToPref(data['Temperature']) as float, unit: getTemperatureScale())
child?.sendEvent(name: 'humidity', value: data['Humidity'])
break;
case 'NAModule3':
log.debug "Updating NAModule3 $data"
child?.sendEvent(name: 'rain', value: rainToPref(data['Rain']) as float, unit: settings.rainUnits)
child?.sendEvent(name: 'rainSumHour', value: rainToPref(data['sum_rain_1']) as float, unit: settings.rainUnits)
child?.sendEvent(name: 'rainSumDay', value: rainToPref(data['sum_rain_24']) as float, unit: settings.rainUnits)
child?.sendEvent(name: 'units', value: settings.rainUnits)
break;
case 'NAModule4':
log.debug "Updating NAModule4 $data"
child?.sendEvent(name: 'temperature', value: cToPref(data['Temperature']) as float, unit: getTemperatureScale())
child?.sendEvent(name: 'carbonDioxide', value: data['CO2'])
child?.sendEvent(name: 'humidity', value: data['Humidity'])
break;
}
}
}
def cToPref(temp) {
if(getTemperatureScale() == 'C') {
return temp
} else {
return temp * 1.8 + 32
}
}
def rainToPref(rain) {
if(settings.rainUnits == 'mm') {
return rain
} else {
return rain * 0.039370
}
}
def debugEvent(message, displayEvent) {
def results = [
name: "appdebug",
descriptionText: message,
displayed: displayEvent
]
log.debug "Generating AppDebug Event: ${results}"
sendEvent (results)
}
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 }
}

View File

@@ -0,0 +1,83 @@
/**
* HVAC Auto Off
*
* Author: dianoga7@3dgo.net
* Date: 2013-07-21
*/
// Automatically generated. Make future change here.
definition(
name: "Thermostat Auto Off",
namespace: "dianoga",
author: "dianoga7@3dgo.net",
description: "Automatically turn off thermostat when windows/doors open. Turn it back on when everything is closed up.",
category: "Green Living",
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("Control") {
input("thermostat", "capability.thermostat", title: "Thermostat")
}
section("Open/Close") {
input("sensors", "capability.contactSensor", title: "Sensors", multiple: true)
input("delay", "number", title: "Delay (seconds)")
}
}
def installed() {
log.debug "Installed with settings: ${settings}"
initialize()
}
def updated() {
log.debug "Updated with settings: ${settings}"
unsubscribe()
unschedule()
initialize()
}
def initialize() {
state.changed = false
subscribe(sensors, 'contact', "sensorChange")
}
def sensorChange(evt) {
log.debug "Desc: $evt.value , $state"
if(evt.value == 'open' && !state.changed) {
unschedule()
runIn(delay, 'turnOff')
} else if(evt.value == 'closed' && state.changed) {
// All closed?
def isOpen = false
for(sensor in sensors) {
if(sensor.id != evt.deviceId && sensor.currentValue('contact') == 'open') {
isOpen = true
}
}
if(!isOpen) {
unschedule()
runIn(delay, 'restore')
}
}
}
def turnOff() {
log.debug "Turning off thermostat due to contact open"
state.thermostatMode = thermostat.currentValue("thermostatMode")
thermostat.off()
state.changed = true
log.debug "State: $state"
}
def restore() {
log.debug "Setting thermostat to $state.thermostatMode"
thermostat.setThermostatMode(state.thermostatMode)
state.changed = false
}

View File

@@ -0,0 +1,108 @@
/**
* Whole House Fan
*
* Copyright 2014 Brian Steere
*
* 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: "Whole House Fan",
namespace: "dianoga",
author: "Brian Steere",
description: "Toggle a whole house fan (switch) when: Outside is cooler than inside, Inside is above x temp, Thermostat is off",
category: "Green Living",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Developers/whole-house-fan.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Developers/whole-house-fan%402x.png"
)
preferences {
section("Outdoor") {
input "outTemp", "capability.temperatureMeasurement", title: "Outdoor Thermometer", required: true
}
section("Indoor") {
input "inTemp", "capability.temperatureMeasurement", title: "Indoor Thermometer", required: true
input "minTemp", "number", title: "Minimum Indoor Temperature"
input "fans", "capability.switch", title: "Vent Fan", multiple: true, required: true
}
section("Thermostat") {
input "thermostat", "capability.thermostat", title: "Thermostat"
}
section("Windows/Doors") {
paragraph "[Optional] Only turn on the fan if at least one of these is open"
input "checkContacts", "enum", title: "Check windows/doors", options: ['Yes', 'No'], required: true
input "contacts", "capability.contactSensor", title: "Windows/Doors", multiple: true, required: false
}
}
def installed() {
log.debug "Installed with settings: ${settings}"
initialize()
}
def updated() {
log.debug "Updated with settings: ${settings}"
unsubscribe()
initialize()
}
def initialize() {
state.fanRunning = false;
subscribe(outTemp, "temperature", "checkThings");
subscribe(inTemp, "temperature", "checkThings");
subscribe(thermostat, "thermostatMode", "checkThings");
subscribe(contacts, "contact", "checkThings");
}
def checkThings(evt) {
def outsideTemp = settings.outTemp.currentTemperature
def insideTemp = settings.inTemp.currentTemperature
def thermostatMode = settings.thermostat.currentThermostatMode
def somethingOpen = settings.checkContacts == 'No' || settings.contacts?.find { it.currentContact == 'open' }
log.debug "Inside: $insideTemp, Outside: $outsideTemp, Thermostat: $thermostatMode, Something Open: $somethingOpen"
def shouldRun = true;
if(thermostatMode != 'off') {
log.debug "Not running due to thermostat mode"
shouldRun = false;
}
if(insideTemp < outsideTemp) {
log.debug "Not running due to insideTemp > outdoorTemp"
shouldRun = false;
}
if(insideTemp < settings.minTemp) {
log.debug "Not running due to insideTemp < minTemp"
shouldRun = false;
}
if(!somethingOpen) {
log.debug "Not running due to nothing open"
shouldRun = false
}
if(shouldRun && !state.fanRunning) {
fans.on();
state.fanRunning = true;
} else if(!shouldRun && state.fanRunning) {
fans.off();
state.fanRunning = false;
}
}

View File

@@ -0,0 +1,113 @@
/**
* Its too humid!
*
* Copyright 2014 Brian Critchlow
* Based on Its too cold code by 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: "Humidity Alert!",
namespace: "docwisdom",
author: "Brian Critchlow",
description: "Notify me when the humidity rises above or falls below the given threshold. It will turn on a switch when it rises above the first threshold and off when it falls below the second threshold.",
category: "Convenience",
iconUrl: "https://graph.api.smartthings.com/api/devices/icons/st.Weather.weather9-icn",
iconX2Url: "https://graph.api.smartthings.com/api/devices/icons/st.Weather.weather9-icn?displaySize=2x"
)
preferences {
section("Monitor the humidity of:") {
input "humiditySensor1", "capability.relativeHumidityMeasurement"
}
section("When the humidity rises above:") {
input "humidity1", "number", title: "Percentage ?"
}
section("When the humidity falls below:") {
input "humidity2", "number", title: "Percentage ?"
}
section( "Notifications" ) {
input "sendPushMessage", "enum", title: "Send a push notification?", metadata:[values:["Yes","No"]], required:false
input "phone1", "phone", title: "Send a Text Message?", required: false
}
section("Control this switch:") {
input "switch1", "capability.switch", required: false
}
}
def installed() {
subscribe(humiditySensor1, "humidity", humidityHandler)
}
def updated() {
unsubscribe()
subscribe(humiditySensor1, "humidity", humidityHandler)
}
def humidityHandler(evt) {
log.trace "humidity: ${evt.value}"
log.trace "set point: ${humidity1}"
def currentHumidity = Double.parseDouble(evt.value.replace("%", ""))
def tooHumid = humidity1
def notHumidEnough = humidity2
def mySwitch = settings.switch1
def deltaMinutes = 10
def timeAgo = new Date(now() - (1000 * 60 * deltaMinutes).toLong())
def recentEvents = humiditySensor1.eventsSince(timeAgo)
log.trace "Found ${recentEvents?.size() ?: 0} events in the last ${deltaMinutes} minutes"
def alreadySentSms = recentEvents.count { Double.parseDouble(it.value.replace("%", "")) >= tooHumid } > 1 || recentEvents.count { Double.parseDouble(it.value.replace("%", "")) <= notHumidEnough } > 1
if (currentHumidity >= tooHumid) {
log.debug "Checking how long the humidity sensor has been reporting >= ${tooHumid}"
// Don't send a continuous stream of text messages
if (alreadySentSms) {
log.debug "Notification already sent within the last ${deltaMinutes} minutes"
} else {
log.debug "Humidity Rose Above ${tooHumid}: sending SMS to $phone1 and activating ${mySwitch}"
send("${humiditySensor1.label} sensed high humidity level of ${evt.value}")
switch1?.on()
}
}
if (currentHumidity <= notHumidEnough) {
log.debug "Checking how long the humidity sensor has been reporting <= ${notHumidEnough}"
if (alreadySentSms) {
log.debug "Notification already sent within the last ${deltaMinutes} minutes"
} else {
log.debug "Humidity Fell Below ${notHumidEnough}: sending SMS to $phone1 and activating ${mySwitch}"
send("${humiditySensor1.label} sensed high humidity level of ${evt.value}")
switch1?.off()
}
}
}
private send(msg) {
if ( sendPushMessage != "No" ) {
log.debug( "sending push message" )
sendPush( msg )
}
if ( phone1 ) {
log.debug( "sending text message" )
sendSms( phone1, msg )
}
log.debug msg
}

View File

@@ -0,0 +1,49 @@
/**
* Let There Be Dark!
* Turn your lights off when a Contact Sensor is opened and turn them back on when it is closed, ONLY if the Lights were previouly on.
*
* Author: SmartThings modified by Douglas Rich
*/
definition(
name: "Let There Be Dark!",
namespace: "Dooglave",
author: "Dooglave",
description: "Turn your lights off when a Contact Sensor is opened and turn them back on when it is closed, ONLY if the Lights were previouly on",
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 off a light") {
input "switch1", "capability.switch"
}
}
def installed() {
subscribe(contact1, "contact", contactHandler)
subscribe(switch1, "switch.on", switchOnHandler)
subscribe(switch1, "switch.off", switchOffHandler)
}
def updated() {
unsubscribe()
subscribe(contact1, "contact", contactHandler)
subscribe(switch1, "switch.on", switchOnHandler)
subscribe(switch1, "switch.off", switchOffHandler)
}
def contactHandler(evt) {
log.debug "$evt.value"
if (evt.value == "open") {
state.wasOn = switch1.currentValue("switch") == "on"
switch1.off()
}
if (evt.value == "closed") {
if(state.wasOn)switch1.on()
}
}

View File

@@ -0,0 +1,153 @@
/**
* Smart Windows
* Compares two temperatures indoor vs outdoor, for example then sends an alert if windows are open (or closed!).
*
* Copyright 2014 Eric Gideon
*
* Based in part on the "When it's going to rain" SmartApp by the SmartThings team,
* primarily the message throttling code.
*
* 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: "Smart Windows",
namespace: "egid",
author: "Eric Gideon",
description: "Compares two temperatures indoor vs outdoor, for example then sends an alert if windows are open (or closed!). If you don't use an external temperature device, your zipcode will be used instead.",
iconUrl: "https://s3.amazonaws.com/smartthings-device-icons/Home/home9-icn.png",
iconX2Url: "https://s3.amazonaws.com/smartthings-device-icons/Home/home9-icn@2x.png"
)
preferences {
section( "Set the temperature range for your comfort zone..." ) {
input "minTemp", "number", title: "Minimum temperature"
input "maxTemp", "number", title: "Maximum temperature"
}
section( "Select windows to check..." ) {
input "sensors", "capability.contactSensor", multiple: true
}
section( "Select temperature devices to monitor..." ) {
input "inTemp", "capability.temperatureMeasurement", title: "Indoor"
input "outTemp", "capability.temperatureMeasurement", title: "Outdoor (optional)", required: false
}
section( "Set your location" ) {
input "zipCode", "text", title: "Zip code"
}
section( "Notifications" ) {
input "sendPushMessage", "enum", title: "Send a push notification?", metadata:[values:["Yes","No"]], required:false
input "retryPeriod", "number", title: "Minutes between notifications:"
}
}
def installed() {
log.debug "Installed: $settings"
subscribe( inTemp, "temperature", temperatureHandler )
}
def updated() {
log.debug "Updated: $settings"
unsubscribe()
subscribe( inTemp, "temperature", temperatureHandler )
}
def temperatureHandler(evt) {
def currentOutTemp = null
if ( outTemp ) {
currentOutTemp = outTemp.latestValue("temperature")
} else {
log.debug "No external temperature device set. Checking WUnderground...."
currentOutTemp = weatherCheck()
}
def currentInTemp = evt.doubleValue
def openWindows = sensors.findAll { it?.latestValue("contact") == 'open' }
log.trace "Temp event: $evt"
log.info "In: $currentInTemp; Out: $currentOutTemp"
// Don't spam notifications
// *TODO* use state.foo from Severe Weather Alert to do this better
if (!retryPeriod) {
def retryPeriod = 30
}
def timeAgo = new Date(now() - (1000 * 60 * retryPeriod).toLong())
def recentEvents = inTemp.eventsSince(timeAgo)
log.trace "Found ${recentEvents?.size() ?: 0} events in the last $retryPeriod minutes"
// Figure out if we should notify
if ( currentInTemp > minTemp && currentInTemp < maxTemp ) {
log.info "In comfort zone: $currentInTemp is between $minTemp and $maxTemp."
log.debug "No notifications sent."
} else if ( currentInTemp > maxTemp ) {
// Too warm. Can we do anything?
def alreadyNotified = recentEvents.count { it.doubleValue > currentOutTemp } > 1
if ( !alreadyNotified ) {
if ( currentOutTemp < maxTemp && !openWindows ) {
send( "Open some windows to cool down the house! Currently ${currentInTemp}°F inside and ${currentOutTemp}°F outside." )
} else if ( currentOutTemp > maxTemp && openWindows ) {
send( "It's gotten warmer outside! You should close these windows: ${openWindows.join(', ')}. Currently ${currentInTemp}°F inside and ${currentOutTemp}°F outside." )
} else {
log.debug "No notifications sent. Everything is in the right place."
}
} else {
log.debug "Already notified! No notifications sent."
}
} else if ( currentInTemp < minTemp ) {
// Too cold! Is it warmer outside?
def alreadyNotified = recentEvents.count { it.doubleValue < currentOutTemp } > 1
if ( !alreadyNotified ) {
if ( currentOutTemp > minTemp && !openWindows ) {
send( "Open some windows to warm up the house! Currently ${currentInTemp}°F inside and ${currentOutTemp}°F outside." )
} else if ( currentOutTemp < minTemp && openWindows ) {
send( "It's gotten colder outside! You should close these windows: ${openWindows.join(', ')}. Currently ${currentInTemp}°F inside and ${currentOutTemp}°F outside." )
} else {
log.debug "No notifications sent. Everything is in the right place."
}
} else {
log.debug "Already notified! No notifications sent."
}
}
}
def weatherCheck() {
def json = getWeatherFeature("conditions", zipCode)
def currentTemp = json?.current_observation?.temp_f
if ( currentTemp ) {
log.trace "Temp: $currentTemp (WeatherUnderground)"
return currentTemp
} else {
log.warn "Did not get a temp: $json"
return false
}
}
private send(msg) {
if ( sendPushMessage != "No" ) {
log.debug( "sending push message" )
sendPush( msg )
sendEvent(linkText:app.label, descriptionText:msg, eventType:"SOLUTION_EVENT", displayed: true, name:"summary")
}
if ( phone1 ) {
log.debug( "sending text message" )
sendSms( phone1, msg )
}
log.info msg
}

View File

@@ -0,0 +1,152 @@
/**
* Weather Windows
* Compares two temperatures indoor vs outdoor, for example then sends an alert if windows are open (or closed!).
*
* Copyright 2015 Eric Gideon
*
* Based in part on the "When it's going to rain" SmartApp by the SmartThings team,
* primarily the message throttling code.
*
* 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: "Weather Windows",
namespace: "egid",
author: "Eric Gideon",
category: "Convenience",
description: "Compares two temperatures indoor vs outdoor, for example then sends an alert if windows are open (or closed!). If you don't use an external temperature device, your zipcode will be used instead.",
iconUrl: "https://s3.amazonaws.com/smartthings-device-icons/Home/home9-icn.png",
iconX2Url: "https://s3.amazonaws.com/smartthings-device-icons/Home/home9-icn@2x.png"
)
preferences {
section( "Set the temperature range for your comfort zone..." ) {
input "minTemp", "number", title: "Minimum temperature"
input "maxTemp", "number", title: "Maximum temperature"
}
section( "Select windows to check..." ) {
input "sensors", "capability.contactSensor", multiple: true
}
section( "Select temperature devices to monitor..." ) {
input "inTemp", "capability.temperatureMeasurement", title: "Indoor"
input "outTemp", "capability.temperatureMeasurement", title: "Outdoor (optional)", required: false
}
section( "Set your location" ) {
input "zipCode", "text", title: "Zip code"
}
section( "Notifications" ) {
input "sendPushMessage", "enum", title: "Send a push notification?", metadata:[values:["Yes","No"]], required:false
input "retryPeriod", "number", title: "Minutes between notifications:"
}
}
def installed() {
log.debug "Installed: $settings"
subscribe( inTemp, "temperature", temperatureHandler )
}
def updated() {
log.debug "Updated: $settings"
unsubscribe()
subscribe( inTemp, "temperature", temperatureHandler )
}
def temperatureHandler(evt) {
def currentOutTemp = null
if ( outTemp ) {
currentOutTemp = outTemp.latestValue("temperature")
} else {
log.debug "No external temperature device set. Checking WUnderground...."
currentOutTemp = weatherCheck()
}
def currentInTemp = evt.doubleValue
def openWindows = sensors.findAll { it?.latestValue("contact") == 'open' }
log.trace "Temp event: $evt"
log.info "In: $currentInTemp; Out: $currentOutTemp"
// Don't spam notifications
// *TODO* use state.foo from Severe Weather Alert to do this better
def retryPeriodInMinutes = retryPeriod ?: 30
def timeAgo = new Date(now() - (1000 * 60 * retryPeriodInMinutes).toLong())
def recentEvents = inTemp.eventsSince(timeAgo)
log.trace "Found ${recentEvents?.size() ?: 0} events in the last $retryPeriodInMinutes minutes"
// Figure out if we should notify
if ( currentInTemp > minTemp && currentInTemp < maxTemp ) {
log.info "In comfort zone: $currentInTemp is between $minTemp and $maxTemp."
log.debug "No notifications sent."
} else if ( currentInTemp > maxTemp ) {
// Too warm. Can we do anything?
def alreadyNotified = recentEvents.count { it.doubleValue > currentOutTemp } > 1
if ( !alreadyNotified ) {
if ( currentOutTemp < maxTemp && !openWindows ) {
send( "Open some windows to cool down the house! Currently ${currentInTemp}°F inside and ${currentOutTemp}°F outside." )
} else if ( currentOutTemp > maxTemp && openWindows ) {
send( "It's gotten warmer outside! You should close these windows: ${openWindows.join(', ')}. Currently ${currentInTemp}°F inside and ${currentOutTemp}°F outside." )
} else {
log.debug "No notifications sent. Everything is in the right place."
}
} else {
log.debug "Already notified! No notifications sent."
}
} else if ( currentInTemp < minTemp ) {
// Too cold! Is it warmer outside?
def alreadyNotified = recentEvents.count { it.doubleValue < currentOutTemp } > 1
if ( !alreadyNotified ) {
if ( currentOutTemp > minTemp && !openWindows ) {
send( "Open some windows to warm up the house! Currently ${currentInTemp}°F inside and ${currentOutTemp}°F outside." )
} else if ( currentOutTemp < minTemp && openWindows ) {
send( "It's gotten colder outside! You should close these windows: ${openWindows.join(', ')}. Currently ${currentInTemp}°F inside and ${currentOutTemp}°F outside." )
} else {
log.debug "No notifications sent. Everything is in the right place."
}
} else {
log.debug "Already notified! No notifications sent."
}
}
}
def weatherCheck() {
def json = getWeatherFeature("conditions", zipCode)
def currentTemp = json?.current_observation?.temp_f
if ( currentTemp ) {
log.trace "Temp: $currentTemp (WeatherUnderground)"
return currentTemp
} else {
log.warn "Did not get a temp: $json"
return false
}
}
private send(msg) {
if ( sendPushMessage != "No" ) {
log.debug( "sending push message" )
sendPush( msg )
sendEvent(linkText:app.label, descriptionText:msg, eventType:"SOLUTION_EVENT", displayed: true, name:"summary")
}
if ( phone1 ) {
log.debug( "sending text message" )
sendSms( phone1, msg )
}
log.info msg
}

View File

@@ -0,0 +1,55 @@
/**
* Coffee After Shower
*
*
* 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: "Coffee After Shower",
namespace: "hwustrack",
author: "Hans Wustrack",
description: "This app is designed simply to turn on your coffee machine while you are taking a shower.",
category: "My Apps",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png")
preferences {
section("About") {
paragraph "This app is designed simply to turn on your coffee machine " +
"while you are taking a shower."
}
section("Bathroom humidity sensor") {
input "bathroom", "capability.relativeHumidityMeasurement", title: "Which humidity sensor?"
}
section("Coffee maker to turn on") {
input "coffee", "capability.switch", title: "Which switch?"
}
section("Humidity level to switch coffee on at") {
input "relHum", "number", title: "Humidity level?", defaultValue: 50
}
}
def installed() {
subscribe(bathroom, "humidity", coffeeMaker)
}
def updated() {
unsubscribe()
subscribe(bathroom, "humidity", coffeeMaker)
}
def coffeeMaker(shower) {
log.info "Humidity value: $shower.value"
if (shower.value.toInteger() > relHum) {
coffee.on()
}
}

View File

@@ -0,0 +1,88 @@
/**
* Door Knocker
*
* Author: brian@bevey.org
* Date: 9/10/13
*
* Let me know when someone knocks on the door, but ignore
* when someone is opening the door.
*/
definition(
name: "Door Knocker",
namespace: "imbrianj",
author: "brian@bevey.org",
description: "Alert if door is knocked, but not opened.",
category: "Convenience",
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("When Someone Knocks?") {
input name: "knockSensor", type: "capability.accelerationSensor", title: "Where?"
}
section("But not when they open this door?") {
input name: "openSensor", type: "capability.contactSensor", title: "Where?"
}
section("Knock Delay (defaults to 5s)?") {
input name: "knockDelay", type: "number", title: "How Long?", required: false
}
section("Notifications") {
input "sendPushMessage", "enum", title: "Send a push notification?", metadata: [values: ["Yes", "No"]], required: false
input "phone", "phone", title: "Send a Text Message?", required: false
}
}
def installed() {
init()
}
def updated() {
unsubscribe()
init()
}
def init() {
state.lastClosed = 0
subscribe(knockSensor, "acceleration.active", handleEvent)
subscribe(openSensor, "contact.closed", doorClosed)
}
def doorClosed(evt) {
state.lastClosed = now()
}
def doorKnock() {
if((openSensor.latestValue("contact") == "closed") &&
(now() - (60 * 1000) > state.lastClosed)) {
log.debug("${knockSensor.label ?: knockSensor.name} detected a knock.")
send("${knockSensor.label ?: knockSensor.name} detected a knock.")
}
else {
log.debug("${knockSensor.label ?: knockSensor.name} knocked, but looks like it was just someone opening the door.")
}
}
def handleEvent(evt) {
def delay = knockDelay ?: 5
runIn(delay, "doorKnock")
}
private send(msg) {
if(sendPushMessage != "No") {
log.debug("Sending push message")
sendPush(msg)
}
if(phone) {
log.debug("Sending text message")
sendSms(phone, msg)
}
log.debug(msg)
}

View File

@@ -0,0 +1,119 @@
/**
* Forgiving Security
*
* Author: brian@bevey.org
* Date: 10/25/13
*
* Arm a simple security system based on mode. Has a grace period to allow an
* ever present lag in presence detection.
*/
definition(
name: "Forgiving Security",
namespace: "imbrianj",
author: "brian@bevey.org",
description: "Alerts you if something happens while you're away. Has a settable grace period to compensate for presence sensors that may take a few seconds to be noticed.",
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"
)
preferences {
section("Things to secure?") {
input "contacts", "capability.contactSensor", title: "Contact Sensors", multiple: true, required: false
input "motions", "capability.motionSensor", title: "Motion Sensors", multiple: true, required: false
}
section("Alarms to go off?") {
input "alarms", "capability.alarm", title: "Which Alarms?", multiple: true, required: false
input "lights", "capability.switch", title: "Turn on which lights?", multiple: true, required: false
}
section("Delay for presence lag?") {
input name: "presenceDelay", type: "number", title: "Seconds (defaults to 15s)", required: false
}
section("Notifications?") {
input "sendPushMessage", "enum", title: "Send a push notification?", metadata: [values: ["Yes", "No"]], required: false
input "phone", "phone", title: "Send a Text Message?", required: false
}
section("Message interval?") {
input name: "messageDelay", type: "number", title: "Minutes (default to every message)", required: false
}
}
def installed() {
init()
}
def updated() {
unsubscribe()
init()
}
def init() {
state.lastTrigger = now()
state.deviceTriggers = []
subscribe(contacts, "contact.open", triggerAlarm)
subscribe(motions, "motion.active", triggerAlarm)
}
def triggerAlarm(evt) {
def presenceDelay = presenceDelay ?: 15
if(now() - (presenceDelay * 1000) > state.lastTrigger) {
log.warn("Stale event - ignoring")
state.deviceTriggers = []
}
state.deviceTriggers.add(evt.displayName)
state.triggerMode = location.mode
state.lastTrigger = now()
log.info(evt.displayName + " triggered an alarm. Waiting for presence lag.")
runIn(presenceDelay, "fireAlarm")
}
def fireAlarm() {
if(state.deviceTriggers.size() > 0) {
def devices = state.deviceTriggers.unique().join(", ")
if(location.mode == state.triggerMode) {
log.info(devices + " alarm triggered and mode hasn't changed.")
send(devices + " alarm has been triggered!")
lights?.on()
alarms?.both()
}
else {
log.info(devices + " alarm triggered, but it looks like you were just coming home. Ignoring.")
}
}
state.deviceTriggers = []
}
private send(msg) {
def delay = (messageDelay != null && messageDelay != "") ? messageDelay * 60 * 1000 : 0
if(now() - delay > state.lastMessage) {
state.lastMessage = now()
if(sendPushMessage == "Yes") {
log.debug("Sending push message.")
sendPush(msg)
}
if(phone) {
log.debug("Sending text message.")
sendSms(phone, msg)
}
log.debug(msg)
}
else {
log.info("Have a message to send, but user requested to not get it.")
}
}

View File

@@ -0,0 +1,77 @@
/**
* Hall Light: Welcome Home
*
* Author: brian@bevey.org
* Date: 9/25/13
*
* Turn on the hall light if someone comes home (presence) and the door opens.
*/
definition(
name: "Hall Light: Welcome Home",
namespace: "imbrianj",
author: "brian@bevey.org",
description: "Turn on the hall light if someone comes home (presence) and the door opens.",
category: "Convenience",
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("People to watch for?") {
input "people", "capability.presenceSensor", multiple: true
}
section("Front Door?") {
input "sensors", "capability.contactSensor", multiple: true
}
section("Hall Light?") {
input "lights", "capability.switch", title: "Switch Turned On", multilple: true
}
section("Presence Delay (defaults to 30s)?") {
input name: "presenceDelay", type: "number", title: "How Long?", required: false
}
section("Door Contact Delay (defaults to 10s)?") {
input name: "contactDelay", type: "number", title: "How Long?", required: false
}
}
def installed() {
init()
}
def updated() {
unsubscribe()
init()
}
def init() {
state.lastClosed = now()
subscribe(people, "presence.present", presence)
subscribe(sensors, "contact.open", doorOpened)
}
def presence(evt) {
def delay = contactDelay ?: 10
state.lastPresence = now()
if(now() - (delay * 1000) < state.lastContact) {
log.info('Presence was delayed, but you probably still want the light on.')
lights?.on()
}
}
def doorOpened(evt) {
def delay = presenceDelay ?: 30
state.lastContact = now()
if(now() - (delay * 1000) < state.lastPresence) {
log.info('Welcome home! Let me get that light for you.')
lights?.on()
}
}

View File

@@ -0,0 +1,164 @@
/**
* Nobody Home
*
* Author: brian@bevey.org
* Date: 12/19/14
*
* Monitors a set of presence detectors and triggers a mode change when everyone has left.
* When everyone has left, sets mode to a new defined mode.
* When at least one person returns home, set the mode back to a new defined mode.
* When someone is home - or upon entering the home, their mode may change dependent on sunrise / sunset.
*/
definition(
name: "Nobody Home",
namespace: "imbrianj",
author: "brian@bevey.org",
description: "When everyone leaves, change mode. If at least one person home, switch mode based on sun position.",
category: "Mode Magic",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience%402x.png"
)
preferences {
section("When all of these people leave home") {
input "people", "capability.presenceSensor", multiple: true
}
section("Change to this mode to...") {
input "newAwayMode", "mode", title: "Everyone is away"
input "newSunsetMode", "mode", title: "At least one person home and nightfall"
input "newSunriseMode", "mode", title: "At least one person home and sunrise"
}
section("Away threshold (defaults to 10 min)") {
input "awayThreshold", "decimal", title: "Number of minutes", required: false
}
section("Notifications") {
input "sendPushMessage", "enum", title: "Send a push notification?", metadata:[values:["Yes","No"]], required:false
}
}
def installed() {
init()
}
def updated() {
unsubscribe()
init()
}
def init() {
subscribe(people, "presence", presence)
subscribe(location, "sunrise", setSunrise)
subscribe(location, "sunset", setSunset)
state.sunMode = location.mode
}
def setSunrise(evt) {
changeSunMode(newSunriseMode)
}
def setSunset(evt) {
changeSunMode(newSunsetMode)
}
def changeSunMode(newMode) {
state.sunMode = newMode
if(everyoneIsAway() && (location.mode == newAwayMode)) {
log.debug("Mode is away, not evaluating")
}
else if(location.mode != newMode) {
def message = "${app.label} changed your mode to '${newMode}'"
send(message)
setLocationMode(newMode)
}
else {
log.debug("Mode is the same, not evaluating")
}
}
def presence(evt) {
if(evt.value == "not present") {
log.debug("Checking if everyone is away")
if(everyoneIsAway()) {
log.info("Starting ${newAwayMode} sequence")
def delay = (awayThreshold != null && awayThreshold != "") ? awayThreshold * 60 : 10 * 60
runIn(delay, "setAway")
}
}
else {
if(location.mode != state.sunMode) {
log.debug("Checking if anyone is home")
if(anyoneIsHome()) {
log.info("Starting ${state.sunMode} sequence")
changeSunMode(state.sunMode)
}
}
else {
log.debug("Mode is the same, not evaluating")
}
}
}
def setAway() {
if(everyoneIsAway()) {
if(location.mode != newAwayMode) {
def message = "${app.label} changed your mode to '${newAwayMode}' because everyone left home"
log.info(message)
send(message)
setLocationMode(newAwayMode)
}
else {
log.debug("Mode is the same, not evaluating")
}
}
else {
log.info("Somebody returned home before we set to '${newAwayMode}'")
}
}
private everyoneIsAway() {
def result = true
if(people.findAll { it?.currentPresence == "present" }) {
result = false
}
log.debug("everyoneIsAway: ${result}")
return result
}
private anyoneIsHome() {
def result = false
if(people.findAll { it?.currentPresence == "present" }) {
result = true
}
log.debug("anyoneIsHome: ${result}")
return result
}
private send(msg) {
if(sendPushMessage != "No") {
log.debug("Sending push message")
sendPush(msg)
}
log.debug(msg)
}

View File

@@ -0,0 +1,134 @@
/**
* Ready for Rain
*
* Author: brian@bevey.org
* Date: 9/10/13
*
* Warn if doors or windows are open when inclement weather is approaching.
*/
definition(
name: "Ready For Rain",
namespace: "imbrianj",
author: "brian@bevey.org",
description: "Warn if doors or windows are open when inclement weather is approaching.",
category: "Convenience",
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("Zip code?") {
input "zipcode", "text", title: "Zipcode?"
}
section("Things to check?") {
input "sensors", "capability.contactSensor", multiple: true
}
section("Notifications?") {
input "sendPushMessage", "enum", title: "Send a push notification?", metadata: [values: ["Yes", "No"]], required: false
input "phone", "phone", title: "Send a Text Message?", required: false
}
section("Message interval?") {
input name: "messageDelay", type: "number", title: "Minutes (default to every message)", required: false
}
}
def installed() {
init()
}
def updated() {
unsubscribe()
unschedule()
init()
}
def init() {
state.lastMessage = 0
state.lastCheck = ["time": 0, "result": false]
schedule("0 0,30 * * * ?", scheduleCheck) // Check at top and half-past of every hour
subscribe(sensors, "contact.open", scheduleCheck)
}
def scheduleCheck(evt) {
def open = sensors.findAll { it?.latestValue("contact") == "open" }
def plural = open.size() > 1 ? "are" : "is"
// Only need to poll if we haven't checked in a while - and if something is left open.
if((now() - (30 * 60 * 1000) > state.lastCheck["time"]) && open) {
log.info("Something's open - let's check the weather.")
def response = getWeatherFeature("forecast", zipcode)
def weather = isStormy(response)
if(weather) {
send("${open.join(', ')} ${plural} open and ${weather} coming.")
}
}
else if(((now() - (30 * 60 * 1000) <= state.lastCheck["time"]) && state.lastCheck["result"]) && open) {
log.info("We have fresh weather data, no need to poll.")
send("${open.join(', ')} ${plural} open and ${state.lastCheck["result"]} coming.")
}
else {
log.info("Everything looks closed, no reason to check weather.")
}
}
private send(msg) {
def delay = (messageDelay != null && messageDelay != "") ? messageDelay * 60 * 1000 : 0
if(now() - delay > state.lastMessage) {
state.lastMessage = now()
if(sendPushMessage == "Yes") {
log.debug("Sending push message.")
sendPush(msg)
}
if(phone) {
log.debug("Sending text message.")
sendSms(phone, msg)
}
log.debug(msg)
}
else {
log.info("Have a message to send, but user requested to not get it.")
}
}
private isStormy(json) {
def types = ["rain", "snow", "showers", "sprinkles", "precipitation"]
def forecast = json?.forecast?.txt_forecast?.forecastday?.first()
def result = false
if(forecast) {
def text = forecast?.fcttext?.toLowerCase()
log.debug(text)
if(text) {
for (int i = 0; i < types.size() && !result; i++) {
if(text.contains(types[i])) {
result = types[i]
}
}
}
else {
log.warn("Got forecast, couldn't parse.")
}
}
else {
log.warn("Did not get a forecast: ${json}")
}
state.lastCheck = ["time": now(), "result": result]
return result
}

View File

@@ -0,0 +1,131 @@
/**
* Safe Watch
*
* Author: brian@bevey.org
* Date: 2013-11-17
*
* Watch a series of sensors for any anomalies for securing a safe or room.
*/
definition(
name: "Safe Watch",
namespace: "imbrianj",
author: "brian@bevey.org",
description: "Watch a series of sensors for any anomalies for securing a safe.",
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"
)
preferences {
section("Things to secure?") {
input "contact", "capability.contactSensor", title: "Contact Sensor", required: false
input "motion", "capability.motionSensor", title: "Motion Sensor", required: false
input "knock", "capability.accelerationSensor", title: "Knock Sensor", required: false
input "axis", "capability.threeAxis", title: "Three-Axis Sensor", required: false
}
section("Temperature monitor?") {
input "temp", "capability.temperatureMeasurement", title: "Temp Sensor", required: false
input "maxTemp", "number", title: "Max Temp?", required: false
input "minTemp", "number", title: "Min Temp?", required: false
}
section("When which people are away?") {
input "people", "capability.presenceSensor", multiple: true
}
section("Notifications?") {
input "sendPushMessage", "enum", title: "Send a push notification?", metadata: [values: ["Yes", "No"]], required: false
input "phone", "phone", title: "Send a Text Message?", required: false
}
section("Message interval?") {
input name: "messageDelay", type: "number", title: "Minutes (default to every message)", required: false
}
}
def installed() {
init()
}
def updated() {
unsubscribe()
init()
}
def init() {
subscribe(contact, "contact.open", triggerContact)
subscribe(motion, "motion.active", triggerMotion)
subscribe(knock, "acceleration.active", triggerKnock)
subscribe(temp, "temperature", triggerTemp)
subscribe(axis, "threeAxis", triggerAxis)
}
def triggerContact(evt) {
if(everyoneIsAway()) {
send("Safe Watch: ${contact.label ?: contact.name} was opened!")
}
}
def triggerMotion(evt) {
if(everyoneIsAway()) {
send("Safe Watch: ${motion.label ?: motion.name} sensed motion!")
}
}
def triggerKnock(evt) {
if(everyoneIsAway()) {
send("Safe Watch: ${knock.label ?: knock.name} was knocked!")
}
}
def triggerTemp(evt) {
def temperature = evt.doubleValue
if((maxTemp && maxTemp < temperature) ||
(minTemp && minTemp > temperature)) {
send("Safe Watch: ${temp.label ?: temp.name} is ${temperature}")
}
}
def triggerAxis(evt) {
if(everyoneIsAway()) {
send("Safe Watch: ${axis.label ?: axis.name} was tilted!")
}
}
private everyoneIsAway() {
def result = true
if(people.findAll { it?.currentPresence == "present" }) {
result = false
}
log.debug("everyoneIsAway: ${result}")
return result
}
private send(msg) {
def delay = (messageDelay != null && messageDelay != "") ? messageDelay * 60 * 1000 : 0
if(now() - delay > state.lastMessage) {
state.lastMessage = now()
if(sendPushMessage == "Yes") {
log.debug("Sending push message.")
sendPush(msg)
}
if(phone) {
log.debug("Sending text message.")
sendSms(phone, msg)
}
log.debug(msg)
}
else {
log.info("Have a message to send, but user requested to not get it.")
}
}

View File

@@ -0,0 +1,128 @@
/**
* Thermostat Window Check
*
* Author: brian@bevey.org
* Date: 9/13/13
*
* If your heating or cooling system come on, it gives you notice if there are
* any windows or doors left open, preventing the system from working
* optimally.
*/
definition(
name: "Thermostat Window Check",
namespace: "imbrianj",
author: "brian@bevey.org",
description: "If your heating or cooling system come on, it gives you notice if there are any windows or doors left open, preventing the system from working optimally.",
category: "Green Living",
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("Things to check?") {
input "sensors", "capability.contactSensor", multiple: true
}
section("Thermostats to monitor") {
input "thermostats", "capability.thermostat", multiple: true
}
section("Notifications") {
input "sendPushMessage", "enum", title: "Send a push notification?", metadata: [values: ["Yes", "No"]], required: false
input "phone", "phone", title: "Send a Text Message?", required: false
}
section("Turn thermostat off automatically?") {
input "turnOffTherm", "enum", metadata: [values: ["Yes", "No"]], required: false
}
section("Delay to wait before turning thermostat off (defaults to 1 minute)") {
input "turnOffDelay", "decimal", title: "Number of minutes", required: false
}
}
def installed() {
subscribe(thermostats, "thermostatMode", thermoChange);
subscribe(sensors, "contact.open", windowChange);
}
def updated() {
unsubscribe()
subscribe(thermostats, "thermostatMode", thermoChange);
subscribe(sensors, "contact.open", windowChange);
}
def thermoChange(evt) {
if(evt.value == "heat" ||
evt.value == "cool") {
def open = sensors.findAll { it?.latestValue("contact") == "open" }
if(open) {
def plural = open.size() > 1 ? "are" : "is"
send("${open.join(', ')} ${plural} still open and the thermostat just came on.")
thermoShutOffTrigger()
}
else {
log.info("Thermostat came on and nothing is open.");
}
}
}
def windowChange(evt) {
def heating = thermostats.findAll { it?.latestValue("thermostatMode") == "heat" }
def cooling = thermostats.findAll { it?.latestValue("thermostatMode") == "cool" }
if(heating || cooling) {
def open = sensors.findAll { it?.latestValue("contact") == "open" }
def tempDirection = heating ? "heating" : "cooling"
def plural = open.size() > 1 ? "were" : "was"
send("${open.join(', ')} ${plural} opened and the thermostat is still ${tempDirection}.")
thermoShutOffTrigger()
}
}
def thermoShutOffTrigger() {
if(turnOffTherm == "Yes") {
log.info("Starting timer to turn off thermostat")
def delay = (turnOffDelay != null && turnOffDelay != "") ? turnOffDelay * 60 : 60
state.turnOffTime = now()
runIn(delay, "thermoShutOff")
}
}
def thermoShutOff() {
def open = sensors.findAll { it?.latestValue("contact") == "open" }
def tempDirection = heating ? "heating" : "cooling"
def plural = open.size() > 1 ? "are" : "is"
log.info("Checking if we need to turn thermostats off")
if(open.size()) {
send("Thermostats turned off: ${open.join(', ')} ${plural} open and thermostats ${tempDirection}.")
log.info("Windows still open, turning thermostats off")
thermostats?.off()
}
else {
log.info("Looks like everything is shut now - no need to turn off thermostats")
}
}
private send(msg) {
if(sendPushMessage != "No") {
log.debug("Sending push message")
sendPush(msg)
}
if(phone) {
log.debug("Sending text message")
sendSms(phone, msg)
}
log.debug(msg)
}

View File

@@ -0,0 +1,305 @@
/**
* Initial State Event Streamer
*
* Copyright 2015 David Sulpy
*
* 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: "Initial State Event Streamer",
namespace: "initialstate.events",
author: "David Sulpy",
description: "A SmartThings SmartApp to allow SmartThings events to be viewable inside an Initial State Event Bucket in your https://www.initialstate.com account.",
category: "SmartThings Labs",
iconUrl: "https://s3.amazonaws.com/initialstate-web-cdn/IS-wordmark-vertica_small.png",
iconX2Url: "https://s3.amazonaws.com/initialstate-web-cdn/IS-wordmark-vertical.png",
iconX3Url: "https://s3.amazonaws.com/initialstate-web-cdn/IS-wordmark-vertical.png",
oauth: [displayName: "Initial State", displayLink: "https://www.initialstate.com"])
import groovy.json.JsonSlurper
preferences {
section("Choose which devices to monitor...") {
//input "accelerometers", "capability.accelerationSensor", title: "Accelerometers", multiple: true, required: false
input "alarms", "capability.alarm", title: "Alarms", multiple: true, required: false
//input "batteries", "capability.battery", title: "Batteries", multiple: true, required: false
//input "beacons", "capability.beacon", title: "Beacons", multiple: true, required: false
//input "buttons", "capability.button", title: "Buttons", multiple: true, required: false
//input "cos", "capability.carbonMonoxideDetector", title: "Carbon Monoxide Detectors", multiple: true, required: false
//input "colors", "capability.colorControl", title: "Color Controllers", multiple: true, required: false
input "contacts", "capability.contactSensor", title: "Contact Sensors", multiple: true, required: false
//input "doorsControllers", "capability.doorControl", title: "Door Controllers", multiple: true, required: false
//input "energyMeters", "capability.energyMeter", title: "Energy Meters", multiple: true, required: false
//input "illuminances", "capability.illuminanceMeasurement", title: "Illuminance Meters", multiple: true, required: false
input "locks", "capability.lock", title: "Locks", multiple: true, required: false
input "motions", "capability.motionSensor", title: "Motion Sensors", multiple: true, required: false
//input "musicPlayers", "capability.musicPlayer", title: "Music Players", multiple: true, required: false
//input "powerMeters", "capability.powerMeter", title: "Power Meters", multiple: true, required: false
input "presences", "capability.presenceSensor", title: "Presence Sensors", multiple: true, required: false
input "humidities", "capability.relativeHumidityMeasurement", title: "Humidity Meters", multiple: true, required: false
//input "relaySwitches", "capability.relaySwitch", title: "Relay Switches", multiple: true, required: false
//input "sleepSensors", "capability.sleepSensor", title: "Sleep Sensors", multiple: true, required: false
//input "smokeDetectors", "capability.smokeDetector", title: "Smoke Detectors", multiple: true, required: false
//input "peds", "capability.stepSensor", title: "Pedometers", multiple: true, required: false
input "switches", "capability.switch", title: "Switches", multiple: true, required: false
input "switchLevels", "capability.switchLevel", title: "Switch Levels", multiple: true, required: false
input "temperatures", "capability.temperatureMeasurement", title: "Temperature Sensors", multiple: true, required: false
input "thermostats", "capability.thermostat", title: "Thermostats", multiple: true, required: false
//input "valves", "capability.valve", title: "Valves", multiple: true, required: false
input "waterSensors", "capability.waterSensor", title: "Water Sensors", multiple: true, required: false
}
}
mappings {
path("/access_key") {
action: [
GET: "getAccessKey",
PUT: "setAccessKey"
]
}
path("/bucket") {
action: [
GET: "getBucketKey",
PUT: "setBucketKey"
]
}
}
def subscribeToEvents() {
/*if (accelerometers != null) {
subscribe(accelerometers, "acceleration", genericHandler)
}*/
if (alarms != null) {
subscribe(alarms, "alarm", genericHandler)
}
/*if (batteries != null) {
subscribe(batteries, "battery", genericHandler)
}*/
/*if (beacons != null) {
subscribe(beacons, "presence", genericHandler)
}*/
/*
if (buttons != null) {
subscribe(buttons, "button", genericHandler)
}*/
/*if (cos != null) {
subscribe(cos, "carbonMonoxide", genericHandler)
}*/
/*if (colors != null) {
subscribe(colors, "hue", genericHandler)
subscribe(colors, "saturation", genericHandler)
subscribe(colors, "color", genericHandler)
}*/
if (contacts != null) {
subscribe(contacts, "contact", genericHandler)
}
/*if (doorsControllers != null) {
subscribe(doorsControllers, "door", genericHandler)
}*/
/*if (energyMeters != null) {
subscribe(energyMeters, "energy", genericHandler)
}*/
/*if (illuminances != null) {
subscribe(illuminances, "illuminance", genericHandler)
}*/
if (locks != null) {
subscribe(locks, "lock", genericHandler)
}
if (motions != null) {
subscribe(motions, "motion", genericHandler)
}
/*if (musicPlayers != null) {
subscribe(musicPlayers, "status", genericHandler)
subscribe(musicPlayers, "level", genericHandler)
subscribe(musicPlayers, "trackDescription", genericHandler)
subscribe(musicPlayers, "trackData", genericHandler)
subscribe(musicPlayers, "mute", genericHandler)
}*/
/*if (powerMeters != null) {
subscribe(powerMeters, "power", genericHandler)
}*/
if (presences != null) {
subscribe(presences, "presence", genericHandler)
}
if (humidities != null) {
subscribe(humidities, "humidity", genericHandler)
}
/*if (relaySwitches != null) {
subscribe(relaySwitches, "switch", genericHandler)
}*/
/*if (sleepSensors != null) {
subscribe(sleepSensors, "sleeping", genericHandler)
}*/
/*if (smokeDetectors != null) {
subscribe(smokeDetectors, "smoke", genericHandler)
}*/
/*if (peds != null) {
subscribe(peds, "steps", genericHandler)
subscribe(peds, "goal", genericHandler)
}*/
if (switches != null) {
subscribe(switches, "switch", genericHandler)
}
if (switchLevels != null) {
subscribe(switchLevels, "level", genericHandler)
}
if (temperatures != null) {
subscribe(temperatures, "temperature", genericHandler)
}
if (thermostats != null) {
subscribe(thermostats, "temperature", genericHandler)
subscribe(thermostats, "heatingSetpoint", genericHandler)
subscribe(thermostats, "coolingSetpoint", genericHandler)
subscribe(thermostats, "thermostatSetpoint", genericHandler)
subscribe(thermostats, "thermostatMode", genericHandler)
subscribe(thermostats, "thermostatFanMode", genericHandler)
subscribe(thermostats, "thermostatOperatingState", genericHandler)
}
/*if (valves != null) {
subscribe(valves, "contact", genericHandler)
}*/
if (waterSensors != null) {
subscribe(waterSensors, "water", genericHandler)
}
}
def getAccessKey() {
log.trace "get access key"
if (state.accessKey == null) {
httpError(404, "Access Key Not Found")
} else {
[
accessKey: state.accessKey
]
}
}
def getBucketKey() {
log.trace "get bucket key"
if (state.bucketKey == null) {
httpError(404, "Bucket key Not Found")
} else {
[
bucketKey: state.bucketKey,
bucketName: state.bucketName
]
}
}
def setBucketKey() {
log.trace "set bucket key"
def newBucketKey = request.JSON?.bucketKey
def newBucketName = request.JSON?.bucketName
log.debug "bucket name: $newBucketName"
log.debug "bucket key: $newBucketKey"
if (newBucketKey && (newBucketKey != state.bucketKey || newBucketName != state.bucketName)) {
state.bucketKey = "$newBucketKey"
state.bucketName = "$newBucketName"
state.isBucketCreated = false
}
}
def setAccessKey() {
log.trace "set access key"
def newAccessKey = request.JSON?.accessKey
if (newAccessKey && newAccessKey != state.accessKey) {
state.accessKey = "$newAccessKey"
state.isBucketCreated = false
}
}
def installed() {
subscribeToEvents()
state.isBucketCreated = false
}
def updated() {
unsubscribe()
if (state.bucketKey != null && state.accessKey != null) {
state.isBucketCreated = false
}
subscribeToEvents()
}
def createBucket() {
if (!state.bucketName) {
state.bucketName = state.bucketKey
}
def bucketName = "${state.bucketName}"
def bucketKey = "${state.bucketKey}"
def accessKey = "${state.accessKey}"
def bucketCreateBody = new JsonSlurper().parseText("{\"bucketKey\": \"$bucketKey\", \"bucketName\": \"$bucketName\"}")
def bucketCreatePost = [
uri: 'https://groker.initialstate.com/api/buckets',
headers: [
"Content-Type": "application/json",
"X-IS-AccessKey": accessKey
],
body: bucketCreateBody
]
log.debug bucketCreatePost
httpPostJson(bucketCreatePost) {
log.debug "bucket posted"
state.isBucketCreated = true
}
}
def genericHandler(evt) {
log.trace "$evt.displayName($evt.name:$evt.unit) $evt.value"
def key = "$evt.displayName($evt.name)"
if (evt.unit != null) {
key = "$evt.displayName(${evt.name}_$evt.unit)"
}
def value = "$evt.value"
eventHandler(key, value)
}
def eventHandler(name, value) {
if (state.accessKey == null || state.bucketKey == null) {
return
}
if (!state.isBucketCreated) {
createBucket()
}
def eventBody = new JsonSlurper().parseText("[{\"key\": \"$name\", \"value\": \"$value\"}]")
def eventPost = [
uri: 'https://groker.initialstate.com/api/events',
headers: [
"Content-Type": "application/json",
"X-IS-BucketKey": "${state.bucketKey}",
"X-IS-AccessKey": "${state.accessKey}"
],
body: eventBody
]
log.debug eventPost
httpPostJson(eventPost) {
log.debug "event data posted"
}
}

View File

@@ -0,0 +1,76 @@
/**
* My Light Toggle
*
* Copyright 2015 Jesse Silverberg
*
* 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: "My Light Toggle",
namespace: "JLS",
author: "Jesse Silverberg",
description: "Toggle lights on/off with a motion sensor",
category: "Convenience",
iconUrl: "https://www.dropbox.com/s/6kxtd2v5reggonq/lightswitch.gif?raw=1",
iconX2Url: "https://www.dropbox.com/s/6kxtd2v5reggonq/lightswitch.gif?raw=1",
iconX3Url: "https://www.dropbox.com/s/6kxtd2v5reggonq/lightswitch.gif?raw=1")
preferences {
section("When this sensor detects motion...") {
input "motionToggler", "capability.motionSensor", title: "Motion Here", required: true, multiple: false
}
section("Master switch for the toggle reference...") {
input "masterToggle", "capability.switch", title: "Reference switch", required: true, multiple: false
}
section("Toggle lights...") {
input "switchesToToggle", "capability.switch", title: "These go on/off", required: true, multiple: true
}
}
def installed() {
log.debug "Installed with settings: ${settings}"
initialize()
}
def updated() {
log.debug "Updated with settings: ${settings}"
unsubscribe()
initialize()
}
def initialize() {
subscribe(motionToggler, "motion", toggleSwitches)
}
def toggleSwitches(evt) {
log.debug "$evt.value"
if (evt.value == "active" && masterToggle.currentSwitch == "off") {
// for (thisSwitch in switchesToToggle) {
// log.debug "$thisSwitch.label"
// thisSwitch.on()
switchesToToggle.on()
masterToggle.on()
} else if (evt.value == "active" && masterToggle.currentSwitch == "on") {
// for (thisSwitch in switchesToToggle) {
// log.debug "$thisSwitch.label"
// thisSwitch.off()
switchesToToggle.off()
masterToggle.off()
}
}

View File

@@ -0,0 +1,277 @@
/**
* Auto Humidity Vent
*
* Copyright 2014 Jonathan Andersson
*
* 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: "Auto Humidity Vent",
namespace: "jonathan-a",
author: "Jonathan Andersson",
description: "When the humidity reaches a specified level, activate one or more vent fans until the humidity is reduced to a specified level.",
category: "Convenience",
iconUrl: "https://s3.amazonaws.com/smartthings-device-icons/Appliances/appliances11-icn.png",
iconX2Url: "https://s3.amazonaws.com/smartthings-device-icons/Appliances/appliances11-icn@2x.png"
)
preferences {
section("Enable / Disable the following functionality:") {
input "app_enabled", "bool", title: "Auto Humidity Vent", required:true, defaultValue:true
input "fan_control_enabled", "bool", title: "Vent Fan Control", required:true, defaultValue:true
}
section("Choose a humidity sensor...") {
input "humidity_sensor", "capability.relativeHumidityMeasurement", title: "Humidity Sensor", required: true
}
section("Enter the relative humudity level (%) above which the vent fans will activate:") {
input "humidity_a", "number", title: "Humidity Activation Level", required: true, defaultValue:70
}
section("Enter the relative humudity level (%) below which the vent fans will deactivate:") {
input "humidity_d", "number", title: "Humidity Deactivation Level", required: true, defaultValue:65
}
section("Select the vent fans to control...") {
input "fans", "capability.switch", title: "Vent Fans", multiple: true, required: true
}
section("Select the vent fan energy meters to monitor...") {
input "emeters", "capability.energyMeter", title: "Energy Meters", multiple: true, required: false
input "price_kwh", "decimal", title: "Cost in cents per kWh (12 is US avg)", required: true, defaultValue:12
}
section("Set notification options:") {
input "sendPushMessage", "bool", title: "Push notifications", required:true, defaultValue:false
input "phone", "phone", title: "Send text messages to", required: false
}
}
def installed() {
log.debug "${app.label} installed with settings: ${settings}"
state.app_enabled = false
state.fan_control_enabled = false
state.fansOn = false
state.fansOnTime = now()
state.fansLastRunTime = 0
initialize()
}
def uninstalled()
{
send("${app.label} uninstalled.")
state.app_enabled = false
set_fans(false)
state.fan_control_enabled = false
}
def updated() {
log.debug "${app.label} updated with settings: ${settings}"
unsubscribe()
initialize()
}
def initialize() {
if (settings.fan_control_enabled) {
if(state.fan_control_enabled == false) {
send("Vent Fan Control Enabled.")
} else {
log.debug "Vent Fan Control Enabled."
}
state.fan_control_enabled = true
} else {
if(state.fan_control_enabled == true) {
send("Vent Fan Control Disabled.")
} else {
log.debug "Vent Fan Control Disabled."
}
state.fan_control_enabled = false
}
if (settings.app_enabled) {
if(state.app_enabled == false) {
send("${app.label} Enabled.")
} else {
log.debug "${app.label} Enabled."
}
subscribe(humidity_sensor, "humidity", "handleThings")
state.app_enabled = true
} else {
if(state.app_enabled == true) {
send("${app.label} Disabled.")
} else {
log.debug "${app.label} Disabled."
}
state.app_enabled = false
}
handleThings()
}
def handleThings(evt) {
log.debug "handleThings()"
if(evt) {
log.debug "$evt.descriptionText"
}
def h = 0.0 as BigDecimal
if (settings.app_enabled) {
h = settings.humidity_sensor.currentValue('humidity')
/*
//Simulator is broken and requires this work around for testing.
if (settings.humidity_sensor.latestState('humidity')) {
log.debug settings.humidity_sensor.latestState('humidity').stringValue[0..-2]
h = settings.humidity_sensor.latestState('humidity').stringValue[0..-2].toBigDecimal()
} else {
h = 20
}
*/
}
log.debug "Humidity: $h%, Activate: $humidity_a%, Deactivate: $humidity_d%"
def activateFans = false
def deactivateFans = false
if (settings.app_enabled) {
if (state.fansOn) {
if (h > humidity_d) {
log.debug "Humidity not sufficient to deactivate vent fans: $h > $humidity_d"
} else {
log.debug "Humidity sufficient to deactivate vent fans: $h <= $humidity_d"
deactivateFans = true
}
} else {
if (h < humidity_a) {
log.debug "Humidity not sufficient to activate vent fans: $h < $humidity_a"
} else {
log.debug "Humidity sufficient to activate vent fans: $h >= $humidity_a"
activateFans = true
}
}
}
if(activateFans) {
set_fans(true)
}
if(deactivateFans) {
set_fans(false)
}
}
def set_fans(fan_state) {
if (fan_state) {
if (state.fansOn == false) {
send("${app.label} fans On.")
state.fansOnTime = now()
if (settings.fan_control_enabled) {
if (emeters) {
emeters.reset()
}
fans.on()
} else {
send("${app.label} fan control is disabled.")
}
state.fansOn = true
} else {
log.debug "${app.label} fans already On."
}
} else {
if (state.fansOn == true) {
send("${app.label} fans Off.")
state.fansLastRunTime = (now() - state.fansOnTime)
BigInteger ms = new java.math.BigInteger(state.fansLastRunTime)
int seconds = (BigInteger) (((BigInteger) ms / (1000I)) % 60I)
int minutes = (BigInteger) (((BigInteger) ms / (1000I * 60I)) % 60I)
int hours = (BigInteger) (((BigInteger) ms / (1000I * 60I * 60I)) % 24I)
int days = (BigInteger) ((BigInteger) ms / (1000I * 60I * 60I * 24I))
def sb = String.format("${app.label} cycle: %d:%02d:%02d:%02d", days, hours, minutes, seconds)
send(sb)
if (settings.fan_control_enabled) {
fans.off()
if (emeters) {
log.debug emeters.currentValue('energy')
//TODO: How to ensure latest (most accurate) energy reading?
emeters.poll() //[configure, refresh, on, off, poll, reset]
// emeters.refresh() //[configure, refresh, on, off, poll, reset]
state.fansLastRunEnergy = emeters.currentValue('energy').sum()
state.fansLastRunCost = ((state.fansLastRunEnergy * price_kwh) / 100.0)
send("${app.label} cycle: ${state.fansLastRunEnergy}kWh @ \$${state.fansLastRunCost}")
}
} else {
send("${app.label} fan control is disabled.")
}
state.fansOn = false
state.fansHoldoff = now()
} else {
log.debug "${app.label} fans already Off."
}
}
}
private send(msg) {
if (sendPushMessage) {
sendPush(msg)
}
if (phone) {
sendSms(phone, msg)
}
log.debug(msg)
}

View File

@@ -0,0 +1,85 @@
/**
* Jawbone Panic Button
*
* Copyright 2014 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.
*
*/
// Automatically generated. Make future change here.
definition(
name: "Jawbone Button Notifier",
namespace: "juano2310",
author: "Juan Risso",
category: "SmartThings Labs",
description: "Send push notifications or text messages with your Jawbone Up when you hold the button.",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/jawbone-up.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/jawbone-up@2x.png",
)
preferences {
section("Use this Jawbone as a notification button and...") {
input "jawbone", "device.jawboneUser", multiple: true
}
section("Send a message when you press and hold the button...") {
input "warnMessage", "text", title: "Warning Message"
}
section("Or text message to these numbers (optional)") {
input ("phone1", "contact", required: false) {
input "phone1", "phone", required: false
}
input ("phone2", "contact", required: false) {
input "phone2", "phone", required: false
}
input ("phone3", "contact", 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() {
subscribe(jawbone, "sleeping", sendit)
}
def sendit(evt) {
log.debug "$evt.value: $evt, $settings"
sendMessage()
}
def sendMessage() {
log.debug "Sending Message"
def msg = warnMessage
log.info msg
if (phone1) {
sendSms phone1, msg
}
if (phone2) {
sendSms phone2, msg
}
if (phone3) {
sendSms phone3, msg
}
if (!phone1 && !phone2 && !phone3) {
sendPush msg
}
}

View File

@@ -0,0 +1,502 @@
/**
* Jawbone Service Manager
*
* Author: Juan Risso
* Date: 2013-12-19
*/
definition(
name: "Jawbone UP (Connect)",
namespace: "juano2310",
author: "Juan Pablo Risso",
description: "Connect your Jawbone UP to SmartThings",
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",
oauth: true,
usePreferencesForAuthorization: false
) {
appSetting "clientId"
appSetting "clientSecret"
appSetting "serverUrl"
}
preferences {
page(name: "Credentials", title: "Jawbone UP", content: "authPage", install: false)
}
mappings {
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" ] }
}
def getSmartThingsClientId() {
return appSettings.clientId
}
def getSmartThingsClientSecret() {
return appSettings.clientSecret
}
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.JawboneAccessToken) {
log.debug "Access token already exists"
setup()
success()
} else {
def code = params.code
if (code) {
if (code.size() > 6) {
// Jawbone code
log.debug "Exchanging code for access token"
receiveToken(redirectUrl)
} else {
// SmartThings code, which we ignore, as we don't need to exchange for an access token.
// Instead, go initiate the Jawbone OAuth flow.
log.debug "Executing callback redirect to auth page"
def stcid = getSmartThingsClientId()
state.oauthInitState = UUID.randomUUID().toString()
def oauthParams = [response_type: "code", client_id: stcid, scope: "move_read sleep_read", redirect_uri: "${serverUrl}/oauth/callback"]
redirect(location: "https://jawbone.com/auth/oauth2/auth?${toQueryString(oauthParams)}")
}
} else {
log.debug "This code should be unreachable"
success()
}
}
}
def authPage() {
log.debug "authPage"
def description = null
if (state.JawboneAccessToken == null) {
if (!state.accessToken) {
log.debug "About to create access token"
createAccessToken()
}
description = "Click to enter Jawbone Credentials"
def redirectUrl = oauthInitUrl()
// log.debug "RedirectURL = ${redirectUrl}"
return dynamicPage(name: "Credentials", title: "Jawbone UP", nextPage: null, uninstall: true, install:false) {
section { href url:redirectUrl, style:"embedded", required:true, title:"Jawbone UP", description:description }
}
} else {
description = "Jawbone Credentials Already Entered."
return dynamicPage(name: "Credentials", title: "Jawbone UP", uninstall: true, install:true) {
section { href url: buildRedirectUrl("receivedToken"), style:"embedded", state: "complete", title:"Jawbone UP", description:description }
}
}
}
def oauthInitUrl() {
log.debug "oauthInitUrl"
def stcid = getSmartThingsClientId()
state.oauthInitState = UUID.randomUUID().toString()
def oauthParams = [ response_type: "code", client_id: stcid, scope: "move_read sleep_read", redirect_uri: buildRedirectUrl("receiveToken") ]
return "https://jawbone.com/auth/oauth2/auth?${toQueryString(oauthParams)}"
}
def receiveToken(redirectUrl = null) {
log.debug "receiveToken"
def stcid = getSmartThingsClientId()
def oauthClientSecret = getSmartThingsClientSecret()
def oauthParams = [ client_id: stcid, client_secret: oauthClientSecret, grant_type: "authorization_code", code: params.code ]
def params = [
uri: "https://jawbone.com/auth/oauth2/token?${toQueryString(oauthParams)}",
]
httpGet(params) { response ->
log.debug "${response.data}"
log.debug "Setting access token to ${response.data.access_token}, refresh token to ${response.data.refresh_token}"
state.JawboneAccessToken = response.data.access_token
state.refreshToken = response.data.refresh_token
}
setup()
if (state.JawboneAccessToken) {
success()
} else {
def message = """
<p>The connection could not be established!</p>
<p>Click 'Done' to return to the menu.</p>
"""
connectionStatus(message)
}
}
def success() {
def message = """
<p>Your Jawbone Account is now connected to SmartThings!</p>
<p>Click 'Done' to finish setup.</p>
"""
connectionStatus(message)
}
def receivedToken() {
def message = """
<p>Your Jawbone 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://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSMuoEIQ7gQhFtc02vXkybwmH0o7L1cs5mtbcJye0mgNqop_LOZbg" alt="Jawbone UP 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 getServerUrl() { return appSettings.serverUrl ?: "https://graph.api.smartthings.com" }
def buildRedirectUrl(page) {
// log.debug "buildRedirectUrl"
// /api/token/:st_token/smartapps/installations/:id/something
return "${serverUrl}/api/token/${state.accessToken}/smartapps/installations/${app.id}/${page}"
}
def validateCurrentToken() {
log.debug "validateCurrentToken"
def url = "https://jawbone.com/nudge/api/v.1.1/users/@me/refreshToken"
def requestBody = "secret=${getSmartThingsClientSecret()}"
try {
httpPost(uri: url, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ], body: requestBody) {response ->
if (response.status == 200) {
log.debug "${response.data}"
log.debug "Setting refresh token to ${response.data.data.refresh_token}"
state.refreshToken = response.data.data.refresh_token
}
}
} catch (groovyx.net.http.HttpResponseException e) {
if (e.statusCode == 401) { // token is expired
log.debug "Access token is expired"
if (state.refreshToken) { // if we have this we are okay
def stcid = getSmartThingsClientId()
def oauthClientSecret = getSmartThingsClientSecret()
def oauthParams = [client_id: stcid, client_secret: oauthClientSecret, grant_type: "refresh_token", refresh_token: state.refreshToken]
def tokenUrl = "https://jawbone.com/auth/oauth2/token?${toQueryString(oauthParams)}"
def params = [
uri: tokenUrl
]
httpGet(params) { refreshResponse ->
def data = refreshResponse.data
log.debug "Status: ${refreshResponse.status}, data: ${data}"
if (data.error) {
if (data.error == "access_denied") {
// User has removed authorization (probably)
log.warn "Access denied, because: ${data.error_description}"
state.remove("JawboneAccessToken")
state.remove("refreshToken")
}
} else {
log.debug "Setting access token to ${data.access_token}, refresh token to ${data.refresh_token}"
state.JawboneAccessToken = data.access_token
state.refreshToken = data.refresh_token
}
}
}
}
} catch (java.net.SocketTimeoutException e) {
log.warn "Connection timed out, not much we can do here"
}
}
def initialize() {
def hookUrl = "${serverUrl}/api/token/${state.accessToken}/smartapps/installations/${app.id}/hookCallback"
def webhook = "https://jawbone.com/nudge/api/v.1.1/users/@me/pubsub?webhook=$hookUrl"
log.debug "Callback URL: $webhook"
httpPost(uri: webhook, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ])
}
def setup() {
// make sure this is going to work
validateCurrentToken()
if (state.JawboneAccessToken) {
def urlmember = "https://jawbone.com/nudge/api/users/@me/"
def member = null
httpGet(uri: urlmember, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
member = response.data.data
}
if (member) {
state.member = member
def externalId = "${app.id}.${member.xid}"
// 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
log.debug "Device $externalId: $deviceWrapper"
if (!deviceWrapper) {
def childDevice = addChildDevice('juano2310', "Jawbone User", "${app.id}.${member.xid}",null,[name:"Jawbone UP - " + member.first, completedSetup: true])
if (childDevice) {
log.debug "Child Device Successfully Created"
generateInitialEvent (member, childDevice)
}
}
}
initialize()
}
}
def installed() {
enableCallback()
if (!state.accessToken) {
log.debug "About to create access token"
createAccessToken()
}
if (state.JawboneAccessToken) {
setup()
}
}
def updated() {
enableCallback()
if (!state.accessToken) {
log.debug "About to create access token"
createAccessToken()
}
if (state.JawboneAccessToken) {
setup()
}
}
def uninstalled() {
if (state.JawboneAccessToken) {
try {
httpDelete(uri: "https://jawbone.com/nudge/api/v.1.0/users/@me/PartnerAppMembership", headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) { response ->
log.debug "Success disconnecting Jawbone from SmartThings"
}
} catch (groovyx.net.http.HttpResponseException e) {
log.error "Error disconnecting Jawbone from SmartThings: ${e.statusCode}"
}
}
}
def pollChild(childDevice) {
def member = state.member
generatePollingEvents (member, childDevice)
}
def generatePollingEvents (member, childDevice) {
// lets figure out if the member is currently "home" (At the place)
def urlgoals = "https://jawbone.com/nudge/api/users/@me/goals"
def urlmoves = "https://jawbone.com/nudge/api/users/@me/moves"
def urlsleeps = "https://jawbone.com/nudge/api/users/@me/sleeps"
def goals = null
def moves = null
def sleeps = null
httpGet(uri: urlgoals, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
goals = response.data.data
}
httpGet(uri: urlmoves, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
moves = response.data.data.items[0]
}
try { // we are going to just ignore any errors
log.debug "Member = ${member.first}"
log.debug "Moves Goal = ${goals.move_steps} Steps"
log.debug "Moves = ${moves.details.steps} Steps"
childDevice?.sendEvent(name:"steps", value: moves.details.steps)
childDevice?.sendEvent(name:"goal", value: goals.move_steps)
//setColor(moves.details.steps,goals.move_steps,childDevice)
}
catch (e) {
// eat it
}
}
def generateInitialEvent (member, childDevice) {
// lets figure out if the member is currently "home" (At the place)
def urlgoals = "https://jawbone.com/nudge/api/users/@me/goals"
def urlmoves = "https://jawbone.com/nudge/api/users/@me/moves"
def urlsleeps = "https://jawbone.com/nudge/api/users/@me/sleeps"
def goals = null
def moves = null
def sleeps = null
httpGet(uri: urlgoals, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
goals = response.data.data
}
httpGet(uri: urlmoves, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
moves = response.data.data.items[0]
}
try { // we are going to just ignore any errors
log.debug "Member = ${member.first}"
log.debug "Moves Goal = ${goals.move_steps} Steps"
log.debug "Moves = ${moves.details.steps} Steps"
log.debug "Sleeping state = false"
childDevice?.generateSleepingEvent(false)
childDevice?.sendEvent(name:"steps", value: moves.details.steps)
childDevice?.sendEvent(name:"goal", value: goals.move_steps)
//setColor(moves.details.steps,goals.move_steps,childDevice)
}
catch (e) {
// eat it
}
}
def setColor (steps,goal,childDevice) {
def result = steps * 100 / goal
if (result < 25)
childDevice?.sendEvent(name:"steps", value: "steps", label: steps)
else if ((result >= 25) && (result < 50))
childDevice?.sendEvent(name:"steps", value: "steps1", label: steps)
else if ((result >= 50) && (result < 75))
childDevice?.sendEvent(name:"steps", value: "steps1", label: steps)
else if (result >= 75)
childDevice?.sendEvent(name:"steps", value: "stepsgoal", label: steps)
}
def hookEventHandler() {
// log.debug "In hookEventHandler method."
log.debug "request = ${request}"
def json = request.JSON
// get some stuff we need
def userId = json.events.user_xid[0]
def json_type = json.events.type[0]
def json_action = json.events.action[0]
//log.debug json
log.debug "Userid = ${userId}"
log.debug "Notification Type: " + json_type
log.debug "Notification Action: " + json_action
// find the appropriate child device based on my app id and the device network id
def externalId = "${app.id}.${userId}"
def childDevice = getChildDevice("${externalId}")
if (childDevice) {
switch (json_action) {
case "enter_sleep_mode":
childDevice?.generateSleepingEvent(true)
break
case "exit_sleep_mode":
childDevice?.generateSleepingEvent(false)
break
case "creation":
childDevice?.sendEvent(name:"steps", value: 0)
break
case "updation":
def urlgoals = "https://jawbone.com/nudge/api/users/@me/goals"
def urlmoves = "https://jawbone.com/nudge/api/users/@me/moves"
def goals = null
def moves = null
httpGet(uri: urlgoals, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
goals = response.data.data
}
httpGet(uri: urlmoves, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
moves = response.data.data.items[0]
}
log.debug "Goal = ${goals.move_steps} Steps"
log.debug "Steps = ${moves.details.steps} Steps"
childDevice?.sendEvent(name:"steps", value: moves.details.steps)
childDevice?.sendEvent(name:"goal", value: goals.move_steps)
//setColor(moves.details.steps,goals.move_steps,childDevice)
break
case "deletion":
app.delete()
break
}
}
else {
log.debug "Couldn't find child device associated with Jawbone."
}
def html = """{"code":200,"message":"OK"}"""
render contentType: 'application/json', data: html
}

View File

@@ -0,0 +1,82 @@
/**
* Turn Off With Motion
*
* Copyright 2014 Kristopher Kubicki
*
* 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: "Turn Off With Motion",
namespace: "KristopherKubicki",
author: "Kristopher Kubicki",
description: "Turns off a device if there is motion",
category: "My Apps",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png")
preferences {
section("Turn off when there's movement..."){
input "motion1", "capability.motionSensor", title: "Where?", multiple: true
}
section("And on when there's been no movement for..."){
input "minutes1", "number", title: "Minutes?"
}
section("Turn off/on light(s)..."){
input "switches", "capability.switch", multiple: true
}
}
def installed()
{
subscribe(motion1, "motion", motionHandler)
schedule("0 * * * * ?", "scheduleCheck")
}
def updated()
{
unsubscribe()
subscribe(motion1, "motion", motionHandler)
unschedule()
schedule("0 * * * * ?", "scheduleCheck")
}
def motionHandler(evt) {
log.debug "$evt.name: $evt.value"
if (evt.value == "active") {
log.debug "turning on lights"
switches.off()
state.inactiveAt = null
} else if (evt.value == "inactive") {
if (!state.inactiveAt) {
state.inactiveAt = now()
}
}
}
def scheduleCheck() {
log.debug "schedule check, ts = ${state.inactiveAt}"
if (state.inactiveAt) {
def elapsed = now() - state.inactiveAt
def threshold = 1000 * 60 * minutes1
if (elapsed >= threshold) {
log.debug "turning off lights"
switches.on()
state.inactiveAt = null
}
else {
log.debug "${elapsed / 1000} sec since motion stopped"
}
}
}

View File

@@ -0,0 +1,96 @@
definition(
name: "Enhanced Auto Lock Door",
namespace: "Lock Auto Super Enhanced",
author: "Arnaud",
description: "Automatically locks a specific door after X minutes when closed and unlocks it when open after X seconds.",
category: "Safety & Security",
iconUrl: "http://www.gharexpert.com/mid/4142010105208.jpg",
iconX2Url: "http://www.gharexpert.com/mid/4142010105208.jpg"
)
preferences{
section("Select the door lock:") {
input "lock1", "capability.lock", required: true
}
section("Select the door contact sensor:") {
input "contact", "capability.contactSensor", required: true
}
section("Automatically lock the door when closed...") {
input "minutesLater", "number", title: "Delay (in minutes):", required: true
}
section("Automatically unlock the door when open...") {
input "secondsLater", "number", title: "Delay (in seconds):", required: true
}
section( "Notifications" ) {
input "sendPushMessage", "enum", title: "Send a push notification?", metadata:[values:["Yes", "No"]], required: false
input "phoneNumber", "phone", title: "Enter phone number to send text notification.", required: false
}
}
def installed(){
initialize()
}
def updated(){
unsubscribe()
unschedule()
initialize()
}
def initialize(){
log.debug "Settings: ${settings}"
subscribe(lock1, "lock", doorHandler, [filterEvents: false])
subscribe(lock1, "unlock", doorHandler, [filterEvents: false])
subscribe(contact, "contact.open", doorHandler)
subscribe(contact, "contact.closed", doorHandler)
}
def lockDoor(){
log.debug "Locking the door."
lock1.lock()
log.debug ( "Sending Push Notification..." )
if ( sendPushMessage != "No" ) sendPush( "${lock1} locked after ${contact} was closed for ${minutesLater} minutes!" )
log.debug("Sending text message...")
if ( phoneNumber != "0" ) sendSms( phoneNumber, "${lock1} locked after ${contact} was closed for ${minutesLater} minutes!" )
}
def unlockDoor(){
log.debug "Unlocking the door."
lock1.unlock()
log.debug ( "Sending Push Notification..." )
if ( sendPushMessage != "No" ) sendPush( "${lock1} unlocked after ${contact} was opened for ${secondsLater} seconds!" )
log.debug("Sending text message...")
if ( phoneNumber != "0" ) sendSms( phoneNumber, "${lock1} unlocked after ${contact} was opened for ${secondsLater} seconds!" )
}
def doorHandler(evt){
if ((contact.latestValue("contact") == "open") && (evt.value == "locked")) { // If the door is open and a person locks the door then...
def delay = (secondsLater) // runIn uses seconds
runIn( delay, unlockDoor ) // ...schedule (in minutes) to unlock... We don't want the door to be closed while the lock is engaged.
}
else if ((contact.latestValue("contact") == "open") && (evt.value == "unlocked")) { // If the door is open and a person unlocks it then...
unschedule( unlockDoor ) // ...we don't need to unlock it later.
}
else if ((contact.latestValue("contact") == "closed") && (evt.value == "locked")) { // If the door is closed and a person manually locks it then...
unschedule( lockDoor ) // ...we don't need to lock it later.
}
else if ((contact.latestValue("contact") == "closed") && (evt.value == "unlocked")) { // If the door is closed and a person unlocks it then...
def delay = (minutesLater * 60) // runIn uses seconds
runIn( delay, lockDoor ) // ...schedule (in minutes) to lock.
}
else if ((lock1.latestValue("lock") == "unlocked") && (evt.value == "open")) { // If a person opens an unlocked door...
unschedule( lockDoor ) // ...we don't need to lock it later.
}
else if ((lock1.latestValue("lock") == "unlocked") && (evt.value == "closed")) { // If a person closes an unlocked door...
def delay = (minutesLater * 60) // runIn uses seconds
runIn( delay, lockDoor ) // ...schedule (in minutes) to lock.
}
else { //Opening or Closing door when locked (in case you have a handle lock)
log.debug "Unlocking the door."
lock1.unlock()
log.debug ( "Sending Push Notification..." )
if ( sendPushMessage != "No" ) sendPush( "${lock1} unlocked after ${contact} was opened or closed when ${lock1} was locked!" )
log.debug("Sending text message...")
if ( phoneNumber != "0" ) sendSms( phoneNumber, "${lock1} unlocked after ${contact} was opened or closed when ${lock1} was locked!" )
}
}

View File

@@ -0,0 +1,53 @@
/**
*
* Lights On When Door Open After Sundown
*
* Based on "Turn It On When It Opens" by SmartThings
*
* Author: Aaron Crocco
*/
preferences {
section("When the door opens..."){
input "contact1", "capability.contactSensor", title: "Where?"
}
section("Turn on these lights..."){
input "switches", "capability.switch", multiple: true
}
section("and change mode to...") {
input "HomeAfterDarkMode", "mode", title: "Mode?"
}
}
def installed()
{
subscribe(contact1, "contact.open", contactOpenHandler)
}
def updated()
{
unsubscribe()
subscribe(contact1, "contact.open", contactOpenHandler)
}
def contactOpenHandler(evt) {
log.debug "$evt.value: $evt, $settings"
//Check current time to see if it's after sundown.
def s = getSunriseAndSunset(zipCode: zipCode, sunriseOffset: sunriseOffset, sunsetOffset: sunsetOffset)
def now = new Date()
def setTime = s.sunset
log.debug "Sunset is at $setTime. Current time is $now"
if (setTime.before(now)) { //Executes only if it's after sundown.
log.trace "Turning on switches: $switches"
switches.on()
log.trace "Changing house mode to $HomeAfterDarkMode"
setLocationMode(HomeAfterDarkMode)
sendPush("Welcome home! Changing mode to $HomeAfterDarkMode.")
}
}

View File

@@ -0,0 +1,111 @@
/**
* Weather Underground PWS Connect
*
* Copyright 2015 Andrew Mager
*
* 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.
*
*/
// This imports the Java class "DecimalFormat"
import java.text.DecimalFormat
definition(
name: "Weather Underground PWS Connect",
namespace: "mager",
author: "Andrew Mager",
description: "Connect your SmartSense Temp/Humidity sensor to your Weather Underground Personal Weather Station.",
category: "Green Living",
iconUrl: "http://i.imgur.com/HU0ANBp.png",
iconX2Url: "http://i.imgur.com/HU0ANBp.png",
iconX3Url: "http://i.imgur.com/HU0ANBp.png",
oauth: true)
preferences {
section("Select a sensor") {
input "temp", "capability.temperatureMeasurement", title: "Temperature", required: true
input "humidity", "capability.relativeHumidityMeasurement", title: "Humidity", required: true
}
section("Configure your Weather Underground credentials") {
input "weatherID", "text", title: "Weather Station ID", required: true
input "password", "password", title: "Weather Underground password", required: true
}
}
def installed() {
log.debug "Installed with settings: ${settings}"
initialize()
}
def updated() {
log.debug "Updated with settings: ${settings}"
unsubscribe()
initialize()
}
def initialize() {
/*
Check to see if the sensor is reporting temperature, then run the updateCurrentWeather
every 10 minutes
*/
if (temp.currentTemperature) {
runEvery5Minutes(updateCurrentWeather)
}
}
/*
Updates the Weather Underground Personal Weather Station (PWS) Upload Protocol
Reference: http://wiki.wunderground.com/index.php/PWS_-_Upload_Protocol
*/
def updateCurrentWeather() {
// Logs of the current data from the sensor
log.trace "Temp: " + temp.currentTemperature
log.trace "Humidity: " + humidity.currentHumidity
log.trace "Dew Point: " + calculateDewPoint(temp.currentTemperature, humidity.currentHumidity)
// Builds the URL that will be sent to Weather Underground to update your PWS
def params = [
uri: "http://weatherstation.wunderground.com",
path: "/weatherstation/updateweatherstation.php",
query: [
"ID": weatherID,
"PASSWORD": password,
"dateutc": "now",
"tempf": temp.currentTemperature,
"humidity": humidity.currentHumidity,
"dewptf": calculateDewPoint(temp.currentTemperature, humidity.currentHumidity),
"action": "updateraw",
"softwaretype": "SmartThings"
]
]
try {
// Make the HTTP request using httpGet()
httpGet(params) { resp -> // This is how we define the "return data". Can also use $it.
log.debug "response data: ${resp.data}"
}
} catch (e) {
log.error "something went wrong: $e"
}
}
// Calculates dewpoint based on temperature and humidity
def calculateDewPoint(t, rh) {
def dp = 243.04 * ( Math.log(rh / 100) + ( (17.625 * t) / (243.04 + t) ) ) / (17.625 - Math.log(rh / 100) - ( (17.625 * t) / (243.04 + t) ) )
// Format the response for Weather Underground
return new DecimalFormat("##.##").format(dp)
}

View File

@@ -0,0 +1,132 @@
/**
* Color Coordinator
* Version 1.0.0 - 7/4/15
* By Michael Struck
*
* 1.0.0 - Initial release
*
*
* 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: "Color Coordinator",
namespace: "MichaelStruck",
author: "Michael Struck",
description: "Ties multiple colored lights to one specific light's settings",
category: "Convenience",
iconUrl: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/Other-SmartApps/ColorCoordinator/CC.png",
iconX2Url: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/Other-SmartApps/ColorCoordinator/CC@2x.png"
)
preferences {
page name: "mainPage"
}
def mainPage() {
dynamicPage(name: "mainPage", title: "", install: true, uninstall: true) {
section("Master Light") {
input "master", "capability.colorControl", title: "Colored Light"
}
section("Lights that follow the master settings") {
input "slaves", "capability.colorControl", title: "Colored Lights", multiple: true, required: false
}
section([mobileOnly:true], "Options") {
label(title: "Assign a name", required: false)
href "pageAbout", title: "About ${textAppName()}", description: "Tap to get application version, license and instructions"
}
}
}
page(name: "pageAbout", title: "About ${textAppName()}") {
section {
paragraph "${textVersion()}\n${textCopyright()}\n\n${textLicense()}\n"
}
section("Instructions") {
paragraph textHelp()
}
}
def installed() {
init()
}
def updated(){
unsubscribe()
init()
}
def init() {
subscribe(master, "switch", onOffHandler)
subscribe(master, "level", colorHandler)
subscribe(master, "hue", colorHandler)
subscribe(master, "saturation", colorHandler)
subscribe(master, "colorTemperature", tempHandler)
}
//-----------------------------------
def onOffHandler(evt){
if (master.currentValue("switch") == "on"){
slaves?.on()
}
else {
slaves?.off()
}
}
def colorHandler(evt) {
def dimLevel = master.currentValue("level")
def hueLevel = master.currentValue("hue")
def saturationLevel = master.currentValue("saturation")
def newValue = [hue: hueLevel, saturation: saturationLevel, level: dimLevel as Integer]
slaves?.setColor(newValue)
}
def tempHandler(evt){
if (evt.value != "--") {
def tempLevel = master.currentValue("colorTemperature")
slaves?.setColorTemperature(tempLevel)
}
}
//Version/Copyright/Information/Help
private def textAppName() {
def text = "Color Coordinator"
}
private def textVersion() {
def text = "Version 1.0.0 (07/04/2015)"
}
private def textCopyright() {
def text = "Copyright © 2015 Michael Struck"
}
private def textLicense() {
def text =
"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"+
"\n\n"+
" http://www.apache.org/licenses/LICENSE-2.0"+
"\n\n"+
"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."
}
private def textHelp() {
def text =
"This application will allow you to control the settings of multiple colored lights with one control. " +
"Simply choose a master control light, and then choose the lights that will follow the settings of the master, "+
"including on/off conditions, hue, saturation, level and color temperature."
}

View File

@@ -0,0 +1,419 @@
/**
* Smart Home Ventilation
* Version 2.1.2 - 5/31/15
*
* Copyright 2015 Michael Struck
*
* 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: "Smart Home Ventilation",
namespace: "MichaelStruck",
author: "Michael Struck",
description: "Allows for setting up various schedule scenarios for turning on and off home ventilation switches.",
category: "Convenience",
iconUrl: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/Other-SmartApps/Smart-Home-Ventilation/HomeVent.png",
iconX2Url: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/Other-SmartApps/Smart-Home-Ventilation/HomeVent@2x.png",
iconX3Url: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/Other-SmartApps/Smart-Home-Ventilation/HomeVent@2x.png")
preferences {
page name: "mainPage"
}
def mainPage() {
dynamicPage(name: "mainPage", title: "", install: true, uninstall: true) {
section("Select ventilation switches..."){
input "switches", title: "Switches", "capability.switch", multiple: true
}
section ("Scheduling scenarios...") {
href(name: "toA_Scenario", page: "A_Scenario", title: getTitle (titleA, "A"), description: schedDesc(timeOnA1,timeOffA1,timeOnA2,timeOffA2,timeOnA3,timeOffA3,timeOnA4,timeOffA4, modeA, daysA), state: greyOut(timeOnA1,timeOnA2,timeOnA3,timeOnA4))
href(name: "toB_Scenario", page: "B_Scenario", title: getTitle (titleB, "B"), description: schedDesc(timeOnB1,timeOffB1,timeOnB2,timeOffB2,timeOnB3,timeOffB3,timeOnB4,timeOffB4, modeB, daysB), state: greyOut(timeOnB1,timeOnB2,timeOnB3,timeOnB4))
href(name: "toC_Scenario", page: "C_Scenario", title: getTitle (titleC, "C"), description: schedDesc(timeOnC1,timeOffC1,timeOnC2,timeOffC2,timeOnC3,timeOffC3,timeOnC4,timeOffC4, modeC, daysC), state: greyOut(timeOnC1,timeOnC2,timeOnC3,timeOnC4))
href(name: "toD_Scenario", page: "D_Scenario", title: getTitle (titleD, "D"), description: schedDesc(timeOnD1,timeOffD1,timeOnD2,timeOffD2,timeOnD3,timeOffD3,timeOnD4,timeOffD4, modeD, daysD), state: greyOut(timeOnD1,timeOnD2,timeOnD3,timeOnD4))
}
section([mobileOnly:true], "Options") {
label(title: "Assign a name", required: false, defaultValue: "Smart Home Ventilation")
href "pageAbout", title: "About ${textAppName()}", description: "Tap to get application version, license and instructions"
}
}
}
//----Scheduling Pages
page(name: "A_Scenario", title: getTitle (titleA, "A")) {
section{
input "timeOnA1", title: "Schedule 1 time to turn on", "time", required: false
input "timeOffA1", title: "Schedule 1 time to turn off", "time", required: false
}
section{
input "timeOnA2", title: "Schedule 2 time to turn on", "time", required: false
input "timeOffA2", title: "Schedule 2 time to turn off", "time", required: false
}
section{
input "timeOnA3", title: "Schedule 3 time to turn on", "time", required: false
input "timeOffA3", title: "Schedule 3 time to turn off", "time", required: false
}
section{
input "timeOnA4", title: "Schedule 4 time to turn on", "time", required: false
input "timeOffA4", title: "Schedule 4 time to turn off", "time", required: false
}
section ("Options") {
input "titleA", title: "Assign a scenario name", "text", required: false
input "modeA", "mode", required: false, multiple: true, title: "Run in specific mode(s)", description: "Choose Modes"
input "daysA", "enum", multiple: true, title: "Run on specific day(s)", description: "Choose Days", required: false, options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
}
}
page(name: "B_Scenario", title: getTitle (titleB, "B")) {
section{
input "timeOnB1", title: "Schedule 1 time to turn on", "time", required: false
input "timeOffB1", title: "Schedule 1 time to turn off", "time", required: false
}
section{
input "timeOnB2", title: "Schedule 2 time to turn on", "time", required: false
input "timeOffB2", title: "Schedule 2 time to turn off", "time", required: false
}
section{
input "timeOnB3", title: "Schedule 3 time to turn on", "time", required: false
input "timeOffB3", title: "Schedule 3 time to turn off", "time", required: false
}
section{
input "timeOnB4", title: "Schedule 4 time to turn on", "time", required: false
input "timeOffB4", title: "Schedule 4 time to turn off", "time", required: false
}
section("Options") {
input "titleB", title: "Assign a scenario name", "text", required: false
input "modeB", "mode", required: false, multiple: true, title: "Run in specific mode(s)", description: "Choose Modes"
input "daysB", "enum", multiple: true, title: "Run on specific day(s)", description: "Choose Days", required: false, options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
}
}
page(name: "C_Scenario", title: getTitle (titleC, "C")) {
section{
input "timeOnC1", title: "Schedule 1 time to turn on", "time", required: false
input "timeOffC1", title: "Schedule 1 time to turn off", "time", required: false
}
section{
input "timeOnC2", title: "Schedule 2 time to turn on", "time", required: false
input "timeOffC2", title: "Schedule 2 time to turn off", "time", required: false
}
section{
input "timeOnC3", title: "Schedule 3 time to turn on", "time", required: false
input "timeOffC3", title: "Schedule 3 time to turn off", "time", required: false
}
section{
input "timeOnC4", title: "Schedule 4 time to turn on", "time", required: false
input "timeOffC4", title: "Schedule 4 time to turn off", "time", required: false
}
section("Options") {
input "titleC", title: "Assign a scenario name", "text", required: false
input "modeC", "mode", required: false, multiple: true, title: "Run in specific mode(s)", description: "Choose Modes"
input "daysC", "enum", multiple: true, title: "Run on specific day(s)", description: "Choose Days", required: false, options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
}
}
page(name: "D_Scenario", title: getTitle (titleD, "D")) {
section{
input "timeOnD1", title: "Schedule 1 time to turn on", "time", required: false
input "timeOffD1", title: "Schedule 1 time to turn off", "time", required: false
}
section{
input "timeOnD2", title: "Schedule 2 time to turn on", "time", required: false
input "timeOffD2", title: "Schedule 2 time to turn off", "time", required: false
}
section{
input "timeOnD3", title: "Schedule 3 time to turn on", "time", required: false
input "timeOffD3", title: "Schedule 3 time to turn off", "time", required: false
}
section{
input "timeOnD4", title: "Schedule 4 time to turn on", "time", required: false
input "timeOffD4", title: "Schedule 4 time to turn off", "time", required: false
}
section("Options") {
input "titleD", title: "Assign a scenario name", "text", required: false
input "modeD", "mode", required: false, multiple: true, title: "Run in specific mode(s)", description: "Choose Modes"
input "daysD", "enum", multiple: true, title: "Run on specific day(s)", description: "Choose Days", required: false, options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
}
}
page(name: "pageAbout", title: "About ${textAppName()}") {
section {
paragraph "${textVersion()}\n${textCopyright()}\n\n${textLicense()}\n"
}
section("Instructions") {
paragraph textHelp()
}
}
// Install and initiate
def installed() {
log.debug "Installed with settings: ${settings}"
init()
}
def updated() {
unschedule()
turnOffSwitch() //Turn off all switches if the schedules are changed while in mid-schedule
unsubscribe
log.debug "Updated with settings: ${settings}"
init()
}
def init() {
def midnightTime = timeToday("2000-01-01T00:01:00.999-0000", location.timeZone)
schedule (midnightTime, midNight)
subscribe(location, "mode", locationHandler)
startProcess()
}
// Common methods
def startProcess () {
createDayArray()
state.dayCount=state.data.size()
if (state.dayCount){
state.counter = 0
startDay()
}
}
def startDay() {
def start = convertEpoch(state.data[state.counter].start)
def stop = convertEpoch(state.data[state.counter].stop)
runOnce(start, turnOnSwitch, [overwrite: true])
runOnce(stop, incDay, [overwrite: true])
}
def incDay() {
turnOffSwitch()
if (state.modeChange) {
startProcess()
}
else {
state.counter = state.counter + 1
if (state.counter < state.dayCount) {
startDay()
}
}
}
def locationHandler(evt) {
def result = false
state.modeChange = true
switches.each {
if (it.currentValue("switch")=="on"){
result = true
}
}
if (!result) {
startProcess()
}
}
def midNight(){
startProcess()
}
def turnOnSwitch() {
switches.on()
log.debug "Home ventilation switches are on."
}
def turnOffSwitch() {
switches.each {
if (it.currentValue("switch")=="on"){
it.off()
}
}
log.debug "Home ventilation switches are off."
}
def schedDesc(on1, off1, on2, off2, on3, off3, on4, off4, modeList, dayList) {
def title = ""
def dayListClean = "On "
def modeListClean ="Scenario runs in "
if (dayList && dayList.size() < 7) {
def dayListSize = dayList.size()
for (dayName in dayList) {
dayListClean = "${dayListClean}"+"${dayName}"
dayListSize = dayListSize -1
if (dayListSize) {
dayListClean = "${dayListClean}, "
}
}
}
else {
dayListClean = "Every day"
}
if (modeList) {
def modeListSize = modeList.size()
def modePrefix ="modes"
if (modeListSize == 1) {
modePrefix = "mode"
}
for (modeName in modeList) {
modeListClean = "${modeListClean}"+"'${modeName}'"
modeListSize = modeListSize -1
if (modeListSize) {
modeListClean = "${modeListClean}, "
}
else {
modeListClean = "${modeListClean} ${modePrefix}"
}
}
}
else {
modeListClean = "${modeListClean}all modes"
}
if (on1 && off1){
title += "Schedule 1: ${humanReadableTime(on1)} to ${humanReadableTime(off1)}"
}
if (on2 && off2) {
title += "\nSchedule 2: ${humanReadableTime(on2)} to ${humanReadableTime(off2)}"
}
if (on3 && off3) {
title += "\nSchedule 3: ${humanReadableTime(on3)} to ${humanReadableTime(off3)}"
}
if (on4 && off4) {
title += "\nSchedule 4: ${humanReadableTime(on4)} to ${humanReadableTime(off4)}"
}
if (on1 || on2 || on3 || on4) {
title += "\n$modeListClean"
title += "\n$dayListClean"
}
if (!on1 && !on2 && !on3 && !on4) {
title="Click to configure scenario"
}
title
}
def greyOut(on1, on2, on3, on4){
def result = on1 || on2 || on3 || on4 ? "complete" : ""
}
public humanReadableTime(dateTxt) {
new Date().parse("yyyy-MM-dd'T'HH:mm:ss.SSSZ", dateTxt).format("h:mm a", timeZone(dateTxt))
}
public convertEpoch(epochDate) {
new Date(epochDate).format("yyyy-MM-dd'T'HH:mm:ss.SSSZ", location.timeZone)
}
private getTitle(txt, scenario) {
def title = txt ? txt : "Scenario ${scenario}"
}
private daysOk(dayList) {
def result = true
if (dayList) {
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 = dayList.contains(day)
}
result
}
private timeOk(starting, ending) {
if (starting && ending) {
def currTime = now()
def start = timeToday(starting).time
def stop = timeToday(ending).time
if (start < stop && start >= currTime && stop>=currTime) {
state.data << [start:start, stop:stop]
}
}
}
def createDayArray() {
state.modeChange = false
state.data = []
if (modeA && modeA.contains(location.mode)) {
if (daysOk(daysA)){
timeOk(timeOnA1, timeOffA1)
timeOk(timeOnA2, timeOffA2)
timeOk(timeOnA3, timeOffA3)
timeOk(timeOnA4, timeOffA4)
}
}
if (modeB && modeB.contains(location.mode)) {
if (daysOk(daysB)){
timeOk(timeOnB1, timeOffB1)
timeOk(timeOnB2, timeOffB2)
timeOk(timeOnB3, timeOffB3)
timeOk(timeOnB4, timeOffB4)
}
}
if (modeC && modeC.contains(location.mode)) {
if (daysOk(daysC)){
timeOk(timeOnC1, timeOffC1)
timeOk(timeOnC2, timeOffC2)
timeOk(timeOnC3, timeOffC3)
timeOk(timeOnC4, timeOffC4)
}
}
if (modeD && modeD.contains(location.mode)) {
if (daysOk(daysD)){
timeOk(timeOnD1, timeOffD1)
timeOk(timeOnD2, timeOffD2)
timeOk(timeOnD3, timeOffD3)
timeOk(timeOnD4, timeOffD4)
}
}
state.data.sort{it.start}
}
//Version/Copyright/Information/Help
private def textAppName() {
def text = "Smart Home Ventilation"
}
private def textVersion() {
def text = "Version 2.1.2 (05/31/2015)"
}
private def textCopyright() {
def text = "Copyright © 2015 Michael Struck"
}
private def textLicense() {
def text =
"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"+
"\n\n"+
" http://www.apache.org/licenses/LICENSE-2.0"+
"\n\n"+
"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."
}
private def textHelp() {
def text =
"Within each scenario, choose a start and end time for the ventilation fan. You can have up to 4 different " +
"venting scenarios, and 4 schedules within each scenario. Each scenario can be restricted to specific modes or certain days of the week. It is recommended "+
"that each scenario does not overlap and run in separate modes (i.e. Home, Out of town, etc). Also note that you should " +
"avoid scheduling the ventilation fan at exactly midnight; the app resets itself at that time. It is suggested to start any new schedule " +
"at 12:15 am or later."
}

View File

@@ -0,0 +1,146 @@
/**
* Switch Activates Home Phrase or Mode
*
* Copyright 2015 Michael Struck
* Version 1.0.1 6/20/15
*
* 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.
*
* Ties a Hello, Home phrase to a switch's (virtual or real) on/off state. Perfect for use with IFTTT.
* Simple define a switch to be used, then tie the on/off state of the switch to a specific Hello, Home phrases.
* Connect the switch to an IFTTT action, and the Hello, Home phrase will fire with the switch state change.
*
*
*/
definition(
name: "Switch Activates Home Phrase or Mode",
namespace: "MichaelStruck",
author: "Michael Struck",
description: "Ties a Hello, Home phrase or mode to a switch's state. Perfect for use with IFTTT.",
category: "Convenience",
iconUrl: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/IFTTT-SmartApps/App1.png",
iconX2Url: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/IFTTT-SmartApps/App1@2x.png",
iconX3Url: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/IFTTT-SmartApps/App1@2x.png")
preferences {
page(name: "getPref")
}
def getPref() {
dynamicPage(name: "getPref", install:true, uninstall: true) {
section("Choose a switch to use...") {
input "controlSwitch", "capability.switch", title: "Switch", multiple: false, required: true
}
def phrases = location.helloHome?.getPhrases()*.label
if (phrases) {
phrases.sort()
section("Perform which phrase when...") {
input "phrase_on", "enum", title: "Switch is on", options: phrases, required: false
input "phrase_off", "enum", title: "Switch is off", options: phrases, required: false
}
}
section("Change to which mode when...") {
input "onMode", "mode", title: "Switch is on", required: false
input "offMode", "mode", title: "Switch is off", required: false
}
section([mobileOnly:true], "Options") {
label(title: "Assign a name", required: false)
mode title: "Set for specific mode(s)", required: false
href "pageAbout", title: "About ${textAppName()}", description: "Tap to get application version, license and instructions"
}
}
}
page(name: "pageAbout", title: "About ${textAppName()}") {
section {
paragraph "${textVersion()}\n${textCopyright()}\n\n${textLicense()}\n"
}
section("Instructions") {
paragraph textHelp()
}
}
def installed() {
log.debug "Installed with settings: ${settings}"
subscribe(controlSwitch, "switch", "switchHandler")
}
def updated() {
log.debug "Updated with settings: ${settings}"
unsubscribe()
subscribe(controlSwitch, "switch", "switchHandler")
}
def switchHandler(evt) {
if (evt.value == "on" && (phrase_on || onMode)) {
if (phrase_on){
location.helloHome.execute(settings.phrase_on)
}
if (onMode) {
changeMode(onMode)
}
}
else if (evt.value == "off" && (phrase_off || offMode)) {
if (phrase_off){
location.helloHome.execute(settings.phrase_off)
}
if (offMode) {
changeMode(offMode)
}
}
}
def changeMode(newMode) {
if (location.mode != newMode) {
if (location.modes?.find{it.name == newMode}) {
setLocationMode(newMode)
} else {
log.debug "Unable to change to undefined mode '${newMode}'"
}
}
}
//Version/Copyright/Information/Help
private def textAppName() {
def text = "Switch Activates Home Phrase or Mode"
}
private def textVersion() {
def text = "Version 1.0.1 (06/20/2015)"
}
private def textCopyright() {
def text = "Copyright © 2015 Michael Struck"
}
private def textLicense() {
def text =
"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"+
"\n\n"+
" http://www.apache.org/licenses/LICENSE-2.0"+
"\n\n"+
"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."
}
private def textHelp() {
def text =
"Ties a Hello, Home phrase or mode to a switch's (virtual or real) on/off state. Perfect for use with IFTTT. "+
"Simple define a switch to be used, then tie the on/off state of the switch to a specific Hello, Home phrases or mode. "+
"Connect the switch to an IFTTT action, and the Hello, Home phrase or mode will fire with the switch state change."
}

View File

@@ -0,0 +1,72 @@
/**
* Switch Activates Hello, Home Phrase
*
* Copyright 2015 Michael Struck
* Version 1.01 3/8/15
*
* 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.
*
* Ties a Hello, Home phrase to a switch's (virtual or real) on/off state. Perfect for use with IFTTT.
* Simple define a switch to be used, then tie the on/off state of the switch to a specific Hello, Home phrases.
* Connect the switch to an IFTTT action, and the Hello, Home phrase will fire with the switch state change.
*
*
*/
definition(
name: "Switch Activates Home Phrase",
namespace: "MichaelStruck",
author: "Michael Struck",
description: "Ties a Hello, Home phrase to a switch's state. Perfect for use with IFTTT.",
category: "Convenience",
iconUrl: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/IFTTT-SmartApps/App1.png",
iconX2Url: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/IFTTT-SmartApps/App1@2x.png",
iconX3Url: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/IFTTT-SmartApps/App1@2x.png")
preferences {
page(name: "getPref")
}
def getPref() {
dynamicPage(name: "getPref", title: "Choose Switch and Phrases", install:true, uninstall: true) {
section("Choose a switch to use...") {
input "controlSwitch", "capability.switch", title: "Switch", multiple: false, required: true
}
def phrases = location.helloHome?.getPhrases()*.label
if (phrases) {
phrases.sort()
section("Perform the following phrase when...") {
log.trace phrases
input "phrase_on", "enum", title: "Switch is on", required: true, options: phrases
input "phrase_off", "enum", title: "Switch is off", required: true, options: phrases
}
}
}
}
def installed() {
log.debug "Installed with settings: ${settings}"
subscribe(controlSwitch, "switch", "switchHandler")
}
def updated() {
log.debug "Updated with settings: ${settings}"
unsubscribe()
subscribe(controlSwitch, "switch", "switchHandler")
}
def switchHandler(evt) {
if (evt.value == "on") {
location.helloHome.execute(settings.phrase_on)
} else {
location.helloHome.execute(settings.phrase_off)
}
}

View File

@@ -0,0 +1,76 @@
/**
* Switch Changes Mode
*
* Copyright 2015 Michael Struck
* Version 1.01 3/8/15
*
* 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.
*
* Ties a mode to a switch's (virtual or real) on/off state. Perfect for use with IFTTT.
* Simple define a switch to be used, then tie the on/off state of the switch to a specific mode.
* Connect the switch to an IFTTT action, and the mode will fire with the switch state change.
*
*
*/
definition(
name: "Switch Changes Mode",
namespace: "MichaelStruck",
author: "Michael Struck",
description: "Ties a mode to a switch's state. Perfect for use with IFTTT.",
category: "Convenience",
iconUrl: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/IFTTT-SmartApps/App1.png",
iconX2Url: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/IFTTT-SmartApps/App1@2x.png",
iconX3Url: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/IFTTT-SmartApps/App1@2x.png")
preferences {
page(name: "getPref", title: "Choose Switch and Modes", install:true, uninstall: true) {
section("Choose a switch to use...") {
input "controlSwitch", "capability.switch", title: "Switch", multiple: false, required: true
}
section("Change to a new mode when...") {
input "onMode", "mode", title: "Switch is on", required: false
input "offMode", "mode", title: "Switch is off", required: false
}
}
}
def installed() {
log.debug "Installed with settings: ${settings}"
subscribe(controlSwitch, "switch", "switchHandler")
}
def updated() {
log.debug "Updated with settings: ${settings}"
unsubscribe()
subscribe(controlSwitch, "switch", "switchHandler")
}
def switchHandler(evt) {
if (evt.value == "on") {
changeMode(onMode)
} else {
changeMode(offMode)
}
}
def changeMode(newMode) {
if (newMode && location.mode != newMode) {
if (location.modes?.find{it.name == newMode}) {
setLocationMode(newMode)
}
else {
log.debug "Unable to change to undefined mode '${newMode}'"
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,80 @@
/**
* Lights Off with No Motion and Presence
*
* Author: Bruce Adelsman
*/
definition(
name: "Lights Off with No Motion and Presence",
namespace: "naissan",
author: "Bruce Adelsman",
description: "Turn lights off when no motion and presence is detected for a set period of time.",
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("Light switches to turn off") {
input "switches", "capability.switch", title: "Choose light switches", multiple: true
}
section("Turn off when there is no motion and presence") {
input "motionSensor", "capability.motionSensor", title: "Choose motion sensor"
input "presenceSensors", "capability.presenceSensor", title: "Choose presence sensors", multiple: true
}
section("Delay before turning off") {
input "delayMins", "number", title: "Minutes of inactivity?"
}
}
def installed() {
subscribe(motionSensor, "motion", motionHandler)
subscribe(presenceSensors, "presence", presenceHandler)
}
def updated() {
unsubscribe()
subscribe(motionSensor, "motion", motionHandler)
subscribe(presenceSensors, "presence", presenceHandler)
}
def motionHandler(evt) {
log.debug "handler $evt.name: $evt.value"
if (evt.value == "inactive") {
runIn(delayMins * 60, scheduleCheck, [overwrite: false])
}
}
def presenceHandler(evt) {
log.debug "handler $evt.name: $evt.value"
if (evt.value == "not present") {
runIn(delayMins * 60, scheduleCheck, [overwrite: false])
}
}
def isActivePresence() {
// check all the presence sensors, make sure none are present
def noPresence = presenceSensors.find{it.currentPresence == "present"} == null
!noPresence
}
def scheduleCheck() {
log.debug "scheduled check"
def motionState = motionSensor.currentState("motion")
if (motionState.value == "inactive") {
def elapsed = now() - motionState.rawDateCreated.time
def threshold = 1000 * 60 * delayMins - 1000
if (elapsed >= threshold) {
if (!isActivePresence()) {
log.debug "Motion has stayed inactive since last check ($elapsed ms) and no presence: turning lights off"
switches.off()
} else {
log.debug "Presence is active: do nothing"
}
} else {
log.debug "Motion has not stayed inactive long enough since last check ($elapsed ms): do nothing"
}
} else {
log.debug "Motion is active: do nothing"
}
}

View File

@@ -0,0 +1,148 @@
/**
* Smart Timer
* Loosely based on "Light Follows Me"
*
* This prevent them from turning off when the timer expires, if they were already turned on
*
* If the switch is already on, if won't be affected by the timer (Must be turned of manually)
* If the switch is toggled while in timeout-mode, it will remain on and ignore the timer (Must be turned of manually)
*
* The timeout perid begins when the contact is closed, or motion stops, so leaving a door open won't start the timer until it's closed.
*
* Author: andersheie@gmail.com
* Date: 2014-08-31
*/
definition(
name: "Smart Light Timer, X minutes unless already on",
namespace: "Pope",
author: "listpope@cox.net",
description: "Turns on a switch for X minutes, then turns it off. Unless, the switch is already on, in which case it stays on. If the switch is toggled while the timer is running, the timer is canceled.",
category: "Convenience",
iconUrl: "http://upload.wikimedia.org/wikipedia/commons/6/6a/Light_bulb_icon_tips.svg",
iconX2Url: "http://upload.wikimedia.org/wikipedia/commons/6/6a/Light_bulb_icon_tips.svg")
preferences {
section("Turn on when there's movement..."){
input "motions", "capability.motionSensor", multiple: true, title: "Select motion detectors", required: false
}
section("Or, turn on when one of these contacts opened"){
input "contacts", "capability.contactSensor", multiple: true, title: "Select Contacts", required: false
}
section("And off after no more triggers after..."){
input "minutes1", "number", title: "Minutes?", defaultValue: "5"
}
section("Turn on/off light(s)..."){
input "switches", "capability.switch", multiple: true, title: "Select Lights"
}
}
def installed()
{
subscribe(switches, "switch", switchChange)
subscribe(motions, "motion", motionHandler)
subscribe(contacts, "contact", contactHandler)
schedule("0 * * * * ?", "scheduleCheck")
state.myState = "ready"
}
def updated()
{
unsubscribe()
subscribe(motions, "motion", motionHandler)
subscribe(switches, "switch", switchChange)
subscribe(contacts, "contact", contactHandler)
state.myState = "ready"
log.debug "state: " + state.myState
}
def switchChange(evt) {
log.debug "SwitchChange: $evt.name: $evt.value"
if(evt.value == "on") {
// Slight change of Race condition between motion or contact turning the switch on,
// versus user turning the switch on. Since we can't pass event parameters :-(, we rely
// on the state and hope the best.
if(state.myState == "activating") {
// OK, probably an event from Activating something, and not the switch itself. Go to Active mode.
state.myState = "active"
} else if(state.myState != "active") {
state.myState = "already on"
}
} else {
// If active and switch is turned of manually, then stop the schedule and go to ready state
if(state.myState == "active" || state.myState == "activating") {
unschedule()
}
state.myState = "ready"
}
log.debug "state: " + state.myState
}
def contactHandler(evt) {
log.debug "contactHandler: $evt.name: $evt.value"
if (evt.value == "open") {
if(state.myState == "ready") {
log.debug "Turning on lights by contact opening"
switches.on()
state.inactiveAt = null
state.myState = "activating"
}
} else if (evt.value == "closed") {
if (!state.inactiveAt && state.myState == "active" || state.myState == "activating") {
// When contact closes, we reset the timer if not already set
setActiveAndSchedule()
}
}
log.debug "state: " + state.myState
}
def motionHandler(evt) {
log.debug "motionHandler: $evt.name: $evt.value"
if (evt.value == "active") {
if(state.myState == "ready" || state.myState == "active" || state.myState == "activating" ) {
log.debug "turning on lights"
switches.on()
state.inactiveAt = null
state.myState = "activating"
}
} else if (evt.value == "inactive") {
if (!state.inactiveAt && state.myState == "active" || state.myState == "activating") {
// When Motion ends, we reset the timer if not already set
setActiveAndSchedule()
}
}
log.debug "state: " + state.myState
}
def setActiveAndSchedule() {
unschedule()
state.myState = "active"
state.inactiveAt = now()
schedule("0 * * * * ?", "scheduleCheck")
}
def scheduleCheck() {
log.debug "schedule check, ts = ${state.inactiveAt}"
if(state.myState != "already on") {
if(state.inactiveAt != null) {
def elapsed = now() - state.inactiveAt
log.debug "${elapsed / 1000} sec since motion stopped"
def threshold = 1000 * 60 * minutes1
if (elapsed >= threshold) {
if (state.myState == "active") {
log.debug "turning off lights"
switches.off()
}
state.inactiveAt = null
state.myState = "ready"
}
}
}
log.debug "state: " + state.myState
}

View File

@@ -0,0 +1,51 @@
/**
* Monitor on Sense
*
* Copyright 2014 Rachel Steele
*
* 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: "Monitor on Sense",
namespace: "resteele",
author: "Rachel Steele",
description: "Turn on Monitor when vibration is sensed",
category: "My Apps",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
oauth: [displayName: "Monitor on Vibrate", displayLink: ""])
preferences {
section("When the keyboard is used...") {
input "accelerationSensor", "capability.accelerationSensor", title: "Which Sensor?"
}
section("Turn on/off a light...") {
input "switch1", "capability.switch"
}
}
def installed() {
subscribe(accelerationSensor, "acceleration.active", accelerationActiveHandler)
}
def updated() {
unsubscribe()
subscribe(accelerationSensor, "acceleration.active", accelerationActiveHandler)
}
def accelerationActiveHandler(evt) {
switch1.on()
}

View File

@@ -0,0 +1,208 @@
/**
* HebcalModes
*
* Author: danielbarak@live.com
* Date: 2014-02-21
*/
// Automatically generated. Make future change here.
definition(
name: "Shabbat and Holiday Modes",
namespace: "ShabbatHolidayMode",
author: "danielbarak@live.com",
description: "Changes the mode at candle lighting and back after havdalah. Uses the HebCal.com API to look for days that are shabbat or chag and pull real time candle lighting and havdalah times to change modes automatically",
category: "My Apps",
iconUrl: "http://upload.wikimedia.org/wikipedia/commons/thumb/4/49/Star_of_David.svg/200px-Star_of_David.svg.png",
iconX2Url: "http://upload.wikimedia.org/wikipedia/commons/thumb/4/49/Star_of_David.svg/200px-Star_of_David.svg.png",
iconX3Url: "http://upload.wikimedia.org/wikipedia/commons/thumb/4/49/Star_of_David.svg/200px-Star_of_David.svg.png")
preferences {
section("At Candlelighting Change Mode To:")
{
input "startMode", "mode", title: "Mode?"
}
section("At Havdalah Change Mode To:")
{
input "endMode", "mode", title: "Mode?"
}
section("Havdalah Offset (Usually 50 or 72)") {
input "havdalahOffset", "number", title: "Minutes After Sundown", required:true
}
section("Your ZipCode") {
input "zipcode", "number", title: "ZipCode", required:true
}
section( "Notifications" ) {
input "sendPushMessage", "enum", title: "Send a push notification?", metadata:[values:["Yes","No"]], required:false
input "phone", "phone", title: "Send a Text Message?", required: false
}
/**/
}
def installed() {
log.debug "Installed with settings: ${settings}"
initialize()
}
def updated() {
log.debug "Updated with settings: ${settings}"
unsubscribe()
initialize()
}
def initialize() {
poll();
schedule("0 0 8 1/1 * ? *", poll)
}
//Check hebcal for today's candle lighting or havdalah
def poll()
{
unschedule("endChag")
unschedule("setChag")
Hebcal_WebRequest()
}//END def poll()
/**********************************************
// HEBCAL FUNCTIONS
-----------------------------------------------*/
//This function is the web request and response parse
def Hebcal_WebRequest(){
def today = new Date().format("yyyy-MM-dd")
//def today = "2014-11-14"
def zip = settings.zip as String
def locale = getWeatherFeature("geolookup", zip)
def timezone = TimeZone.getTimeZone(locale.location.tz_long)
def hebcal_date
def hebcal_category
def hebcal_title
def candlelighting
def candlelightingLocalTime
def havdalah
def havdalahLocalTime
def pushMessage
def testmessage
def urlRequest = "http://www.hebcal.com/hebcal/?v=1&cfg=json&nh=off&nx=off&year=now&month=now&mf=off&c=on&zip=${zipcode}&m=${havdalahOffset}&s=off&D=off&d=off&o=off&ss=off"
log.trace "${urlRequest}"
def hebcal = { response ->
hebcal_date = response.data.items.date
hebcal_category = response.data.items.category
hebcal_title = response.data.items.title
for (int i = 0; i < hebcal_date.size; i++)
{
if(hebcal_date[i].split("T")[0]==today)
{
if(hebcal_category[i]=="candles")
{
candlelightingLocalTime = HebCal_GetTime12(hebcal_title[i])
pushMessage = "Candle Lighting is at ${candlelightingLocalTime}"
candlelightingLocalTime = HebCal_GetTime24(hebcal_date[i])
candlelighting = timeToday(candlelightingLocalTime, timezone)
sendMessage(pushMessage)
schedule(candlelighting, setChag)
log.debug pushMessage
}//END if(hebcal_category=="candles")
else if(hebcal_category[i]=="havdalah")
{
havdalahLocalTime = HebCal_GetTime12(hebcal_title[i])
pushMessage = "Havdalah is at ${havdalahLocalTime}"
havdalahLocalTime = HebCal_GetTime24(hebcal_date[i])
havdalah = timeToday(havdalahLocalTime, timezone)
testmessage = "Scheduling for ${havdalah}"
schedule(havdalah, endChag)
log.debug pushMessage
log.debug testmessage
}//END if(hebcal_category=="havdalah"){
}//END if(hebcal_date[i].split("T")[0]==today)
}//END for (int i = 0; i < hebcal_date.size; i++)
}//END def hebcal = { response ->
httpGet(urlRequest, hebcal);
}//END def queryHebcal()
//This function gets candle lighting time
def HebCal_GetTime12(hebcal_title){
def returnTime = hebcal_title.split(":")[1] + ":" + hebcal_title.split(":")[2] + " "
return returnTime
}//END def HebCal_GetTime12()
//This function gets candle lighting time
def HebCal_GetTime24(hebcal_date){
def returnTime = hebcal_date.split("T")[1]
returnTime = returnTime.split("-")[0]
return returnTime
}//END def HebCal_GetTime12()
/*-----------------------------------------------
END OF HEBCAL FUNCTIONS
-----------------------------------------------*/
def setChag()
{
if (location.mode != startMode)
{
if (location.modes?.find{it.name == startMode})
{
setLocationMode(startMode)
//sendMessage("Changed the mode to '${startMode}'")
def dayofweek = new Date().format("EEE")
if(dayofweek=='Fri'){
sendMessage("Shabbat Shalom!")
}
else{
sendMessage("Chag Sameach!")
}
}//END if (location.modes?.find{it.name == startMode})
else
{
sendMessage("Tried to change to undefined mode '${startMode}'")
}//END else
}//END if (location.mode != newMode)
unschedule("setChag")
}//END def setChag()
def endChag()
{
if (location.mode != endMode)
{
if (location.modes?.find{it.name == endMode})
{
setLocationMode(endMode)
sendMessage("Changed the mode to '${endMode}'")
}//END if (location.modes?.find{it.name == endMode})
else
{
sendMessage("Tried to change to undefined mode '${endMode}'")
}//END else
}//END if (location.mode != endMode)
//sendMessage("Shavuah Tov!")
unschedule("endChag")
}//END def setChag()
def sendMessage(msg){
if ( sendPushMessage != "No" ) {
log.debug( "sending push message" )
//sendPush( msg )
}
if ( phone ) {
log.debug( "sending text message" )
sendSms( phone, msg )
}
}//END def sendMessage(msg)

View File

@@ -0,0 +1,128 @@
/**
* Smart Humidifier
*
* Copyright 2014 Sheikh Dawood
*
* 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: "Smart Humidifier",
namespace: "Sheikhsphere",
author: "Sheikh Dawood",
description: "Turn on/off humidifier based on relative humidity from a sensor.",
category: "Convenience",
iconUrl: "https://graph.api.smartthings.com/api/devices/icons/st.Weather.weather12-icn",
iconX2Url: "https://graph.api.smartthings.com/api/devices/icons/st.Weather.weather12-icn?displaySize=2x"
)
preferences {
section("Monitor the humidity of:") {
input "humiditySensor1", "capability.relativeHumidityMeasurement"
}
section("When the humidity rises above:") {
input "humidityHigh", "number", title: "Percentage ?"
}
section("When the humidity drops below:") {
input "humidityLow", "number", title: "Percentage ?"
}
section("Control Humidifier:") {
input "switch1", "capability.switch"
}
section( "Notifications" ) {
input "sendPushMessage", "enum", title: "Send a push notification?", metadata:[values:["Yes","No"]], required:false
input "phone1", "phone", title: "Send a Text Message?", required: false
}
}
def installed() {
subscribe(humiditySensor1, "humidity", humidityHandler)
}
def updated() {
unsubscribe()
subscribe(humiditySensor1, "humidity", humidityHandler)
}
def humidityHandler(evt) {
log.trace "humidity: $evt.value"
log.trace "set high point: $humidityHigh"
log.trace "set low point: $humidityLow"
def currentHumidity = Double.parseDouble(evt.value.replace("%", ""))
def humidityHigh1 = humidityHigh
def humidityLow1 = humidityLow
def mySwitch = settings.switch1
if (currentHumidity >= humidityHigh1) {
log.debug "Checking how long the humidity sensor has been reporting >= $humidityHigh1"
// Don't send a continuous stream of text messages
def deltaMinutes = 10
def timeAgo = new Date(now() - (1000 * 60 * deltaMinutes).toLong())
def recentEvents = humiditySensor1.eventsSince(timeAgo)
log.trace "Found ${recentEvents?.size() ?: 0} events in the last $deltaMinutes minutes"
def alreadySentSms1 = recentEvents.count { Double.parseDouble(it.value.replace("%", "")) >= humidityHigh1 } > 1
if (alreadySentSms1) {
log.debug "Notification already sent within the last $deltaMinutes minutes"
} else {
if (state.lastStatus != "off") {
log.debug "Humidity Rose Above $humidityHigh1: sending SMS to $phone1 and deactivating $mySwitch"
send("${humiditySensor1.label} sensed high humidity level of ${evt.value}, turning off ${switch1.label}")
switch1?.off()
state.lastStatus = "off"
}
}
}
else if (currentHumidity <= humidityLow1) {
log.debug "Checking how long the humidity sensor has been reporting <= $humidityLow1"
// Don't send a continuous stream of text messages
def deltaMinutes = 10
def timeAgo = new Date(now() - (1000 * 60 * deltaMinutes).toLong())
def recentEvents = humiditySensor1.eventsSince(timeAgo)
log.trace "Found ${recentEvents?.size() ?: 0} events in the last $deltaMinutes minutes"
def alreadySentSms2 = recentEvents.count { Double.parseDouble(it.value.replace("%", "")) <= humidityLow1 } > 1
if (alreadySentSms2) {
log.debug "Notification already sent within the last $deltaMinutes minutes"
} else {
if (state.lastStatus != "on") {
log.debug "Humidity Dropped Below $humidityLow1: sending SMS to $phone1 and activating $mySwitch"
send("${humiditySensor1.label} sensed low humidity level of ${evt.value}, turning on ${switch1.label}")
switch1?.on()
state.lastStatus = "on"
}
}
}
else {
//log.debug "Humidity remained in threshold: sending SMS to $phone1 and activating $mySwitch"
//send("${humiditySensor1.label} sensed humidity level of ${evt.value} is within threshold, keeping on ${switch1.label}")
//switch1?.on()
}
}
private send(msg) {
if ( sendPushMessage != "No" ) {
log.debug( "sending push message" )
sendPush( msg )
}
if ( phone1 ) {
log.debug( "sending text message" )
sendSms( phone1, msg )
}
log.debug msg
}

View File

@@ -0,0 +1,73 @@
/**
* Smart turn it on
*
* Author: sidjohn1@gmail.com
* Date: 2013-10-21
*/
// Automatically generated. Make future change here.
definition(
name: "Smart turn it on",
namespace: "sidjohn1",
author: "sidjohn1@gmail.com",
description: "Turns on selected device(s) at a set time on selected days of the week only if a selected person is present and turns off selected device(s) after a set time.",
category: "Convenience",
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("Turn on which device?"){
input "switchOne", "capability.switch",title:"Select Light", required: true, multiple: true
}
section("For Whom?") {
input "presenceOne", "capability.presenceSensor", title: "Select Person", required: true, multiple: true
}
section("On which Days?") {
input "dayOne", "enum", title:"Select Days", required: true, multiple:true, metadata: [values: ['Mon','Tue','Wed','Thu','Fri','Sat','Sun']]
}
section("At what time?") {
input name: "timeOne", title: "Select Time", type: "time", required: true
}
section("For how long?") {
input name: "timeTwo", title: "Number of minutes", type: "number", required: true
}
}
def installed() {
if (timeOne)
{
log.debug "scheduling 'Smart turn it on' to run at $timeOne"
schedule(timeOne, "turnOn")
}
}
def updated() {
unsubscribe()
unschedule()
if (timeOne)
{
log.debug "scheduling 'Smart turn it on' to run at $timeOne"
schedule(timeOne, "turnOn")
}
}
def turnOn(){
log.debug "Start"
def dayCheck = dayOne.contains(new Date().format("EEE"))
def dayTwo = new Date().format("EEE");
if(dayCheck){
def presenceTwo = presenceOne.latestValue("presence").contains("present")
if (presenceTwo) {
switchOne.on()
def delay = timeTwo * 60
runIn(delay, "turnOff")
}
}
}
def turnOff() {
switchOne.off()
}

View File

@@ -0,0 +1,58 @@
/**
* Door Lock Code Distress Message
*
* Copyright 2014 skp19
*
*/
definition(
name: "Door Lock Code Distress Message",
namespace: "skp19",
author: "skp19",
description: "Sends a text to someone when a specific code is entered",
category: "Safety & Security",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png")
import groovy.json.JsonSlurper
preferences {
section("Choose Locks") {
input "lock1", "capability.lock", multiple: true
}
section("Enter User Code Number (This is not the code used to unlock the door)") {
input "distressCode", "number", defaultValue: "0"
}
section("Distress Message Details") {
input "phone1", "phone", title: "Phone number to send message to"
input "distressMsg", "text", title: "Message to send"
}
section("User Code Discovery Mode (Enable and unlock the door using desired code. A message will be sent containing the user code used to unlock the door.)") {
input "discoveryMode", "bool", title: "Enable"
}
}
def installed() {
subscribe(lock1, "lock", checkCode)
}
def updated() {
unsubscribe()
subscribe(lock1, "lock", checkCode)
}
def checkCode(evt) {
log.debug "$evt.value: $evt, $settings"
if(evt.value == "unlocked" && evt.data) {
def lockData = new JsonSlurper().parseText(evt.data)
if(discoveryMode) {
sendPush "Door unlocked with user code $lockData.usedCode"
}
if(lockData.usedCode == distressCode && discoveryMode == false) {
log.info "Distress Message Sent"
sendSms(phone1, distressMsg)
}
}
}

View File

@@ -0,0 +1,156 @@
/**
* Smart Lock / Unlock
*
* Copyright 2014 Arnaud
*
* 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: "Smart Lock / Unlock",
namespace: "",
author: "Arnaud",
description: "Automatically locks door X minutes after being closed and keeps door unlocked if door is open.",
category: "Safety & Security",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png")
preferences
{
section("Select the door lock:") {
input "lock1", "capability.lock", required: true
}
section("Select the door contact sensor:") {
input "contact1", "capability.contactSensor", required: true
}
section("Automatically lock the door when closed...") {
input "minutesLater", "number", title: "Delay (in minutes):", required: true
}
section("Automatically unlock the door when open...") {
input "secondsLater", "number", title: "Delay (in seconds):", required: true
}
section( "Push notification?" ) {
input "sendPushMessage", "enum", title: "Send push notification?", metadata:[values:["Yes", "No"]], required: false
}
section( "Text message?" ) {
input "sendText", "enum", title: "Send text message notification?", metadata:[values:["Yes", "No"]], required: false
input "phoneNumber", "phone", title: "Enter phone number:", required: false
}
}
def installed()
{
initialize()
}
def updated()
{
unsubscribe()
unschedule()
initialize()
}
def initialize()
{
log.debug "Settings: ${settings}"
subscribe(lock1, "lock", doorHandler, [filterEvents: false])
subscribe(lock1, "unlock", doorHandler, [filterEvents: false])
subscribe(contact1, "contact.open", doorHandler)
subscribe(contact1, "contact.closed", doorHandler)
}
def lockDoor()
{
if (lock1.latestValue("lock") == "unlocked")
{
log.debug "Locking $lock1..."
lock1.lock()
log.debug ("Sending Push Notification...")
if (sendPushMessage != "No") sendPush("$lock1 locked after $contact1 was closed for $minutesLater minute(s)!")
log.debug("Sending text message...")
if ((sendText == "Yes") && (phoneNumber != "0")) sendSms(phoneNumber, "$lock1 locked after $contact1 was closed for $minutesLater minute(s)!")
}
else if (lock1.latestValue("lock") == "locked")
{
log.debug "$lock1 was already locked..."
}
}
def unlockDoor()
{
if (lock1.latestValue("lock") == "locked")
{
log.debug "Unlocking $lock1..."
lock1.unlock()
log.debug ("Sending Push Notification...")
if (sendPushMessage != "No") sendPush("$lock1 unlocked after $contact1 was open for $secondsLater seconds(s)!")
log.debug("Sending text message...")
if ((sendText == "Yes") && (phoneNumber != "0")) sendSms(phoneNumber, "$lock1 unlocked after $contact1 was open for $secondsLater seconds(s)!")
}
else if (lock1.latestValue("lock") == "unlocked")
{
log.debug "$lock1 was already unlocked..."
}
}
def doorHandler(evt)
{
if ((contact1.latestValue("contact") == "open") && (evt.value == "locked"))
{
def delay = secondsLater
runIn (delay, unlockDoor)
}
else if ((contact1.latestValue("contact") == "open") && (evt.value == "unlocked"))
{
unschedule (unlockDoor)
}
else if ((contact1.latestValue("contact") == "closed") && (evt.value == "locked"))
{
unschedule (lockDoor)
}
else if ((contact1.latestValue("contact") == "closed") && (evt.value == "unlocked"))
{
log.debug "Unlocking $lock1..."
lock1.unlock()
def delay = (minutesLater * 60)
runIn (delay, lockDoor)
}
else if ((lock1.latestValue("lock") == "unlocked") && (evt.value == "open"))
{
unschedule (lockDoor)
log.debug "Unlocking $lock1..."
lock1.unlock()
}
else if ((lock1.latestValue("lock") == "unlocked") && (evt.value == "closed"))
{
log.debug "Unlocking $lock1..."
lock1.unlock()
def delay = (minutesLater * 60)
runIn (delay, lockDoor)
}
else if ((lock1.latestValue("lock") == "locked") && (evt.value == "open"))
{
unschedule (lockDoor)
log.debug "Unlocking $lock1..."
lock1.unlock()
}
else if ((lock1.latestValue("lock") == "locked") && (evt.value == "closed"))
{
unschedule (lockDoor)
log.debug "Unlocking $lock1..."
lock1.unlock()
}
else
{
log.debug "Problem with $lock1, the lock might be jammed!"
unschedule (lockDoor)
unschedule (unlockDoor)
}
}

View 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]}"
}
}

View 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()
}

View 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()
}

View 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
}

View File

@@ -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()
}
}

View File

@@ -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()
}

View File

@@ -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") : ""
}

View File

@@ -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()
}

View File

@@ -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
}

View 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.
*
* 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!")
}
}

View File

@@ -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()
}
}

View 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
}

View File

@@ -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()
}

View 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
}

View File

@@ -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()
}
}

View 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)
}

View File

@@ -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"
}
}

View File

@@ -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
}

View 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)
}
}
}

View 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()
}
}

View File

@@ -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.
}

View 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()
}

View 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)
}
}
}
}

View 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);
}

View File

@@ -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
}

View File

@@ -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)
}

View 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 != "")
}

View 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
}

View File

@@ -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
}

View 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)
}
}

View File

@@ -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")
}
}
}

View File

@@ -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}")
}

View 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 }
}

View File

@@ -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

View 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"
}
}

View 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")
}
}
}

View 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
}

View 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
}

View 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 ()"
}

View 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}"
}

View 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
}
}
}

View 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
}
}
}

View File

@@ -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()
}
}

View 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."
}
}
}

View File

@@ -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"
}
}

View File

@@ -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"
}
}

View File

@@ -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()
}

View File

@@ -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
}
}
}

View File

@@ -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
]
}

View 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!")
}
}
}
}

View 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_"
}

View File

@@ -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
}

View File

@@ -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()
}

View 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]}"
}

View 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()
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More