Compare commits

..

1 Commits

Author SHA1 Message Date
Vincent Romo
4cc578ee64 MSA-2049: chime 2017-06-21 20:23:07 -07:00
18 changed files with 948 additions and 413 deletions

View File

@@ -1,183 +0,0 @@
/**
* Z-Wave Garage Door Opener
*
* 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.
*
*/
metadata {
definition (name: "Telguard GDC1", namespace: "fortrezz", author: "FortrezZ, LLC") {
capability "Actuator"
capability "Door Control"
capability "Garage Door Control"
capability "Contact Sensor"
capability "Refresh"
capability "Sensor"
capability "Switch"
fingerprint deviceId: "0x1000", inClusters: "0x72, 0x86, 0x25"
}
simulator {
status "closed": "command: 9881, payload: 00 66 03 00"
status "opening": "command: 9881, payload: 00 66 03 FE"
status "open": "command: 9881, payload: 00 66 03 FF"
status "closing": "command: 9881, payload: 00 66 03 FC"
status "unknown": "command: 9881, payload: 00 66 03 FD"
reply "988100660100": "command: 9881, payload: 00 66 03 FC"
reply "9881006601FF": "command: 9881, payload: 00 66 03 FE"
}
tiles {
standardTile("toggle", "device.door", width: 2, height: 2) {
state("unknown", label:'${name}', action:"refresh.refresh", icon:"st.doors.garage.garage-open", backgroundColor:"#ffa81e")
state("closed", label:'${name}', action:"door control.open", icon:"st.doors.garage.garage-closed", backgroundColor:"#79b821", nextState:"opening")
state("open", label:'${name}', action:"door control.close", icon:"st.doors.garage.garage-open", backgroundColor:"#ffa81e", nextState:"closing")
state("opening", label:'${name}', icon:"st.doors.garage.garage-opening", backgroundColor:"#ffe71e")
state("closing", label:'${name}', icon:"st.doors.garage.garage-closing", backgroundColor:"#ffe71e")
}
standardTile("open", "device.door", inactiveLabel: false, decoration: "flat") {
state "default", label:'open', action:"door control.open", icon:"st.doors.garage.garage-opening"
}
standardTile("close", "device.door", inactiveLabel: false, decoration: "flat") {
state "default", label:'close', action:"door control.close", icon:"st.doors.garage.garage-closing"
}
standardTile("refresh", "device.door", inactiveLabel: false, decoration: "flat") {
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
}
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.off", backgroundColor:"#79b821", nextState:"turningOff"
state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.on", backgroundColor:"#ffffff", nextState:"turningOn"
state "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.off", backgroundColor:"#79b821", nextState:"turningOff"
state "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.on", backgroundColor:"#ffffff", nextState:"turningOn"
state "offline", label:'${name}', icon:"st.switches.switch.off", backgroundColor:"#ff0000"
}
main "toggle"
details(["toggle", "open", "close", "refresh"])
}
}
def parse(String description) {
def results = []
log.debug(description)
if (description.startsWith("Err")) {
results << createEvent(descriptionText:description, displayed:true)
} else {
def cmd = zwave.parse(description)
if (cmd) {
results = zwaveEvent(cmd)
}
}
log.debug "\"$description\" parsed to ${results.inspect()}"
return results
}
def open() {
sendEvent(name: "door", value: "opening", descriptionText: "$device.displayName: opening")
delayBetween([
zwave.basicV1.basicSet(value: 0xFF).format(),
zwave.basicV1.basicGet().format()
], 15000)
}
def close() {
sendEvent(name: "door", value: "closing", descriptionText: "$device.displayName: closing")
zwave.basicV1.basicSet(value: 0x00).format()
}
def on() {
open()
}
def off() {
close()
}
def refresh() {
zwave.basicV1.basicGet().format()
}
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) {
def map = [name: "door"]
def map2 = [name: "switch"]
if (cmd.value == 0xFF)
{
map.value = "open"
map2.value = "on"
sendEvent(map2)
}
else if (cmd.value == 0x00)
{
map.value = "closed"
map2.value = "off"
sendEvent(map2)
}
else
{
map.value = "unknown"
}
createEvent(map)
}
def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) {
def map = [name: "door"]
def map2 = [name: "switch"]
if (cmd.value == 0xFF)
{
map.value = "open"
map2.value = "on"
sendEvent(map2)
}
else if (cmd.value == 0x00)
{
map.value = "closed"
map2.value = "off"
sendEvent(map2)
}
else
{
map.value = "unknown"
}
createEvent(map)
}
def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) {
def result = []
def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId)
log.debug "msr: $msr"
updateDataValue("MSR", msr)
result << createEvent(descriptionText: "$device.displayName MSR: $msr", isStateChange: false)
result
}
def zwaveEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd) {
def fw = "${cmd.applicationVersion}.${cmd.applicationSubVersion}"
updateDataValue("fw", fw)
def text = "$device.displayName: firmware version: $fw, Z-Wave version: ${cmd.zWaveProtocolVersion}.${cmd.zWaveProtocolSubVersion}"
createEvent(descriptionText: text, isStateChange: false)
}
def zwaveEvent(physicalgraph.zwave.commands.applicationstatusv1.ApplicationBusy cmd) {
def msg = cmd.status == 0 ? "try again later" :
cmd.status == 1 ? "try again in $cmd.waitTime seconds" :
cmd.status == 2 ? "request queued" : "sorry"
createEvent(displayed: true, descriptionText: "$device.displayName is busy, $msg")
}
def zwaveEvent(physicalgraph.zwave.Command cmd) {
createEvent(displayed: false, descriptionText: "$device.displayName: $cmd")
}

View File

@@ -0,0 +1,813 @@
/**
* Zooz Smart Chime v1.2.1
* (Model: ZSE33)
*
* Author:
* Kevin LaFramboise (krlaframboise)
*
* URL to documentation: https://community.smartthings.com/t/release-zooz-smart-chime/77152?u=krlaframboise
*
*
* Changelog:
*
* 1.2.1 (04/23/2017)
* - SmartThings broke parse method response handling so switched to sendhubaction.
* - Bug fix for location timezone issue.
*
* 1.2 (04/14/2017)
* - Added Switch Level capability
*
* 1.1 (02/19/2017)
* - Added Health Check and self polling.
*
* 1.0 (01/16/2017)
* - 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.
*
*/
metadata {
definition (
name: "Zooz Smart Chime",
namespace: "krlaframboise",
author: "Kevin LaFramboise"
) {
capability "Actuator"
capability "Alarm"
capability "Battery"
capability "Configuration"
capability "Refresh"
capability "Switch"
capability "Tone"
capability "Health Check"
capability "Polling"
capability "Switch Level"
attribute "lastCheckin", "string"
attribute "status", "enum", ["alarm", "beep", "off", "on", "custom"]
command "customChime"
fingerprint deviceId: "0x1005", inClusters: "0x25, 0x59, 0x5A, 0x5E, 0x70, 0x71, 0x72, 0x73, 0x80, 0x85, 0x86, 0x87"
fingerprint mfr:"027A", prod:"0003", model:"0088"
}
simulator { }
preferences {
input "sirenSound", "number",
title: "Siren Sound [1-10]:",
range: "1..10",
displayDuringSetup: true,
defaultValue: sirenSoundSetting
input "sirenVolume", "number",
title: "Siren Volume [1-3]:${getNameValueSettingDesc(volumeOptions)}",
range: "1..3",
required: false,
defaultValue: sirenVolumeSetting,
displayDuringSetup: true
input "sirenLength", "number",
title: "Siren Length [0-4]:${getNameValueSettingDesc(sirenLengthOptions)}",
range: "0..4",
defaultValue: sirenLengthSetting,
required: false,
displayDuringSetup: true
input "sirenLED", "number",
title: "Siren LED [0-1]:${getNameValueSettingDesc(ledOptions)}",
range: "0..1",
defaultValue: sirenLEDSetting,
required: false,
displayDuringSetup: true
input "onChimeSound", "number",
title: "Switch On Chime Sound [1-10]:",
range: "1..10",
required: false,
displayDuringSetup: true,
defaultValue: onChimeSoundSetting
input "beepChimeSound", "number",
title: "Beep Chime Sound [1-10]:",
range: "1..10",
required: false,
displayDuringSetup: true,
defaultValue: beepChimeSoundSetting
input "chimeVolume", "number",
title: "Chime Volume [1-3]:${getNameValueSettingDesc(volumeOptions)}",
range: "1..3",
required: false,
defaultValue: chimeVolumeSetting,
displayDuringSetup: true
input "chimeRepeat", "number",
title: "Chime Repeat [1-255]:\n(1-254 = # of Cycles)\n(255 = ${noLengthMsg})",
range: "1..255",
required: false,
displayDuringSetup: true,
defaultValue: chimeRepeatSetting
input "chimeLED", "number",
title: "Chime LED [0-1]:${getNameValueSettingDesc(ledOptions)}",
range: "0..1",
defaultValue: chimeLEDSetting,
required: false,
displayDuringSetup: true
input "checkinInterval", "enum",
title: "Checkin Interval:",
defaultValue: checkinIntervalSetting,
required: false,
displayDuringSetup: true,
options: checkinIntervalOptions.collect { it.name }
input "debugOutput", "bool",
title: "Enable debug logging?",
defaultValue: true,
required: false
}
tiles(scale: 2) {
multiAttributeTile(name:"status", type: "generic", width: 6, height: 4, canChangeIcon: true){
tileAttribute ("device.status", key: "PRIMARY_CONTROL") {
attributeState "off",
label:'Off',
action: "off",
icon: "st.Entertainment.entertainment2",
backgroundColor:"#ffffff"
attributeState "on",
label:'Chime (On)!',
action: "off",
icon:"st.Entertainment.entertainment2",
backgroundColor: "#99c2ff"
attributeState "alarm",
label:'Siren!',
action: "off",
icon:"st.alarm.alarm.alarm",
backgroundColor:"#ff9999"
attributeState "beep",
label:'Chime (Beep)!',
action: "off",
icon:"st.Entertainment.entertainment2",
backgroundColor:"#99ff99"
attributeState "custom",
label:'Chime (Custom)!',
action: "off",
icon:"st.Entertainment.entertainment2",
backgroundColor:"#cc99cc"
}
}
standardTile("playAlarm", "device.alarm", width: 2, height: 2) {
state "default",
label:'Alarm',
action:"alarm.both",
icon:"st.security.alarm.clear",
backgroundColor:"#ff9999"
state "both",
label:'Turn Off',
action:"alarm.off",
icon:"st.alarm.alarm.alarm",
background: "#ffffff"
}
standardTile("playOn", "device.switch", width: 2, height: 2) {
state "default",
label:'Turn On',
action:"switch.on",
icon:"st.Entertainment.entertainment2",
backgroundColor:"#99c2ff"
state "on",
label:'Turn Off',
action:"switch.off",
icon:"st.Entertainment.entertainment2",
background: "#ffffff"
}
standardTile("playBeep", "device.status", width: 2, height: 2) {
state "default",
label:'Beep',
action:"tone.beep",
icon:"st.Entertainment.entertainment2",
backgroundColor: "#99FF99"
state "beep",
label:'Stop',
action:"off",
icon:"st.Entertainment.entertainment2",
background: "#ffffff"
}
standardTile("turnOff", "device.off", width: 2, height: 2) {
state "default",
label:'Off',
action:"switch.off",
backgroundColor: "#ffffff"
}
standardTile("refresh", "device.refresh", width: 2, height: 2) {
state "refresh", label:'Refresh', action: "refresh", icon:"st.secondary.refresh-icon"
}
valueTile("battery", "device.battery", decoration: "flat", width: 2, height: 2){
state "battery", label:'${currentValue}% battery', unit:""
}
main "status"
details(["status", "playOn", "playBeep", "playAlarm", "turnOff", "refresh", "battery"])
}
}
def updated() {
// This method always gets called twice when preferences are saved.
if (!isDuplicateCommand(state.lastUpdated, 3000)) {
state.lastUpdated = new Date().time
state.activeEvents = []
logTrace "updated()"
initializeCheckin()
if (state.firstUpdate == false) {
def result = []
result += configure()
if (result) {
return sendResponse(result)
}
}
else {
// Skip first time updating because it calls the configure method while it's already running.
state.firstUpdate = false
}
}
}
private sendResponse(cmds) {
def actions = []
cmds?.each { cmd ->
actions << new physicalgraph.device.HubAction(cmd)
}
sendHubCommand(actions)
return []
}
private initializeCheckin() {
// Set the Health Check interval so that it pings the device if it's 1 minute past the scheduled checkin.
def checkInterval = ((checkinIntervalSettingMinutes * 60) + 60)
sendEvent(name: "checkInterval", value: checkInterval, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID])
unschedule(healthPoll)
switch (checkinIntervalSettingMinutes) {
case 5:
runEvery5Minutes(healthPoll)
break
case 10:
runEvery10Minutes(healthPoll)
break
case 15:
runEvery15Minutes(healthPoll)
break
case 30:
runEvery30Minutes(healthPoll)
break
case [60, 120]:
runEvery1Hour(healthPoll)
break
default:
runEvery3Hours(healthPoll)
}
}
def healthPoll() {
logTrace "healthPoll()"
sendHubCommand([new physicalgraph.device.HubAction(batteryGetCmd())], 100)
}
def ping() {
logTrace "ping()"
if (canCheckin()) {
logDebug "Attempting to ping device."
// Restart the polling schedule in case that's the reason why it's gone too long without checking in.
initializeCheckin()
return poll()
}
}
def poll() {
if (canCheckin()) {
logTrace "Polling Device"
return batteryGetCmd()
}
else {
logTrace "Skipped Poll"
}
}
def configure() {
logTrace "configure()"
def cmds = []
def refreshAll = (!state.isConfigured || state.pendingRefresh || !settings?.sirenSound)
if (!state.isConfigured) {
logTrace "Waiting 1 second because this is the first time being configured"
cmds << "delay 1000"
}
configData.each {
cmds += updateConfigVal(it.paramNum, it.value, refreshAll)
}
if (refreshAll) {
cmds << switchBinaryGetCmd()
cmds << batteryGetCmd()
}
if (cmds) {
logDebug "Sending configuration to device."
return delayBetween(cmds, 250)
}
else {
return cmds
}
}
private updateConfigVal(paramNum, val, refreshAll) {
def result = []
def configVal = state["configVal${paramNum}"]
if (refreshAll || (configVal != val)) {
result << configSetCmd(paramNum, val)
result << configGetCmd(paramNum)
}
return result
}
def off() {
logDebug "Turning Off()"
return sirenToggleCmds(0x00)
}
def on() {
logDebug "Playing On Chime (#${onChimeSoundSetting})"
addPendingSound("switch", "on")
return chimePlayCmds(onChimeSoundSetting)
}
def setLevel(level, rate=null) {
logTrace "Executing setLevel($level)"
if (!device.currentValue("level")) {
sendEvent(name:"level", value:0, displayed:false)
}
return customChime(extractSoundFromLevel(level))
}
private extractSoundFromLevel(level) {
def sound = safeToInt(level, 1)
if (sound <= 10) {
return 1
}
else {
if ((sound % 10) != 0) {
sound = (sound - (sound % 10))
}
return (sound / 10)
}
}
def beep() {
logDebug "Playing Beep Chime (#${beepChimeSoundSetting})"
addPendingSound("status", "beep")
return chimePlayCmds(beepChimeSoundSetting)
}
def customChime(sound) {
def val = validateSound(sound, beepChimeSoundSetting)
if ("${sound}" != "${val}") {
logDebug "Playing Custom Chime (#${val}) - (${sound} is not a valid sound number)"
}
else {
logDebug "Playing Custom Chime (#${val})"
}
addPendingSound("status", "custom")
return chimePlayCmds(val)
}
def siren() { return both() }
def strobe() { return both() }
def both() {
logDebug "Playing Siren (#${sirenSoundSetting})"
addPendingSound("alarm", "both")
if (sirenLengthSetting == 0) {
// Siren Length is set to chime.
return chimePlayCmds(sirenSoundSetting)
}
else {
def result = []
result += sirenToggleCmds(0xFF)
return result
}
}
private addPendingSound(name, value) {
state.pendingSound = [name: "$name", value: "$value", time: new Date().time]
}
def refresh() {
logTrace "refresh()"
state.pendingRefresh = true
return configure()
}
def parse(String description) {
def result = []
if (description.startsWith("Err")) {
log.warn "Parse Error: $description"
result << createEvent(descriptionText: "$device.displayName $description", isStateChange: true)
}
else {
def cmd = zwave.parse(description, getCommandClassVersions())
if (cmd) {
result += zwaveEvent(cmd)
}
else {
logDebug "Unable to parse description: $description"
}
}
if (canCheckin()) {
result << createLastCheckinEvent()
}
return result
}
private getCommandClassVersions() {
[
0x59: 1, // AssociationGrpInfo
0x5A: 1, // DeviceResetLocally
0x5E: 2, // ZwaveplusInfo
0x70: 1, // Configuration
0x71: 3, // Notification v4
0x72: 2, // ManufacturerSpecific
0x73: 1, // Powerlevel
0x80: 1, // Battery
0x85: 2, // Association
0x86: 1, // Version (2)
0x87: 1, // Indicator
0x25: 1, // Switch Binary
]
}
private canCheckin() {
def minimumCheckinInterval = ((checkinIntervalSettingMinutes * 60 * 1000) - 5000)
return (!state.lastCheckinTime || ((new Date().time - state.lastCheckinTime) >= minimumCheckinInterval))
}
private createLastCheckinEvent() {
logDebug "Device Checked In"
state.lastCheckinTime = new Date().time
return createEvent(name: "lastCheckin", value: convertToLocalTimeString(new Date()), displayed: false)
}
private convertToLocalTimeString(dt) {
def timeZoneId = location?.timeZone?.ID
if (timeZoneId) {
return dt.format("MM/dd/yyyy hh:mm:ss a", TimeZone.getTimeZone(timeZoneId))
}
else {
return "$dt"
}
}
def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) {
logTrace "BatteryReport: $cmd"
def isNew = (device.currentValue("battery") != cmd.batteryLevel)
def val = (cmd.batteryLevel == 0xFF ? 1 : cmd.batteryLevel)
def result = []
result << createEvent(name: "battery", value: val, unit: "%", displayed: isNew, isStateChange: true)
return result
}
def zwaveEvent(physicalgraph.zwave.commands.configurationv1.ConfigurationReport cmd) {
def name = configData.find { it.paramNum == cmd.parameterNumber }?.name
if (name) {
def val = cmd.configurationValue[0]
logDebug "${name} = ${val}"
state."configVal${cmd.parameterNumber}" = val
}
else {
logDebug "Parameter ${cmd.parameterNumber}: ${cmd.configurationValue}"
}
state.isConfigured = true
state.pendingRefresh = false
return []
}
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) {
def result = []
logTrace "BasicReport: ${cmd}"
if (cmd.value == 0x00) {
result += handleDeviceOff()
}
else {
result += sendResponse(["delay 3000", basicGetCmd()])
}
return result
}
def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) {
def result = []
logTrace "SwitchBinaryReport: ${cmd}"
return result
}
def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) {
def result = []
logTrace "NotificationReport: ${cmd}"
if (cmd.notificationType == 14) {
if (cmd.event == 0) {
result += handleDeviceOff()
}
else if (cmd.event == 1) {
result += handleDeviceOn(state.activeSound, state.pendingSound)
}
}
return result
}
private handleDeviceOn(activeSound, pendingSound) {
def result = []
def activeSoundName = activeSound?.name
state.activeSound = pendingSound
if (pendingSound) {
result << createEvent(getEventMap(pendingSound, true))
def statusVal = ""
if (pendingSound.name == "alarm") {
statusVal = "alarm"
}
else if (pendingSound.name == "switch") {
statusVal = "on"
}
if (statusVal) {
result << createEvent(getStatusEventMap(statusVal))
}
}
else {
logTrace "Unable to create event on because the pending sound has not been set."
}
return result
}
private handleDeviceOff() {
def result = []
["alarm", "switch", "status"].each { n ->
def displayed = false
if ("${n}" == "${state.activeSound?.name}") {
// Only the active event was initially displayed so it's the only off event that gets displayed.
displayed = true
state.activeSound = null
}
result << createEvent(getEventMap([name: "$n", value: "off"], displayed))
}
return result
}
private getStatusEventMap(val) {
return getEventMap([name: "status", value: val], false)
}
private getEventMap(event, displayed=false) {
def isStateChange = (device.currentValue(event.name) != event.value)
def eventMap = [
name: event.name,
value: event.value,
displayed: displayed,
isStateChange: true
]
logTrace "Creating Event: ${eventMap}"
return eventMap
}
def zwaveEvent(physicalgraph.zwave.Command cmd) {
logDebug "Unhandled Command: $cmd"
return []
}
private chimePlayCmds(sound) {
def cmds = []
cmds << indicatorSetCmd(sound)
if ((sound in [1, 9, 10]) && chimeRepeatSetting == 1) {
// Fixes problem where these sounds stop playing before the start events are created causing the off events to never get created.
cmds << basicGetCmd()
cmds << "delay 3000"
}
return cmds
}
private indicatorSetCmd(val) {
return zwave.indicatorV1.indicatorSet(value: val).format()
}
private sirenToggleCmds(val) {
return [
switchBinarySetCmd(val),
switchBinaryGetCmd()
]
}
private switchBinaryGetCmd() {
return zwave.switchBinaryV1.switchBinaryGet().format()
}
private switchBinarySetCmd(val) {
return zwave.switchBinaryV1.switchBinarySet(switchValue: val).format()
}
private basicGetCmd() {
return zwave.basicV1.basicGet().format()
}
private batteryGetCmd() {
return zwave.batteryV1.batteryGet().format()
}
private configGetCmd(paramNum) {
return zwave.configurationV1.configurationGet(parameterNumber: paramNum).format()
}
private configSetCmd(paramNum, val) {
return zwave.configurationV1.configurationSet(parameterNumber: paramNum, size: 1, scaledConfigurationValue: val).format()
}
// Configuration Parameters
private getConfigData() {
// [paramNum: 6, name: "Chime Sound"]
return [
[paramNum: 5, name: "Siren Sound", value: sirenSoundSetting],
[paramNum: 1, name: "Siren Volume", value: sirenVolumeSetting],
[paramNum: 2, name: "Siren Length", value: (sirenLengthSetting == 4 ? 255 : sirenLengthSetting)],
[paramNum: 8, name: "Siren LED", value: sirenLEDSetting],
[paramNum: 4, name: "Chime Volume", value: chimeVolumeSetting],
[paramNum: 3, name: "Chime Repeat", value: chimeRepeatSetting],
[paramNum: 9, name: "Chime LED", value: chimeLEDSetting],
[paramNum: 7, name: "Chime Mode", value: chimeModeSetting]
]
}
// Settings
private getDebugOutputSetting() {
return (settings?.debugOutput != false)
}
private getSirenSoundSetting() {
return validateSound(settings?.sirenSound, 9)
}
private getSirenVolumeSetting() {
return safeToInt(settings?.sirenVolume, 3)
}
private getSirenLengthSetting() {
return safeToInt(settings?.sirenLength, 0)
}
private getSirenLEDSetting() {
return safeToInt(settings?.sirenLED, 1)
}
private getOnChimeSoundSetting() {
return validateSound(settings?.onChimeSound, 1)
}
private getBeepChimeSoundSetting() {
return validateSound(settings?.beepChimeSound, 3)
}
private getChimeVolumeSetting() {
return safeToInt(settings?.chimeVolume, 3)
}
private getChimeRepeatSetting() {
return safeToInt(settings?.chimeRepeat, 1)
}
private getChimeLEDSetting() {
return safeToInt(settings?.chimeLED, 0)
}
private getChimeModeSetting() {
return 1 // Chime Mode should always be disabled.
}
private getCheckinIntervalSettingMinutes() {
return convertOptionSettingToInt(checkinIntervalOptions, checkinIntervalSetting)
}
private getCheckinIntervalSetting() {
return settings?.checkinInterval ?: findDefaultOptionName(checkinIntervalOptions)
}
private validateSound(sound, defaultVal) {
def val = safeToInt(sound, defaultVal)
if (val > 10) {
val = 10
}
else if (val < 1) {
val = 1
}
return val
}
private getVolumeOptions() {
[
[name: "Low", value: 1],
[name: "Medium", value: 2],
[name: "High", value: 3]
]
}
private getLedOptions() {
[
[name: "Off", value: 0],
[name: "On", value: 1]
]
}
private getSirenLengthOptions() {
[
[name: "Chime", value: 0],
[name: "30 Seconds", value: 1],
[name: "1 Minute", value: 2],
[name: "5 Minutes", value: 3],
[name: "${noLengthMsg}", value: 4] // config value is 255
]
}
private getNoLengthMsg() {
return "Play until battery is depleted"
}
private getNameValueSettingDesc(nameValueMap) {
def desc = ""
nameValueMap?.sort { it.value }.each {
desc = "${desc}\n(${it.value} - ${it.name})"
}
return desc
}
private getCheckinIntervalOptions() {
[
[name: "5 Minutes", value: 5],
[name: "10 Minutes", value: 10],
[name: "15 Minutes", value: 15],
[name: "30 Minutes", value: 30],
[name: "1 Hour", value: 60],
[name: "2 Hours", value: 120],
[name: "3 Hours", value: 180],
[name: "6 Hours", value: 360],
[name: "9 Hours", value: 540],
[name: formatDefaultOptionName("12 Hours"), value: 720],
[name: "18 Hours", value: 1080],
[name: "24 Hours", value: 1440]
]
}
private convertOptionSettingToInt(options, settingVal) {
return safeToInt(options?.find { "${settingVal}" == it.name }?.value, 0)
}
private formatDefaultOptionName(val) {
return "${val}${defaultOptionSuffix}"
}
private findDefaultOptionName(options) {
def option = options?.find { it.name?.contains("${defaultOptionSuffix}") }
return option?.name ?: ""
}
private getDefaultOptionSuffix() {
return " (Default)"
}
private safeToInt(val, defaultVal=-1) {
return "${val}"?.isInteger() ? "${val}".toInteger() : defaultVal
}
private isDuplicateCommand(lastExecuted, allowedMil) {
!lastExecuted ? false : (lastExecuted + allowedMil > new Date().time)
}
private logDebug(msg) {
if (debugOutputSetting) {
log.debug "$msg"
}
}
private logTrace(msg) {
// log.trace "$msg"
}

View File

@@ -1,5 +1,5 @@
/**
* Spruce Sensor -updated with SLP model number 5/2017
* Spruce Sensor -Pre-release V2 10/8/2015
*
* Copyright 2014 Plaid Systems
*
@@ -14,33 +14,25 @@
*
-------10/20/2015 Updates--------
-Fix/add battery reporting interval to update
-remove polling and/or refresh
-------5/2017 Updates--------
-Add fingerprints for SLP
-add device health, check every 60mins + 2mins
-remove polling and/or refresh(?)
*/
metadata {
definition (name: "Spruce Sensor", namespace: "plaidsystems", author: "Plaid Systems") {
definition (name: "Spruce Sensor", namespace: "plaidsystems", author: "NCauffman") {
capability "Configuration"
capability "Battery"
capability "Relative Humidity Measurement"
capability "Temperature Measurement"
capability "Sensor"
capability "Health Check"
//capability "Polling"
attribute "maxHum", "string"
attribute "minHum", "string"
command "resetHumidity"
command "refresh"
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0402,0405", outClusters: "0003, 0019", manufacturer: "PLAID SYSTEMS", model: "PS-SPRZMS-01", deviceJoinName: "Spruce Sensor"
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0402,0405", outClusters: "0003, 0019", manufacturer: "PLAID SYSTEMS", model: "PS-SPRZMS-SLP1", deviceJoinName: "Spruce Sensor"
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0402,0405", outClusters: "0003, 0019", manufacturer: "PLAID SYSTEMS", model: "PS-SPRZMS-01"
}
preferences {
@@ -301,11 +293,6 @@ def setConfig(){
sendEvent(name: 'configuration',value: configInterval, descriptionText: "Configuration initialized")
}
def installed(){
//check every 1 hour + 2mins
sendEvent(name: "checkInterval", value: 1 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
}
//when device preferences are changed
def updated(){
log.debug "device updated"
@@ -316,8 +303,6 @@ def updated(){
sendEvent(name: 'configuration',value: 0, descriptionText: "Settings changed and will update at next report. Measure interval set to ${interval} mins")
}
}
//check every 1 hour + 2mins
sendEvent(name: "checkInterval", value: 1 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
}
//poll
@@ -410,4 +395,4 @@ private byte[] reverseArray(byte[] array) {
i++;
}
return array
}
}

View File

@@ -27,9 +27,13 @@ Works with:
## Device Health
Aeon Labs MultiSensor 6 is polled by the hub.
Aeon MultiSensor reports in once every hour.
As of hubCore version 0.14.38 the hub sends up reports every 15 minutes regardless of whether the state changed.
Device-Watch allows 2 check-in misses from device plus some lag time. So Check-in interval = (2*15 + 2)mins = 32 mins.
Not to mention after going OFFLINE when the device is plugged back in, it might take a considerable amount of time for
the device to appear as ONLINE again. This is because if this listening device does not respond to two poll requests in a row,
it is not polled for 5 minutes by the hub. This can delay up the process of being marked ONLINE by quite some time.
* __122min__ checkInterval
* __32min__ checkInterval
## Troubleshooting

View File

@@ -130,13 +130,13 @@ metadata {
}
def installed(){
// Device-Watch simply pings if no device events received for 122min(checkInterval)
sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID])
// Device-Watch simply pings if no device events received for 32min(checkInterval)
sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID])
}
def updated() {
// Device-Watch simply pings if no device events received for 122min(checkInterval)
sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID])
// Device-Watch simply pings if no device events received for 32min(checkInterval)
sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID])
log.debug "Updated with settings: ${settings}"
log.debug "${device.displayName} is now ${device.latestValue("powerSupply")}"

View File

@@ -70,7 +70,7 @@ metadata {
state "heat", action:"switchMode", nextState: "updating", icon: "st.thermostat.heat"
state "cool", action:"switchMode", nextState: "updating", icon: "st.thermostat.cool"
state "auto", action:"switchMode", nextState: "updating", icon: "st.thermostat.auto"
state "auxheatonly", action:"switchMode", icon: "st.thermostat.emergency-heat"
state "emergency heat", action:"switchMode", icon: "st.thermostat.emergency-heat" // emergency heat = auxHeatOnly
state "updating", label:"Working", icon: "st.secondary.secondary"
}
standardTile("fanMode", "device.thermostatFanMode", inactiveLabel: false, decoration: "flat") {
@@ -156,49 +156,47 @@ void poll() {
def generateEvent(Map results) {
log.debug "parsing data $results"
if(results) {
results.each { name, value ->
def linkText = getLinkText(device)
def supportedThermostatModes = ["off"]
def thermostatMode = null
results.each { name, value ->
def isChange = false
def isDisplayed = true
def event = [name: name, linkText: linkText, descriptionText: getThermostatDescriptionText(name, value, linkText),
handlerName: name]
if (name=="temperature" || name=="heatingSetpoint" || name=="coolingSetpoint" ) {
def sendValue = location.temperatureScale == "C"? roundC(convertFtoC(value.toDouble())) : value.toInteger()
event << [value: sendValue, unit: temperatureScale]
isChange = isTemperatureStateChange(device, name, value.toString())
isDisplayed = isChange
event << [value: sendValue, unit: temperatureScale, isStateChange: isChange, displayed: isDisplayed]
} else if (name=="maxCoolingSetpoint" || name=="minCoolingSetpoint" || name=="maxHeatingSetpoint" || name=="minHeatingSetpoint") {
def sendValue = location.temperatureScale == "C"? roundC(convertFtoC(value.toDouble())) : value.toInteger()
event << [value: sendValue, unit: temperatureScale, displayed: false]
} else if (name=="heatMode" || name=="coolMode" || name=="autoMode" || name=="auxHeatMode"){
if (value == true) {
supportedThermostatModes << ((name == "auxHeatMode") ? "auxheatonly" : name - "Mode")
}
return // as we don't want to send this event here, proceed to next name/value pair
isChange = isStateChange(device, name, value.toString())
event << [value: value.toString(), isStateChange: isChange, displayed: false]
} else if (name=="thermostatFanMode"){
sendEvent(name: "supportedThermostatFanModes", value: fanModes(), displayed: false)
event << [value: value.toString(), data:[supportedThermostatFanModes: fanModes()]]
isChange = isStateChange(device, name, value.toString())
event << [value: value.toString(), isStateChange: isChange, displayed: false]
} else if (name=="humidity") {
event << [value: value.toString(), displayed: false, unit: "%"]
isChange = isStateChange(device, name, value.toString())
event << [value: value.toString(), isStateChange: isChange, displayed: false, unit: "%"]
} else if (name == "deviceAlive") {
isChange = isStateChange(device, name, value.toString())
event['isStateChange'] = isChange
event['displayed'] = false
} else if (name == "thermostatMode") {
thermostatMode = value.toLowerCase()
return // as we don't want to send this event here, proceed to next name/value pair
def mode = value.toString()
mode = (mode == "auxHeatOnly") ? "emergency heat" : mode
isChange = isStateChange(device, name, mode)
event << [value: mode, isStateChange: isChange, displayed: isDisplayed]
} else {
event << [value: value.toString()]
isChange = isStateChange(device, name, value.toString())
isDisplayed = isChange
event << [value: value.toString(), isStateChange: isChange, displayed: isDisplayed]
}
sendEvent(event)
}
if (state.supportedThermostatModes != supportedThermostatModes) {
state.supportedThermostatModes = supportedThermostatModes
sendEvent(name: "supportedThermostatModes", value: supportedThermostatModes, displayed: false)
}
if (thermostatMode) {
sendEvent(name: "thermostatMode", value: thermostatMode, data:[supportedThermostatModes:state.supportedThermostatModes], linkText: linkText,
descriptionText: getThermostatDescriptionText("thermostatMode", thermostatMode, linkText), handlerName: "thermostatMode")
}
generateSetpointEvent ()
generateStatusEvent ()
}
@@ -324,7 +322,15 @@ void resumeProgram() {
}
def modes() {
return state.supportedThermostatModes
if (state.modes) {
log.debug "Modes = ${state.modes}"
return state.modes
}
else {
state.modes = parent.availableModes(this)
log.debug "Modes = ${state.modes}"
return state.modes
}
}
def fanModes() {
@@ -407,13 +413,11 @@ def setThermostatFanMode(String mode) {
}
def generateModeEvent(mode) {
sendEvent(name: "thermostatMode", value: mode, data:[supportedThermostatModes: state.supportedThermostatModes],
descriptionText: "$device.displayName is in ${mode} mode")
sendEvent(name: "thermostatMode", value: mode, descriptionText: "$device.displayName is in ${mode} mode", displayed: true)
}
def generateFanModeEvent(fanMode) {
sendEvent(name: "thermostatFanMode", value: fanMode, data:[supportedThermostatFanModes: fanModes()],
descriptionText: "$device.displayName fan is in ${fanMode} mode")
sendEvent(name: "thermostatFanMode", value: fanMode, descriptionText: "$device.displayName fan is in ${fanMode} mode", displayed: true)
}
def generateOperatingStateEvent(operatingState) {
@@ -449,14 +453,14 @@ def heat() {
}
def emergencyHeat() {
auxheatonly()
auxHeatOnly()
}
def auxheatonly() {
log.debug "auxheatonly()"
def auxHeatOnly() {
log.debug "auxHeatOnly = emergency heat"
def deviceId = device.deviceNetworkId.split(/\./).last()
if (parent.setMode ("auxHeatOnly", deviceId))
generateModeEvent("auxheatonly")
generateModeEvent("emergency heat") // emergency heat = auxHeatOnly
else {
log.debug "Error setting new mode."
def currentMode = device.currentState("thermostatMode")?.value
@@ -589,7 +593,7 @@ def generateSetpointEvent() {
} else if (mode == "off") {
sendEvent("name":"thermostatSetpoint", "value":averageSetpoint, "unit":location.temperatureScale)
sendEvent("name":"displayThermostatSetpoint", "value":"Off", displayed: false)
} else if (mode == "auxheatonly") {
} else if (mode == "emergency heat") { // emergency heat = auxHeatOnly
sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint, "unit":location.temperatureScale)
sendEvent("name":"displayThermostatSetpoint", "value":heatingSetpoint, "unit":location.temperatureScale, displayed: false)
}
@@ -628,7 +632,7 @@ void raiseSetpoint() {
targetvalue = thermostatSetpoint ? thermostatSetpoint : 0
targetvalue = location.temperatureScale == "F"? targetvalue + 1 : targetvalue + 0.5
if ((mode == "heat" || mode == "auxheatonly") && targetvalue > maxHeatingSetpoint) {
if ((mode == "heat" || mode == "emergency heat") && targetvalue > maxHeatingSetpoint) { // emergency heat = auxHeatOnly
targetvalue = maxHeatingSetpoint
} else if (mode == "cool" && targetvalue > maxCoolingSetpoint) {
targetvalue = maxCoolingSetpoint
@@ -674,7 +678,7 @@ void lowerSetpoint() {
targetvalue = thermostatSetpoint ? thermostatSetpoint : 0
targetvalue = location.temperatureScale == "F"? targetvalue - 1 : targetvalue - 0.5
if ((mode == "heat" || mode == "auxheatonly") && targetvalue < minHeatingSetpoint) {
if ((mode == "heat" || mode == "emergency heat") && targetvalue < minHeatingSetpoint) { // emergency heat = auxHeatOnly
targetvalue = minHeatingSetpoint
} else if (mode == "cool" && targetvalue < minCoolingSetpoint) {
targetvalue = minCoolingSetpoint
@@ -715,7 +719,7 @@ void alterSetpoint(temp) {
}
//step1: check thermostatMode, enforce limits before sending request to cloud
if (mode == "heat" || mode == "auxheatonly"){
if (mode == "heat" || mode == "emergency heat"){ // emergency heat = auxHeatOnly
if (temp.value > coolingSetpoint){
targetHeatingSetpoint = temp.value
targetCoolingSetpoint = temp.value
@@ -750,7 +754,7 @@ void alterSetpoint(temp) {
log.debug "alterSetpoint in mode $mode succeed change setpoint to= ${temp.value}"
} else {
log.error "Error alterSetpoint()"
if (mode == "heat" || mode == "auxheatonly"){
if (mode == "heat" || mode == "emergency heat"){ // emergency heat = auxHeatOnly
sendEvent("name": "thermostatSetpoint", "value": heatingSetpoint.toString(), displayed: false)
sendEvent("name": "displayThermostatSetpoint", "value": heatingSetpoint.toString(), displayed: false)
} else if (mode == "cool") {
@@ -779,7 +783,7 @@ def generateStatusEvent() {
log.debug "Cooling set point = ${coolingSetpoint}"
log.debug "HVAC Mode = ${mode}"
if (mode == "heat" || mode == "auxheatonly") {
if (mode == "heat") {
if (temperature >= heatingSetpoint) {
statusText = "Right Now: Idle"
} else {
@@ -802,6 +806,8 @@ def generateStatusEvent() {
}
} else if (mode == "off") {
statusText = "Right Now: Off"
} else if (mode == "emergency heat") { // emergency heat = auxHeatOnly
statusText = "Emergency Heat"
} else {
statusText = "?"
}

View File

@@ -23,6 +23,7 @@ metadata {
capability "Refresh"
capability "Sensor"
capability "Health Check"
capability "Light"
capability "Outlet"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B04,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3200", deviceJoinName: "Outlet"

View File

@@ -105,8 +105,6 @@ def parse(String description) {
} else {
log.warn "TEMP REPORTING CONFIG FAILED- error code: ${descMap.data[0]}"
}
} else if (descMap?.clusterInt == zigbee.IAS_ZONE_CLUSTER && descMap.attrInt == zigbee.ATTRIBUTE_IAS_ZONE_STATUS && descMap?.value) {
map = translateZoneStatus(new ZoneStatus(zigbee.convertToInt(descMap?.value)))
}
}
} else if (map.name == "temperature") {
@@ -131,10 +129,6 @@ def parse(String description) {
private Map parseIasMessage(String description) {
ZoneStatus zs = zigbee.parseZoneStatus(description)
translateZoneStatus(zs)
}
private Map translateZoneStatus(ZoneStatus zs) {
return zs.isAlarm1Set() ? getMoistureResult('wet') : getMoistureResult('dry')
}
@@ -203,8 +197,7 @@ def ping() {
def refresh() {
log.debug "Refreshing Temperature and Battery"
def refreshCmds = zigbee.readAttribute(zigbee.TEMPERATURE_MEASUREMENT_CLUSTER, 0x0000) +
zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0020) +
zigbee.readAttribute(zigbee.IAS_ZONE_CLUSTER, zigbee.ATTRIBUTE_IAS_ZONE_STATUS)
zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0020)
return refreshCmds + zigbee.enrollResponse()
}

View File

@@ -111,8 +111,6 @@ def parse(String description) {
def value = descMap.value.endsWith("01") ? "active" : "inactive"
log.debug "Doing a read attr motion event"
map = getMotionResult(value)
} else if (descMap?.clusterInt == zigbee.IAS_ZONE_CLUSTER && descMap.attrInt == zigbee.ATTRIBUTE_IAS_ZONE_STATUS && descMap?.value) {
map = translateZoneStatus(new ZoneStatus(zigbee.convertToInt(descMap?.value)))
}
}
} else if (map.name == "temperature") {
@@ -137,10 +135,6 @@ def parse(String description) {
private Map parseIasMessage(String description) {
ZoneStatus zs = zigbee.parseZoneStatus(description)
translateZoneStatus(zs)
}
private Map translateZoneStatus(ZoneStatus zs) {
// Some sensor models that use this DTH use alarm1 and some use alarm2 to signify motion
return (zs.isAlarm1Set() || zs.isAlarm2Set()) ? getMotionResult('active') : getMotionResult('inactive')
}
@@ -171,8 +165,8 @@ private Map getBatteryResult(rawValue) {
def pct = batteryMap[volts]
result.value = pct
} else {
def minVolts = 2.4
def maxVolts = 2.7
def minVolts = 2.1
def maxVolts = 3.0
def pct = (volts - minVolts) / (maxVolts - minVolts)
def roundedPct = Math.round(pct * 100)
if (roundedPct <= 0)
@@ -206,8 +200,7 @@ def refresh() {
log.debug "refresh called"
def refreshCmds = zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0020) +
zigbee.readAttribute(zigbee.TEMPERATURE_MEASUREMENT_CLUSTER, 0x0000) +
zigbee.readAttribute(zigbee.IAS_ZONE_CLUSTER, zigbee.ATTRIBUTE_IAS_ZONE_STATUS)
zigbee.readAttribute(zigbee.TEMPERATURE_MEASUREMENT_CLUSTER, 0x0000)
return refreshCmds + zigbee.enrollResponse()
}

View File

@@ -134,9 +134,8 @@ def parse(String description) {
} else {
log.warn "TEMP REPORTING CONFIG FAILED- error code: ${descMap.data[0]}"
}
} else if (descMap?.clusterInt == zigbee.IAS_ZONE_CLUSTER && descMap.attrInt == zigbee.ATTRIBUTE_IAS_ZONE_STATUS && descMap?.value) {
maps += translateZoneStatus(new ZoneStatus(zigbee.convertToInt(descMap?.value)))
} else {
maps += handleAcceleration(descMap)
}
}
@@ -230,11 +229,6 @@ private List<Map> parseAxis(List<Map> attrData) {
private List<Map> parseIasMessage(String description) {
ZoneStatus zs = zigbee.parseZoneStatus(description)
translateZoneStatus(zs)
}
private List<Map> translateZoneStatus(ZoneStatus zs) {
List<Map> results = []
if (garageSensor != "Yes") {
@@ -274,7 +268,7 @@ private Map getBatteryResult(rawValue) {
result.value = pct
} else {
def minVolts = 2.1
def maxVolts = 2.7
def maxVolts = 3.0
def pct = (volts - minVolts) / (maxVolts - minVolts)
def roundedPct = Math.round(pct * 100)
if (roundedPct <= 0)
@@ -319,7 +313,7 @@ def refresh() {
def refreshCmds = zigbee.readAttribute(zigbee.TEMPERATURE_MEASUREMENT_CLUSTER, 0x0000) +
zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0020) +
zigbee.readAttribute(0xFC02, 0x0010, [mfgCode: manufacturerCode]) +
zigbee.readAttribute(zigbee.IAS_ZONE_CLUSTER, zigbee.ATTRIBUTE_IAS_ZONE_STATUS) + zigbee.enrollResponse()
zigbee.enrollResponse()
return refreshCmds
}

View File

@@ -1,2 +0,0 @@
.st-ignore
README.md

View File

@@ -1,42 +0,0 @@
# ZigBee Button
Cloud Execution
Works with:
* [OSRAM LIGHTIFY Dimming Switch](https://support.smartthings.com/hc/en-us/articles/115000236823-SYLVANIA-Dimming-Switch)
* [Iris Smart Button](https://support.smartthings.com/hc/en-us/articles/115000190186-Iris-Smart-Button)
* [Iris KeyFob](https://support.smartthings.com/hc/en-us/articles/217409686-Iris-Smart-Fob)
## Table of contents
* [Capabilities](#capabilities)
* [Health](#device-health)
* [Troubleshooting](#troubleshooting)
## Capabilities
* **Actuator** - It represents that a device has commands.
* **Battery** - It defines that the device has a battery
* **Button** - It defines that a device has one or more buttons
* **Holdable Button** - It defines that a device has one or more holdable buttons
* **Configuration** - _configure()_ command called when device is installed or device preferences updated
* **Refresh** - _refresh()_ command for status updates
* **Sensor** - it represents that a Device has attributes.
* **Health Check** - indicates ability to get device health notifications
## Device Health
ZigBee Button is marked offline only in the case when Hub is offline.
## Troubleshooting
If the device doesn't pair when trying from the SmartThings mobile app, it is possible that the device is out of range.
Pairing needs to be tried again by placing the device closer to the hub.
It may also happen that you need to reset the device.
Instructions related to pairing, resetting and removing the device from SmartThings can be found in the following link:
* [OSRAM LIGHTIFY Dimming Switch Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/115000236823-SYLVANIA-Dimming-Switch)
* [Iris Smart Button Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/115000190186-Iris-Smart-Button)
* [Iris KeyFob Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/217409686-Iris-Smart-Fob)

View File

@@ -13,8 +13,6 @@
* for the specific language governing permissions and limitations under the License.
*
*/
import groovy.json.JsonOutput
import physicalgraph.zigbee.zcl.DataType
metadata {
@@ -26,7 +24,6 @@ metadata {
capability "Configuration"
capability "Refresh"
capability "Sensor"
capability "Health Check"
command "enrollResponse"
@@ -252,8 +249,6 @@ def updated() {
}
def initialize() {
// Arrival sensors only goes OFFLINE when Hub is off
sendEvent(name: "DeviceWatch-Enroll", value: JsonOutput.toJson([protocol: "zigbee", scheme:"untracked"]), displayed: false)
if ((device.getDataValue("manufacturer") == "OSRAM") && (device.getDataValue("model") == "LIGHTIFY Dimming Switch")) {
sendEvent(name: "numberOfButtons", value: 2)
}

View File

@@ -15,9 +15,10 @@ metadata {
definition (name: "Z-Wave Thermostat", namespace: "smartthings", author: "SmartThings") {
capability "Actuator"
capability "Temperature Measurement"
capability "Relative Humidity Measurement"
capability "Thermostat"
capability "Configuration"
capability "Refresh"
capability "Polling"
capability "Sensor"
capability "Health Check"
@@ -116,7 +117,7 @@ metadata {
state "cool", label:'${currentValue}° cool', backgroundColor:"#ffffff"
}
standardTile("refresh", "device.thermostatMode", inactiveLabel: false, decoration: "flat") {
state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
state "default", action:"polling.poll", icon:"st.secondary.refresh"
}
main "temperature"
details(["temperature", "mode", "fanMode", "heatSliderControl", "heatingSetpoint", "coolSliderControl", "coolingSetpoint", "refresh"])
@@ -124,20 +125,13 @@ metadata {
}
def installed(){
sendHubCommand(new physicalgraph.device.HubAction(zwave.thermostatModeV2.thermostatModeSupportedGet().format()))
initialize()
// Device-Watch simply pings if no device events received for 32min(checkInterval)
sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID])
}
def updated(){
initialize()
}
def initialize() {
// Device-Watch simply pings if no device events received for 32min(checkInterval)
sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID])
unschedule()
runEvery5Minutes("refresh")
refresh()
}
def parse(String description)
@@ -155,7 +149,6 @@ def parse(String description)
]
if (map.name == "thermostatMode") {
state.lastTriedMode = map.value
map.data = [supportedThermostatModes:state.supportedThermostatModes]
if (map.value == "cool") {
map2.value = device.latestValue("coolingSetpoint")
log.info "THERMOSTAT, latest cooling setpoint = ${map2.value}"
@@ -179,7 +172,6 @@ def parse(String description)
}
} else if (map.name == "thermostatFanMode" && map.isStateChange) {
state.lastTriedFanMode = map.value
map.data = [supportedThermostatFanModes: state.supportedThermostatFanModes]
}
log.debug "Parse returned $result"
result
@@ -313,26 +305,26 @@ def zwaveEvent(physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanMod
}
def zwaveEvent(physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeSupportedReport cmd) {
def supportedModes = []
if(cmd.off) { supportedModes << "off" }
if(cmd.heat) { supportedModes << "heat" }
if(cmd.cool) { supportedModes << "cool" }
if(cmd.auto) { supportedModes << "auto" }
if(cmd.auxiliaryemergencyHeat) { supportedModes << "emergency heat" }
def supportedModes = ""
if(cmd.off) { supportedModes += "off " }
if(cmd.heat) { supportedModes += "heat " }
if(cmd.auxiliaryemergencyHeat) { supportedModes += "emergency heat " }
if(cmd.cool) { supportedModes += "cool " }
if(cmd.auto) { supportedModes += "auto " }
state.supportedThermostatModes = supportedModes
sendEvent(name: "supportedThermostatModes", value: supportedModes, displayed: false)
state.supportedModes = supportedModes
// No events to be generated, return empty map
return [:]
}
def zwaveEvent(physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeSupportedReport cmd) {
def supportedFanModes = []
if(cmd.auto) { supportedFanModes << "auto" } // "fanAuto "
if(cmd.circulation) { supportedFanModes << "circulate" } // "fanCirculate"
if(cmd.low) { supportedFanModes << "on" } // "fanOn"
def supportedFanModes = ""
if(cmd.auto) { supportedFanModes += "auto " } // "fanAuto "
if(cmd.low) { supportedFanModes += "on " } // "fanOn"
if(cmd.circulation) { supportedFanModes += "circulate " } // "fanCirculate"
state.supportedThermostatFanModes = supportedFanModes
sendEvent(name: "supportedThermostatFanModes", value: supportedFanModes, displayed: false)
state.supportedFanModes = supportedFanModes
// No events to be generated, return empty map
return [:]
}
@@ -345,17 +337,15 @@ def zwaveEvent(physicalgraph.zwave.Command cmd) {
}
// Command Implementations
def refresh() {
def cmds = []
cmds << new physicalgraph.device.HubAction(zwave.thermostatModeV2.thermostatModeSupportedGet().format())
cmds << new physicalgraph.device.HubAction(zwave.thermostatFanModeV3.thermostatFanModeSupportedGet().format())
cmds << new physicalgraph.device.HubAction(zwave.thermostatModeV2.thermostatModeGet().format())
cmds << new physicalgraph.device.HubAction(zwave.thermostatFanModeV3.thermostatFanModeGet().format())
cmds << new physicalgraph.device.HubAction(zwave.sensorMultilevelV2.sensorMultilevelGet().format()) // current temperature
cmds << new physicalgraph.device.HubAction(zwave.thermostatOperatingStateV1.thermostatOperatingStateGet().format())
cmds << new physicalgraph.device.HubAction(zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 1).format())
cmds << new physicalgraph.device.HubAction(zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 2).format())
sendHubCommand(cmds)
def poll() {
delayBetween([
zwave.sensorMultilevelV3.sensorMultilevelGet().format(), // current temperature
zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 1).format(),
zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 2).format(),
zwave.thermostatModeV2.thermostatModeGet().format(),
zwave.thermostatFanModeV3.thermostatFanModeGet().format(),
zwave.thermostatOperatingStateV1.thermostatOperatingStateGet().format()
], 2300)
}
def quickSetHeat(degrees) {
@@ -426,14 +416,28 @@ def ping() {
poll()
}
def configure() {
delayBetween([
zwave.thermostatModeV2.thermostatModeSupportedGet().format(),
zwave.thermostatFanModeV3.thermostatFanModeSupportedGet().format(),
zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:[zwaveHubNodeId]).format(),
zwave.sensorMultilevelV3.sensorMultilevelGet().format(), // current temperature
zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 1).format(),
zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 2).format(),
zwave.thermostatModeV2.thermostatModeGet().format(),
zwave.thermostatFanModeV3.thermostatFanModeGet().format(),
zwave.thermostatOperatingStateV1.thermostatOperatingStateGet().format()
], 2300)
}
def modes() {
return state.supportedThermostatModes
["off", "heat", "cool", "auto", "emergency heat"]
}
def switchMode() {
def currentMode = device.currentState("thermostatMode")?.value
def lastTriedMode = state.lastTriedMode ?: currentMode ?: ["off"]
def supportedModes = getDataByName("supportedThermostatModes")
def lastTriedMode = state.lastTriedMode ?: currentMode ?: "off"
def supportedModes = getDataByName("supportedModes")
def modeOrder = modes()
def next = { modeOrder[modeOrder.indexOf(it) + 1] ?: modeOrder[0] }
def nextMode = next(lastTriedMode)
@@ -450,7 +454,7 @@ def switchMode() {
}
def switchToMode(nextMode) {
def supportedModes = getDataByName("supportedThermostatModes")
def supportedModes = getDataByName("supportedModes")
if(supportedModes && !supportedModes.contains(nextMode)) log.warn "thermostat mode '$nextMode' is not supported"
if (nextMode in modes()) {
state.lastTriedMode = nextMode
@@ -462,9 +466,9 @@ def switchToMode(nextMode) {
def switchFanMode() {
def currentMode = device.currentState("thermostatFanMode")?.value
def lastTriedMode = state.lastTriedFanMode ?: currentMode ?: ["off"]
def supportedModes = getDataByName("supportedThermostatFanModes") ?: ["auto", "on"]
def modeOrder = state.supportedThermostatFanModes
def lastTriedMode = state.lastTriedFanMode ?: currentMode ?: "off"
def supportedModes = getDataByName("supportedFanModes") ?: "auto on" // "fanAuto fanOn"
def modeOrder = ["auto", "circulate", "on"] // "fanAuto", "fanCirculate", "fanOn"
def next = { modeOrder[modeOrder.indexOf(it) + 1] ?: modeOrder[0] }
def nextMode = next(lastTriedMode)
while (!supportedModes?.contains(nextMode) && nextMode != "auto") { // "fanAuto"
@@ -474,7 +478,7 @@ def switchFanMode() {
}
def switchToFanMode(nextMode) {
def supportedFanModes = getDataByName("supportedThermostatFanModes")
def supportedFanModes = getDataByName("supportedFanModes")
if(supportedFanModes && !supportedFanModes.contains(nextMode)) log.warn "thermostat mode '$nextMode' is not supported"
def returnCommand

View File

@@ -58,7 +58,6 @@ metadata {
def installed() {
// Device-Watch simply pings if no device events received for 32min(checkInterval)
sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID])
response(refresh())
}
def updated() {

View File

@@ -27,9 +27,10 @@ definition(
preferences {
page(name: "selectButton")
for (def i=1; i<=8; i++) {
page(name: "configureButton$i")
}
page(name: "configureButton1")
page(name: "configureButton2")
page(name: "configureButton3")
page(name: "configureButton4")
page(name: "timeIntervalInput", title: "Only during a certain time") {
section {
@@ -59,45 +60,22 @@ def selectButton() {
}
}
def createPage(pageNum) {
if ((state.numButton == pageNum) || (pageNum == 8))
state.installCondition = true
dynamicPage(name: "configureButton$pageNum", title: "Set up button $pageNum here",
nextPage: "configureButton${pageNum+1}", install: state.installCondition, uninstall: configured(), getButtonSections(pageNum))
}
def configureButton1() {
state.numButton = buttonDevice.currentState("numberOfButtons")?.longValue ?: 4
log.debug "state variable numButton: ${state.numButton}"
state.installCondition = false
createPage(1)
dynamicPage(name: "configureButton1", title: "Now let's decide how to use the first button",
nextPage: "configureButton2", uninstall: configured(), getButtonSections(1))
}
def configureButton2() {
createPage(2)
dynamicPage(name: "configureButton2", title: "If you have a second button, set it up here",
nextPage: "configureButton3", uninstall: configured(), getButtonSections(2))
}
def configureButton3() {
createPage(3)
dynamicPage(name: "configureButton3", title: "If you have a third button, you can do even more here",
nextPage: "configureButton4", uninstall: configured(), getButtonSections(3))
}
def configureButton4() {
createPage(4)
}
def configureButton5() {
createPage(5)
}
def configureButton6() {
createPage(6)
}
def configureButton7() {
createPage(7)
}
def configureButton8() {
createPage(8)
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) {

View File

@@ -202,8 +202,7 @@ def inputSelectionPage() {
section("options variations") {
paragraph "tap these elements and look at the differences when selecting an option"
input(type: "enum", name: "selectionSimple", title: "Simple options", description: "no separators in the selectable options", options: ["Thing 1", "Thing 2", "(Complicated) Thing 3"])
input(type: "enum", name: "selectionSimpleGrouped", title: "Simple (Grouped) options", description: "no separators in the selectable options", groupedOptions: addGroup(englishOptions + spanishOptions))
input(type: "enum", name: "selectionSimple", title: "Simple options", description: "no separators in the selectable options", groupedOptions: addGroup(englishOptions + spanishOptions))
input(type: "enum", name: "selectionGrouped", title: "Grouped options", description: "separate groups of options with headers", groupedOptions: groupedOptions)
}
@@ -215,15 +214,15 @@ def inputSelectionPage() {
section("segmented") {
paragraph "segmented should only work if there are either 2 or 3 options to choose from"
input(type: "enum", name: "selectionSegmented1", style: "segmented", title: "1 option", options: ["One"])
input(type: "enum", name: "selectionSegmented4", style: "segmented", title: "4 options", options: ["One", "Two", "Three", "Four"])
input(type: "enum", name: "selectionSegmented1", style: "segmented", title: "1 option", groupedOptions: addGroup(["One"]))
input(type: "enum", name: "selectionSegmented4", style: "segmented", title: "4 options", groupedOptions: addGroup(["One", "Two", "Three", "Four"]))
paragraph "multiple and required will have no effect on segmented selection elements. There will always be exactly 1 option selected"
input(type: "enum", name: "selectionSegmented2", style: "segmented", title: "2 options", options: ["One", "Two"])
input(type: "enum", name: "selectionSegmented3", style: "segmented", title: "3 options", options: ["One", "Two", "Three"])
paragraph "specifying defaultValue still works with segmented selection elements"
input(type: "enum", name: "selectionSegmentedWithDefault", style: "segmented", title: "defaulted to 'two'", options: ["One", "Two", "Three"], defaultValue: "Two")
input(type: "enum", name: "selectionSegmentedWithDefault", title: "defaulted to 'two'", groupedOptions: addGroup(["One", "Two", "Three"]), defaultValue: "Two")
}
section("required: true") {
@@ -232,8 +231,6 @@ def inputSelectionPage() {
section("multiple: true") {
input(type: "enum", name: "selectionMultiple", title: "This allows multiple selections", description: "It should look different when nothing is selected", groupedOptions: addGroup(["an option", "another option", "no way, one more?"]), multiple: true)
input(type: "enum", name: "selectionMultipleDefault1", title: "This allows multiple selections with a single default", description: "It should look different when nothing is selected", groupedOptions: addGroup(["an option", "another option", "no way, one more?"]), multiple: true, defaultValue: "an option")
input(type: "enum", name: "selectionMultipleDefault2", title: "This allows multiple selections with multiple defaults", description: "It should look different when nothing is selected", groupedOptions: addGroup(["an option", "another option", "no way, one more?"]), multiple: true, defaultValue: ["an option", "another option"])
}
section("with image") {

View File

@@ -72,7 +72,7 @@ def authPage() {
log.debug "have LIFX access token"
def options = locationOptions() ?: []
def count = options.size().toString()
def count = options.size()
return dynamicPage(name:"Credentials", title:"", nextPage:"", install:true, uninstall: true) {
section("Select your location") {