Compare commits

...

10 Commits

Author SHA1 Message Date
Tom Manley
e529624d36 Merge pull request #349 from tpmanley/feature/updated_smartsense_devices
Added fingerprints/support for new version of multi/moisture/motion sensors
2015-12-11 00:23:21 -06:00
Tom Manley
6ba37caa03 Added fingerprints/support for new version of multi/moisture/motion sensors
Partially resolves:
https://smartthings.atlassian.net/browse/DVCSMP-1305
2015-12-10 22:50:15 -06:00
Yaima
8d0fa7f561 Merge pull request #351 from SmartThingsCommunity/DEVC-256-11
DEVC-256: DEVC-256 Bose SoundTouch Control SmartApp - Approval
2015-12-10 15:03:33 -08:00
Donald C. Kirker
6ede225715 DEVC-256: Bose SmartApp - Approval 2015-12-10 16:47:28 -06:00
Yaima
30844676b6 Merge pull request #342 from Yaima/master
Display issue with Wattvision integration
2015-12-08 15:27:44 -08:00
Yaima Valdivia
94aa114ccb Display issue with Wattvision integration
https://smartthings.atlassian.net/browse/DVCSMP-1300
2015-12-08 15:00:35 -08:00
Yaima
a9e68d086c Merge pull request #340 from Yaima/master
Added both() command
2015-12-08 14:56:07 -08:00
Warodom Khamphanchai
d427ab8709 Merge pull request #339 from kwarodom/ecobee
Ecobee: retries to get refresh tokens if exceptions are caught
2015-12-07 15:23:40 -08:00
Yaima Valdivia
a205a94d78 Added both() command
https://smartthings.atlassian.net/browse/DVCSMP-1155
2015-12-07 14:37:10 -08:00
Warodom Khamphanchai
e775752496 Ecobee: retries to get refresh tokens if exceptions are caught in refreshAuthToken 2015-12-07 12:50:29 -08:00
8 changed files with 389 additions and 18 deletions

View File

@@ -99,7 +99,7 @@ metadata {
}
preferences {
input "holdType", "enum", title: "Hold Type", description: "When changing temperature, use Temporary or Permanent hold", required: false, options:["Temporary", "Permanent"]
input "holdType", "enum", title: "Hold Type", description: "When changing temperature, use Temporary or Permanent hold (default)", required: false, options:["Temporary", "Permanent"]
}
}

View File

@@ -27,7 +27,7 @@ metadata {
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3315-S", deviceJoinName: "Water Leak Sensor"
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3315"
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3315-Seu", deviceJoinName: "Water Leak Sensor"
fingerprint inClusters: "0000,0001,0003,000F,0020,0402,0500", outClusters: "0019", manufacturer: "SmartThings", model: "moisturev4", deviceJoinName: "Water Leak Sensor"
}
simulator {

View File

@@ -29,6 +29,7 @@ metadata {
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3305"
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3325"
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3326"
fingerprint inClusters: "0000,0001,0003,000F,0020,0402,0500", outClusters: "0019", manufacturer: "SmartThings", model: "motionv4", deviceJoinName: "Motion Sensor"
}
simulator {
@@ -352,4 +353,4 @@ private byte[] reverseArray(byte[] array) {
i++;
}
return array
}
}

View File

@@ -30,6 +30,7 @@
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3320"
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3321"
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3321-S", deviceJoinName: "Multipurpose Sensor"
fingerprint inClusters: "0000,0001,0003,000F,0020,0402,0500,FC02", outClusters: "0019", manufacturer: "SmartThings", model: "multiv4", deviceJoinName: "Multipurpose Sensor"
attribute "status", "string"
}
@@ -374,26 +375,26 @@ def getTemperature(value) {
/* sensitivity - default value (8) */
"zcl mfg-code 0x104E", "delay 200",
"zcl mfg-code ${manufacturerCode}", "delay 200",
"zcl global write 0xFC02 0 0x20 {02}", "delay 200",
"send 0x${device.deviceNetworkId} 1 1", "delay 400",
"st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200",
"st rattr 0x${device.deviceNetworkId} 1 1 0x20", "delay 200",
"zcl mfg-code 0x104E", "delay 200",
"zcl mfg-code ${manufacturerCode}", "delay 200",
"zcl global read 0xFC02 0x0010",
"send 0x${device.deviceNetworkId} 1 1","delay 400",
"zcl mfg-code 0x104E", "delay 200",
"zcl mfg-code ${manufacturerCode}", "delay 200",
"zcl global read 0xFC02 0x0012",
"send 0x${device.deviceNetworkId} 1 1","delay 400",
"zcl mfg-code 0x104E", "delay 200",
"zcl mfg-code ${manufacturerCode}", "delay 200",
"zcl global read 0xFC02 0x0013",
"send 0x${device.deviceNetworkId} 1 1","delay 400",
"zcl mfg-code 0x104E", "delay 200",
"zcl mfg-code ${manufacturerCode}", "delay 200",
"zcl global read 0xFC02 0x0014",
"send 0x${device.deviceNetworkId} 1 1", "delay 400"
]
@@ -420,19 +421,19 @@ def getTemperature(value) {
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0xFC02 {${device.zigbeeId}} {}", "delay 200",
"zcl mfg-code 0x104E",
"zcl mfg-code ${manufacturerCode}",
"zcl global send-me-a-report 0xFC02 0x0010 0x18 10 3600 {01}",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
"zcl mfg-code 0x104E",
"zcl mfg-code ${manufacturerCode}",
"zcl global send-me-a-report 0xFC02 0x0012 0x29 1 3600 {01}",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
"zcl mfg-code 0x104E",
"zcl mfg-code ${manufacturerCode}",
"zcl global send-me-a-report 0xFC02 0x0013 0x29 1 3600 {01}",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
"zcl mfg-code 0x104E",
"zcl mfg-code ${manufacturerCode}",
"zcl global send-me-a-report 0xFC02 0x0014 0x29 1 3600 {01}",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500"
@@ -530,6 +531,14 @@ private Map getXyzResult(results, description) {
]
}
private getManufacturerCode() {
if (device.getDataValue("manufacturer") == "SmartThings") {
return "0x110A"
} else {
return "0x104E"
}
}
private hexToInt(value) {
new BigInteger(value, 16)
}

View File

@@ -33,14 +33,14 @@ metadata {
state "power", label: '${currentValue} W'
}
tile(name: "powerChart", attribute: "powerContent", type: "HTML", url: '${currentValue}', width: 3, height: 2) { }
htmlTile(name: "powerContent", attribute: "powerContent", type: "HTML", whitelist: "www.wattvision.com" , url: '${currentValue}', width: 3, height: 2)
standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat") {
state "default", label: '', action: "refresh.refresh", icon: "st.secondary.refresh"
}
main "power"
details(["powerChart", "power", "refresh"])
details(["powerContent", "power", "refresh"])
}
}
@@ -74,10 +74,10 @@ public addWattvisionData(json) {
log.trace "Adding data from Wattvision"
def data = json.data
def data = parseJson(json.data.toString())
def units = json.units ?: "watts"
if (data) {
if (data.size() > 0) {
def latestData = data[-1]
data.each {
sendPowerEvent(it.t, it.v, units, (latestData == it))
@@ -103,3 +103,7 @@ private sendPowerEvent(time, value, units, isLatest = false) {
sendEvent(eventData)
}
def parseJson(String s) {
new groovy.json.JsonSlurper().parseText(s)
}

View File

@@ -115,6 +115,10 @@ def strobe() {
]
}
def both() {
on()
}
def refresh() {
log.debug "sending battery refresh command"
zwave.batteryV1.batteryGet().format()

View File

@@ -0,0 +1,341 @@
/**
* Copyright 2015 SmartThings
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
* for the specific language governing permissions and limitations under the License.
*
* Bose® SoundTouch® Control
*
* Author: SmartThings & Joe Geiger
*
* Date: 2015-30-09
*/
definition(
name: "Bose® SoundTouch® Control",
namespace: "smartthings",
author: "SmartThings & Joe Geiger",
description: "Control your Bose® SoundTouch® when certain actions take place in your home.",
category: "SmartThings Labs",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience%402x.png",
iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience%402x.png"
)
preferences {
page(name: "mainPage", title: "Control your Bose® SoundTouch® when something happens", install: true, uninstall: true)
page(name: "timeIntervalInput", title: "Only during a certain time") {
section {
input "starting", "time", title: "Starting", required: false
input "ending", "time", title: "Ending", required: false
}
}
}
def mainPage() {
dynamicPage(name: "mainPage") {
def anythingSet = anythingSet()
if (anythingSet) {
section("When..."){
ifSet "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true
ifSet "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true
ifSet "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true
ifSet "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true
ifSet "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true
ifSet "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true
ifSet "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true
ifSet "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true
ifSet "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true
ifSet "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true
ifSet "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production
ifSet "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true
ifSet "timeOfDay", "time", title: "At a Scheduled Time", required: false
}
}
section(anythingSet ? "Select additional triggers" : "When...", hideable: anythingSet, hidden: true){
ifUnset "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true
ifUnset "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true
ifUnset "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true
ifUnset "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true
ifUnset "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true
ifUnset "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true
ifUnset "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true
ifUnset "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true
ifUnset "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true
ifUnset "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true
ifUnset "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production
ifUnset "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true
ifUnset "timeOfDay", "time", title: "At a Scheduled Time", required: false
}
section("Perform this action"){
input "actionType", "enum", title: "Action?", required: true, defaultValue: "play", options: [
"Turn On & Play",
"Turn Off",
"Toggle Play/Pause",
"Skip to Next Track",
"Skip to Beginning/Previous Track",
"Play Preset 1",
"Play Preset 2",
"Play Preset 3",
"Play Preset 4",
"Play Preset 5",
"Play Preset 6"
]
}
section {
input "bose", "capability.musicPlayer", title: "Bose® SoundTouch® music player", required: true
}
section("More options", hideable: true, hidden: true) {
input "volume", "number", title: "Set the volume volume", description: "0-100%", required: false
input "frequency", "decimal", title: "Minimum time between actions (defaults to every event)", description: "Minutes", required: false
href "timeIntervalInput", title: "Only during a certain time", description: timeLabel ?: "Tap to set", state: timeLabel ? "complete" : "incomplete"
input "days", "enum", title: "Only on certain days of the week", multiple: true, required: false,
options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
if (settings.modes) {
input "modes", "mode", title: "Only when mode is", multiple: true, required: false
}
input "oncePerDay", "bool", title: "Only once per day", required: false, defaultValue: false
}
section([mobileOnly:true]) {
label title: "Assign a name", required: false
mode title: "Set for specific mode(s)"
}
}
}
private anythingSet() {
for (name in ["motion","contact","contactClosed","acceleration","mySwitch","mySwitchOff","arrivalPresence","departurePresence","smoke","water","button1","triggerModes","timeOfDay"]) {
if (settings[name]) {
return true
}
}
return false
}
private ifUnset(Map options, String name, String capability) {
if (!settings[name]) {
input(options, name, capability)
}
}
private ifSet(Map options, String name, String capability) {
if (settings[name]) {
input(options, name, capability)
}
}
def installed() {
log.debug "Installed with settings: ${settings}"
subscribeToEvents()
}
def updated() {
log.debug "Updated with settings: ${settings}"
unsubscribe()
unschedule()
subscribeToEvents()
}
def subscribeToEvents() {
log.trace "subscribeToEvents()"
subscribe(app, appTouchHandler)
subscribe(contact, "contact.open", eventHandler)
subscribe(contactClosed, "contact.closed", eventHandler)
subscribe(acceleration, "acceleration.active", eventHandler)
subscribe(motion, "motion.active", eventHandler)
subscribe(mySwitch, "switch.on", eventHandler)
subscribe(mySwitchOff, "switch.off", eventHandler)
subscribe(arrivalPresence, "presence.present", eventHandler)
subscribe(departurePresence, "presence.not present", eventHandler)
subscribe(smoke, "smoke.detected", eventHandler)
subscribe(smoke, "smoke.tested", eventHandler)
subscribe(smoke, "carbonMonoxide.detected", eventHandler)
subscribe(water, "water.wet", eventHandler)
subscribe(button1, "button.pushed", eventHandler)
if (triggerModes) {
subscribe(location, modeChangeHandler)
}
if (timeOfDay) {
schedule(timeOfDay, scheduledTimeHandler)
}
}
def eventHandler(evt) {
if (allOk) {
def lastTime = state[frequencyKey(evt)]
if (oncePerDayOk(lastTime)) {
if (frequency) {
if (lastTime == null || now() - lastTime >= frequency * 60000) {
takeAction(evt)
}
else {
log.debug "Not taking action because $frequency minutes have not elapsed since last action"
}
}
else {
takeAction(evt)
}
}
else {
log.debug "Not taking action because it was already taken today"
}
}
}
def modeChangeHandler(evt) {
log.trace "modeChangeHandler $evt.name: $evt.value ($triggerModes)"
if (evt.value in triggerModes) {
eventHandler(evt)
}
}
def scheduledTimeHandler() {
eventHandler(null)
}
def appTouchHandler(evt) {
takeAction(evt)
}
private takeAction(evt) {
log.debug "takeAction($actionType)"
def options = [:]
if (volume) {
bose.setLevel(volume as Integer)
options.delay = 1000
}
switch (actionType) {
case "Turn On & Play":
options ? bose.on(options) : bose.on()
break
case "Turn Off":
options ? bose.off(options) : bose.off()
break
case "Toggle Play/Pause":
def currentStatus = bose.currentValue("playpause")
if (currentStatus == "play") {
options ? bose.pause(options) : bose.pause()
}
else if (currentStatus == "pause") {
options ? bose.play(options) : bose.play()
}
break
case "Skip to Next Track":
options ? bose.nextTrack(options) : bose.nextTrack()
break
case "Skip to Beginning/Previous Track":
options ? bose.previousTrack(options) : bose.previousTrack()
break
case "Play Preset 1":
options ? bose.preset1(options) : bose.preset1()
break
case "Play Preset 2":
options ? bose.preset2(options) : bose.preset2()
break
case "Play Preset 3":
options ? bose.preset3(options) : bose.preset3()
break
case "Play Preset 4":
options ? bose.preset4(options) : bose.preset4()
break
case "Play Preset 5":
options ? bose.preset5(options) : bose.preset5()
break
case "Play Preset 6":
options ? bose.preset6(options) : bose.preset6()
break
default:
log.error "Action type '$actionType' not defined"
}
if (frequency) {
state.lastActionTimeStamp = now()
}
}
private frequencyKey(evt) {
//evt.deviceId ?: evt.value
"lastActionTimeStamp"
}
private dayString(Date date) {
def df = new java.text.SimpleDateFormat("yyyy-MM-dd")
if (location.timeZone) {
df.setTimeZone(location.timeZone)
}
else {
df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
}
df.format(date)
}
private oncePerDayOk(Long lastTime) {
def result = true
if (oncePerDay) {
result = lastTime ? dayString(new Date()) != dayString(new Date(lastTime)) : true
log.trace "oncePerDayOk = $result"
}
result
}
// TODO - centralize somehow
private getAllOk() {
modeOk && daysOk && timeOk
}
private getModeOk() {
def result = !modes || modes.contains(location.mode)
log.trace "modeOk = $result"
result
}
private getDaysOk() {
def result = true
if (days) {
def df = new java.text.SimpleDateFormat("EEEE")
if (location.timeZone) {
df.setTimeZone(location.timeZone)
}
else {
df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
}
def day = df.format(new Date())
result = days.contains(day)
}
log.trace "daysOk = $result"
result
}
private getTimeOk() {
def result = true
if (starting && ending) {
def currTime = now()
def start = timeToday(starting, location?.timeZone).time
def stop = timeToday(ending, location?.timeZone).time
result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start
}
log.trace "timeOk = $result"
result
}
private hhmm(time, fmt = "h:mm a")
{
def t = timeToday(time, location.timeZone)
def f = new java.text.SimpleDateFormat(fmt)
f.setTimeZone(location.timeZone ?: timeZone(time))
f.format(t)
}
private timeIntervalLabel()
{
(starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z") : ""
}
// TODO - End Centralize

View File

@@ -356,6 +356,8 @@ def initialize() {
//since access_token expires every 2 hours
runEvery1Hour("refreshAuthToken")
atomicState.reAttempt = 0
}
def pollHandler() {
@@ -640,8 +642,18 @@ private refreshAuthToken() {
}
} catch(Exception e) {
log.error "refreshAuthToken() >> Error: e.statusCode ${e.statusCode}"
if (e.statusCode == 401) {
sendPushAndFeeds(notificationMessage)
def reAttemptPeriod = 300 // in sec
if (e.statusCode != 401) { //this issue might comes from exceed 20sec app execution, connectivity issue etc.
runIn(reAttemptPeriod, "refreshAuthToken")
} else if (e.statusCode == 401) { //refresh token is expired
atomicState.reAttempt = atomicState.reAttempt + 1
log.warn "reAttempt refreshAuthToken to try = ${atomicState.reAttempt}"
if (atomicState.reAttempt <= 3) {
runIn(reAttemptPeriod, "refreshAuthToken")
} else {
sendPushAndFeeds(notificationMessage)
atomicState.reAttempt = 0
}
}
}
}