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 f49ec95..99b529b 100644 --- a/devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy +++ b/devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy @@ -3,6 +3,7 @@ * * Author: SmartThings */ + // for the UI metadata { // Automatically generated. Make future change here. @@ -27,10 +28,10 @@ metadata { tiles (scale: 2){ multiAttributeTile(name:"rich-control", type: "lighting", width: 6, height: 4, canChangeIcon: true){ tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { - 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:"#ffffff", nextState:"turningOn" - 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:"#ffffff", nextState:"turningOn" + attributeState "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#00A0DC", nextState:"turningOff" + attributeState "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#C6C7CC", nextState:"turningOn" + attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#00A0DC", nextState:"turningOff" + attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#C6C7CC", nextState:"turningOn" } tileAttribute ("device.level", key: "SLIDER_CONTROL") { attributeState "level", action:"switch level.setLevel", range:"(0..100)" @@ -43,16 +44,10 @@ 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)") { state "colorTemperature", action:"color temperature.setColorTemperature" } + valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { state "colorTemperature", label: '${currentValue} K' } @@ -60,29 +55,12 @@ metadata { 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" } - 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" } - 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(["switch"]) + main(["rich-control"]) details(["rich-control", "colorTempSliderControl", "colorTemp", "reset", "refresh"]) } } @@ -125,62 +103,104 @@ void nextLevel() { } void setLevel(percent) { - log.debug "Executing 'setLevel'" - parent.setLevel(this, percent) - sendEvent(name: "level", value: 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) + 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) + 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)} - if (value.saturation) { sendEvent(name: "saturation", value: value.saturation)} - if (value.hex) { sendEvent(name: "color", value: value.hex)} - if (value.level) { sendEvent(name: "level", value: value.level)} - if (value.switch) { sendEvent(name: "switch", value: value.switch)} + 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'" - def value = [level:100, hex:"#90C638", saturation:56, hue:23] + 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) { @@ -199,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 49e7f2f..408c2e2 100644 --- a/devicetypes/smartthings/hue-lux-bulb.src/hue-lux-bulb.groovy +++ b/devicetypes/smartthings/hue-lux-bulb.src/hue-lux-bulb.groovy @@ -36,13 +36,6 @@ 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)") { state "level", action:"switch level.setLevel" } @@ -51,7 +44,7 @@ metadata { state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" } - main(["switch"]) + main(["rich-control"]) details(["rich-control", "refresh"]) } } @@ -86,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/smartpower-outlet.src/i18n/messages.properties b/devicetypes/smartthings/smartpower-outlet.src/i18n/messages.properties index 49ce523..e423e19 100644 --- a/devicetypes/smartthings/smartpower-outlet.src/i18n/messages.properties +++ b/devicetypes/smartthings/smartpower-outlet.src/i18n/messages.properties @@ -28,4 +28,8 @@ '''{{ device.displayName }} is On'''.ko={{ device.displayName }}켜졌습니다. '''{{ device.displayName }} is Off'''.ko={{ device.displayName }}꺼졌습니다. '''{{ device.displayName }} power is {{ value }} Watts'''.ko={{ device.displayName }} 전원은 {{ value }}와트입니다 +'''On'''.ko=켜기 +'''Off'''.ko=끄기 +'''Turning On'''.ko=켜기 +'''Turning Off'''.ko=끄기 #============================================================================== diff --git a/devicetypes/smartthings/smartpower-outlet.src/smartpower-outlet.groovy b/devicetypes/smartthings/smartpower-outlet.src/smartpower-outlet.groovy index 0dea33e..176bcca 100644 --- a/devicetypes/smartthings/smartpower-outlet.src/smartpower-outlet.groovy +++ b/devicetypes/smartthings/smartpower-outlet.src/smartpower-outlet.groovy @@ -65,10 +65,10 @@ metadata { tiles(scale: 2) { multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){ tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { - attributeState "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821", nextState: "turningOff" - attributeState "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff", nextState: "turningOn" - attributeState "turningOn", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821", nextState: "turningOff" - attributeState "turningOff", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff", nextState: "turningOn" + attributeState "on", label: 'On', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821", nextState: "turningOff" + attributeState "off", label: 'Off', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff", nextState: "turningOn" + attributeState "turningOn", label: 'Turning On', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821", nextState: "turningOff" + attributeState "turningOff", label: 'Turning Off', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff", nextState: "turningOn" } tileAttribute ("power", key: "SECONDARY_CONTROL") { attributeState "power", label:'${currentValue} W' diff --git a/devicetypes/smartthings/smartsense-multi-sensor.src/i18n/messages.properties b/devicetypes/smartthings/smartsense-multi-sensor.src/i18n/messages.properties index 0f97e6c..be9cc7f 100644 --- a/devicetypes/smartthings/smartsense-multi-sensor.src/i18n/messages.properties +++ b/devicetypes/smartthings/smartsense-multi-sensor.src/i18n/messages.properties @@ -43,3 +43,7 @@ '''{{ device.displayName }} battery was {{ value }}%'''.ko={{ device.displayName }}남아있는 배터리는 {{ value }}%입니다. '''Updating device to garage sensor'''.ko=기기-차고 센서 업데이트 중 '''Updating device to open/close sensor'''.ko=기기-열림/닫힘 센서 업데이트 중 +'''Inactive'''.ko=비활성 +'''Active'''.ko=활성 +'''Open'''.ko=열다 +'''Closed'''.ko=닫은 \ No newline at end of file diff --git a/devicetypes/smartthings/smartsense-multi-sensor.src/smartsense-multi-sensor.groovy b/devicetypes/smartthings/smartsense-multi-sensor.src/smartsense-multi-sensor.groovy index ab26143..21d31b0 100644 --- a/devicetypes/smartthings/smartsense-multi-sensor.src/smartsense-multi-sensor.groovy +++ b/devicetypes/smartthings/smartsense-multi-sensor.src/smartsense-multi-sensor.groovy @@ -83,19 +83,19 @@ metadata { tiles(scale: 2) { multiAttributeTile(name:"status", type: "generic", width: 6, height: 4){ tileAttribute ("device.status", key: "PRIMARY_CONTROL") { - attributeState "open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e" - attributeState "closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821" + attributeState "open", label:'Open', icon:"st.contact.contact.open", backgroundColor:"#ffa81e" + attributeState "closed", label:'Closed', icon:"st.contact.contact.closed", backgroundColor:"#79b821" attributeState "garage-open", label:'Open', icon:"st.doors.garage.garage-open", backgroundColor:"#ffa81e" attributeState "garage-closed", label:'Closed', icon:"st.doors.garage.garage-closed", backgroundColor:"#79b821" } } standardTile("contact", "device.contact", width: 2, height: 2) { - state("open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e") - state("closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821") + state("open", label:'Open', icon:"st.contact.contact.open", backgroundColor:"#ffa81e") + state("closed", label:'Closed', icon:"st.contact.contact.closed", backgroundColor:"#79b821") } standardTile("acceleration", "device.acceleration", width: 2, height: 2) { - state("active", label:'${name}', icon:"st.motion.acceleration.active", backgroundColor:"#53a7c0") - state("inactive", label:'${name}', icon:"st.motion.acceleration.inactive", backgroundColor:"#ffffff") + state("active", label:'Active', icon:"st.motion.acceleration.active", backgroundColor:"#53a7c0") + state("inactive", label:'Inactive', icon:"st.motion.acceleration.inactive", backgroundColor:"#ffffff") } valueTile("temperature", "device.temperature", width: 2, height: 2) { state("temperature", label:'${currentValue}°', 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 93453ac..0da5905 100644 --- a/smartapps/smartthings/hue-connect.src/hue-connect.groovy +++ b/smartapps/smartthings/hue-connect.src/hue-connect.groovy @@ -24,7 +24,7 @@ definition( category: "SmartThings Labs", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/hue.png", iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/hue@2x.png", - //singleInstance: true + singleInstance: true ) preferences { @@ -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() } @@ -161,7 +161,7 @@ private sendDeveloperReq() { headers: [ HOST: host ], - body: [devicetype: "$token-0", username: "$token-0"]], "${selectedHue}")) + body: [devicetype: "$token-0"]], "${selectedHue}")) } private discoverHueBulbs() { @@ -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,35 +657,53 @@ def setColorTemperature(childDevice, huesettings) { } def setColor(childDevice, 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 + log.debug "Executing 'setColor($huesettings)'" + + def value = [:] + def hue = null + def sat = null + def xy = null + + 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) + } + + // Default behavior is to turn light on + value.on = true - def value = [sat: sat, hue: hue, alert: alert, transitiontime: transition] - 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 - } + 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.switch) { - value.on = huesettings.switch == "on" - } + // 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) { @@ -743,6 +776,57 @@ private getBridgeIP() { 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) { Integer.parseInt(hex,16) } 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..517d5ee 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: @@ -34,6 +40,7 @@ preferences { page name:"pageSetup" page name:"Setup" page name:"Settings" + page name: "timeIntervalInput" } @@ -51,8 +58,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 +76,7 @@ def Setup() { def newMode = [ name: "newMode", type: "mode", - title: "Which?", + title: "Modes", multiple: true, required: true ] @@ -96,14 +102,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 +114,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 +129,6 @@ def Setup() { section("Number of active lights at any given time") { input number_of_active_lights } - section("People") { - input people - } } } @@ -162,30 +158,58 @@ 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 } } } -page(name: "timeIntervalInput", title: "Only during a certain time", refreshAfterSelection:true) { +def timeIntervalInput() { + dynamicPage(name: "timeIntervalInput") { 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() { initialize() } @@ -201,10 +225,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 +239,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 +319,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 +343,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 +404,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 +451,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