diff --git a/devicetypes/smartthings/hue-bridge.src/hue-bridge.groovy b/devicetypes/smartthings/hue-bridge.src/hue-bridge.groovy index 7d63107..9537fef 100644 --- a/devicetypes/smartthings/hue-bridge.src/hue-bridge.groovy +++ b/devicetypes/smartthings/hue-bridge.src/hue-bridge.groovy @@ -17,16 +17,13 @@ metadata { tiles(scale: 2) { multiAttributeTile(name:"rich-control"){ - tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { + tileAttribute ("", key: "PRIMARY_CONTROL") { attributeState "default", label: "Hue Bridge", action: "", icon: "st.Lighting.light99-hue", backgroundColor: "#F3C200" - } + } tileAttribute ("serialNumber", key: "SECONDARY_CONTROL") { 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) { state "default", label:'SN: ${currentValue}' } @@ -34,7 +31,7 @@ metadata { state "default", label:'${currentValue}', height: 1, width: 2, inactiveLabel: false } - main (["icon"]) + main (["rich-control"]) details(["rich-control", "networkAddress"]) } } @@ -75,6 +72,7 @@ def parse(description) { } else if (contentType?.contains("xml")) { log.debug "HUE BRIDGE ALREADY PRESENT" + parent.hubVerification(device.hub.id, msg.body) } } } diff --git a/devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy b/devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy index a5d41ef..99b529b 100644 --- a/devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy +++ b/devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy @@ -103,62 +103,104 @@ void nextLevel() { } void setLevel(percent) { - log.debug "Executing 'setLevel'" - parent.setLevel(this, percent) - sendEvent(name: "level", value: percent, descriptionText: "Level has changed to ${percent}%") + log.debug "Executing 'setLevel'" + if (verifyPercent(percent)) { + parent.setLevel(this, percent) + sendEvent(name: "level", value: percent, descriptionText: "Level has changed to ${percent}%") + sendEvent(name: "switch", value: "on") + } } void setSaturation(percent) { - log.debug "Executing 'setSaturation'" - parent.setSaturation(this, percent) - sendEvent(name: "saturation", value: percent, displayed: false) + log.debug "Executing 'setSaturation'" + if (verifyPercent(percent)) { + parent.setSaturation(this, percent) + sendEvent(name: "saturation", value: percent, displayed: false) + } } void setHue(percent) { - log.debug "Executing 'setHue'" - parent.setHue(this, percent) - sendEvent(name: "hue", value: percent, displayed: false) + log.debug "Executing 'setHue'" + if (verifyPercent(percent)) { + parent.setHue(this, percent) + sendEvent(name: "hue", value: percent, displayed: false) + } } void setColor(value) { - log.debug "setColor: ${value}, $this" - parent.setColor(this, value) - if (value.hue) { sendEvent(name: "hue", value: value.hue, displayed: false)} - if (value.saturation) { sendEvent(name: "saturation", value: value.saturation, displayed: false)} - if (value.hex) { sendEvent(name: "color", value: value.hex)} - if (value.level) { sendEvent(name: "level", value: value.level, descriptionText: "Level has changed to ${value.level}%")} - sendEvent(name: "switch", value: "on") + log.debug "setColor: ${value}, $this" + def events = [] + def validValues = [:] + + if (verifyPercent(value.hue)) { + events << createEvent(name: "hue", value: value.hue, displayed: false) + validValues.hue = value.hue + } + 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() { - log.debug "Executing 'reset'" + log.debug "Executing 'reset'" def value = [level:100, saturation:56, hue:23] setAdjustedColor(value) - parent.poll() + parent.poll() } void setAdjustedColor(value) { - if (value) { + if (value) { log.trace "setAdjustedColor: ${value}" def adjusted = value + [:] adjusted.hue = adjustOutgoingHue(value.hue) // Needed because color picker always sends 100 adjusted.level = null setColor(adjusted) + } else { + log.warn "Invalid color input" } } void setColorTemperature(value) { - if (value) { + if (value) { log.trace "setColorTemperature: ${value}k" parent.setColorTemperature(this, value) sendEvent(name: "colorTemperature", value: value) - } + sendEvent(name: "switch", value: "on") + } else { + log.warn "Invalid color temperature" + } } void refresh() { - log.debug "Executing 'refresh'" - parent.manualRefresh() + log.debug "Executing 'refresh'" + parent.manualRefresh() } def adjustOutgoingHue(percent) { @@ -177,3 +219,14 @@ def adjustOutgoingHue(percent) { log.info "percent: $percent, 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 + } +} \ No newline at end of file diff --git a/devicetypes/smartthings/hue-lux-bulb.src/hue-lux-bulb.groovy b/devicetypes/smartthings/hue-lux-bulb.src/hue-lux-bulb.groovy index 18affa4..408c2e2 100644 --- a/devicetypes/smartthings/hue-lux-bulb.src/hue-lux-bulb.groovy +++ b/devicetypes/smartthings/hue-lux-bulb.src/hue-lux-bulb.groovy @@ -79,8 +79,12 @@ void off() { void setLevel(percent) { log.debug "Executing 'setLevel'" - parent.setLevel(this, percent) - sendEvent(name: "level", value: percent) + if (percent != null && percent >= 0 && percent <= 100) { + parent.setLevel(this, percent) + sendEvent(name: "level", value: percent) + } else { + log.warn "$percent is not 0-100" + } } void refresh() { diff --git a/devicetypes/smartthings/zigbee-dimmer-power.src/zigbee-dimmer-power.groovy b/devicetypes/smartthings/zigbee-dimmer-power.src/zigbee-dimmer-power.groovy index 14cac15..af8c227 100644 --- a/devicetypes/smartthings/zigbee-dimmer-power.src/zigbee-dimmer-power.groovy +++ b/devicetypes/smartthings/zigbee-dimmer-power.src/zigbee-dimmer-power.groovy @@ -56,21 +56,17 @@ metadata { def parse(String description) { log.debug "description is $description" - def resultMap = zigbee.getKnownDescription(description) - if (resultMap) { - log.info resultMap - if (resultMap.type == "update") { - log.info "$device updates: ${resultMap.value}" - } - else if (resultMap.type == "power") { - def powerValue + def event = zigbee.getEvent(description) + if (event) { + log.info event + if (event.name == "power") { if (device.getDataValue("manufacturer") != "OSRAM") { //OSRAM devices do not reliably update power - powerValue = (resultMap.value as Integer)/10 //TODO: The divisor value needs to be set as part of configuration - sendEvent(name: "power", value: powerValue) + event.value = (event.value as Integer) / 10 //TODO: The divisor value needs to be set as part of configuration + sendEvent(event) } } else { - sendEvent(name: resultMap.type, value: resultMap.value) + sendEvent(event) } } else { diff --git a/devicetypes/smartthings/zigbee-dimmer.src/zigbee-dimmer.groovy b/devicetypes/smartthings/zigbee-dimmer.src/zigbee-dimmer.groovy index 770639c..72809f5 100644 --- a/devicetypes/smartthings/zigbee-dimmer.src/zigbee-dimmer.groovy +++ b/devicetypes/smartthings/zigbee-dimmer.src/zigbee-dimmer.groovy @@ -51,15 +51,9 @@ metadata { def parse(String description) { log.debug "description is $description" - def resultMap = zigbee.getKnownDescription(description) - if (resultMap) { - log.info resultMap - if (resultMap.type == "update") { - log.info "$device updates: ${resultMap.value}" - } - else { - sendEvent(name: resultMap.type, value: resultMap.value) - } + def event = zigbee.getEvent(description) + if (event) { + sendEvent(event) } else { log.warn "DID NOT PARSE MESSAGE for description : $description" diff --git a/devicetypes/smartthings/zigbee-lock.src/zigbee-lock.groovy b/devicetypes/smartthings/zigbee-lock.src/zigbee-lock.groovy index 39e78d2..2c6bcfc 100644 --- a/devicetypes/smartthings/zigbee-lock.src/zigbee-lock.groovy +++ b/devicetypes/smartthings/zigbee-lock.src/zigbee-lock.groovy @@ -83,32 +83,19 @@ def uninstalled() { } def configure() { - /* def cmds = - zigbee.configSetup("${CLUSTER_DOORLOCK}", "${DOORLOCK_ATTR_LOCKSTATE}", - "${TYPE_ENUM8}", 0, 3600, "{01}") + - zigbee.configSetup("${CLUSTER_POWER}", "${POWER_ATTR_BATTERY_PERCENTAGE_REMAINING}", - "${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", - ] + zigbee.configureReporting(CLUSTER_DOORLOCK, DOORLOCK_ATTR_LOCKSTATE, + TYPE_ENUM8, 0, 3600, null) + + zigbee.configureReporting(CLUSTER_POWER, POWER_ATTR_BATTERY_PERCENTAGE_REMAINING, + TYPE_U8, 600, 21600, 0x01) log.info "configure() --- cmds: $cmds" return cmds + refresh() // send refresh cmds as part of config } def refresh() { def cmds = - zigbee.refreshData("${CLUSTER_DOORLOCK}", "${DOORLOCK_ATTR_LOCKSTATE}") + - zigbee.refreshData("${CLUSTER_POWER}", "${POWER_ATTR_BATTERY_PERCENTAGE_REMAINING}") + zigbee.readAttribute(CLUSTER_DOORLOCK, DOORLOCK_ATTR_LOCKSTATE) + + zigbee.readAttribute(CLUSTER_POWER, POWER_ATTR_BATTERY_PERCENTAGE_REMAINING) log.info "refresh() --- cmds: $cmds" return cmds } @@ -121,34 +108,27 @@ def parse(String description) { map = parseReportAttributeMessage(description) } - log.debug "parse() --- Parse returned $map" def result = map ? createEvent(map) : null + log.debug "parse() --- returned: $result" return result } // Lock capability commands def lock() { - //def cmds = zigbee.zigbeeCommand("${CLUSTER_DOORLOCK}", "${DOORLOCK_CMD_LOCK_DOOR}", "{}") - //log.info "lock() -- cmds: $cmds" - //return cmds - "st cmd 0x${device.deviceNetworkId} 0x${device.endpointId} ${CLUSTER_DOORLOCK} ${DOORLOCK_CMD_LOCK_DOOR} {}" + def cmds = zigbee.command(CLUSTER_DOORLOCK, DOORLOCK_CMD_LOCK_DOOR) + log.info "lock() -- cmds: $cmds" + return cmds } def unlock() { - //def cmds = zigbee.zigbeeCommand("${CLUSTER_DOORLOCK}", "${DOORLOCK_CMD_UNLOCK_DOOR}", "{}") - //log.info "unlock() -- cmds: $cmds" - //return cmds - "st cmd 0x${device.deviceNetworkId} 0x${device.endpointId} ${CLUSTER_DOORLOCK} ${DOORLOCK_CMD_UNLOCK_DOOR} {}" + def cmds = zigbee.command(CLUSTER_DOORLOCK, DOORLOCK_CMD_UNLOCK_DOOR) + log.info "unlock() -- cmds: $cmds" + return cmds } // Private methods private Map parseReportAttributeMessage(String description) { - log.trace "parseReportAttributeMessage() --- description: $description" - Map descMap = zigbee.parseDescriptionAsMap(description) - - log.debug "parseReportAttributeMessage() --- descMap: $descMap" - Map resultMap = [:] if (descMap.clusterInt == CLUSTER_POWER && descMap.attrInt == POWER_ATTR_BATTERY_PERCENTAGE_REMAINING) { resultMap.name = "battery" @@ -156,18 +136,24 @@ private Map parseReportAttributeMessage(String description) { if (device.getDataValue("manufacturer") == "Yale") { //Handling issue with Yale locks incorrect battery reporting resultMap.value = Integer.parseInt(descMap.value, 16) } - log.info "parseReportAttributeMessage() --- battery: ${resultMap.value}" } else if (descMap.clusterInt == CLUSTER_DOORLOCK && descMap.attrInt == DOORLOCK_ATTR_LOCKSTATE) { def value = Integer.parseInt(descMap.value, 16) + def linkText = getLinkText(device) resultMap.name = "lock" - resultMap.putAll([0:["value":"unknown", - "descriptionText":"Not fully locked"], - 1:["value":"locked"], - 2:["value":"unlocked"]].get(value, - ["value":"unknown", - "descriptionText":"Unknown lock state"])) - log.info "parseReportAttributeMessage() --- lock: ${resultMap.value}" + if (value == 0) { + resultMap.value = "unknown" + resultMap.descriptionText = "${linkText} is not fully locked" + } else if (value == 1) { + resultMap.value = "locked" + resultMap.descriptionText = "${linkText} is locked" + } else if (value == 2) { + resultMap.value = "unlocked" + resultMap.descriptionText = "${linkText} is unlocked" + } else { + resultMap.value = "unknown" + resultMap.descriptionText = "${linkText} is in unknown lock state" + } } else { log.debug "parseReportAttributeMessage() --- ignoring attribute" diff --git a/devicetypes/smartthings/zigbee-switch-power.src/zigbee-switch-power.groovy b/devicetypes/smartthings/zigbee-switch-power.src/zigbee-switch-power.groovy index 430b66c..a5eb0ac 100644 --- a/devicetypes/smartthings/zigbee-switch-power.src/zigbee-switch-power.groovy +++ b/devicetypes/smartthings/zigbee-switch-power.src/zigbee-switch-power.groovy @@ -51,22 +51,15 @@ metadata { // Parse incoming device messages to generate events def parse(String description) { log.debug "description is $description" - - def resultMap = zigbee.getKnownDescription(description) - if (resultMap) { - log.info resultMap - if (resultMap.type == "update") { - log.info "$device updates: ${resultMap.value}" - } - else if (resultMap.type == "power") { + def event = zigbee.getEvent(description) + if (event) { + if (event.name == "power") { def powerValue - if (device.getDataValue("manufacturer") != "OSRAM") { //OSRAM devices do not reliably update power - powerValue = (resultMap.value as Integer)/10 //TODO: The divisor value needs to be set as part of configuration - sendEvent(name: "power", value: powerValue) - } + powerValue = (event.value as Integer)/10 //TODO: The divisor value needs to be set as part of configuration + sendEvent(name: "power", value: powerValue) } else { - sendEvent(name: resultMap.type, value: resultMap.value) + sendEvent(event) } } else { diff --git a/devicetypes/smartthings/zigbee-switch.src/zigbee-switch.groovy b/devicetypes/smartthings/zigbee-switch.src/zigbee-switch.groovy index d861b95..4440054 100644 --- a/devicetypes/smartthings/zigbee-switch.src/zigbee-switch.groovy +++ b/devicetypes/smartthings/zigbee-switch.src/zigbee-switch.groovy @@ -53,16 +53,9 @@ metadata { // Parse incoming device messages to generate events def parse(String description) { log.debug "description is $description" - - def resultMap = zigbee.getKnownDescription(description) - if (resultMap) { - log.info resultMap - if (resultMap.type == "update") { - log.info "$device updates: ${resultMap.value}" - } - else { - sendEvent(name: resultMap.type, value: resultMap.value) - } + def event = zigbee.getEvent(description) + if (event) { + sendEvent(event) } else { log.warn "DID NOT PARSE MESSAGE for description : $description" diff --git a/devicetypes/smartthings/zigbee-white-color-temperature-bulb.src/zigbee-white-color-temperature-bulb.groovy b/devicetypes/smartthings/zigbee-white-color-temperature-bulb.src/zigbee-white-color-temperature-bulb.groovy index b8ad984..50f270d 100644 --- a/devicetypes/smartthings/zigbee-white-color-temperature-bulb.src/zigbee-white-color-temperature-bulb.groovy +++ b/devicetypes/smartthings/zigbee-white-color-temperature-bulb.src/zigbee-white-color-temperature-bulb.groovy @@ -73,16 +73,9 @@ metadata { // Parse incoming device messages to generate events def parse(String description) { log.debug "description is $description" - - def finalResult = zigbee.getKnownDescription(description) - if (finalResult) { - log.info finalResult - if (finalResult.type == "update") { - log.info "$device updates: ${finalResult.value}" - } - else { - sendEvent(name: finalResult.type, value: finalResult.value) - } + def event = zigbee.getEvent(description) + if (event) { + sendEvent(event) } else { log.warn "DID NOT PARSE MESSAGE for description : $description" diff --git a/devicetypes/smartthings/zwave-motion-sensor.src/zwave-motion-sensor.groovy b/devicetypes/smartthings/zwave-motion-sensor.src/zwave-motion-sensor.groovy index 8618631..7a532e9 100644 --- a/devicetypes/smartthings/zwave-motion-sensor.src/zwave-motion-sensor.groovy +++ b/devicetypes/smartthings/zwave-motion-sensor.src/zwave-motion-sensor.groovy @@ -57,7 +57,7 @@ def parse(String description) { return result } -def sensorValueEvent(Short value) { +def sensorValueEvent(value) { if (value) { createEvent(name: "motion", value: "active", descriptionText: "$device.displayName detected motion") } else { @@ -94,24 +94,24 @@ def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cm { def result = [] if (cmd.notificationType == 0x07) { - if (cmd.event == 0x01 || cmd.event == 0x02) { + if (cmd.v1AlarmType == 0x07) { // special case for nonstandard messages from Monoprice ensors + result << sensorValueEvent(cmd.v1AlarmLevel) + } else if (cmd.event == 0x01 || cmd.event == 0x02 || cmd.event == 0x07 || cmd.event == 0x08) { result << sensorValueEvent(1) + } else if (cmd.event == 0x00) { + result << sensorValueEvent(0) } else if (cmd.event == 0x03) { - result << createEvent(descriptionText: "$device.displayName covering was removed", isStateChange: true) - result << response(zwave.wakeUpV1.wakeUpIntervalSet(seconds:4*3600, nodeid:zwaveHubNodeId)) - if(!state.MSR) result << response(zwave.manufacturerSpecificV2.manufacturerSpecificGet()) + result << createEvent(name: "tamper", value: "detected", descriptionText: "$device.displayName covering was removed", isStateChange: true) + result << response(zwave.batteryV1.batteryGet()) } else if (cmd.event == 0x05 || cmd.event == 0x06) { 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) { def text = "Notification $cmd.notificationType: event ${([cmd.event] + cmd.eventParameter).join(", ")}" - result << createEvent(name: "notification$cmd.notificationType", value: "$cmd.event", descriptionText: text, displayed: false) + result << createEvent(name: "notification$cmd.notificationType", value: "$cmd.event", descriptionText: text, isStateChange: true, displayed: false) } else { def value = cmd.v1AlarmLevel == 255 ? "active" : cmd.v1AlarmLevel ?: "inactive" - result << createEvent(name: "alarm $cmd.v1AlarmType", value: value, displayed: false) + result << createEvent(name: "alarm $cmd.v1AlarmType", value: value, isStateChange: true, displayed: false) } result } diff --git a/devicetypes/smartthings/zwave-smoke-alarm.src/zwave-smoke-alarm.groovy b/devicetypes/smartthings/zwave-smoke-alarm.src/zwave-smoke-alarm.groovy index 6f1f1ab..aa426e4 100644 --- a/devicetypes/smartthings/zwave-smoke-alarm.src/zwave-smoke-alarm.groovy +++ b/devicetypes/smartthings/zwave-smoke-alarm.src/zwave-smoke-alarm.groovy @@ -61,37 +61,44 @@ def parse(String description) { zwaveEvent(cmd, results) } } - // log.debug "\"$description\" parsed to ${results.inspect()}" + log.debug "'$description' parsed to ${results.inspect()}" return results } def createSmokeOrCOEvents(name, results) { def text = null - if (name == "smoke") { - text = "$device.displayName smoke was detected!" - // 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) - } else if (name == "carbonMonoxide") { - text = "$device.displayName carbon monoxide was detected!" - results << createEvent(name: "carbonMonoxide", value: "detected", descriptionText: text, displayed: false) - } else if (name == "tested") { - text = "$device.displayName was tested" - results << createEvent(name: "smoke", value: "tested", descriptionText: text, displayed: false) - results << createEvent(name: "carbonMonoxide", value: "tested", descriptionText: text, displayed: false) - } else if (name == "smokeClear") { - text = "$device.displayName smoke is clear" - results << createEvent(name: "smoke", value: "clear", descriptionText: text, displayed: false) - name = "clear" - } else if (name == "carbonMonoxideClear") { - text = "$device.displayName carbon monoxide is clear" - results << createEvent(name: "carbonMonoxide", value: "clear", descriptionText: text, displayed: false) - name = "clear" - } else if (name == "testClear") { - text = "$device.displayName smoke is clear" - results << createEvent(name: "smoke", value: "clear", descriptionText: text, displayed: false) - results << createEvent(name: "carbonMonoxide", value: "clear", displayed: false) - name = "clear" + switch (name) { + case "smoke": + text = "$device.displayName smoke was detected!" + // 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) + break + case "carbonMonoxide": + text = "$device.displayName carbon monoxide was detected!" + results << createEvent(name: "carbonMonoxide", value: "detected", descriptionText: text, displayed: false) + break + case "tested": + text = "$device.displayName was tested" + results << createEvent(name: "smoke", value: "tested", descriptionText: text, displayed: false) + results << createEvent(name: "carbonMonoxide", value: "tested", descriptionText: text, displayed: false) + break + case "smokeClear": + text = "$device.displayName smoke is clear" + results << createEvent(name: "smoke", value: "clear", descriptionText: text, displayed: false) + name = "clear" + break + case "carbonMonoxideClear": + text = "$device.displayName carbon monoxide is clear" + results << createEvent(name: "carbonMonoxide", value: "clear", descriptionText: text, displayed: false) + 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 results << createEvent(name: "alarmState", value: name, descriptionText: text) @@ -117,8 +124,10 @@ def zwaveEvent(physicalgraph.zwave.commands.alarmv2.AlarmReport cmd, results) { createSmokeOrCOEvents(cmd.alarmLevel ? "tested" : "testClear", results) break case 13: // sent every hour -- not sure what this means, just a wake up notification? - if (cmd.alarmLevel != 255) { - results << createEvent(descriptionText: "$device.displayName code 13 is $cmd.alarmLevel", displayed: true) + if (cmd.alarmLevel == 255) { + results << createEvent(descriptionText: "$device.displayName checked in", isStateChange: false) + } 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 @@ -127,9 +136,8 @@ def zwaveEvent(physicalgraph.zwave.commands.alarmv2.AlarmReport cmd, results) { } // Check battery if we don't have a recent battery event - def prevBattery = device.currentState("battery") - if (!prevBattery || (new Date().time - prevBattery.date.time)/60000 >= 60 * 53) { - results << new physicalgraph.device.HubAction(zwave.batteryV1.batteryGet().format()) + if (!state.lastbatt || (now() - state.lastbatt) >= 48*60*60*1000) { + results << response(zwave.batteryV1.batteryGet()) } break default: @@ -158,12 +166,17 @@ def zwaveEvent(physicalgraph.zwave.commands.sensoralarmv1.SensorAlarmReport cmd, } 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) + 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 map = [ name: "battery", unit: "%" ] + def map = [ name: "battery", unit: "%", isStateChange: true ] + state.lastbatt = now() if (cmd.batteryLevel == 0xFF) { map.value = 1 map.descriptionText = "$device.displayName battery is low!" diff --git a/smartapps/smartthings/hue-connect.src/hue-connect.groovy b/smartapps/smartthings/hue-connect.src/hue-connect.groovy index 5b908d8..1a8829a 100644 --- a/smartapps/smartthings/hue-connect.src/hue-connect.groovy +++ b/smartapps/smartthings/hue-connect.src/hue-connect.groovy @@ -68,7 +68,7 @@ def bridgeDiscovery(params=[:]) } //setup.xml request every 3 seconds except on discoveries - if(((bridgeRefreshCount % 1) == 0) && ((bridgeRefreshCount % 5) != 0)) { + if(((bridgeRefreshCount % 3) == 0) && ((bridgeRefreshCount % 5) != 0)) { verifyHueBridges() } @@ -175,6 +175,7 @@ private discoverHueBulbs() { } private verifyHueBridge(String deviceNetworkId, String host) { + log.trace "Verify Hue Bridge $deviceNetworkId" sendHubCommand(new physicalgraph.device.HubAction([ method: "GET", path: "/description.xml", @@ -602,6 +603,20 @@ 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) { log.debug "Executing 'on'" put("lights/${getId(childDevice)}/state", [on: true]) @@ -642,39 +657,53 @@ def setColorTemperature(childDevice, huesettings) { } def setColor(childDevice, huesettings) { - log.debug "Executing 'setColor($huesettings)'" + log.debug "Executing 'setColor($huesettings)'" + + def value = [:] def hue = null def sat = null def xy = null - if (huesettings.hex) { - xy = getHextoXY(huesettings.hex) - } else if (huesettings.hue && huesettings.saturation) { - hue = Math.min(Math.round(huesettings.hue * 65535 / 100), 65535) - sat = Math.min(Math.round(huesettings.saturation * 255 / 100), 255) + + if (huesettings.hex != null) { + value.xy = getHextoXY(huesettings.hex) + } 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) } - def alert = huesettings.alert ? huesettings.alert : "none" - def transition = huesettings.transition ? huesettings.transition : 4 + + // Default behavior is to turn light on + value.on = true - def value = [xy: xy, sat: sat, hue: hue, alert: alert, transitiontime: transition, on: true] + 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 - if (huesettings.level != null) { - if (huesettings.level == 1) value.bri = 1 else value.bri = Math.min(Math.round(huesettings.level * 255 / 100), 255) - value.on = value.bri > 0 - } + // 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) + log.debug "sending command $value" + put("lights/${getId(childDevice)}/state", value) + return "Color set to $value" } def nextLevel(childDevice) { - def level = device.latestValue("level") as Integer ?: 0 - if (level < 100) { - level = Math.min(25 * (Math.round(level / 25) + 1), 100) as Integer - } - else { - level = 25 - } - setLevel(childDevice,level) + def level = device.latestValue("level") as Integer ?: 0 + if (level < 100) { + level = Math.min(25 * (Math.round(level / 25) + 1), 100) as Integer + } else { + level = 25 + } + setLevel(childDevice,level) } private getId(childDevice) { @@ -773,16 +802,14 @@ private getHextoXY(String colorStr) { // Make green more vivid if (normalizedToOne[1] > 0.04045) { - green = (float) Math.pow((normalizedToOne[1] + 0.055) - / (1.0 + 0.055), 2.4); + 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); + blue = (float) Math.pow((normalizedToOne[2] + 0.055) / (1.0 + 0.055), 2.4); } else { blue = (float) (normalizedToOne[2] / 12.92); } @@ -791,8 +818,8 @@ private getHextoXY(String colorStr) { 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 / (X + Y + Z); - float y = Y / (X + Y + Z); + 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; diff --git a/smartapps/tslagle13/vacation-lighting-director.src/vacation-lighting-director.groovy b/smartapps/tslagle13/vacation-lighting-director.src/vacation-lighting-director.groovy index f71c126..8ea150e 100644 --- a/smartapps/tslagle13/vacation-lighting-director.src/vacation-lighting-director.groovy +++ b/smartapps/tslagle13/vacation-lighting-director.src/vacation-lighting-director.groovy @@ -1,11 +1,17 @@ /** * Vacation Lighting Director * - * Version 2.4 - Added information paragraphs + * Version 2.5 - Moved scheduling over to Cron and added time as a trigger. + * 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 * - * Copyright 2015 Tim Slagle + * Copyright 2016 Tim Slagle * * 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: @@ -51,8 +57,7 @@ def pageSetup() { return dynamicPage(pageProperties) { section(""){ paragraph "This app can be used to make your home seem occupied anytime you are away from your home. " + - "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'. " + "Please use each of the the sections below to setup the different preferences to your liking. " } section("Setup Menu") { href "Setup", title: "Setup", description: "", state:greyedOut() @@ -70,7 +75,7 @@ def Setup() { def newMode = [ name: "newMode", type: "mode", - title: "Which?", + title: "Modes", multiple: true, required: true ] @@ -96,14 +101,6 @@ def Setup() { 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 pageProperties = [ @@ -116,10 +113,11 @@ def Setup() { section(""){ paragraph "In this section you need to setup the deatils of how you want your lighting to be affected while " + - paragraph "you are away. All of these settings are required in order for the simulator to run correctly." + "you are away. All of these settings are required in order for the simulator to run correctly." } - section("Which mode change triggers the simulator? (This app will only run in selected mode(s))") { - input newMode + section("Simulator Triggers") { + input newMode + href "timeIntervalInput", title: "Times", description: timeIntervalLabel(), refreshAfterSelection:true } section("Light switches to turn on/off") { input switches @@ -130,9 +128,6 @@ def Setup() { section("Number of active lights at any given time") { input number_of_active_lights } - section("People") { - input people - } } } @@ -162,18 +157,29 @@ def Settings() { title: "Settings", 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) { section(""){ paragraph "In this section you can restrict how your simulator runs. For instance you can restrict on which days it will run " + - 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." + "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") { 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") { - href "timeIntervalInput", title: "Only during a certain time", description: getTimeLabel(starting, ending), state: greyedOutTime(starting, ending), refreshAfterSelection:true input days } } @@ -181,9 +187,24 @@ def Settings() { page(name: "timeIntervalInput", title: "Only during a certain time", refreshAfterSelection:true) { section { - input "starting", "time", title: "Starting", required: false - input "ending", "time", title: "Ending", required: false + input "startTimeType", "enum", title: "Starting at", options: [["time": "A specific time"], ["sunrise": "Sunrise"], ["sunset": "Sunset"]], defaultValue: "time", submitOnChange: true + if (startTimeType in ["sunrise","sunset"]) { + 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() { @@ -201,10 +222,13 @@ def initialize(){ if (newMode != null) { subscribe(location, modeChangeHandler) } + if (starting != null) { + schedule(starting, modeChangeHandler) + } + log.debug "Installed with settings: ${settings}" } def modeChangeHandler(evt) { - log.debug "Mode change to: ${evt.value}" def delay = (falseAlarmThreshold != null && falseAlarmThreshold != "") ? falseAlarmThreshold * 60 : 2 * 60 runIn(delay, scheduleCheck) } @@ -212,48 +236,54 @@ 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 def scheduleCheck(evt) { -if(allOk){ -log.debug("Running") - // turn off all the switches - switches.off() - - // grab a random switch - def random = new Random() - def inactive_switches = switches - for (int i = 0 ; i < number_of_active_lights ; i++) { - // if there are no inactive switches to turn on then let's break - if (inactive_switches.size() == 0){ - break + if(allOk){ + log.debug("Running") + // turn off all the switches + switches.off() + + // grab a random switch + def random = new Random() + def inactive_switches = switches + for (int i = 0 ; i < number_of_active_lights ; i++) { + // if there are no inactive switches to turn on then let's break + if (inactive_switches.size() == 0){ + 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) } - - // 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 - runIn(frequency_minutes * 60, 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) { - log.debug("mode OK. Running again") - runIn(frequency_minutes * 60, scheduleCheck) - 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") + //Check to see if mode is ok but not time/day. If mode is still ok, check again after frequency period. + else if (modeOk) { + log.debug("mode OK. Running again") + 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") + unschedule() + } + else{ + log.debug("Stopping Check for Light and turning off all lights") + switches.off() + unschedule() + } } - else{ - log.debug("Stopping Check for Light and turning off all lights") - switches.off() + else if (!modeOk) { + unschedule() + } } -} } @@ -286,26 +316,6 @@ private getDaysOk() { 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() { def result = true @@ -330,25 +340,59 @@ private getSomeoneIsHome() { return result } - -//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 getTimeLabel(starting, ending){ - - def timeLabel = "Tap to set" - - if(starting && ending){ - timeLabel = "Between" + " " + hhmm(starting) + " " + "and" + " " + hhmm(ending) - } - else if (starting) { - timeLabel = "Start at" + " " + hhmm(starting) - } - else if(ending){ - timeLabel = "End at" + hhmm(ending) - } - timeLabel +private getTimeOk() { + def result = true + def start = timeWindowStart() + def stop = timeWindowStop() + if (start && stop && location.timeZone) { + result = timeOfDayIsBetween(start, stop, new Date(), location.timeZone) + } + log.trace "timeOk = $result" + result +} + +private timeWindowStart() { + def result = null + if (startTimeType == "sunrise") { + result = location.currentState("sunriseTime")?.dateValue + if (result && startTimeOffset) { + 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") { def t = timeToday(time, location.timeZone) @@ -357,6 +401,41 @@ private hhmm(time, fmt = "h:mm a") 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 def greyedOut(){ def result = "" @@ -369,16 +448,7 @@ def greyedOut(){ //sets complete/not complete for the settings section on the main dynamic page def greyedOutSettings(){ def result = "" - 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) { + if (people || days || falseAlarmThreshold ) { result = "complete" } result