Compare commits

..

1 Commits

30 changed files with 825 additions and 893 deletions

View File

@@ -52,9 +52,6 @@ hipchatShareFile {
hipchatSendNotification { hipchatSendNotification {
String branch = project.hasProperty('branch') ? project.property('branch') : 'unknown' String branch = project.hasProperty('branch') ? project.property('branch') : 'unknown'
message = "Began executable deploy of SmartThingsPublic(${branch})." message = "Began executable deploy of SmartThingsPublic(${branch})."
if (branch == 'master') {
message += ' (dev shards)'
}
color = branch == 'master' ? 'yellow' : 'red' color = branch == 'master' ? 'yellow' : 'red'
notify = true notify = true
} }

View File

@@ -379,16 +379,12 @@ def getDataByName(String name) {
state[name] ?: device.getDataValue(name) state[name] ?: device.getDataValue(name)
} }
def setThermostatMode(String mode) { def setThermostatMode(String value) {
log.debug "setThermostatMode($mode)" log.debug "setThermostatMode({$value})"
mode = mode.toLowerCase()
switchToMode(mode)
} }
def setThermostatFanMode(String mode) { def setThermostatFanMode(String value) {
log.debug "setThermostatFanMode($mode)" log.debug "setThermostatFanMode({$value})"
mode = mode.toLowerCase()
switchToFanMode(mode)
} }
def generateModeEvent(mode) { def generateModeEvent(mode) {

View File

@@ -17,13 +17,16 @@ metadata {
tiles(scale: 2) { tiles(scale: 2) {
multiAttributeTile(name:"rich-control"){ multiAttributeTile(name:"rich-control"){
tileAttribute ("", key: "PRIMARY_CONTROL") { tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
attributeState "default", label: "Hue Bridge", action: "", icon: "st.Lighting.light99-hue", backgroundColor: "#F3C200" attributeState "default", label: "Hue Bridge", action: "", icon: "st.Lighting.light99-hue", backgroundColor: "#F3C200"
} }
tileAttribute ("serialNumber", key: "SECONDARY_CONTROL") { tileAttribute ("serialNumber", key: "SECONDARY_CONTROL") {
attributeState "default", label:'SN: ${currentValue}' attributeState "default", label:'SN: ${currentValue}'
} }
} }
standardTile("icon", "icon", width: 1, height: 1, canChangeIcon: false, inactiveLabel: true, canChangeBackground: false) {
state "default", label: "Hue Bridge", action: "", icon: "st.Lighting.light99-hue", backgroundColor: "#FFFFFF"
}
valueTile("serialNumber", "device.serialNumber", decoration: "flat", height: 1, width: 2, inactiveLabel: false) { valueTile("serialNumber", "device.serialNumber", decoration: "flat", height: 1, width: 2, inactiveLabel: false) {
state "default", label:'SN: ${currentValue}' state "default", label:'SN: ${currentValue}'
} }
@@ -31,7 +34,7 @@ metadata {
state "default", label:'${currentValue}', height: 1, width: 2, inactiveLabel: false state "default", label:'${currentValue}', height: 1, width: 2, inactiveLabel: false
} }
main (["rich-control"]) main (["icon"])
details(["rich-control", "networkAddress"]) details(["rich-control", "networkAddress"])
} }
} }
@@ -72,7 +75,6 @@ def parse(description) {
} }
else if (contentType?.contains("xml")) { else if (contentType?.contains("xml")) {
log.debug "HUE BRIDGE ALREADY PRESENT" log.debug "HUE BRIDGE ALREADY PRESENT"
parent.hubVerification(device.hub.id, msg.body)
} }
} }
} }

View File

@@ -3,7 +3,6 @@
* *
* Author: SmartThings * Author: SmartThings
*/ */
// for the UI // for the UI
metadata { metadata {
// Automatically generated. Make future change here. // Automatically generated. Make future change here.
@@ -28,10 +27,10 @@ metadata {
tiles (scale: 2){ tiles (scale: 2){
multiAttributeTile(name:"rich-control", type: "lighting", width: 6, height: 4, canChangeIcon: true){ multiAttributeTile(name:"rich-control", type: "lighting", width: 6, height: 4, canChangeIcon: true){
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
attributeState "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#00A0DC", nextState:"turningOff" attributeState "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
attributeState "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#C6C7CC", nextState:"turningOn" attributeState "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#00A0DC", nextState:"turningOff" attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#C6C7CC", nextState:"turningOn" attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
} }
tileAttribute ("device.level", key: "SLIDER_CONTROL") { tileAttribute ("device.level", key: "SLIDER_CONTROL") {
attributeState "level", action:"switch level.setLevel", range:"(0..100)" attributeState "level", action:"switch level.setLevel", range:"(0..100)"
@@ -44,10 +43,16 @@ metadata {
} }
} }
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
state "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
state "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
state "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
state "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
}
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", width: 4, height: 2, inactiveLabel: false, range:"(2000..6500)") { controlTile("colorTempSliderControl", "device.colorTemperature", "slider", width: 4, height: 2, inactiveLabel: false, range:"(2000..6500)") {
state "colorTemperature", action:"color temperature.setColorTemperature" state "colorTemperature", action:"color temperature.setColorTemperature"
} }
valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "colorTemperature", label: '${currentValue} K' state "colorTemperature", label: '${currentValue} K'
} }
@@ -55,12 +60,29 @@ metadata {
standardTile("reset", "device.reset", height: 2, width: 2, inactiveLabel: false, decoration: "flat") { standardTile("reset", "device.reset", height: 2, width: 2, inactiveLabel: false, decoration: "flat") {
state "default", label:"Reset Color", action:"reset", icon:"st.lights.philips.hue-single" state "default", label:"Reset Color", action:"reset", icon:"st.lights.philips.hue-single"
} }
standardTile("refresh", "device.switch", height: 2, width: 2, inactiveLabel: false, decoration: "flat") {
standardTile("refresh", "device.refresh", height: 2, width: 2, inactiveLabel: false, decoration: "flat") {
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
} }
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false, range:"(0..100)") {
state "level", action:"switch level.setLevel"
}
valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") {
state "level", label: 'Level ${currentValue}%'
}
controlTile("saturationSliderControl", "device.saturation", "slider", height: 1, width: 2, inactiveLabel: false) {
state "saturation", action:"color control.setSaturation"
}
valueTile("saturation", "device.saturation", inactiveLabel: false, decoration: "flat") {
state "saturation", label: 'Sat ${currentValue} '
}
controlTile("hueSliderControl", "device.hue", "slider", height: 1, width: 2, inactiveLabel: false) {
state "hue", action:"color control.setHue"
}
valueTile("hue", "device.hue", inactiveLabel: false, decoration: "flat") {
state "hue", label: 'Hue ${currentValue} '
}
main(["rich-control"]) main(["switch"])
details(["rich-control", "colorTempSliderControl", "colorTemp", "reset", "refresh"]) details(["rich-control", "colorTempSliderControl", "colorTemp", "reset", "refresh"])
} }
} }
@@ -103,104 +125,62 @@ void nextLevel() {
} }
void setLevel(percent) { void setLevel(percent) {
log.debug "Executing 'setLevel'" log.debug "Executing 'setLevel'"
if (verifyPercent(percent)) { parent.setLevel(this, percent)
parent.setLevel(this, percent) sendEvent(name: "level", value: percent)
sendEvent(name: "level", value: percent, descriptionText: "Level has changed to ${percent}%")
sendEvent(name: "switch", value: "on")
}
} }
void setSaturation(percent) { void setSaturation(percent) {
log.debug "Executing 'setSaturation'" log.debug "Executing 'setSaturation'"
if (verifyPercent(percent)) { parent.setSaturation(this, percent)
parent.setSaturation(this, percent) sendEvent(name: "saturation", value: percent)
sendEvent(name: "saturation", value: percent, displayed: false)
}
} }
void setHue(percent) { void setHue(percent) {
log.debug "Executing 'setHue'" log.debug "Executing 'setHue'"
if (verifyPercent(percent)) { parent.setHue(this, percent)
parent.setHue(this, percent) sendEvent(name: "hue", value: percent)
sendEvent(name: "hue", value: percent, displayed: false)
}
} }
void setColor(value) { void setColor(value) {
log.debug "setColor: ${value}, $this" log.debug "setColor: ${value}, $this"
def events = [] parent.setColor(this, value)
def validValues = [:] if (value.hue) { sendEvent(name: "hue", value: value.hue)}
if (value.saturation) { sendEvent(name: "saturation", value: value.saturation)}
if (verifyPercent(value.hue)) { if (value.hex) { sendEvent(name: "color", value: value.hex)}
events << createEvent(name: "hue", value: value.hue, displayed: false) if (value.level) { sendEvent(name: "level", value: value.level)}
validValues.hue = value.hue if (value.switch) { sendEvent(name: "switch", value: value.switch)}
}
if (verifyPercent(value.saturation)) {
events << createEvent(name: "saturation", value: value.saturation, displayed: false)
validValues.saturation = value.saturation
}
if (value.hex != null) {
if (value.hex ==~ /^\#([A-Fa-f0-9]){6}$/) {
events << createEvent(name: "color", value: value.hex)
validValues.hex = value.hex
} else {
log.warn "$value.hex is not a valid color"
}
}
if (verifyPercent(value.level)) {
events << createEvent(name: "level", value: value.level, descriptionText: "Level has changed to ${value.level}%")
validValues.level = value.level
}
if (value.switch == "off" || (value.level != null && value.level <= 0)) {
events << createEvent(name: "switch", value: "off")
validValues.switch = "off"
} else {
events << createEvent(name: "switch", value: "on")
validValues.switch = "on"
}
if (!events.isEmpty()) {
parent.setColor(this, validValues)
}
events.each {
sendEvent(it)
}
} }
void reset() { void reset() {
log.debug "Executing 'reset'" log.debug "Executing 'reset'"
def value = [level:100, saturation:56, hue:23] def value = [level:100, hex:"#90C638", saturation:56, hue:23]
setAdjustedColor(value) setAdjustedColor(value)
parent.poll() parent.poll()
} }
void setAdjustedColor(value) { void setAdjustedColor(value) {
if (value) { if (value) {
log.trace "setAdjustedColor: ${value}" log.trace "setAdjustedColor: ${value}"
def adjusted = value + [:] def adjusted = value + [:]
adjusted.hue = adjustOutgoingHue(value.hue) adjusted.hue = adjustOutgoingHue(value.hue)
// Needed because color picker always sends 100 // Needed because color picker always sends 100
adjusted.level = null adjusted.level = null
setColor(adjusted) setColor(adjusted)
} else {
log.warn "Invalid color input"
} }
} }
void setColorTemperature(value) { void setColorTemperature(value) {
if (value) { if (value) {
log.trace "setColorTemperature: ${value}k" log.trace "setColorTemperature: ${value}k"
parent.setColorTemperature(this, value) parent.setColorTemperature(this, value)
sendEvent(name: "colorTemperature", value: value) sendEvent(name: "colorTemperature", value: value)
sendEvent(name: "switch", value: "on") }
} else {
log.warn "Invalid color temperature"
}
} }
void refresh() { void refresh() {
log.debug "Executing 'refresh'" log.debug "Executing 'refresh'"
parent.manualRefresh() parent.manualRefresh()
} }
def adjustOutgoingHue(percent) { def adjustOutgoingHue(percent) {
@@ -219,14 +199,3 @@ def adjustOutgoingHue(percent) {
log.info "percent: $percent, adjusted: $adjusted" log.info "percent: $percent, adjusted: $adjusted"
adjusted adjusted
} }
def verifyPercent(percent) {
if (percent == null)
return false
else if (percent >= 0 && percent <= 100) {
return true
} else {
log.warn "$percent is not 0-100"
return false
}
}

View File

@@ -36,6 +36,13 @@ metadata {
} }
} }
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
state "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
state "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
state "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
state "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
}
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false, range:"(0..100)") { controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false, range:"(0..100)") {
state "level", action:"switch level.setLevel" state "level", action:"switch level.setLevel"
} }
@@ -44,7 +51,7 @@ metadata {
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
} }
main(["rich-control"]) main(["switch"])
details(["rich-control", "refresh"]) details(["rich-control", "refresh"])
} }
} }
@@ -79,12 +86,8 @@ void off() {
void setLevel(percent) { void setLevel(percent) {
log.debug "Executing 'setLevel'" log.debug "Executing 'setLevel'"
if (percent != null && percent >= 0 && percent <= 100) { parent.setLevel(this, percent)
parent.setLevel(this, percent) sendEvent(name: "level", value: percent)
sendEvent(name: "level", value: percent)
} else {
log.warn "$percent is not 0-100"
}
} }
void refresh() { void refresh() {

View File

@@ -23,7 +23,6 @@
# Korean (ko) # Korean (ko)
# Device Preferences # Device Preferences
'''Give your device a name'''.ko=기기 이름 바꾸기 '''Give your device a name'''.ko=기기 이름 바꾸기
'''Outlet'''.ko=플러그
# Events descriptionText # Events descriptionText
'''{{ device.displayName }} is On'''.ko={{ device.displayName }}켜졌습니다. '''{{ device.displayName }} is On'''.ko={{ device.displayName }}켜졌습니다.
'''{{ device.displayName }} is Off'''.ko={{ device.displayName }}꺼졌습니다. '''{{ device.displayName }} is Off'''.ko={{ device.displayName }}꺼졌습니다.

View File

@@ -22,6 +22,7 @@
* 1. 20160117 TW - Update/Edit to support i18n translations * 1. 20160117 TW - Update/Edit to support i18n translations
=============================================================================== ===============================================================================
*/ */
metadata { metadata {
// Automatically generated. Make future change here. // Automatically generated. Make future change here.
definition (name: "SmartPower Outlet", namespace: "smartthings", author: "SmartThings") { definition (name: "SmartPower Outlet", namespace: "smartthings", author: "SmartThings") {
@@ -103,8 +104,8 @@ def parse(String description) {
log.info "$device updates: ${finalResult.value}" log.info "$device updates: ${finalResult.value}"
} }
else if (finalResult.type == "power") { else if (finalResult.type == "power") {
def powerValue = (finalResult.value as Integer)/10 def value = (finalResult.value as Integer)/10
sendEvent(name: "power", value: powerValue, descriptionText: '{{ device.displayName }} power is {{ value }} Watts', translatable: true ) createEvent(name: "power", value: value, descriptionText: '{{ device.displayName }} power is {{ value }} Watts', translatable: true )
/* /*
Dividing by 10 as the Divisor is 10000 and unit is kW for the device. AttrId: 0302 and 0300. Simplifying to 10 Dividing by 10 as the Divisor is 10000 and unit is kW for the device. AttrId: 0302 and 0300. Simplifying to 10
power level is an integer. The exact power level with correct units needs to be handled in the device type power level is an integer. The exact power level with correct units needs to be handled in the device type
@@ -112,8 +113,10 @@ def parse(String description) {
*/ */
} }
else { else {
def descriptionText = finalResult.value == "on" ? '{{ device.displayName }} is On' : '{{ device.displayName }} is Off' if ( finalResult.value == "on" )
sendEvent(name: finalResult.type, value: finalResult.value, descriptionText: descriptionText, translatable: true) createEvent(name: finalResult.type, value: finalResult.value, descriptionText: '{{ device.displayName }} is On', translatable: true)
else
createEvent(name: finalResult.type, value: finalResult.value, descriptionText: '{{ device.displayName }} is Off', translatable: true)
} }
} }
else { else {

View File

@@ -30,12 +30,11 @@
'''Degrees'''.ko=온도 '''Degrees'''.ko=온도
'''Adjust temperature by this many degrees'''.ko=몇 도씩 온도를 조절하십시오 '''Adjust temperature by this many degrees'''.ko=몇 도씩 온도를 조절하십시오
'''Give your device a name'''.ko=기기 이름 바꾸기 '''Give your device a name'''.ko=기기 이름 바꾸기
'''Water Leak Sensor'''.ko=누수센서
# Events descriptionText # Events descriptionText
'''{{ device.displayName }} is dry'''.ko={{ device.displayName }}가 건조 '''{{ device.displayName }} is dry'''.ko={{ device.displayName }}가 건조
'''{{ device.displayName }} is wet'''.ko={{ device.displayName }}누수 '''{{ device.displayName }} is wet'''.ko={{ device.displayName }}누수
'''{{ device.displayName }} was {{ value }}°C'''.ko={{ device.displayName }}이(가) {{ value }}°C였습니다 '''{{ device.displayName }} was {{ value }}°C'''.ko={{ device.displayName }}이(가) {{ value }}°C였습니다
'''{{ device.displayName }} was {{ value }}°F'''.ko={{ device.displayName }}이(가) {{ value }}°F였습니다 '''{{ device.displayName }} was {{ value }}°F'''.ko={{ device.displayName }}이(가) {{ value }}°F였습니다
'''{{ device.displayName }} battery has too much power: (> 3.5) volts.'''.ko={{ device.displayName }} 배터리 전력이 너무 높습니다(3.5볼트 초과). '''{{ device.displayName }} battery has too much power: (> 3.5) volts.'''.ko={{ device.displayName }} 배터리 전력이 너무 높습니다(3.5볼트 초과).
'''{{ device.displayName }} battery was {{ value }}%'''.ko={{ device.displayName }}남아있는 배터리는 {{ value }}%입니다. '''{{ device.displayName }} battery was {{ value }}'''.ko={{ device.displayName }}남아있는 배터리는 {{ value }}입니다.
#============================================================================== #==============================================================================

View File

@@ -32,8 +32,7 @@ metadata {
capability "Temperature Measurement" capability "Temperature Measurement"
capability "Water Sensor" capability "Water Sensor"
command "enrollResponse" command "enrollResponse"
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-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"
@@ -67,7 +66,7 @@ metadata {
} }
} }
valueTile("temperature", "device.temperature", inactiveLabel: false, width: 2, height: 2) { valueTile("temperature", "device.temperature", inactiveLabel: false, width: 2, height: 2) {
state "temperature", label:'${currentValue}°', state "temperature", label:'${currentValue}°',
backgroundColors:[ backgroundColors:[
[value: 31, color: "#153591"], [value: 31, color: "#153591"],
[value: 44, color: "#1e9cbb"], [value: 44, color: "#1e9cbb"],
@@ -103,29 +102,29 @@ def parse(String description) {
else if (description?.startsWith('temperature: ')) { else if (description?.startsWith('temperature: ')) {
map = parseCustomMessage(description) map = parseCustomMessage(description)
} }
else if (description?.startsWith('zone status')) { else if (description?.startsWith('zone status')) {
map = parseIasMessage(description) map = parseIasMessage(description)
} }
log.debug "Parse returned $map" log.debug "Parse returned $map"
def result = map ? createEvent(map) : null def result = map ? createEvent(map) : null
if (description?.startsWith('enroll request')) { if (description?.startsWith('enroll request')) {
List cmds = enrollResponse() List cmds = enrollResponse()
log.debug "enroll response: ${cmds}" log.debug "enroll response: ${cmds}"
result = cmds?.collect { new physicalgraph.device.HubAction(it) } result = cmds?.collect { new physicalgraph.device.HubAction(it) }
} }
return result return result
} }
private Map parseCatchAllMessage(String description) { private Map parseCatchAllMessage(String description) {
Map resultMap = [:] Map resultMap = [:]
def cluster = zigbee.parse(description) def cluster = zigbee.parse(description)
if (shouldProcessMessage(cluster)) { if (shouldProcessMessage(cluster)) {
switch(cluster.clusterId) { switch(cluster.clusterId) {
case 0x0001: case 0x0001:
resultMap = getBatteryResult(cluster.data.last()) resultMap = getBatteryResult(cluster.data.last())
break break
case 0x0402: case 0x0402:
// temp is last 2 data values. reverse to swap endian // temp is last 2 data values. reverse to swap endian
@@ -140,13 +139,13 @@ private Map parseCatchAllMessage(String description) {
} }
private boolean shouldProcessMessage(cluster) { private boolean shouldProcessMessage(cluster) {
// 0x0B is default response indicating message got through // 0x0B is default response indicating message got through
// 0x07 is bind message // 0x07 is bind message
boolean ignoredMessage = cluster.profileId != 0x0104 || boolean ignoredMessage = cluster.profileId != 0x0104 ||
cluster.command == 0x0B || cluster.command == 0x0B ||
cluster.command == 0x07 || cluster.command == 0x07 ||
(cluster.data.size() > 0 && cluster.data.first() == 0x3e) (cluster.data.size() > 0 && cluster.data.first() == 0x3e)
return !ignoredMessage return !ignoredMessage
} }
private Map parseReportAttributeMessage(String description) { private Map parseReportAttributeMessage(String description) {
@@ -178,42 +177,42 @@ private Map parseCustomMessage(String description) {
} }
private Map parseIasMessage(String description) { private Map parseIasMessage(String description) {
List parsedMsg = description.split(' ') List parsedMsg = description.split(' ')
String msgCode = parsedMsg[2] String msgCode = parsedMsg[2]
Map resultMap = [:] Map resultMap = [:]
switch(msgCode) { switch(msgCode) {
case '0x0020': // Closed/No Motion/Dry case '0x0020': // Closed/No Motion/Dry
resultMap = getMoistureResult('dry') resultMap = getMoistureResult('dry')
break break
case '0x0021': // Open/Motion/Wet case '0x0021': // Open/Motion/Wet
resultMap = getMoistureResult('wet') resultMap = getMoistureResult('wet')
break break
case '0x0022': // Tamper Alarm case '0x0022': // Tamper Alarm
break break
case '0x0023': // Battery Alarm case '0x0023': // Battery Alarm
break break
case '0x0024': // Supervision Report case '0x0024': // Supervision Report
log.debug 'dry with tamper alarm' log.debug 'dry with tamper alarm'
resultMap = getMoistureResult('dry') resultMap = getMoistureResult('dry')
break break
case '0x0025': // Restore Report case '0x0025': // Restore Report
log.debug 'water with tamper alarm' log.debug 'water with tamper alarm'
resultMap = getMoistureResult('wet') resultMap = getMoistureResult('wet')
break break
case '0x0026': // Trouble/Failure case '0x0026': // Trouble/Failure
break break
case '0x0028': // Test Mode case '0x0028': // Test Mode
break break
} }
return resultMap return resultMap
} }
def getTemperature(value) { def getTemperature(value) {
@@ -257,7 +256,8 @@ private Map getBatteryResult(rawValue) {
def pct = batteryMap[volts] def pct = batteryMap[volts]
if (pct != null) { if (pct != null) {
result.value = pct result.value = pct
result.descriptionText = "{{ device.displayName }} battery was {{ value }}%" def value = pct
result.descriptionText = "{{ device.displayName }} battery was {{ value }}"
} }
} }
else { else {
@@ -265,14 +265,13 @@ private Map getBatteryResult(rawValue) {
def maxVolts = 3.0 def maxVolts = 3.0
def pct = (volts - minVolts) / (maxVolts - minVolts) def pct = (volts - minVolts) / (maxVolts - minVolts)
result.value = Math.min(100, (int) pct * 100) result.value = Math.min(100, (int) pct * 100)
result.descriptionText = "{{ device.displayName }} battery was {{ value }}%" result.descriptionText = "{{ device.displayName }} battery was {{ value }}"
} }
} }
} }
return result return result
} }
private Map getTemperatureResult(value) { private Map getTemperatureResult(value) {
log.debug 'TEMP' log.debug 'TEMP'
if (tempOffset) { if (tempOffset) {
@@ -282,9 +281,9 @@ private Map getTemperatureResult(value) {
} }
def descriptionText def descriptionText
if ( temperatureScale == 'C' ) if ( temperatureScale == 'C' )
descriptionText = '{{ device.displayName }} was {{ value }}°C' descriptionText = '{{ device.displayName }} was {{ value }}°C'
else else
descriptionText = '{{ device.displayName }} was {{ value }}°F' descriptionText = '{{ device.displayName }} was {{ value }}°F'
return [ return [
name: 'temperature', name: 'temperature',
@@ -312,7 +311,7 @@ private Map getMoistureResult(value) {
def refresh() { def refresh() {
log.debug "Refreshing Temperature and Battery" log.debug "Refreshing Temperature and Battery"
def refreshCmds = [ def refreshCmds = [
"st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200", "st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200",
"st rattr 0x${device.deviceNetworkId} 1 1 0x20", "delay 200" "st rattr 0x${device.deviceNetworkId} 1 1 0x20", "delay 200"
] ]
@@ -328,26 +327,26 @@ def configure() {
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 500", "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 500",
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs "zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
"send 0x${device.deviceNetworkId} 1 1", "delay 500", "send 0x${device.deviceNetworkId} 1 1", "delay 500",
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 500", "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 500",
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}", "zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
"send 0x${device.deviceNetworkId} 1 1", "delay 500" "send 0x${device.deviceNetworkId} 1 1", "delay 500"
] ]
return configCmds + refresh() // send refresh cmds as part of config return configCmds + refresh() // send refresh cmds as part of config
} }
def enrollResponse() { def enrollResponse() {
log.debug "Sending enroll response" log.debug "Sending enroll response"
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui) String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
[ [
//Resending the CIE in case the enroll request is sent before CIE is written //Resending the CIE in case the enroll request is sent before CIE is written
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200", "zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500", "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
//Enroll Response //Enroll Response
"raw 0x500 {01 23 00 00 00}", "raw 0x500 {01 23 00 00 00}",
"send 0x${device.deviceNetworkId} 1 1", "delay 200" "send 0x${device.deviceNetworkId} 1 1", "delay 200"
] ]
} }
private getEndpointId() { private getEndpointId() {
@@ -359,19 +358,19 @@ private hex(value) {
} }
private String swapEndianHex(String hex) { private String swapEndianHex(String hex) {
reverseArray(hex.decodeHex()).encodeHex() reverseArray(hex.decodeHex()).encodeHex()
} }
private byte[] reverseArray(byte[] array) { private byte[] reverseArray(byte[] array) {
int i = 0; int i = 0;
int j = array.length - 1; int j = array.length - 1;
byte tmp; byte tmp;
while (j > i) { while (j > i) {
tmp = array[j]; tmp = array[j];
array[j] = array[i]; array[j] = array[i];
array[i] = tmp; array[i] = tmp;
j--; j--;
i++; i++;
} }
return array return array
} }

View File

@@ -29,12 +29,11 @@
'''Degrees'''.ko=온도 '''Degrees'''.ko=온도
'''Adjust temperature by this many degrees'''.ko=몇 도씩 온도를 조절하십시오 '''Adjust temperature by this many degrees'''.ko=몇 도씩 온도를 조절하십시오
'''Give your device a name'''.ko=기기 이름 바꾸기 '''Give your device a name'''.ko=기기 이름 바꾸기
'''Motion Sensor'''.ko=모션 센서
# Events descriptionText # Events descriptionText
'''{{ device.displayName }} detected motion'''.ko={{ device.displayName }} 가 움직임을 감지하였습니다. '''{{ device.displayName }} detected motion'''.ko={{ device.displayName }} 가 움직임을 감지하였습니다.
'''{{ device.displayName }} motion has stopped'''.ko={{ device.displayName }}움직임이 중단되었습니다 '''{{ device.displayName }} motion has stopped'''.ko={{ device.displayName }}움직임이 중단되었습니다
'''{{ device.displayName }} was {{ value }}°C'''.ko={{ device.displayName }}이(가){{ value }}°C였습니다. '''{{ device.displayName }} was {{ value }}°C'''.ko={{ device.displayName }}이(가){{ value }}°C였습니다.
'''{{ device.displayName }} was {{ value }}°F'''.ko={{ device.displayName }}이(가) {{ value }}°F였습니다 '''{{ device.displayName }} was {{ value }}°F'''.ko={{ device.displayName }}이(가) {{ value }}°F였습니다
'''{{ device.displayName }} battery has too much power: (> 3.5) volts.'''.ko={{ device.displayName }} 배터리 전력이 너무 높습니다(3.5볼트 초과). '''{{ device.displayName }} battery has too much power: (> 3.5) volts.'''.ko={{ device.displayName }} 배터리 전력이 너무 높습니다(3.5볼트 초과).
'''{{ device.displayName }} battery was {{ value }}%'''.ko={{ device.displayName }}남아있는 배터리는 {{ value }}%입니다. '''{{ device.displayName }} battery was {{ value }}'''.ko={{ device.displayName }}남아있는 배터리는 {{ value }}입니다.
#============================================================================== #==============================================================================

View File

@@ -29,18 +29,17 @@ metadata {
capability "Motion Sensor" capability "Motion Sensor"
capability "Configuration" capability "Configuration"
capability "Battery" capability "Battery"
capability "Temperature Measurement" capability "Temperature Measurement"
capability "Refresh" capability "Refresh"
command "enrollResponse" command "enrollResponse"
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3305-S" fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3305-S"
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3325-S", deviceJoinName: "Motion Sensor" fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3325-S", deviceJoinName: "Motion Sensor"
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: "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: "3325"
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3326" fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3326"
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3326-L", deviceJoinName: "Iris Motion Sensor" fingerprint inClusters: "0000,0001,0003,000F,0020,0402,0500", outClusters: "0019", manufacturer: "SmartThings", model: "motionv4", deviceJoinName: "Motion Sensor"
fingerprint inClusters: "0000,0001,0003,000F,0020,0402,0500", outClusters: "0019", manufacturer: "SmartThings", model: "motionv4", deviceJoinName: "Motion Sensor"
} }
simulator { simulator {
@@ -107,55 +106,55 @@ def parse(String description) {
else if (description?.startsWith('temperature: ')) { else if (description?.startsWith('temperature: ')) {
map = parseCustomMessage(description) map = parseCustomMessage(description)
} }
else if (description?.startsWith('zone status')) { else if (description?.startsWith('zone status')) {
map = parseIasMessage(description) map = parseIasMessage(description)
} }
log.debug "Parse returned $map" log.debug "Parse returned $map"
def result = map ? createEvent(map) : null def result = map ? createEvent(map) : null
if (description?.startsWith('enroll request')) { if (description?.startsWith('enroll request')) {
List cmds = enrollResponse() List cmds = enrollResponse()
log.debug "enroll response: ${cmds}" log.debug "enroll response: ${cmds}"
result = cmds?.collect { new physicalgraph.device.HubAction(it) } result = cmds?.collect { new physicalgraph.device.HubAction(it) }
} }
return result return result
} }
private Map parseCatchAllMessage(String description) { private Map parseCatchAllMessage(String description) {
Map resultMap = [:] Map resultMap = [:]
def cluster = zigbee.parse(description) def cluster = zigbee.parse(description)
if (shouldProcessMessage(cluster)) { if (shouldProcessMessage(cluster)) {
switch(cluster.clusterId) { switch(cluster.clusterId) {
case 0x0001: case 0x0001:
resultMap = getBatteryResult(cluster.data.last()) resultMap = getBatteryResult(cluster.data.last())
break break
case 0x0402: case 0x0402:
// temp is last 2 data values. reverse to swap endian // temp is last 2 data values. reverse to swap endian
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join() String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
def value = getTemperature(temp) def value = getTemperature(temp)
resultMap = getTemperatureResult(value) resultMap = getTemperatureResult(value)
break break
case 0x0406: case 0x0406:
log.debug 'motion' log.debug 'motion'
resultMap.name = 'motion' resultMap.name = 'motion'
break break
} }
} }
return resultMap return resultMap
} }
private boolean shouldProcessMessage(cluster) { private boolean shouldProcessMessage(cluster) {
// 0x0B is default response indicating message got through // 0x0B is default response indicating message got through
// 0x07 is bind message // 0x07 is bind message
boolean ignoredMessage = cluster.profileId != 0x0104 || boolean ignoredMessage = cluster.profileId != 0x0104 ||
cluster.command == 0x0B || cluster.command == 0x0B ||
cluster.command == 0x07 || cluster.command == 0x07 ||
(cluster.data.size() > 0 && cluster.data.first() == 0x3e) (cluster.data.size() > 0 && cluster.data.first() == 0x3e)
return !ignoredMessage return !ignoredMessage
} }
private Map parseReportAttributeMessage(String description) { private Map parseReportAttributeMessage(String description) {
@@ -173,10 +172,10 @@ private Map parseReportAttributeMessage(String description) {
else if (descMap.cluster == "0001" && descMap.attrId == "0020") { else if (descMap.cluster == "0001" && descMap.attrId == "0020") {
resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16)) resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16))
} }
else if (descMap.cluster == "0406" && descMap.attrId == "0000") { else if (descMap.cluster == "0406" && descMap.attrId == "0000") {
def value = descMap.value.endsWith("01") ? "active" : "inactive" def value = descMap.value.endsWith("01") ? "active" : "inactive"
resultMap = getMotionResult(value) resultMap = getMotionResult(value)
} }
return resultMap return resultMap
} }
@@ -191,44 +190,44 @@ private Map parseCustomMessage(String description) {
} }
private Map parseIasMessage(String description) { private Map parseIasMessage(String description) {
List parsedMsg = description.split(' ') List parsedMsg = description.split(' ')
String msgCode = parsedMsg[2] String msgCode = parsedMsg[2]
Map resultMap = [:] Map resultMap = [:]
switch(msgCode) { switch(msgCode) {
case '0x0020': // Closed/No Motion/Dry case '0x0020': // Closed/No Motion/Dry
resultMap = getMotionResult('inactive') resultMap = getMotionResult('inactive')
break break
case '0x0021': // Open/Motion/Wet case '0x0021': // Open/Motion/Wet
resultMap = getMotionResult('active') resultMap = getMotionResult('active')
break break
case '0x0022': // Tamper Alarm case '0x0022': // Tamper Alarm
log.debug 'motion with tamper alarm' log.debug 'motion with tamper alarm'
resultMap = getMotionResult('active') resultMap = getMotionResult('active')
break break
case '0x0023': // Battery Alarm case '0x0023': // Battery Alarm
break break
case '0x0024': // Supervision Report case '0x0024': // Supervision Report
log.debug 'no motion with tamper alarm' log.debug 'no motion with tamper alarm'
resultMap = getMotionResult('inactive') resultMap = getMotionResult('inactive')
break break
case '0x0025': // Restore Report case '0x0025': // Restore Report
break break
case '0x0026': // Trouble/Failure case '0x0026': // Trouble/Failure
log.debug 'motion with failure alarm' log.debug 'motion with failure alarm'
resultMap = getMotionResult('active') resultMap = getMotionResult('active')
break break
case '0x0028': // Test Mode case '0x0028': // Test Mode
break break
} }
return resultMap return resultMap
} }
def getTemperature(value) { def getTemperature(value) {
@@ -273,7 +272,7 @@ private Map getBatteryResult(rawValue) {
if (pct != null) { if (pct != null) {
result.value = pct result.value = pct
def value = pct def value = pct
result.descriptionText = "{{ device.displayName }} battery was {{ value }}%" result.descriptionText = "{{ device.displayName }} battery was {{ value }}"
} }
} }
else { else {
@@ -281,7 +280,7 @@ private Map getBatteryResult(rawValue) {
def maxVolts = 3.0 def maxVolts = 3.0
def pct = (volts - minVolts) / (maxVolts - minVolts) def pct = (volts - minVolts) / (maxVolts - minVolts)
result.value = Math.min(100, (int) pct * 100) result.value = Math.min(100, (int) pct * 100)
result.descriptionText = "{{ device.displayName }} battery was {{ value }}%" result.descriptionText = "{{ device.displayName }} battery was {{ value }}"
} }
} }
} }
@@ -313,6 +312,7 @@ private Map getTemperatureResult(value) {
private Map getMotionResult(value) { private Map getMotionResult(value) {
log.debug 'motion' log.debug 'motion'
String descriptionText = value == 'active' ? "{{ device.displayName }} detected motion" : "{{ device.displayName }} motion has stopped" String descriptionText = value == 'active' ? "{{ device.displayName }} detected motion" : "{{ device.displayName }} motion has stopped"
//String descriptionText = '{{ device.displayName }} is {{ value | translate }}'
return [ return [
name: 'motion', name: 'motion',
value: value, value: value,
@@ -322,7 +322,7 @@ private Map getMotionResult(value) {
} }
def refresh() { def refresh() {
log.debug "refresh called" log.debug "refresh executed"
def refreshCmds = [ def refreshCmds = [
"st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200", "st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200",
"st rattr 0x${device.deviceNetworkId} 1 1 0x20", "delay 200" "st rattr 0x${device.deviceNetworkId} 1 1 0x20", "delay 200"
@@ -372,19 +372,19 @@ private hex(value) {
} }
private String swapEndianHex(String hex) { private String swapEndianHex(String hex) {
reverseArray(hex.decodeHex()).encodeHex() reverseArray(hex.decodeHex()).encodeHex()
} }
private byte[] reverseArray(byte[] array) { private byte[] reverseArray(byte[] array) {
int i = 0; int i = 0;
int j = array.length - 1; int j = array.length - 1;
byte tmp; byte tmp;
while (j > i) { while (j > i) {
tmp = array[j]; tmp = array[j];
array[j] = array[i]; array[j] = array[i];
array[i] = tmp; array[i] = tmp;
j--; j--;
i++; i++;
} }
return array return array
} }

View File

@@ -32,7 +32,6 @@
'''Do you want to use this sensor on a garage door?'''.ko=차고 문의 센서 사용 설정하기 '''Do you want to use this sensor on a garage door?'''.ko=차고 문의 센서 사용 설정하기
'''Tap to set'''.ko=눌러서 설정 '''Tap to set'''.ko=눌러서 설정
'''Give your device a name'''.ko=기기 이름 바꾸기 '''Give your device a name'''.ko=기기 이름 바꾸기
'''Multipurpose Sensor'''.ko=멀티 센서
# Events descriptionText # Events descriptionText
'''{{ device.displayName }} was opened'''.ko={{ device.displayName }}열림을 감지하였습니다. '''{{ device.displayName }} was opened'''.ko={{ device.displayName }}열림을 감지하였습니다.
'''{{ device.displayName }} was closed'''.ko={{ device.displayName }}닫혔습니다. '''{{ device.displayName }} was closed'''.ko={{ device.displayName }}닫혔습니다.
@@ -40,6 +39,8 @@
'''{{ device.displayName }} was inactive'''.ko={{ device.displayName }}비활성화되었습니다. '''{{ device.displayName }} was inactive'''.ko={{ device.displayName }}비활성화되었습니다.
'''{{ device.displayName }} was {{ value }}°C'''.ko={{ device.displayName }}이(가){{ value }}°C였습니다. '''{{ device.displayName }} was {{ value }}°C'''.ko={{ device.displayName }}이(가){{ value }}°C였습니다.
'''{{ device.displayName }} was {{ value }}°F'''.ko={{ device.displayName }}이(가) {{ value }}°F였습니다 '''{{ device.displayName }} was {{ value }}°F'''.ko={{ device.displayName }}이(가) {{ value }}°F였습니다
'''{{ device.displayName }} battery was {{ value }}%'''.ko={{ device.displayName }}남아있는 배터리는 {{ value }}%입니다. '''{{ device.displayName }} battery was {{ value }}'''.ko={{ device.displayName }}남아있는 배터리는 {{ value }}입니다.
'''Updating device to garage sensor'''.ko=기기-차고 센서 업데이트 중 '''Updating device to garage sensor'''.ko=기기-차고 센서 업데이트 중
'''Updating device to open/close sensor'''.ko=기기-열림/닫힘 센서 업데이트 중 '''Updating device to open/close sensor'''.ko=기기-열림/닫힘 센서 업데이트 중
'''{{ device.displayName }} status was closed'''.ko={{ device.displayName }}은(는) 닫힌 상태입니다
'''{{ device.displayName }} status was opened'''.ko={{ device.displayName }}은(는) 열린 상태입니다

View File

@@ -24,13 +24,12 @@
=============================================================================== ===============================================================================
*/ */
metadata { metadata {
definition (name: "SmartSense Multi Sensor", namespace: "smartthings", author: "SmartThings") { definition (name: "SmartSense Multi Sensor", namespace: "smartthings", author: "SmartThings") {
capability "Three Axis"
capability "Three Axis"
capability "Battery" capability "Battery"
capability "Configuration" capability "Configuration"
capability "Sensor" capability "Sensor"
capability "Contact Sensor" capability "Contact Sensor"
capability "Acceleration Sensor" capability "Acceleration Sensor"
capability "Refresh" capability "Refresh"
@@ -39,13 +38,13 @@ metadata {
command "enrollResponse" command "enrollResponse"
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: "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"
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,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" fingerprint inClusters: "0000,0001,0003,000F,0020,0402,0500,FC02", outClusters: "0019", manufacturer: "SmartThings", model: "multiv4", deviceJoinName: "Multipurpose Sensor"
attribute "status", "string" attribute "status", "string"
} }
simulator { simulator {
status "open": "zone report :: type: 19 value: 0031" status "open": "zone report :: type: 19 value: 0031"
status "closed": "zone report :: type: 19 value: 0030" status "closed": "zone report :: type: 19 value: 0030"
@@ -62,7 +61,7 @@ metadata {
status "x,y,z: 0,1000,0": "x: 0, y: 1000, z: 0" status "x,y,z: 0,1000,0": "x: 0, y: 1000, z: 0"
status "x,y,z: 0,0,1000": "x: 0, y: 0, z: 1000" status "x,y,z: 0,0,1000": "x: 0, y: 0, z: 1000"
} }
preferences { preferences {
section { section {
image(name: 'educationalcontent', multiple: true, images: [ image(name: 'educationalcontent', multiple: true, images: [
"http://cdn.device-gse.smartthings.com/Multi/Multi1.jpg", "http://cdn.device-gse.smartthings.com/Multi/Multi1.jpg",
@@ -76,9 +75,9 @@ metadata {
input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
} }
section { section {
input("garageSensor", "enum", title: "Do you want to use this sensor on a garage door?", description: "Tap to set", options: ["Yes", "No"], defaultValue: "No", required: false, displayDuringSetup: false) input("garageSensor", "enum", title: "Do you want to use this sensor on a garage door?", translatable: true, description: "Tap to set", options: ["Yes","No"], defaultValue: "No", required: false, displayDuringSetup: false)
} }
} }
tiles(scale: 2) { tiles(scale: 2) {
multiAttributeTile(name:"status", type: "generic", width: 6, height: 4){ multiAttributeTile(name:"status", type: "generic", width: 6, height: 4){
@@ -110,16 +109,19 @@ metadata {
] ]
) )
} }
valueTile("3axis", "device.threeAxis", decoration: "flat", wordWrap: false, width: 2, height: 2) {
state("threeAxis", label:'${currentValue}', unit:"", backgroundColor:"#ffffff")
}
valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) { valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) {
state "battery", label:'${currentValue}% battery', unit:"" state "battery", label:'${currentValue}% battery', unit:""
} }
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "default", action:"refresh.refresh", icon:"st.secondary.refresh" state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
} }
main(["status", "acceleration", "temperature"]) main(["status", "acceleration", "temperature"])
details(["status", "acceleration", "temperature", "battery", "refresh"]) details(["status", "acceleration", "temperature", "3axis", "battery", "refresh"])
} }
} }
@@ -128,61 +130,61 @@ def parse(String description) {
if (description?.startsWith('catchall:')) { if (description?.startsWith('catchall:')) {
map = parseCatchAllMessage(description) map = parseCatchAllMessage(description)
} }
else if (description?.startsWith('temperature: ')) { else if (description?.startsWith('temperature: ')) {
map = parseCustomMessage(description) map = parseCustomMessage(description)
} }
else if (description?.startsWith('zone status')) { else if (description?.startsWith('zone status')) {
map = parseIasMessage(description) map = parseIasMessage(description)
} }
def result = map ? createEvent(map) : null def result = map ? createEvent(map) : null
if (description?.startsWith('enroll request')) { if (description?.startsWith('enroll request')) {
List cmds = enrollResponse() List cmds = enrollResponse()
log.debug "enroll response: ${cmds}" log.debug "enroll response: ${cmds}"
result = cmds?.collect { new physicalgraph.device.HubAction(it) } result = cmds?.collect { new physicalgraph.device.HubAction(it) }
} }
else if (description?.startsWith('read attr -')) { else if (description?.startsWith('read attr -')) {
result = parseReportAttributeMessage(description).each { createEvent(it) } result = parseReportAttributeMessage(description).each { createEvent(it) }
} }
return result return result
} }
private Map parseCatchAllMessage(String description) { private Map parseCatchAllMessage(String description) {
Map resultMap = [:] Map resultMap = [:]
def cluster = zigbee.parse(description) def cluster = zigbee.parse(description)
log.debug cluster log.debug cluster
if (shouldProcessMessage(cluster)) { if (shouldProcessMessage(cluster)) {
switch(cluster.clusterId) { switch(cluster.clusterId) {
case 0x0001: case 0x0001:
resultMap = getBatteryResult(cluster.data.last()) resultMap = getBatteryResult(cluster.data.last())
break break
case 0xFC02: case 0xFC02:
log.debug 'ACCELERATION' log.debug 'ACCELERATION'
break break
case 0x0402: case 0x0402:
log.debug 'TEMP' log.debug 'TEMP'
// temp is last 2 data values. reverse to swap endian // temp is last 2 data values. reverse to swap endian
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join() String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
def value = getTemperature(temp) def value = getTemperature(temp)
resultMap = getTemperatureResult(value) resultMap = getTemperatureResult(value)
break break
} }
} }
return resultMap return resultMap
} }
private boolean shouldProcessMessage(cluster) { private boolean shouldProcessMessage(cluster) {
// 0x0B is default response indicating message got through // 0x0B is default response indicating message got through
// 0x07 is bind message // 0x07 is bind message
boolean ignoredMessage = cluster.profileId != 0x0104 || boolean ignoredMessage = cluster.profileId != 0x0104 ||
cluster.command == 0x0B || cluster.command == 0x0B ||
cluster.command == 0x07 || cluster.command == 0x07 ||
(cluster.data.size() > 0 && cluster.data.first() == 0x3e) (cluster.data.size() > 0 && cluster.data.first() == 0x3e)
return !ignoredMessage return !ignoredMessage
} }
private List parseReportAttributeMessage(String description) { private List parseReportAttributeMessage(String description) {
@@ -209,12 +211,10 @@ private List parseReportAttributeMessage(String description) {
result << parseAxis(threeAxisAttributes) result << parseAxis(threeAxisAttributes)
descMap.value = descMap.value[-2..-1] descMap.value = descMap.value[-2..-1]
} }
result << getAccelerationResult(descMap.value) result << getAccelerationResult(descMap.value)
} }
else if (descMap.cluster == "FC02" && descMap.attrId == "0012" && descMap.value.size() == 24) { else if (descMap.cluster == "FC02" && descMap.attrId == "0012" && descMap.value.size() == 24) {
// The size is checked to ensure the attribute report contains X, Y and Z values result << parseAxis(descMap.value)
// If all three axis are not included then the attribute report is ignored
result << parseAxis(descMap.value)
} }
else if (descMap.cluster == "0001" && descMap.attrId == "0020") { else if (descMap.cluster == "0001" && descMap.attrId == "0020") {
result << getBatteryResult(Integer.parseInt(descMap.value, 16)) result << getBatteryResult(Integer.parseInt(descMap.value, 16))
@@ -238,43 +238,43 @@ private Map parseIasMessage(String description) {
Map resultMap = [:] Map resultMap = [:]
switch(msgCode) { switch(msgCode) {
case '0x0020': // Closed/No Motion/Dry case '0x0020': // Closed/No Motion/Dry
if (garageSensor != "Yes"){ if (garageSensor != "Yes"){
resultMap = getContactResult('closed') resultMap = getContactResult('closed')
} }
break break
case '0x0021': // Open/Motion/Wet case '0x0021': // Open/Motion/Wet
if (garageSensor != "Yes"){ if (garageSensor != "Yes"){
resultMap = getContactResult('open') resultMap = getContactResult('open')
} }
break break
case '0x0022': // Tamper Alarm case '0x0022': // Tamper Alarm
break break
case '0x0023': // Battery Alarm case '0x0023': // Battery Alarm
break break
case '0x0024': // Supervision Report case '0x0024': // Supervision Report
if (garageSensor != "Yes"){ if (garageSensor != "Yes"){
resultMap = getContactResult('closed') resultMap = getContactResult('closed')
} }
break break
case '0x0025': // Restore Report case '0x0025': // Restore Report
if (garageSensor != "Yes"){ if (garageSensor != "Yes"){
resultMap = getContactResult('open') resultMap = getContactResult('open')
} }
break break
case '0x0026': // Trouble/Failure case '0x0026': // Trouble/Failure
break break
case '0x0028': // Test Mode case '0x0028': // Test Mode
break break
} }
return resultMap return resultMap
} }
def updated() { def updated() {
@@ -311,6 +311,7 @@ def getTemperature(value) {
private Map getBatteryResult(rawValue) { private Map getBatteryResult(rawValue) {
log.debug "Battery rawValue = ${rawValue}" log.debug "Battery rawValue = ${rawValue}"
def linkText = getLinkText(device)
def result = [ def result = [
name: 'battery', name: 'battery',
@@ -340,7 +341,8 @@ private Map getBatteryResult(rawValue) {
def pct = batteryMap[volts] def pct = batteryMap[volts]
if (pct != null) { if (pct != null) {
result.value = pct result.value = pct
result.descriptionText = "{{ device.displayName }} battery was {{ value }}%" def value = pct
result.descriptionText = "{{ device.displayName }} battery was {{ value }}"
} }
} }
else { else {
@@ -348,7 +350,7 @@ private Map getBatteryResult(rawValue) {
def maxVolts = 3.0 def maxVolts = 3.0
def pct = (volts - minVolts) / (maxVolts - minVolts) def pct = (volts - minVolts) / (maxVolts - minVolts)
result.value = Math.min(100, (int) pct * 100) result.value = Math.min(100, (int) pct * 100)
result.descriptionText = "{{ device.displayName }} battery was {{ value }}%" result.descriptionText = "{{ device.displayName }} battery was {{ value }}"
} }
} }
} }
@@ -357,32 +359,52 @@ private Map getBatteryResult(rawValue) {
} }
private Map getTemperatureResult(value) { private Map getTemperatureResult(value) {
log.debug "Temperature" log.debug 'TEMP'
def name = "temperature"
if (tempOffset) { if (tempOffset) {
def offset = tempOffset as int def offset = tempOffset as int
def v = value as int def v = value as int
value = v + offset value = v + offset
} }
def descriptionText = temperatureScale == 'C' ? '{{ device.displayName }} was {{ value }}°C':
'{{ device.displayName }} was {{ value }}°F' def descriptionText
if ( temperatureScale == 'C' )
descriptionText = '{{ device.displayName }} was {{ value }}°C'
else
descriptionText = '{{ device.displayName }} was {{ value }}°F'
return [ return [
name: 'temperature', name: name,
value: value, value: value,
descriptionText: descriptionText, descriptionText: descriptionText,
translatable: true translatable: true
] ]
} }
private Map getContactResult(value) { private Map getContactResult(value) {
log.debug "Contact: ${device.displayName} value = ${value}" log.debug "Contact: ${device.displayName} value = ${value}"
def descriptionText = value == 'open' ? '{{ device.displayName }} was opened' : '{{ device.displayName }} was closed' def name = "contact"
sendEvent(name: 'contact', value: value, descriptionText: descriptionText, displayed: false, translatable: true)
sendEvent(name: 'status', value: value, descriptionText: descriptionText, translatable: true) def descriptionText
if ( value == 'open' )
descriptionText = '{{ device.displayName }} was opened'
else
descriptionText = '{{ device.displayName }} was closed'
sendEvent(name: 'status', value: value, descriptionText: descriptionText, displayed: false)
def isStateChange = isStateChange(device, name, value)
return [
name: name,
value: value,
descriptionText: descriptionText,
isStateChange: isStateChange,
translatable: true
]
} }
private getAccelerationResult(numValue) { private getAccelerationResult(numValue) {
log.debug "Acceleration" log.debug "Acceleration is $value"
def name = "acceleration" def name = "acceleration"
def value def value
def descriptionText def descriptionText
@@ -451,38 +473,37 @@ def refresh() {
def configure() { def configure() {
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui) String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
log.debug "Configuring Reporting" log.debug "Configuring Device Reporting"
def configCmds = [ def configCmds = [
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200", "zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500", "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 200", "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 200",
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", "delay 200", //checkin time 6 hrs "zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500", "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 200", "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 200",
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}", "delay 200", "zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500", "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0xFC02 {${device.zigbeeId}} {}", "delay 200", "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0xFC02 {${device.zigbeeId}} {}", "delay 200",
"zcl mfg-code ${manufacturerCode}", "delay 200", "zcl mfg-code ${manufacturerCode}",
"zcl global send-me-a-report 0xFC02 0x0010 0x18 10 3600 {01}", "delay 200", "zcl global send-me-a-report 0xFC02 0x0010 0x18 10 3600 {01}",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500", "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
"zcl mfg-code ${manufacturerCode}", "delay 200", "zcl mfg-code ${manufacturerCode}",
"zcl global send-me-a-report 0xFC02 0x0012 0x29 1 3600 {0100}", "delay 200", "zcl global send-me-a-report 0xFC02 0x0012 0x29 1 3600 {01}",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500", "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
"zcl mfg-code ${manufacturerCode}", "delay 200", "zcl mfg-code ${manufacturerCode}",
"zcl global send-me-a-report 0xFC02 0x0013 0x29 1 3600 {0100}", "delay 200", "zcl global send-me-a-report 0xFC02 0x0013 0x29 1 3600 {01}",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500", "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
"zcl mfg-code ${manufacturerCode}", "delay 200", "zcl mfg-code ${manufacturerCode}",
"zcl global send-me-a-report 0xFC02 0x0014 0x29 1 3600 {0100}", "delay 200", "zcl global send-me-a-report 0xFC02 0x0014 0x29 1 3600 {01}",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500" "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500"
] ]
return configCmds + refresh() return configCmds + refresh()
} }
@@ -498,12 +519,17 @@ def enrollResponse() {
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200", "zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500", "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
//Enroll Response //Enroll Response
"raw 0x500 {01 23 00 00 00}", "delay 200", "raw 0x500 {01 23 00 00 00}",
"send 0x${device.deviceNetworkId} 1 1", "delay 200" "send 0x${device.deviceNetworkId} 1 1", "delay 200"
] ]
} }
private Map parseAxis(String description) { private Map parseAxis(String description) {
def hexToSignedInt = { hexVal ->
def unsignedVal = hexToInt(hexVal)
unsignedVal > 32767 ? unsignedVal - 65536 : unsignedVal
}
def z = hexToSignedInt(description[0..3]) def z = hexToSignedInt(description[0..3])
def y = hexToSignedInt(description[10..13]) def y = hexToSignedInt(description[10..13])
def x = hexToSignedInt(description[20..23]) def x = hexToSignedInt(description[20..23])
@@ -530,11 +556,6 @@ private Map parseAxis(String description) {
getXyzResult(xyzResults, description) getXyzResult(xyzResults, description)
} }
private hexToSignedInt(hexVal) {
def unsignedVal = hexToInt(hexVal)
unsignedVal > 32767 ? unsignedVal - 65536 : unsignedVal
}
def garageEvent(zValue) { def garageEvent(zValue) {
def absValue = zValue.abs() def absValue = zValue.abs()
def contactValue = null def contactValue = null
@@ -548,9 +569,15 @@ def garageEvent(zValue) {
garageValue = 'garage-open' garageValue = 'garage-open'
} }
if (contactValue != null){ if (contactValue != null){
def descriptionText = contactValue == 'open' ? '{{ device.displayName }} was opened' :'{{ device.displayName }} was closed' def linkText = getLinkText(device)
sendEvent(name: 'contact', value: contactValue, descriptionText: descriptionText, displayed:false, translatable: true) if ( contactValue == 'open' ) {
sendEvent(name: 'status', value: garageValue, descriptionText: descriptionText, translatable: true) descriptionText: '{{ device.displayName }} was opened'
sendEvent(name: 'contact', value: contactValue, descriptionText: '{{ device.displayName }} was opened', displayed:false, translatable: true)
sendEvent(name: 'status', value: garageValue, descriptionText: '{{ device.displayName }} status was opened', translatable: true)
} else {
sendEvent(name: 'contact', value: contactValue, descriptionText: '{{ device.displayName }} was closed', displayed:false, translatable: true)
sendEvent(name: 'status', value: garageValue, descriptionText: '{{ device.displayName }} status was closed', translatable: true)
}
} }
} }

View File

@@ -203,7 +203,7 @@ private List parseContactMessage(String description) {
parts.each { part -> parts.each { part ->
part = part.trim() part = part.trim()
if (part.startsWith('contactState:')) { if (part.startsWith('contactState:')) {
results.addAll(getContactResult(part, description)) results << getContactResult(part, description)
} }
else if (part.startsWith('accelerationState:')) { else if (part.startsWith('accelerationState:')) {
results << getAccelerationResult(part, description) results << getAccelerationResult(part, description)
@@ -316,7 +316,7 @@ private List getContactResult(part, description) {
results results
} }
private Map getAccelerationResult(part, description) { private getAccelerationResult(part, description) {
def name = "acceleration" def name = "acceleration"
def value = part.endsWith("1") ? "active" : "inactive" def value = part.endsWith("1") ? "active" : "inactive"
def linkText = getLinkText(device) def linkText = getLinkText(device)
@@ -335,7 +335,7 @@ private Map getAccelerationResult(part, description) {
] ]
} }
private Map getTempResult(part, description) { private getTempResult(part, description) {
def name = "temperature" def name = "temperature"
def temperatureScale = getTemperatureScale() def temperatureScale = getTemperatureScale()
def value = zigbee.parseSmartThingsTemperatureValue(part, "temp: ", temperatureScale) def value = zigbee.parseSmartThingsTemperatureValue(part, "temp: ", temperatureScale)
@@ -360,7 +360,7 @@ private Map getTempResult(part, description) {
] ]
} }
private Map getXyzResult(results, description) { private getXyzResult(results, description) {
def name = "threeAxis" def name = "threeAxis"
def value = "${results.x},${results.y},${results.z}" def value = "${results.x},${results.y},${results.z}"
def linkText = getLinkText(device) def linkText = getLinkText(device)
@@ -379,7 +379,7 @@ private Map getXyzResult(results, description) {
] ]
} }
private Map getBatteryResult(part, description) { private getBatteryResult(part, description) {
def batteryDivisor = description.split(",").find {it.split(":")[0].trim() == "batteryDivisor"} ? description.split(",").find {it.split(":")[0].trim() == "batteryDivisor"}.split(":")[1].trim() : null def batteryDivisor = description.split(",").find {it.split(":")[0].trim() == "batteryDivisor"} ? description.split(",").find {it.split(":")[0].trim() == "batteryDivisor"}.split(":")[1].trim() : null
def name = "battery" def name = "battery"
def value = zigbee.parseSmartThingsBatteryValue(part, batteryDivisor) def value = zigbee.parseSmartThingsBatteryValue(part, batteryDivisor)
@@ -400,7 +400,7 @@ private Map getBatteryResult(part, description) {
] ]
} }
private Map getRssiResult(part, description, lastHop=false) { private getRssiResult(part, description, lastHop=false) {
def name = lastHop ? "lastHopRssi" : "rssi" def name = lastHop ? "lastHopRssi" : "rssi"
def valueString = part.split(":")[1].trim() def valueString = part.split(":")[1].trim()
def value = (Integer.parseInt(valueString) - 128).toString() def value = (Integer.parseInt(valueString) - 128).toString()
@@ -431,7 +431,7 @@ private Map getRssiResult(part, description, lastHop=false) {
* Note: To make the signal strength indicator more accurate, we could combine * Note: To make the signal strength indicator more accurate, we could combine
* LQI with RSSI. * LQI with RSSI.
*/ */
private Map getLqiResult(part, description, lastHop=false) { private getLqiResult(part, description, lastHop=false) {
def name = lastHop ? "lastHopLqi" : "lqi" def name = lastHop ? "lastHopLqi" : "lqi"
def valueString = part.split(":")[1].trim() def valueString = part.split(":")[1].trim()
def percentageOf = 255 def percentageOf = 255

View File

@@ -56,17 +56,21 @@ metadata {
def parse(String description) { def parse(String description) {
log.debug "description is $description" log.debug "description is $description"
def event = zigbee.getEvent(description) def resultMap = zigbee.getKnownDescription(description)
if (event) { if (resultMap) {
log.info event log.info resultMap
if (event.name == "power") { if (resultMap.type == "update") {
log.info "$device updates: ${resultMap.value}"
}
else if (resultMap.type == "power") {
def powerValue
if (device.getDataValue("manufacturer") != "OSRAM") { //OSRAM devices do not reliably update power if (device.getDataValue("manufacturer") != "OSRAM") { //OSRAM devices do not reliably update power
event.value = (event.value as Integer) / 10 //TODO: The divisor value needs to be set as part of configuration powerValue = (resultMap.value as Integer)/10 //TODO: The divisor value needs to be set as part of configuration
sendEvent(event) sendEvent(name: "power", value: powerValue)
} }
} }
else { else {
sendEvent(event) sendEvent(name: resultMap.type, value: resultMap.value)
} }
} }
else { else {

View File

@@ -51,9 +51,15 @@ metadata {
def parse(String description) { def parse(String description) {
log.debug "description is $description" log.debug "description is $description"
def event = zigbee.getEvent(description) def resultMap = zigbee.getKnownDescription(description)
if (event) { if (resultMap) {
sendEvent(event) log.info resultMap
if (resultMap.type == "update") {
log.info "$device updates: ${resultMap.value}"
}
else {
sendEvent(name: resultMap.type, value: resultMap.value)
}
} }
else { else {
log.warn "DID NOT PARSE MESSAGE for description : $description" log.warn "DID NOT PARSE MESSAGE for description : $description"

View File

@@ -83,19 +83,32 @@ def uninstalled() {
} }
def configure() { def configure() {
/*
def cmds = def cmds =
zigbee.configureReporting(CLUSTER_DOORLOCK, DOORLOCK_ATTR_LOCKSTATE, zigbee.configSetup("${CLUSTER_DOORLOCK}", "${DOORLOCK_ATTR_LOCKSTATE}",
TYPE_ENUM8, 0, 3600, null) + "${TYPE_ENUM8}", 0, 3600, "{01}") +
zigbee.configureReporting(CLUSTER_POWER, POWER_ATTR_BATTERY_PERCENTAGE_REMAINING, zigbee.configSetup("${CLUSTER_POWER}", "${POWER_ATTR_BATTERY_PERCENTAGE_REMAINING}",
TYPE_U8, 600, 21600, 0x01) "${TYPE_U8}", 600, 21600, "{01}")
*/
def zigbeeId = device.zigbeeId
def cmds =
[
"zdo bind 0x${device.deviceNetworkId} 0x${device.endpointId} 1 ${CLUSTER_DOORLOCK} {$zigbeeId} {}", "delay 200",
"zcl global send-me-a-report ${CLUSTER_DOORLOCK} ${DOORLOCK_ATTR_LOCKSTATE} ${TYPE_ENUM8} 0 3600 {01}", "delay 200",
"send 0x${device.deviceNetworkId} 1 0x${device.endpointId}", "delay 200",
"zdo bind 0x${device.deviceNetworkId} 0x${device.endpointId} 1 ${CLUSTER_POWER} {$zigbeeId} {}", "delay 200",
"zcl global send-me-a-report ${CLUSTER_POWER} ${POWER_ATTR_BATTERY_PERCENTAGE_REMAINING} ${TYPE_U8} 600 21600 {01}", "delay 200",
"send 0x${device.deviceNetworkId} 1 0x${device.endpointId}", "delay 200",
]
log.info "configure() --- cmds: $cmds" log.info "configure() --- cmds: $cmds"
return cmds + refresh() // send refresh cmds as part of config return cmds + refresh() // send refresh cmds as part of config
} }
def refresh() { def refresh() {
def cmds = def cmds =
zigbee.readAttribute(CLUSTER_DOORLOCK, DOORLOCK_ATTR_LOCKSTATE) + zigbee.refreshData("${CLUSTER_DOORLOCK}", "${DOORLOCK_ATTR_LOCKSTATE}") +
zigbee.readAttribute(CLUSTER_POWER, POWER_ATTR_BATTERY_PERCENTAGE_REMAINING) zigbee.refreshData("${CLUSTER_POWER}", "${POWER_ATTR_BATTERY_PERCENTAGE_REMAINING}")
log.info "refresh() --- cmds: $cmds" log.info "refresh() --- cmds: $cmds"
return cmds return cmds
} }
@@ -108,27 +121,34 @@ def parse(String description) {
map = parseReportAttributeMessage(description) map = parseReportAttributeMessage(description)
} }
log.debug "parse() --- Parse returned $map"
def result = map ? createEvent(map) : null def result = map ? createEvent(map) : null
log.debug "parse() --- returned: $result"
return result return result
} }
// Lock capability commands // Lock capability commands
def lock() { def lock() {
def cmds = zigbee.command(CLUSTER_DOORLOCK, DOORLOCK_CMD_LOCK_DOOR) //def cmds = zigbee.zigbeeCommand("${CLUSTER_DOORLOCK}", "${DOORLOCK_CMD_LOCK_DOOR}", "{}")
log.info "lock() -- cmds: $cmds" //log.info "lock() -- cmds: $cmds"
return cmds //return cmds
"st cmd 0x${device.deviceNetworkId} 0x${device.endpointId} ${CLUSTER_DOORLOCK} ${DOORLOCK_CMD_LOCK_DOOR} {}"
} }
def unlock() { def unlock() {
def cmds = zigbee.command(CLUSTER_DOORLOCK, DOORLOCK_CMD_UNLOCK_DOOR) //def cmds = zigbee.zigbeeCommand("${CLUSTER_DOORLOCK}", "${DOORLOCK_CMD_UNLOCK_DOOR}", "{}")
log.info "unlock() -- cmds: $cmds" //log.info "unlock() -- cmds: $cmds"
return cmds //return cmds
"st cmd 0x${device.deviceNetworkId} 0x${device.endpointId} ${CLUSTER_DOORLOCK} ${DOORLOCK_CMD_UNLOCK_DOOR} {}"
} }
// Private methods // Private methods
private Map parseReportAttributeMessage(String description) { private Map parseReportAttributeMessage(String description) {
log.trace "parseReportAttributeMessage() --- description: $description"
Map descMap = zigbee.parseDescriptionAsMap(description) Map descMap = zigbee.parseDescriptionAsMap(description)
log.debug "parseReportAttributeMessage() --- descMap: $descMap"
Map resultMap = [:] Map resultMap = [:]
if (descMap.clusterInt == CLUSTER_POWER && descMap.attrInt == POWER_ATTR_BATTERY_PERCENTAGE_REMAINING) { if (descMap.clusterInt == CLUSTER_POWER && descMap.attrInt == POWER_ATTR_BATTERY_PERCENTAGE_REMAINING) {
resultMap.name = "battery" resultMap.name = "battery"
@@ -136,24 +156,18 @@ private Map parseReportAttributeMessage(String description) {
if (device.getDataValue("manufacturer") == "Yale") { //Handling issue with Yale locks incorrect battery reporting if (device.getDataValue("manufacturer") == "Yale") { //Handling issue with Yale locks incorrect battery reporting
resultMap.value = Integer.parseInt(descMap.value, 16) resultMap.value = Integer.parseInt(descMap.value, 16)
} }
log.info "parseReportAttributeMessage() --- battery: ${resultMap.value}"
} }
else if (descMap.clusterInt == CLUSTER_DOORLOCK && descMap.attrInt == DOORLOCK_ATTR_LOCKSTATE) { else if (descMap.clusterInt == CLUSTER_DOORLOCK && descMap.attrInt == DOORLOCK_ATTR_LOCKSTATE) {
def value = Integer.parseInt(descMap.value, 16) def value = Integer.parseInt(descMap.value, 16)
def linkText = getLinkText(device)
resultMap.name = "lock" resultMap.name = "lock"
if (value == 0) { resultMap.putAll([0:["value":"unknown",
resultMap.value = "unknown" "descriptionText":"Not fully locked"],
resultMap.descriptionText = "${linkText} is not fully locked" 1:["value":"locked"],
} else if (value == 1) { 2:["value":"unlocked"]].get(value,
resultMap.value = "locked" ["value":"unknown",
resultMap.descriptionText = "${linkText} is locked" "descriptionText":"Unknown lock state"]))
} else if (value == 2) { log.info "parseReportAttributeMessage() --- lock: ${resultMap.value}"
resultMap.value = "unlocked"
resultMap.descriptionText = "${linkText} is unlocked"
} else {
resultMap.value = "unknown"
resultMap.descriptionText = "${linkText} is in unknown lock state"
}
} }
else { else {
log.debug "parseReportAttributeMessage() --- ignoring attribute" log.debug "parseReportAttributeMessage() --- ignoring attribute"

View File

@@ -51,15 +51,22 @@ metadata {
// Parse incoming device messages to generate events // Parse incoming device messages to generate events
def parse(String description) { def parse(String description) {
log.debug "description is $description" log.debug "description is $description"
def event = zigbee.getEvent(description)
if (event) { def resultMap = zigbee.getKnownDescription(description)
if (event.name == "power") { if (resultMap) {
log.info resultMap
if (resultMap.type == "update") {
log.info "$device updates: ${resultMap.value}"
}
else if (resultMap.type == "power") {
def powerValue def powerValue
powerValue = (event.value as Integer)/10 //TODO: The divisor value needs to be set as part of configuration if (device.getDataValue("manufacturer") != "OSRAM") { //OSRAM devices do not reliably update power
sendEvent(name: "power", value: powerValue) powerValue = (resultMap.value as Integer)/10 //TODO: The divisor value needs to be set as part of configuration
sendEvent(name: "power", value: powerValue)
}
} }
else { else {
sendEvent(event) sendEvent(name: resultMap.type, value: resultMap.value)
} }
} }
else { else {

View File

@@ -53,9 +53,16 @@ metadata {
// Parse incoming device messages to generate events // Parse incoming device messages to generate events
def parse(String description) { def parse(String description) {
log.debug "description is $description" log.debug "description is $description"
def event = zigbee.getEvent(description)
if (event) { def resultMap = zigbee.getKnownDescription(description)
sendEvent(event) if (resultMap) {
log.info resultMap
if (resultMap.type == "update") {
log.info "$device updates: ${resultMap.value}"
}
else {
sendEvent(name: resultMap.type, value: resultMap.value)
}
} }
else { else {
log.warn "DID NOT PARSE MESSAGE for description : $description" log.warn "DID NOT PARSE MESSAGE for description : $description"

View File

@@ -73,9 +73,16 @@ metadata {
// Parse incoming device messages to generate events // Parse incoming device messages to generate events
def parse(String description) { def parse(String description) {
log.debug "description is $description" log.debug "description is $description"
def event = zigbee.getEvent(description)
if (event) { def finalResult = zigbee.getKnownDescription(description)
sendEvent(event) if (finalResult) {
log.info finalResult
if (finalResult.type == "update") {
log.info "$device updates: ${finalResult.value}"
}
else {
sendEvent(name: finalResult.type, value: finalResult.value)
}
} }
else { else {
log.warn "DID NOT PARSE MESSAGE for description : $description" log.warn "DID NOT PARSE MESSAGE for description : $description"

View File

@@ -41,7 +41,6 @@ metadata {
attributeState "locked", label:'locked', action:"lock.unlock", icon:"st.locks.lock.locked", backgroundColor:"#79b821", nextState:"unlocking" attributeState "locked", label:'locked', action:"lock.unlock", icon:"st.locks.lock.locked", backgroundColor:"#79b821", nextState:"unlocking"
attributeState "unlocked", label:'unlocked', action:"lock.lock", icon:"st.locks.lock.unlocked", backgroundColor:"#ffffff", nextState:"locking" attributeState "unlocked", label:'unlocked', action:"lock.lock", icon:"st.locks.lock.unlocked", backgroundColor:"#ffffff", nextState:"locking"
attributeState "unknown", label:"unknown", action:"lock.lock", icon:"st.locks.lock.unknown", backgroundColor:"#ffffff", nextState:"locking" attributeState "unknown", label:"unknown", action:"lock.lock", icon:"st.locks.lock.unknown", backgroundColor:"#ffffff", nextState:"locking"
attributeState "jammed", label:"jammed", action:"lock.lock", icon:"st.locks.lock.unknown", backgroundColor:"#ffffff", nextState:"locking"
attributeState "locking", label:'locking', icon:"st.locks.lock.locked", backgroundColor:"#79b821" attributeState "locking", label:'locking', icon:"st.locks.lock.locked", backgroundColor:"#79b821"
attributeState "unlocking", label:'unlocking', icon:"st.locks.lock.unlocked", backgroundColor:"#ffffff" attributeState "unlocking", label:'unlocking', icon:"st.locks.lock.unlocked", backgroundColor:"#ffffff"
} }
@@ -131,7 +130,7 @@ def zwaveEvent(DoorLockOperationReport cmd) {
if (cmd.doorLockMode == 0xFF) { if (cmd.doorLockMode == 0xFF) {
map.value = "locked" map.value = "locked"
} else if (cmd.doorLockMode >= 0x40) { } else if (cmd.doorLockMode >= 0x40) {
map.value = "unknown" // XXX: Jammed? map.value = "unknown"
} else if (cmd.doorLockMode & 1) { } else if (cmd.doorLockMode & 1) {
map.value = "unlocked with timeout" map.value = "unlocked with timeout"
} else { } else {
@@ -181,7 +180,7 @@ def zwaveEvent(physicalgraph.zwave.commands.alarmv2.AlarmReport cmd) {
map = [ name: "lock", value: "unknown", descriptionText: "$device.displayName was not locked fully" ] map = [ name: "lock", value: "unknown", descriptionText: "$device.displayName was not locked fully" ]
break break
case 0xB: case 0xB:
map = [ name: "lock", value: "jammed", descriptionText: "$device.displayName is jammed", displayed: true, eventType: "ALERT" ] map = [ name: "lock", value: "unknown", descriptionText: "$device.displayName is jammed" ]
break break
case 0xC: case 0xC:
map = [ name: "codeChanged", value: "all", descriptionText: "$device.displayName: all user codes deleted", isStateChange: true ] map = [ name: "codeChanged", value: "all", descriptionText: "$device.displayName: all user codes deleted", isStateChange: true ]
@@ -267,7 +266,7 @@ def zwaveEvent(physicalgraph.zwave.commands.alarmv2.AlarmReport cmd) {
case 17: case 17:
case 23: case 23:
case 26: case 26:
map = [ name: "lock", value: "jammed", descriptionText: "$device.displayName bolt is jammed", displayed: true, eventType: "ALERT" ] map = [ name: "lock", value: "unknown", descriptionText: "$device.displayName bolt is jammed" ]
break break
case 13: case 13:
map = [ name: "codeChanged", value: cmd.alarmLevel, descriptionText: "$device.displayName code $cmd.alarmLevel was added", isStateChange: true ] map = [ name: "codeChanged", value: cmd.alarmLevel, descriptionText: "$device.displayName code $cmd.alarmLevel was added", isStateChange: true ]

View File

@@ -57,7 +57,7 @@ def parse(String description) {
return result return result
} }
def sensorValueEvent(value) { def sensorValueEvent(Short value) {
if (value) { if (value) {
createEvent(name: "motion", value: "active", descriptionText: "$device.displayName detected motion") createEvent(name: "motion", value: "active", descriptionText: "$device.displayName detected motion")
} else { } else {
@@ -94,24 +94,24 @@ def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cm
{ {
def result = [] def result = []
if (cmd.notificationType == 0x07) { if (cmd.notificationType == 0x07) {
if (cmd.v1AlarmType == 0x07) { // special case for nonstandard messages from Monoprice ensors if (cmd.event == 0x01 || cmd.event == 0x02) {
result << sensorValueEvent(cmd.v1AlarmLevel)
} else if (cmd.event == 0x01 || cmd.event == 0x02 || cmd.event == 0x07 || cmd.event == 0x08) {
result << sensorValueEvent(1) result << sensorValueEvent(1)
} else if (cmd.event == 0x00) {
result << sensorValueEvent(0)
} else if (cmd.event == 0x03) { } else if (cmd.event == 0x03) {
result << createEvent(name: "tamper", value: "detected", descriptionText: "$device.displayName covering was removed", isStateChange: true) result << createEvent(descriptionText: "$device.displayName covering was removed", isStateChange: true)
result << response(zwave.batteryV1.batteryGet()) result << response(zwave.wakeUpV1.wakeUpIntervalSet(seconds:4*3600, nodeid:zwaveHubNodeId))
if(!state.MSR) result << response(zwave.manufacturerSpecificV2.manufacturerSpecificGet())
} else if (cmd.event == 0x05 || cmd.event == 0x06) { } else if (cmd.event == 0x05 || cmd.event == 0x06) {
result << createEvent(descriptionText: "$device.displayName detected glass breakage", isStateChange: true) result << createEvent(descriptionText: "$device.displayName detected glass breakage", isStateChange: true)
} else if (cmd.event == 0x07) {
if(!state.MSR) result << response(zwave.manufacturerSpecificV2.manufacturerSpecificGet())
result << sensorValueEvent(1)
} }
} else if (cmd.notificationType) { } else if (cmd.notificationType) {
def text = "Notification $cmd.notificationType: event ${([cmd.event] + cmd.eventParameter).join(", ")}" def text = "Notification $cmd.notificationType: event ${([cmd.event] + cmd.eventParameter).join(", ")}"
result << createEvent(name: "notification$cmd.notificationType", value: "$cmd.event", descriptionText: text, isStateChange: true, displayed: false) result << createEvent(name: "notification$cmd.notificationType", value: "$cmd.event", descriptionText: text, displayed: false)
} else { } else {
def value = cmd.v1AlarmLevel == 255 ? "active" : cmd.v1AlarmLevel ?: "inactive" def value = cmd.v1AlarmLevel == 255 ? "active" : cmd.v1AlarmLevel ?: "inactive"
result << createEvent(name: "alarm $cmd.v1AlarmType", value: value, isStateChange: true, displayed: false) result << createEvent(name: "alarm $cmd.v1AlarmType", value: value, displayed: false)
} }
result result
} }

View File

@@ -61,44 +61,37 @@ def parse(String description) {
zwaveEvent(cmd, results) zwaveEvent(cmd, results)
} }
} }
log.debug "'$description' parsed to ${results.inspect()}" // log.debug "\"$description\" parsed to ${results.inspect()}"
return results return results
} }
def createSmokeOrCOEvents(name, results) { def createSmokeOrCOEvents(name, results) {
def text = null def text = null
switch (name) { if (name == "smoke") {
case "smoke": text = "$device.displayName smoke was detected!"
text = "$device.displayName smoke was detected!" // these are displayed:false because the composite event is the one we want to see in the app
// these are displayed:false because the composite event is the one we want to see in the app results << createEvent(name: "smoke", value: "detected", descriptionText: text, displayed: false)
results << createEvent(name: "smoke", value: "detected", descriptionText: text, displayed: false) } else if (name == "carbonMonoxide") {
break text = "$device.displayName carbon monoxide was detected!"
case "carbonMonoxide": results << createEvent(name: "carbonMonoxide", value: "detected", descriptionText: text, displayed: false)
text = "$device.displayName carbon monoxide was detected!" } else if (name == "tested") {
results << createEvent(name: "carbonMonoxide", value: "detected", descriptionText: text, displayed: false) text = "$device.displayName was tested"
break results << createEvent(name: "smoke", value: "tested", descriptionText: text, displayed: false)
case "tested": results << createEvent(name: "carbonMonoxide", value: "tested", descriptionText: text, displayed: false)
text = "$device.displayName was tested" } else if (name == "smokeClear") {
results << createEvent(name: "smoke", value: "tested", descriptionText: text, displayed: false) text = "$device.displayName smoke is clear"
results << createEvent(name: "carbonMonoxide", value: "tested", descriptionText: text, displayed: false) results << createEvent(name: "smoke", value: "clear", descriptionText: text, displayed: false)
break name = "clear"
case "smokeClear": } else if (name == "carbonMonoxideClear") {
text = "$device.displayName smoke is clear" text = "$device.displayName carbon monoxide is clear"
results << createEvent(name: "smoke", value: "clear", descriptionText: text, displayed: false) results << createEvent(name: "carbonMonoxide", value: "clear", descriptionText: text, displayed: false)
name = "clear" name = "clear"
break } else if (name == "testClear") {
case "carbonMonoxideClear": text = "$device.displayName smoke is clear"
text = "$device.displayName carbon monoxide is clear" results << createEvent(name: "smoke", value: "clear", descriptionText: text, displayed: false)
results << createEvent(name: "carbonMonoxide", value: "clear", descriptionText: text, displayed: false) results << createEvent(name: "carbonMonoxide", value: "clear", displayed: false)
name = "clear" name = "clear"
break
case "testClear":
text = "$device.displayName test cleared"
results << createEvent(name: "smoke", value: "clear", descriptionText: text, displayed: false)
results << createEvent(name: "carbonMonoxide", value: "clear", displayed: false)
name = "clear"
break
} }
// This composite event is used for updating the tile // This composite event is used for updating the tile
results << createEvent(name: "alarmState", value: name, descriptionText: text) results << createEvent(name: "alarmState", value: name, descriptionText: text)
@@ -124,10 +117,8 @@ def zwaveEvent(physicalgraph.zwave.commands.alarmv2.AlarmReport cmd, results) {
createSmokeOrCOEvents(cmd.alarmLevel ? "tested" : "testClear", results) createSmokeOrCOEvents(cmd.alarmLevel ? "tested" : "testClear", results)
break break
case 13: // sent every hour -- not sure what this means, just a wake up notification? case 13: // sent every hour -- not sure what this means, just a wake up notification?
if (cmd.alarmLevel == 255) { if (cmd.alarmLevel != 255) {
results << createEvent(descriptionText: "$device.displayName checked in", isStateChange: false) results << createEvent(descriptionText: "$device.displayName code 13 is $cmd.alarmLevel", displayed: true)
} else {
results << createEvent(descriptionText: "$device.displayName code 13 is $cmd.alarmLevel", isStateChange:true, displayed:false)
} }
// Clear smoke in case they pulled batteries and we missed the clear msg // Clear smoke in case they pulled batteries and we missed the clear msg
@@ -136,8 +127,9 @@ def zwaveEvent(physicalgraph.zwave.commands.alarmv2.AlarmReport cmd, results) {
} }
// Check battery if we don't have a recent battery event // Check battery if we don't have a recent battery event
if (!state.lastbatt || (now() - state.lastbatt) >= 48*60*60*1000) { def prevBattery = device.currentState("battery")
results << response(zwave.batteryV1.batteryGet()) if (!prevBattery || (new Date().time - prevBattery.date.time)/60000 >= 60 * 53) {
results << new physicalgraph.device.HubAction(zwave.batteryV1.batteryGet().format())
} }
break break
default: default:
@@ -166,17 +158,12 @@ def zwaveEvent(physicalgraph.zwave.commands.sensoralarmv1.SensorAlarmReport cmd,
} }
def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd, results) { def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd, results) {
results << new physicalgraph.device.HubAction(zwave.wakeUpV1.wakeUpNoMoreInformation().format())
results << createEvent(descriptionText: "$device.displayName woke up", isStateChange: false) results << createEvent(descriptionText: "$device.displayName woke up", isStateChange: false)
if (!state.lastbatt || (now() - state.lastbatt) >= 56*60*60*1000) {
results << response(zwave.batteryV1.batteryGet(), "delay 2000", zwave.wakeUpV1.wakeUpNoMoreInformation())
} else {
results << response(zwave.wakeUpV1.wakeUpNoMoreInformation())
}
} }
def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd, results) { def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd, results) {
def map = [ name: "battery", unit: "%", isStateChange: true ] def map = [ name: "battery", unit: "%" ]
state.lastbatt = now()
if (cmd.batteryLevel == 0xFF) { if (cmd.batteryLevel == 0xFF) {
map.value = 1 map.value = 1
map.descriptionText = "$device.displayName battery is low!" map.descriptionText = "$device.displayName battery is low!"

View File

@@ -67,11 +67,11 @@ mappings {
def listAllDevices() { def listAllDevices() {
def resp = [] def resp = []
switches.each { switches.each {
resp << [name: it.name, label: it.label, value: it.currentValue("switch"), type: "switch", id: it.id, hub: it.hub?.name] resp << [name: it.name, label: it.label, value: it.currentValue("switch"), type: "switch", id: it.id, hub: it.hub.name]
} }
locks.each { locks.each {
resp << [name: it.name, label: it.label, value: it.currentValue("lock"), type: "lock", id: it.id, hub: it.hub?.name] resp << [name: it.name, label: it.label, value: it.currentValue("lock"), type: "lock", id: it.id, hub: it.hub.name]
} }
return resp return resp
} }

View File

@@ -469,6 +469,7 @@ def pollChild(){
if (pollChildren()){ if (pollChildren()){
devices.each { child -> devices.each { child ->
log.info "***found $child"
if (!child.device.deviceNetworkId.startsWith("ecobee_sensor")){ if (!child.device.deviceNetworkId.startsWith("ecobee_sensor")){
if(atomicState.thermostats[child.device.deviceNetworkId] != null) { if(atomicState.thermostats[child.device.deviceNetworkId] != null) {
def tData = atomicState.thermostats[child.device.deviceNetworkId] def tData = atomicState.thermostats[child.device.deviceNetworkId]

View File

@@ -24,7 +24,7 @@ definition(
category: "SmartThings Labs", category: "SmartThings Labs",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/hue.png", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/hue.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/hue@2x.png", iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/hue@2x.png",
singleInstance: true //singleInstance: true
) )
preferences { preferences {
@@ -68,7 +68,7 @@ def bridgeDiscovery(params=[:])
} }
//setup.xml request every 3 seconds except on discoveries //setup.xml request every 3 seconds except on discoveries
if(((bridgeRefreshCount % 3) == 0) && ((bridgeRefreshCount % 5) != 0)) { if(((bridgeRefreshCount % 1) == 0) && ((bridgeRefreshCount % 5) != 0)) {
verifyHueBridges() verifyHueBridges()
} }
@@ -175,7 +175,6 @@ private discoverHueBulbs() {
} }
private verifyHueBridge(String deviceNetworkId, String host) { private verifyHueBridge(String deviceNetworkId, String host) {
log.trace "Verify Hue Bridge $deviceNetworkId"
sendHubCommand(new physicalgraph.device.HubAction([ sendHubCommand(new physicalgraph.device.HubAction([
method: "GET", method: "GET",
path: "/description.xml", path: "/description.xml",
@@ -603,20 +602,6 @@ def parse(childDevice, description) {
} }
} }
def hubVerification(bodytext) {
log.trace "Bridge sent back description.xml for verification"
def body = new XmlSlurper().parseText(bodytext)
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"
}
}
}
def on(childDevice) { def on(childDevice) {
log.debug "Executing 'on'" log.debug "Executing 'on'"
put("lights/${getId(childDevice)}/state", [on: true]) put("lights/${getId(childDevice)}/state", [on: true])
@@ -657,53 +642,35 @@ def setColorTemperature(childDevice, huesettings) {
} }
def setColor(childDevice, huesettings) { def setColor(childDevice, huesettings) {
log.debug "Executing 'setColor($huesettings)'" log.debug "Executing 'setColor($huesettings)'"
def hue = Math.min(Math.round(huesettings.hue * 65535 / 100), 65535)
def sat = Math.min(Math.round(huesettings.saturation * 255 / 100), 255)
def alert = huesettings.alert ? huesettings.alert : "none"
def transition = huesettings.transition ? huesettings.transition : 4
def value = [:] def value = [sat: sat, hue: hue, alert: alert, transitiontime: transition]
def hue = null if (huesettings.level != null) {
def sat = null if (huesettings.level == 1) value.bri = 1 else value.bri = Math.min(Math.round(huesettings.level * 255 / 100), 255)
def xy = null value.on = value.bri > 0
}
if (huesettings.hex != null) { if (huesettings.switch) {
value.xy = getHextoXY(huesettings.hex) value.on = huesettings.switch == "on"
} else { }
if (huesettings.hue != null)
value.hue = Math.min(Math.round(huesettings.hue * 65535 / 100), 65535)
if (huesettings.saturation != null)
value.sat = Math.min(Math.round(huesettings.saturation * 255 / 100), 255)
}
// Default behavior is to turn light on log.debug "sending command $value"
value.on = true put("lights/${getId(childDevice)}/state", value)
if (huesettings.level != null) {
if (huesettings.level <= 0)
value.on = false
else if (huesettings.level == 1)
value.bri = 1
else
value.bri = Math.min(Math.round(huesettings.level * 255 / 100), 255)
}
value.alert = huesettings.alert ? huesettings.alert : "none"
value.transition = huesettings.transition ? huesettings.transition : 4
// Make sure to turn off light if requested
if (huesettings.switch == "off")
value.on = false
log.debug "sending command $value"
put("lights/${getId(childDevice)}/state", value)
return "Color set to $value"
} }
def nextLevel(childDevice) { def nextLevel(childDevice) {
def level = device.latestValue("level") as Integer ?: 0 def level = device.latestValue("level") as Integer ?: 0
if (level < 100) { if (level < 100) {
level = Math.min(25 * (Math.round(level / 25) + 1), 100) as Integer level = Math.min(25 * (Math.round(level / 25) + 1), 100) as Integer
} else { }
level = 25 else {
} level = 25
setLevel(childDevice,level) }
setLevel(childDevice,level)
} }
private getId(childDevice) { private getId(childDevice) {
@@ -776,57 +743,6 @@ private getBridgeIP() {
return host return host
} }
private getHextoXY(String colorStr) {
// For the hue bulb the corners of the triangle are:
// -Red: 0.675, 0.322
// -Green: 0.4091, 0.518
// -Blue: 0.167, 0.04
def cred = Integer.valueOf( colorStr.substring( 1, 3 ), 16 )
def cgreen = Integer.valueOf( colorStr.substring( 3, 5 ), 16 )
def cblue = Integer.valueOf( colorStr.substring( 5, 7 ), 16 )
double[] normalizedToOne = new double[3];
normalizedToOne[0] = (cred / 255);
normalizedToOne[1] = (cgreen / 255);
normalizedToOne[2] = (cblue / 255);
float red, green, blue;
// Make red more vivid
if (normalizedToOne[0] > 0.04045) {
red = (float) Math.pow(
(normalizedToOne[0] + 0.055) / (1.0 + 0.055), 2.4);
} else {
red = (float) (normalizedToOne[0] / 12.92);
}
// Make green more vivid
if (normalizedToOne[1] > 0.04045) {
green = (float) Math.pow((normalizedToOne[1] + 0.055) / (1.0 + 0.055), 2.4);
} else {
green = (float) (normalizedToOne[1] / 12.92);
}
// Make blue more vivid
if (normalizedToOne[2] > 0.04045) {
blue = (float) Math.pow((normalizedToOne[2] + 0.055) / (1.0 + 0.055), 2.4);
} else {
blue = (float) (normalizedToOne[2] / 12.92);
}
float X = (float) (red * 0.649926 + green * 0.103455 + blue * 0.197109);
float Y = (float) (red * 0.234327 + green * 0.743075 + blue * 0.022598);
float Z = (float) (red * 0.0000000 + green * 0.053077 + blue * 1.035763);
float x = (X != 0 ? X / (X + Y + Z) : 0);
float y = (Y != 0 ? Y / (X + Y + Z) : 0);
double[] xy = new double[2];
xy[0] = x;
xy[1] = y;
return xy;
}
private Integer convertHexToInt(hex) { private Integer convertHexToInt(hex) {
Integer.parseInt(hex,16) Integer.parseInt(hex,16)
} }

View File

@@ -0,0 +1,62 @@
/**
* Copyright 2015 SmartThings
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
* for the specific language governing permissions and limitations under the License.
*
* CoolDeep
*
* Author: CoolDeep
*/
definition(
name: "Smart Timer",
namespace: "smartthings",
author: "CoolDeep",
description: "If a light/device is already on, leave it on. Else, if a light/device switches on on a trigger, switch off in a given time",
category: "My Apps",
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 a sesonsor triggers(turns on)...") {
input "mySwitch", "capability.switch", title: "Switches", multiple: true, required: true
input "mySensor", "capability.contactSensor", title: "Sensors", multiple: true, required: true
}
section("Turn it off after how many minutes?") {
input "minutesLater", "decimal", title: "When?"
}
}
def installed() {
log.debug "Installed with settings: ${settings}"
subscribe(mySensor, "contact.opened", myHandler)
}
def updated() {
log.debug "Updated with settings: ${settings}"
unsubscribe()
subscribe(mySensor, "contact.open", myHandler)
}
def myHandler(evt) {
def lightVal = mySwitch.currentSwitch
log.debug ">> TheSwitch current value turned ${lightVal}"
if (lightVal.contains("off")) {
def delay = minutesLater * 60
log.debug ">>> Turning off in ${minutesLater} minutes (${delay}seconds)"
runIn(delay, turnOffSwitch)
}
}
def turnOffSwitch() {
mySwitch.off()
}

View File

@@ -76,7 +76,7 @@ def mainPage() {
} }
section{ section{
input "actionType", "enum", title: "Action?", required: true, defaultValue: "Bell 1", options: [ input "actionType", "enum", title: "Action?", required: true, defaultValue: "Bell 1", options: [
"Custom Message", //"Custom Message",
"Bell 1", "Bell 1",
"Bell 2", "Bell 2",
"Dogs Barking", "Dogs Barking",
@@ -89,7 +89,7 @@ def mainPage() {
"Someone is arriving", "Someone is arriving",
"Piano", "Piano",
"Lightsaber"] "Lightsaber"]
input "message","text",title:"Play this message", required:false, multiple: false //input "message","text",title:"Play this message", required:false, multiple: false
} }
section { section {
input "sonos", "capability.musicPlayer", title: "On this Speaker player", required: true input "sonos", "capability.musicPlayer", title: "On this Speaker player", required: true
@@ -408,15 +408,13 @@ private loadText() {
case "Lightsaber": case "Lightsaber":
state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/lightsaber.mp3", duration: "10"] state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/lightsaber.mp3", duration: "10"]
break; break;
case "Custom Message": default:
if (message) { /*if (message) {
state.sound = textToSpeech(message instanceof List ? message[0] : message) // not sure why this is (sometimes) needed) state.sound = textToSpeech(message instanceof List ? message[0] : message) // not sure why this is (sometimes) needed)
} }
else { else {
state.sound = textToSpeech("You selected the custom message option but did not enter a message in the $app.label Smart App") state.sound = textToSpeech("You selected the custom message option but did not enter a message in the $app.label Smart App")
} }*/
break;
default:
state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/bell1.mp3", duration: "10"] state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/bell1.mp3", duration: "10"]
break; break;
} }

View File

@@ -234,7 +234,7 @@ def addSwitches() {
def d def d
if (selectedSwitch) { if (selectedSwitch) {
d = getChildDevices()?.find { d = getChildDevices()?.find {
it.deviceNetworkId == selectedSwitch.value.mac || it.device.getDataValue("mac") == selectedSwitch.value.mac it.dni == selectedSwitch.value.mac || it.device.getDataValue("mac") == selectedSwitch.value.mac
} }
} }
@@ -265,7 +265,7 @@ def addMotions() {
def d def d
if (selectedMotion) { if (selectedMotion) {
d = getChildDevices()?.find { d = getChildDevices()?.find {
it.deviceNetworkId == selectedMotion.value.mac || it.device.getDataValue("mac") == selectedMotion.value.mac it.dni == selectedMotion.value.mac || it.device.getDataValue("mac") == selectedMotion.value.mac
} }
} }
@@ -296,7 +296,7 @@ def addLightSwitches() {
def d def d
if (selectedLightSwitch) { if (selectedLightSwitch) {
d = getChildDevices()?.find { d = getChildDevices()?.find {
it.deviceNetworkId == selectedLightSwitch.value.mac || it.device.getDataValue("mac") == selectedLightSwitch.value.mac it.dni == selectedLightSwitch.value.mac || it.device.getDataValue("mac") == selectedLightSwitch.value.mac
} }
} }

View File

@@ -1,17 +1,11 @@
/** /**
* Vacation Lighting Director * Vacation Lighting Director
* *
* Version 2.5 - Moved scheduling over to Cron and added time as a trigger. * Version 2.4 - Added information paragraphs
* Cleaned up formatting and some typos.
* Updated license.
* Made people option optional
* Added sttement to unschedule on mode change if people option is not selected
*
* Version 2.4 - Added information paragraphs
* *
* Source code can be found here: https://github.com/tslagle13/SmartThings/blob/master/smartapps/tslagle13/vacation-lighting-director.groovy * Source code can be found here: https://github.com/tslagle13/SmartThings/blob/master/smartapps/tslagle13/vacation-lighting-director.groovy
* *
* Copyright 2016 Tim Slagle * Copyright 2015 Tim Slagle
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * 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: * in compliance with the License. You may obtain a copy of the License at:
@@ -57,7 +51,8 @@ def pageSetup() {
return dynamicPage(pageProperties) { return dynamicPage(pageProperties) {
section(""){ section(""){
paragraph "This app can be used to make your home seem occupied anytime you are away from your home. " + paragraph "This app can be used to make your home seem occupied anytime you are away from your home. " +
"Please use each of the the sections below to setup the different preferences to your liking. " "Please use each othe the sections below to setup the different preferences to your liking. " +
"I recommend this app be used with at least two away modes. An example would be 'Away Day' 'and Away Night'. "
} }
section("Setup Menu") { section("Setup Menu") {
href "Setup", title: "Setup", description: "", state:greyedOut() href "Setup", title: "Setup", description: "", state:greyedOut()
@@ -75,7 +70,7 @@ def Setup() {
def newMode = [ def newMode = [
name: "newMode", name: "newMode",
type: "mode", type: "mode",
title: "Modes", title: "Which?",
multiple: true, multiple: true,
required: true required: true
] ]
@@ -101,6 +96,14 @@ def Setup() {
required: true, required: true,
] ]
def people = [
name: "people",
type: "capability.presenceSensor",
title: "If these people are home do not change light status",
required: true,
multiple: true
]
def pageName = "Setup" def pageName = "Setup"
def pageProperties = [ def pageProperties = [
@@ -113,11 +116,10 @@ def Setup() {
section(""){ section(""){
paragraph "In this section you need to setup the deatils of how you want your lighting to be affected while " + paragraph "In this section you need to setup the deatils of how you want your lighting to be affected while " +
"you are away. All of these settings are required in order for the simulator to run correctly." paragraph "you are away. All of these settings are required in order for the simulator to run correctly."
} }
section("Simulator Triggers") { section("Which mode change triggers the simulator? (This app will only run in selected mode(s))") {
input newMode input newMode
href "timeIntervalInput", title: "Times", description: timeIntervalLabel(), refreshAfterSelection:true
} }
section("Light switches to turn on/off") { section("Light switches to turn on/off") {
input switches input switches
@@ -128,6 +130,9 @@ def Setup() {
section("Number of active lights at any given time") { section("Number of active lights at any given time") {
input number_of_active_lights input number_of_active_lights
} }
section("People") {
input people
}
} }
} }
@@ -158,28 +163,17 @@ def Settings() {
nextPage: "pageSetup" nextPage: "pageSetup"
] ]
def people = [
name: "people",
type: "capability.presenceSensor",
title: "If these people are home do not change light status",
required: false,
multiple: true
]
return dynamicPage(pageProperties) { return dynamicPage(pageProperties) {
section(""){ section(""){
paragraph "In this section you can restrict how your simulator runs. For instance you can restrict on which days it will run " + paragraph "In this section you can restrict how your simulator runs. For instance you can restrict on which days it will run " +
"as well as a delay for the simulator to start after it is in the correct mode. Delaying the simulator helps with false starts based on a incorrect mode change." paragraph "as well as a delay for the simulator to start after it is in the correct mode. Delaying the simulator helps with false starts based on a incorrect mode change."
} }
section("Delay to start simulator") { section("Delay to start simulator") {
input falseAlarmThreshold input falseAlarmThreshold
} }
section("People") {
paragraph "Not using this setting may cause some lights to remain on when you arrive home"
input people
}
section("More options") { section("More options") {
href "timeIntervalInput", title: "Only during a certain time", description: getTimeLabel(starting, ending), state: greyedOutTime(starting, ending), refreshAfterSelection:true
input days input days
} }
} }
@@ -187,24 +181,9 @@ def Settings() {
page(name: "timeIntervalInput", title: "Only during a certain time", refreshAfterSelection:true) { page(name: "timeIntervalInput", title: "Only during a certain time", refreshAfterSelection:true) {
section { section {
input "startTimeType", "enum", title: "Starting at", options: [["time": "A specific time"], ["sunrise": "Sunrise"], ["sunset": "Sunset"]], defaultValue: "time", submitOnChange: true input "starting", "time", title: "Starting", required: false
if (startTimeType in ["sunrise","sunset"]) { input "ending", "time", title: "Ending", required: false
input "startTimeOffset", "number", title: "Offset in minutes (+/-)", range: "*..*", required: false
}
else {
input "starting", "time", title: "Start time", required: false
}
} }
section {
input "endTimeType", "enum", title: "Ending at", options: [["time": "A specific time"], ["sunrise": "Sunrise"], ["sunset": "Sunset"]], defaultValue: "time", submitOnChange: true
if (endTimeType in ["sunrise","sunset"]) {
input "endTimeOffset", "number", title: "Offset in minutes (+/-)", range: "*..*", required: false
}
else {
input "ending", "time", title: "End time", required: false
}
}
} }
def installed() { def installed() {
@@ -222,13 +201,10 @@ def initialize(){
if (newMode != null) { if (newMode != null) {
subscribe(location, modeChangeHandler) subscribe(location, modeChangeHandler)
} }
if (starting != null) {
schedule(starting, modeChangeHandler)
}
log.debug "Installed with settings: ${settings}"
} }
def modeChangeHandler(evt) { def modeChangeHandler(evt) {
log.debug "Mode change to: ${evt.value}"
def delay = (falseAlarmThreshold != null && falseAlarmThreshold != "") ? falseAlarmThreshold * 60 : 2 * 60 def delay = (falseAlarmThreshold != null && falseAlarmThreshold != "") ? falseAlarmThreshold * 60 : 2 * 60
runIn(delay, scheduleCheck) runIn(delay, scheduleCheck)
} }
@@ -236,55 +212,49 @@ def modeChangeHandler(evt) {
//Main logic to pick a random set of lights from the large set of lights to turn on and then turn the rest off //Main logic to pick a random set of lights from the large set of lights to turn on and then turn the rest off
def scheduleCheck(evt) { def scheduleCheck(evt) {
if(allOk){ if(allOk){
log.debug("Running") log.debug("Running")
// turn off all the switches // turn off all the switches
switches.off() switches.off()
// grab a random switch // grab a random switch
def random = new Random() def random = new Random()
def inactive_switches = switches def inactive_switches = switches
for (int i = 0 ; i < number_of_active_lights ; i++) { for (int i = 0 ; i < number_of_active_lights ; i++) {
// if there are no inactive switches to turn on then let's break // if there are no inactive switches to turn on then let's break
if (inactive_switches.size() == 0){ if (inactive_switches.size() == 0){
break break
}
// grab a random switch and turn it on
def random_int = random.nextInt(inactive_switches.size())
inactive_switches[random_int].on()
// then remove that switch from the pool off switches that can be turned on
inactive_switches.remove(random_int)
}
// re-run again when the frequency demands it
schedule("0 0/${frequency_minutes} * 1/1 * ? *", scheduleCheck)
} }
//Check to see if mode is ok but not time/day. If mode is still ok, check again after frequency period.
else if (modeOk) { // grab a random switch and turn it on
log.debug("mode OK. Running again") def random_int = random.nextInt(inactive_switches.size())
switches.off() inactive_switches[random_int].on()
}
//if none is ok turn off frequency check and turn off lights. // then remove that switch from the pool off switches that can be turned on
else { inactive_switches.remove(random_int)
if(people){ }
//don't turn off lights if anyone is home
if(someoneIsHome()){ // re-run again when the frequency demands it
log.debug("Stopping Check for Light") runIn(frequency_minutes * 60, scheduleCheck)
unschedule() }
} //Check to see if mode is ok but not time/day. If mode is still ok, check again after frequency period.
else{ else if (modeOk) {
log.debug("Stopping Check for Light and turning off all lights") log.debug("mode OK. Running again")
switches.off() runIn(frequency_minutes * 60, scheduleCheck)
unschedule() switches.off()
} }
//if none is ok turn off frequency check and turn off lights.
else if(people){
//don't turn off lights if anyone is home
if(someoneIsHome()){
log.debug("Stopping Check for Light")
} }
else if (!modeOk) { else{
unschedule() log.debug("Stopping Check for Light and turning off all lights")
} switches.off()
} }
} }
}
//below is used to check restrictions //below is used to check restrictions
@@ -316,6 +286,26 @@ private getDaysOk() {
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
}
else if (starting){
result = currTime >= start
}
else if (ending){
result = currTime <= stop
}
log.trace "timeOk = $result"
result
}
private getHomeIsEmpty() { private getHomeIsEmpty() {
def result = true def result = true
@@ -340,59 +330,25 @@ private getSomeoneIsHome() {
return result return result
} }
private getTimeOk() {
def result = true //gets the label for time restriction. Label phrasing changes depending on if there is both start and stop times or just one start/stop time.
def start = timeWindowStart() def getTimeLabel(starting, ending){
def stop = timeWindowStop()
if (start && stop && location.timeZone) { def timeLabel = "Tap to set"
result = timeOfDayIsBetween(start, stop, new Date(), location.timeZone)
} if(starting && ending){
log.trace "timeOk = $result" timeLabel = "Between" + " " + hhmm(starting) + " " + "and" + " " + hhmm(ending)
result }
} else if (starting) {
timeLabel = "Start at" + " " + hhmm(starting)
private timeWindowStart() { }
def result = null else if(ending){
if (startTimeType == "sunrise") { timeLabel = "End at" + hhmm(ending)
result = location.currentState("sunriseTime")?.dateValue }
if (result && startTimeOffset) { timeLabel
result = new Date(result.time + Math.round(startTimeOffset * 60000))
}
}
else if (startTimeType == "sunset") {
result = location.currentState("sunsetTime")?.dateValue
if (result && startTimeOffset) {
result = new Date(result.time + Math.round(startTimeOffset * 60000))
}
}
else if (starting && location.timeZone) {
result = timeToday(starting, location.timeZone)
}
log.trace "timeWindowStart = ${result}"
result
}
private timeWindowStop() {
def result = null
if (endTimeType == "sunrise") {
result = location.currentState("sunriseTime")?.dateValue
if (result && endTimeOffset) {
result = new Date(result.time + Math.round(endTimeOffset * 60000))
}
}
else if (endTimeType == "sunset") {
result = location.currentState("sunsetTime")?.dateValue
if (result && endTimeOffset) {
result = new Date(result.time + Math.round(endTimeOffset * 60000))
}
}
else if (ending && location.timeZone) {
result = timeToday(ending, location.timeZone)
}
log.trace "timeWindowStop = ${result}"
result
} }
//fomrats time to readable format for time label
private hhmm(time, fmt = "h:mm a") private hhmm(time, fmt = "h:mm a")
{ {
def t = timeToday(time, location.timeZone) def t = timeToday(time, location.timeZone)
@@ -401,41 +357,6 @@ private hhmm(time, fmt = "h:mm a")
f.format(t) f.format(t)
} }
private timeIntervalLabel() {
def start = ""
switch (startTimeType) {
case "time":
if (ending) {
start += hhmm(starting)
}
break
case "sunrise":
case "sunset":
start += startTimeType[0].toUpperCase() + startTimeType[1..-1]
if (startTimeOffset) {
start += startTimeOffset > 0 ? "+${startTimeOffset} min" : "${startTimeOffset} min"
}
break
}
def finish = ""
switch (endTimeType) {
case "time":
if (ending) {
finish += hhmm(ending)
}
break
case "sunrise":
case "sunset":
finish += endTimeType[0].toUpperCase() + endTimeType[1..-1]
if (endTimeOffset) {
finish += endTimeOffset > 0 ? "+${endTimeOffset} min" : "${endTimeOffset} min"
}
break
}
start && finish ? "${start} to ${finish}" : ""
}
//sets complete/not complete for the setup section on the main dynamic page //sets complete/not complete for the setup section on the main dynamic page
def greyedOut(){ def greyedOut(){
def result = "" def result = ""
@@ -448,7 +369,16 @@ def greyedOut(){
//sets complete/not complete for the settings section on the main dynamic page //sets complete/not complete for the settings section on the main dynamic page
def greyedOutSettings(){ def greyedOutSettings(){
def result = "" def result = ""
if (people || days || falseAlarmThreshold ) { if (starting || ending || days || falseAlarmThreshold) {
result = "complete"
}
result
}
//sets complete/not complete for time restriction section in settings
def greyedOutTime(starting, ending){
def result = ""
if (starting || ending) {
result = "complete" result = "complete"
} }
result result