From 878f39a6264343bce15816219454c229f545c8e4 Mon Sep 17 00:00:00 2001 From: Donald Kirker Date: Mon, 10 Aug 2020 08:52:35 -0700 Subject: [PATCH] CHAD-4340 Update Window Shade devices to use Window Shade Level (#30151) --- .../axis/axis-gear-st.src/axis-gear-st.groovy | 151 ++++--- .../qubino-flush-shutter.groovy | 5 +- .../springs-window-fashions-shade.groovy | 416 +++++++++--------- .../zigbee-window-shade-battery.groovy | 99 +++-- .../zigbee-window-shade.groovy | 79 +++- .../zwave-window-shade.groovy | 374 ++++++++-------- 6 files changed, 612 insertions(+), 512 deletions(-) diff --git a/devicetypes/axis/axis-gear-st.src/axis-gear-st.groovy b/devicetypes/axis/axis-gear-st.src/axis-gear-st.groovy index e9913b1128f..20628511266 100644 --- a/devicetypes/axis/axis-gear-st.src/axis-gear-st.groovy +++ b/devicetypes/axis/axis-gear-st.src/axis-gear-st.groovy @@ -1,8 +1,9 @@ import groovy.json.JsonOutput metadata { - definition (name: "AXIS Gear ST", namespace: "axis", author: "AXIS Labs", ocfDeviceType: "oic.d.blind", vid: "generic-shade-3") { + definition (name: "AXIS Gear ST", namespace: "axis", author: "AXIS Labs", ocfDeviceType: "oic.d.blind", vid: "generic-shade-3") { capability "Window Shade" + capability "Window Shade Level" capability "Window Shade Preset" capability "Switch Level" capability "Battery" @@ -10,18 +11,18 @@ metadata { capability "Health Check" capability "Actuator" capability "Configuration" - + // added in for Google Assistant Operability - capability "Switch" - + capability "Switch" + //Custom Commandes to achieve 25% increment control command "ShadesUp" command "ShadesDown" - + // command to stop blinds command "stop" command "getversion" - + fingerprint profileID: "0104", manufacturer: "AXIS", model: "Gear", deviceJoinName: "AXIS Window Treatment" //AXIS Gear fingerprint profileId: "0104", deviceId: "0202", inClusters: "0000, 0003, 0006, 0008, 0102, 0020, 0001", outClusters: "0019", manufacturer: "AXIS", model: "Gear", deviceJoinName: "AXIS Window Treatment" //AXIS Gear fingerprint endpointID: "01, C4", profileId: "0104, C25D", deviceId: "0202", inClusters: "0000, 0003, 0006, 0008, 0102, 0020, 0001", outClusters: "0019", manufacturer: "AXIS", model: "Gear", deviceJoinName: "AXIS Window Treatment" //AXIS Gear @@ -36,7 +37,7 @@ metadata { //Updated 2019-08-09 - minor changes and improvements, onoff state reporting fixed //Updated 2019-11-11 - minor changes } - + tiles(scale: 2) { multiAttributeTile(name:"windowShade", type: "lighting", width: 3, height: 3) { tileAttribute("device.windowShade", key: "PRIMARY_CONTROL") { @@ -44,10 +45,10 @@ metadata { attributeState("partially open", label: 'Partially Open', action:"close", icon:"http://i.imgur.com/vBA17WL.png", backgroundColor:"#ffcc33", nextState: "closing") attributeState("closed", label: 'Closed', action:"open", icon:"http://i.imgur.com/mtHdMse.png", backgroundColor:"#bbbbdd", nextState: "opening") attributeState("opening", label: 'Opening', action: "stop", icon: "http://i.imgur.com/vBA17WL.png", backgroundColor: "#ffcc33", nextState: "stopping") - attributeState("closing", label: 'Closing', action: "stop", icon: "http://i.imgur.com/vBA17WL.png", backgroundColor: "#bbbbdd", nextState: "stopping") - attributeState("stopping", label: 'Stopping', icon: "http://i.imgur.com/vBA17WL.png", backgroundColor: "#ff7777") - attributeState("stoppingNS", label: 'Stopping Not Supported', icon: "http://i.imgur.com/vBA17WL.png", backgroundColor: "#ff7777") - attributeState("unknown", label: 'Configuring.... Please Wait', icon:"http://i.imgur.com/vBA17WL.png", backgroundColor: "#ff7777") + attributeState("closing", label: 'Closing', action: "stop", icon: "http://i.imgur.com/vBA17WL.png", backgroundColor: "#bbbbdd", nextState: "stopping") + attributeState("stopping", label: 'Stopping', icon: "http://i.imgur.com/vBA17WL.png", backgroundColor: "#ff7777") + attributeState("stoppingNS", label: 'Stopping Not Supported', icon: "http://i.imgur.com/vBA17WL.png", backgroundColor: "#ff7777") + attributeState("unknown", label: 'Configuring.... Please Wait', icon:"http://i.imgur.com/vBA17WL.png", backgroundColor: "#ff7777") } tileAttribute ("device.level", key: "VALUE_CONTROL") { attributeState("VALUE_UP", action: "ShadesUp") @@ -61,9 +62,9 @@ metadata { state("closed", label:'Closed', action:"open", icon:"http://i.imgur.com/SAiEADI.png", backgroundColor:"#bbbbdd", nextState: "opening") state("opening", label: 'Opening', action: "stop", icon: "http://i.imgur.com/y0ZpmZp.png", backgroundColor: "#ffcc33", nextState: "stopping") state("closing", label: 'Closing', action: "stop", icon: "http://i.imgur.com/y0ZpmZp.png", backgroundColor: "#bbbbdd", nextState: "stopping") - state("stopping", label: 'Stopping', icon: "http://i.imgur.com/y0ZpmZp.png", backgroundColor: "#ff7777") - state("stoppingNS", label: 'Stopping Not Supported', icon: "http://i.imgur.com/y0ZpmZp.png", backgroundColor: "#ff7777") - state("unknown", label: 'Configuring', icon:"http://i.imgur.com/y0ZpmZp.png", backgroundColor: "#ff7777") + state("stopping", label: 'Stopping', icon: "http://i.imgur.com/y0ZpmZp.png", backgroundColor: "#ff7777") + state("stoppingNS", label: 'Stopping Not Supported', icon: "http://i.imgur.com/y0ZpmZp.png", backgroundColor: "#ff7777") + state("unknown", label: 'Configuring', icon:"http://i.imgur.com/y0ZpmZp.png", backgroundColor: "#ff7777") } controlTile("mediumSlider", "device.level", "slider",decoration:"flat",height:2, width: 2, inactiveLabel: true) { state("level", action:"switch level.setLevel") @@ -86,7 +87,7 @@ metadata { preferences { input "preset", "number", title: "Preset position", description: "Set the window shade preset position", defaultValue: 50, required: false, displayDuringSetup: true, range:"1..100" } - + main(["main"]) details(["windowShade", "mediumSlider", "contPause", "home", "version", "battery", "refresh"]) } @@ -117,31 +118,31 @@ private getMIN_WINDOW_COVERING_VERSION() {1093} //Custom command to increment blind position by 25 % def ShadesUp() { - def shadeValue = device.latestValue("level") as Integer ?: 0 - + def shadeValue = device.latestValue("shadeLevel") as Integer ?: device.latestValue("level") as Integer ?: 0 + if (shadeValue < 100) { shadeValue = Math.min(25 * (Math.round(shadeValue / 25) + 1), 100) as Integer } - else { + else { shadeValue = 100 } //sendEvent(name:"level", value:shadeValue, displayed:true) - setLevel(shadeValue) + setShadeLevel(shadeValue) //sendEvent(name: "windowShade", value: "opening") } //Custom command to decrement blind position by 25 % def ShadesDown() { - def shadeValue = device.latestValue("level") as Integer ?: 0 - + def shadeValue = device.latestValue("shadeLevel") as Integer ?: device.latestValue("level") as Integer ?: 0 + if (shadeValue > 0) { shadeValue = Math.max(25 * (Math.round(shadeValue / 25) - 1), 0) as Integer } - else { + else { shadeValue = 0 } //sendEvent(name:"level", value:shadeValue, displayed:true) - setLevel(shadeValue) + setShadeLevel(shadeValue) //sendEvent(name: "windowShade", value: "closing") } @@ -160,11 +161,11 @@ def stop() { } else { if (state.currentVersion >= MIN_WINDOW_COVERING_VERSION){ - return zigbee.readAttribute(CLUSTER_WINDOWCOVERING, WINDOWCOVERING_ATTR_LIFTPERCENTAGE) + return zigbee.readAttribute(CLUSTER_WINDOWCOVERING, WINDOWCOVERING_ATTR_LIFTPERCENTAGE) } else { sendEvent(name: "windowShade", value: "stoppingNS") - return zigbee.readAttribute(CLUSTER_LEVEL, LEVEL_ATTR_LEVEL, [delay:5000]) + return zigbee.readAttribute(CLUSTER_LEVEL, LEVEL_ATTR_LEVEL, [delay:5000]) } } } @@ -176,42 +177,39 @@ def pause() { //Send Command through setLevel() def on() { log.info "on()" - sendEvent(name: "windowShade", value: "opening") sendEvent(name: "switch", value: "on") - - if (state.currentVersion >= MIN_WINDOW_COVERING_VERSION) { - zigbee.command(CLUSTER_WINDOWCOVERING, WINDOWCOVERING_CMD_OPEN) - } - else { - setLevel(100) - } + open() } //Send Command through setLevel() def off() { log.info "off()" - sendEvent(name: "windowShade", value: "closing") sendEvent(name: "switch", value: "off") close() - //zigbee.off() } //Command to set the blind position (%) and log the event def setLevel(value, rate=null) { log.info "setLevel ($value)" - + + setShadeLevel(value) +} + +def setShadeLevel(value) { + log.info "setShadeLevel ($value)" Integer currentLevel = state.level - + def i = value as Integer - sendEvent(name:"level", value: value, displayed:true) - + sendEvent(name:"level", value: value, unit:"%", displayed: false) + sendEvent(name:"shadeLevel", value: value, unit:"%", displayed:true) + if ( i == 0) { sendEvent(name: "switch", value: "off") } else { sendEvent(name: "switch", value: "on") } - + if (i > currentLevel) { sendEvent(name: "windowShade", value: "opening") } @@ -219,7 +217,7 @@ def setLevel(value, rate=null) { sendEvent(name: "windowShade", value: "closing") } //setWindowShade(i) - + if (state.currentVersion >= MIN_WINDOW_COVERING_VERSION){ zigbee.command(CLUSTER_WINDOWCOVERING,WINDOWCOVERING_CMD_GOTOLIFTPERCENTAGE, zigbee.convertToHexString(100-i,2)) } @@ -236,8 +234,8 @@ def open() { zigbee.command(CLUSTER_WINDOWCOVERING, WINDOWCOVERING_CMD_OPEN) } else { - setLevel(100) - } + setShadeLevel(100) + } } //Send Command through setLevel() def close() { @@ -247,13 +245,13 @@ def close() { zigbee.command(CLUSTER_WINDOWCOVERING, WINDOWCOVERING_CMD_CLOSE) } else { - setLevel(0) + setShadeLevel(0) } } def presetPosition() { log.info "presetPosition()" - setLevel(preset ?: state.preset ?: 50) + setShadeLevel(preset ?: state.preset ?: 50) } //Reporting of Battery & position levels @@ -262,7 +260,7 @@ def ping(){ return refresh() } -//Set blind State based on position (which shows appropriate image) +//Set blind State based on position (which shows appropriate image) def setWindowShade(value) { if ((value>0)&&(value<99)){ sendEvent(name: "windowShade", value: "partially open", displayed:true) @@ -279,7 +277,7 @@ def setWindowShade(value) { def refresh() { log.debug "parse() refresh" def cmds_refresh = null - + if (state.currentVersion >= MIN_WINDOW_COVERING_VERSION){ cmds_refresh = zigbee.readAttribute(CLUSTER_WINDOWCOVERING, WINDOWCOVERING_ATTR_LIFTPERCENTAGE) } @@ -287,24 +285,24 @@ def refresh() { cmds_refresh = zigbee.readAttribute(CLUSTER_LEVEL, LEVEL_ATTR_LEVEL) } - - cmds_refresh = cmds_refresh + + + cmds_refresh = cmds_refresh + zigbee.readAttribute(CLUSTER_POWER, POWER_ATTR_BATTERY) + zigbee.readAttribute(CLUSTER_BASIC, BASIC_ATTR_SWBUILDID) - + log.info "refresh() --- cmds: $cmds_refresh" - + return cmds_refresh } def getversion () { //state.currentVersion = 0 - sendEvent(name: "version", value: "Checking Version ... ") + sendEvent(name: "version", value: "Checking Version ... ") return zigbee.readAttribute(CLUSTER_BASIC, BASIC_ATTR_SWBUILDID) } //configure reporting -def configure() { +def configure() { state.currentVersion = 0 sendEvent(name: "windowShade", value: "unknown") log.debug "Configuring Reporting and Bindings." @@ -316,33 +314,42 @@ def configure() { zigbee.readAttribute(CLUSTER_ONOFF, ONOFF_ATTR_ONOFFSTATE) + zigbee.readAttribute(CLUSTER_LEVEL, LEVEL_ATTR_LEVEL) + zigbee.readAttribute(CLUSTER_POWER, POWER_ATTR_BATTERY) - - def cmds = zigbee.configureReporting(CLUSTER_WINDOWCOVERING, WINDOWCOVERING_ATTR_LIFTPERCENTAGE, 0x20, 1, 3600, 0x00) + + + def cmds = zigbee.configureReporting(CLUSTER_WINDOWCOVERING, WINDOWCOVERING_ATTR_LIFTPERCENTAGE, 0x20, 1, 3600, 0x00) + zigbee.configureReporting(CLUSTER_ONOFF, ONOFF_ATTR_ONOFFSTATE, 0x10, 1, 3600, 0x00) + zigbee.configureReporting(CLUSTER_LEVEL, LEVEL_ATTR_LEVEL, 0x20, 1, 3600, 0x00) + zigbee.configureReporting(CLUSTER_POWER, POWER_ATTR_BATTERY, 0x20, 1, 3600, 0x01) - + log.info "configure() --- cmds: $cmds" return attrs_refresh + cmds } def parse(String description) { log.trace "parse() --- description: $description" - + Map map = [:] + if (device.currentValue("shadeLevel") == null && device.currentValue("level") != null) { + sendEvent(name: "shadeLevel", value: device.currentValue("level"), unit: "%") + } + def event = zigbee.getEvent(description) if (event && description?.startsWith('on/off')) { log.trace "sendEvent(event)" sendEvent(event) } - + else if ((description?.startsWith('read attr -')) || (description?.startsWith('attr report -'))) { map = parseReportAttributeMessage(description) def result = map ? createEvent(map) : null + + if (map.name == "level") { + result = [result, createEvent([name: "shadeLevel", value: map.value, unit: map.unit])] + } + log.debug "parse() --- returned: $result" return result - } + } } private Map parseReportAttributeMessage(String description) { @@ -366,23 +373,27 @@ private Map parseReportAttributeMessage(String description) { //Set icon based on device feedback for the open, closed, & partial configuration resultMap.value = levelValue state.level = levelValue + resultMap.unit = "%" + resultMap.displayed = false setWindowShade(levelValue) } else if (descMap.clusterInt == CLUSTER_LEVEL && descMap.attrInt == LEVEL_ATTR_LEVEL) { //log.debug "parse() --- returned level :$state.currentVersion " - def currentLevel = state.level - + def currentLevel = state.level + resultMap.name = "level" def levelValue = Math.round(Integer.parseInt(descMap.value, 16)) def levelValuePercent = Math.round((levelValue/255)*100) //Set icon based on device feedback for the open, closed, & partial configuration resultMap.value = levelValuePercent state.level = levelValuePercent - + resultMap.unit = "%" + resultMap.displayed = false + if (state.currentVersion >= MIN_WINDOW_COVERING_VERSION) { //Integer currentLevel = state.level - sendEvent(name:"level", value: levelValuePercent, displayed:true) - + sendEvent(name:"level", value: levelValuePercent, unit: "%", displayed: false) + if (levelValuePercent > currentLevel) { sendEvent(name: "windowShade", value: "opening") } else if (levelValuePercent < currentLevel) { @@ -396,21 +407,21 @@ private Map parseReportAttributeMessage(String description) { else if (descMap.clusterInt == CLUSTER_BASIC && descMap.attrInt == BASIC_ATTR_SWBUILDID) { resultMap.name = "version" def versionString = descMap.value - + StringBuilder output = new StringBuilder("") StringBuilder output2 = new StringBuilder("") - + for (int i = 0; i < versionString.length(); i += 2) { String str = versionString.substring(i, i + 2) - output.append((char) (Integer.parseInt(str, 16))) + output.append((char) (Integer.parseInt(str, 16))) if (i > 19) { output2.append((char) (Integer.parseInt(str, 16))) } - } - + } + def current = Integer.parseInt(output2.toString()) state.currentVersion = current - resultMap.value = output.toString() + resultMap.value = output.toString() } else { log.debug "parseReportAttributeMessage() --- ignoring attribute" diff --git a/devicetypes/qubino/qubino-flush-shutter.src/qubino-flush-shutter.groovy b/devicetypes/qubino/qubino-flush-shutter.src/qubino-flush-shutter.groovy index 22cd56f40f1..7728e4078f0 100644 --- a/devicetypes/qubino/qubino-flush-shutter.src/qubino-flush-shutter.groovy +++ b/devicetypes/qubino/qubino-flush-shutter.src/qubino-flush-shutter.groovy @@ -12,6 +12,7 @@ * for the specific language governing permissions and limitations under the License. * */ +import groovy.json.JsonOutput metadata { definition (name: "Qubino Flush Shutter", namespace: "qubino", author: "SmartThings", ocfDeviceType: "oic.d.blind", mcdSync: true) { @@ -92,7 +93,7 @@ def installed() { state.currentPreferencesState."$it.key".status = "synced" } // Preferences template end - sendEvent(name: "supportedWindowShadeCommands", value: ["open", "close", "pause"]) + sendEvent(name: "supportedWindowShadeCommands", value: JsonOutput.toJson(["open", "close", "pause"])) } def updated() { @@ -251,7 +252,7 @@ def setShadeLevel(level) { } def setSlats(level) { - def time = (int) (state.timeOfVenetianMovement * 1.1) + def time = (int) (state.timeOfVenetianMovement * 1.1) sendHubCommand([ encap(zwave.switchMultilevelV3.switchMultilevelSet(value: Math.min(0x63, level)), 2), "delay ${time}", diff --git a/devicetypes/smartthings/springs-window-fashions-shade.src/springs-window-fashions-shade.groovy b/devicetypes/smartthings/springs-window-fashions-shade.src/springs-window-fashions-shade.groovy index 7af556576ba..48a0482e4e2 100644 --- a/devicetypes/smartthings/springs-window-fashions-shade.src/springs-window-fashions-shade.groovy +++ b/devicetypes/smartthings/springs-window-fashions-shade.src/springs-window-fashions-shade.groovy @@ -15,275 +15,285 @@ import groovy.json.JsonOutput metadata { - definition (name: "Springs Window Fashions Shade", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.blind") { - capability "Window Shade" - capability "Window Shade Preset" - capability "Battery" - capability "Refresh" - capability "Health Check" - capability "Actuator" - capability "Sensor" - - command "stop" - - capability "Switch Level" // until we get a Window Shade Level capability - - // This device handler is specifically for SWF window coverings - // -// fingerprint type: "0x1107", cc: "0x5E,0x26", deviceJoinName: "Window Shade" -// fingerprint type: "0x9A00", cc: "0x5E,0x26", deviceJoinName: "Window Shade" - fingerprint mfr:"026E", prod:"4353", model:"5A31", deviceJoinName: "Springs Window Treatment" //Window Shade - fingerprint mfr:"026E", prod:"5253", model:"5A31", deviceJoinName: "Springs Window Treatment" //Roller Shade - } - - simulator { - status "open": "command: 2603, payload: FF" - status "closed": "command: 2603, payload: 00" - status "10%": "command: 2603, payload: 0A" - status "66%": "command: 2603, payload: 42" - status "99%": "command: 2603, payload: 63" - status "battery 100%": "command: 8003, payload: 64" - status "battery low": "command: 8003, payload: FF" - - // reply messages - reply "2001FF,delay 1000,2602": "command: 2603, payload: 10 FF FE" - reply "200100,delay 1000,2602": "command: 2603, payload: 60 00 FE" - reply "200142,delay 1000,2602": "command: 2603, payload: 10 42 FE" - reply "200163,delay 1000,2602": "command: 2603, payload: 10 63 FE" - } - - tiles(scale: 2) { - multiAttributeTile(name:"windowShade", type: "generic", width: 6, height: 4){ - tileAttribute ("device.windowShade", key: "PRIMARY_CONTROL") { - attributeState "open", label:'${name}', action:"close", icon:"st.shades.shade-open", backgroundColor:"#79b821", nextState:"closing" - attributeState "closed", label:'${name}', action:"open", icon:"st.shades.shade-closed", backgroundColor:"#ffffff", nextState:"opening" - attributeState "partially open", label:'Open', action:"close", icon:"st.shades.shade-open", backgroundColor:"#79b821", nextState:"closing" - attributeState "opening", label:'${name}', action:"stop", icon:"st.shades.shade-opening", backgroundColor:"#79b821", nextState:"partially open" - attributeState "closing", label:'${name}', action:"stop", icon:"st.shades.shade-closing", backgroundColor:"#ffffff", nextState:"partially open" - } - tileAttribute ("device.level", key: "SLIDER_CONTROL") { - attributeState "level", action:"setLevel" - } - } - - standardTile("home", "device.level", width: 2, height: 2, decoration: "flat") { - state "default", label: "home", action:"presetPosition", icon:"st.Home.home2" - } - - standardTile("refresh", "device.refresh", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { - state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh", nextState: "disabled" - state "disabled", label:'', action:"", icon:"st.secondary.refresh" - } - - valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) { - state "battery", label:'batt.', unit:"", - backgroundColors:[ - [value: 0, color: "#bc2323"], - [value: 6, color: "#44b621"] - ] - } - - preferences { - input "switchDirection", "bool", title: "Flip the orientation of the shade", defaultValue: false, required: false, displayDuringSetup: false -// input "preset", "number", title: "Default half-open position (1-100). Springs Window Fashions users should consult their manuals.", defaultValue: 50, required: false, displayDuringSetup: false - } - - main(["windowShade"]) - details(["windowShade", "home", "refresh", "battery"]) - - } + definition (name: "Springs Window Fashions Shade", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.blind") { + capability "Window Shade" + capability "Window Shade Level" + capability "Window Shade Preset" + capability "Switch Level" + capability "Battery" + capability "Refresh" + capability "Health Check" + capability "Actuator" + capability "Sensor" + + command "stop" + + // This device handler is specifically for SWF window coverings + // + //fingerprint type: "0x1107", cc: "0x5E,0x26", deviceJoinName: "Window Shade" + //fingerprint type: "0x9A00", cc: "0x5E,0x26", deviceJoinName: "Window Shade" + fingerprint mfr:"026E", prod:"4353", model:"5A31", deviceJoinName: "Springs Window Treatment" //Window Shade + fingerprint mfr:"026E", prod:"5253", model:"5A31", deviceJoinName: "Springs Window Treatment" //Roller Shade + } + + simulator { + status "open": "command: 2603, payload: FF" + status "closed": "command: 2603, payload: 00" + status "10%": "command: 2603, payload: 0A" + status "66%": "command: 2603, payload: 42" + status "99%": "command: 2603, payload: 63" + status "battery 100%": "command: 8003, payload: 64" + status "battery low": "command: 8003, payload: FF" + + // reply messages + reply "2001FF,delay 1000,2602": "command: 2603, payload: 10 FF FE" + reply "200100,delay 1000,2602": "command: 2603, payload: 60 00 FE" + reply "200142,delay 1000,2602": "command: 2603, payload: 10 42 FE" + reply "200163,delay 1000,2602": "command: 2603, payload: 10 63 FE" + } + + tiles(scale: 2) { + multiAttributeTile(name:"windowShade", type: "generic", width: 6, height: 4){ + tileAttribute ("device.windowShade", key: "PRIMARY_CONTROL") { + attributeState "open", label:'${name}', action:"close", icon:"st.shades.shade-open", backgroundColor:"#00A0DC", nextState:"closing" + attributeState "closed", label:'${name}', action:"open", icon:"st.shades.shade-closed", backgroundColor:"#ffffff", nextState:"opening" + attributeState "partially open", label:'Open', action:"close", icon:"st.shades.shade-open", backgroundColor:"#00A0DC", nextState:"closing" + attributeState "opening", label:'${name}', action:"stop", icon:"st.shades.shade-opening", backgroundColor:"#00A0DC", nextState:"partially open" + attributeState "closing", label:'${name}', action:"stop", icon:"st.shades.shade-closing", backgroundColor:"#ffffff", nextState:"partially open" + } + tileAttribute ("device.windowShadeLevel", key: "SLIDER_CONTROL") { + attributeState "shadeLevel", action:"setShadeLevel" + } + } + + standardTile("home", "device.level", width: 2, height: 2, decoration: "flat") { + state "default", label: "home", action:"presetPosition", icon:"st.Home.home2" + } + + standardTile("refresh", "device.refresh", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh", nextState: "disabled" + state "disabled", label:'', action:"", icon:"st.secondary.refresh" + } + + valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) { + state "battery", label:'batt.', unit:"", + backgroundColors:[ + [value: 0, color: "#bc2323"], + [value: 6, color: "#44b621"] + ] + } + + preferences { + input "switchDirection", "bool", title: "Flip the orientation of the shade", defaultValue: false, required: false, displayDuringSetup: false + //input "preset", "number", title: "Default half-open position (1-100). Springs Window Fashions users should consult their manuals.", defaultValue: 50, required: false, displayDuringSetup: false + } + + main(["windowShade"]) + details(["windowShade", "home", "refresh", "battery"]) + + } } def parse(String description) { - def result = null - //if (description =~ /command: 2603, payload: ([0-9A-Fa-f]{6})/) - // TODO: Workaround manual parsing of v4 multilevel report - def cmd = zwave.parse(description, [0x20: 1, 0x26: 3]) // TODO: switch to SwitchMultilevel v4 and use target value - if (cmd) { - result = zwaveEvent(cmd) - } - log.debug "Parsed '$description' to ${result.inspect()}" - return result + def result = null + + if (device.currentValue("shadeLevel") == null && device.currentValue("level") != null) { + sendEvent(name: "shadeLevel", value: device.currentValue("level"), unit: "%") + } + + //if (description =~ /command: 2603, payload: ([0-9A-Fa-f]{6})/) + // TODO: Workaround manual parsing of v4 multilevel report + def cmd = zwave.parse(description, [0x20: 1, 0x26: 3]) // TODO: switch to SwitchMultilevel v4 and use target value + if (cmd) { + result = zwaveEvent(cmd) + } + log.debug "Parsed '$description' to ${result.inspect()}" + return result } def getCheckInterval() { - // These are battery-powered devices, and it's not very critical - // to know whether they're online or not – 12 hrs - 4 * 60 * 60 + // These are battery-powered devices, and it's not very critical + // to know whether they're online or not – 12 hrs + 4 * 60 * 60 } def installed() { - sendEvent(name: "checkInterval", value: checkInterval, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) - sendEvent(name: "supportedWindowShadeCommands", value: JsonOutput.toJson(["open", "close", "pause"]), displayed: false) - response(refresh()) + sendEvent(name: "checkInterval", value: checkInterval, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) + sendEvent(name: "supportedWindowShadeCommands", value: JsonOutput.toJson(["open", "close", "pause"]), displayed: false) + response(refresh()) } def updated() { - if (device.latestValue("checkInterval") != checkInterval) { - sendEvent(name: "checkInterval", value: checkInterval, displayed: false) - } - def cmds = [] - if (!device.latestState("battery")) { - cmds << zwave.batteryV1.batteryGet().format() - } - - if (!device.getDataValue("MSR")) { - cmds << zwave.manufacturerSpecificV1.manufacturerSpecificGet().format() - } - - log.debug("Updated with settings $settings") - cmds << zwave.switchMultilevelV1.switchMultilevelGet().format() - response(cmds) + if (device.latestValue("checkInterval") != checkInterval) { + sendEvent(name: "checkInterval", value: checkInterval, displayed: false) + } + def cmds = [] + if (!device.latestState("battery")) { + cmds << zwave.batteryV1.batteryGet().format() + } + + if (!device.getDataValue("MSR")) { + cmds << zwave.manufacturerSpecificV1.manufacturerSpecificGet().format() + } + + log.debug("Updated with settings $settings") + cmds << zwave.switchMultilevelV1.switchMultilevelGet().format() + response(cmds) } def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { - handleLevelReport(cmd) + handleLevelReport(cmd) } def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) { - handleLevelReport(cmd) + handleLevelReport(cmd) } def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelReport cmd) { - handleLevelReport(cmd) + handleLevelReport(cmd) } private handleLevelReport(physicalgraph.zwave.Command cmd) { - def descriptionText = null - def shadeValue = null - - def level = cmd.value as Integer - level = switchDirection ? 99-level : level - if (level >= 99) { - level = 100 - shadeValue = "open" - } else if (level <= 0) { - level = 0 // unlike dimmer switches, the level isn't saved when closed - shadeValue = "closed" - } else { - shadeValue = "partially open" - descriptionText = "${device.displayName} shade is ${level}% open" - } - checkLevelReport(level) - def levelEvent = createEvent(name: "level", value: level, unit: "%", displayed: false) - def stateEvent = createEvent(name: "windowShade", value: shadeValue, descriptionText: descriptionText, isStateChange: levelEvent.isStateChange) - - def result = [stateEvent, levelEvent] - if (!state.lastbatt || now() - state.lastbatt > 24 * 60 * 60 * 1000) { - log.debug "requesting battery" - state.lastbatt = (now() - 23 * 60 * 60 * 1000) // don't queue up multiple battery reqs in a row - result << response(["delay 15000", zwave.batteryV1.batteryGet().format()]) - } - result + def descriptionText = null + def shadeValue = null + + def level = cmd.value as Integer + level = switchDirection ? 99-level : level + if (level >= 99) { + level = 100 + shadeValue = "open" + } else if (level <= 0) { + level = 0 // unlike dimmer switches, the level isn't saved when closed + shadeValue = "closed" + } else { + shadeValue = "partially open" + descriptionText = "${device.displayName} shade is ${level}% open" + } + checkLevelReport(level) + + def levelEvent = createEvent(name: "level", value: level, unit: "%", displayed: false) + def shadeLevelEvent = createEvent(name: "shadeLevel", value: level, unit: "%") + def stateEvent = createEvent(name: "windowShade", value: shadeValue, descriptionText: descriptionText, isStateChange: shadeLevelEvent.isStateChange) + + def result = [stateEvent, shadeLevelEvent, levelEvent] + if (!state.lastbatt || now() - state.lastbatt > 24 * 60 * 60 * 1000) { + log.debug "requesting battery" + state.lastbatt = (now() - 23 * 60 * 60 * 1000) // don't queue up multiple battery reqs in a row + result << response(["delay 15000", zwave.batteryV1.batteryGet().format()]) + } + result } def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelStopLevelChange cmd) { - [ createEvent(name: "windowShade", value: "partially open", displayed: false, descriptionText: "$device.displayName shade stopped"), - response(zwave.switchMultilevelV1.switchMultilevelGet().format()) ] + [ createEvent(name: "windowShade", value: "partially open", displayed: false, descriptionText: "$device.displayName shade stopped"), + response(zwave.switchMultilevelV1.switchMultilevelGet().format()) ] } def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { - def map = [ name: "battery", unit: "%" ] - if (cmd.batteryLevel == 0xFF || cmd.batteryLevel == 0) { - map.value = 1 - map.descriptionText = "${device.displayName} has a low battery" - map.isStateChange = true - } else { - map.value = cmd.batteryLevel - } - state.lastbatt = now() - if (map.value <= 1 && device.latestValue("battery") != null && device.latestValue("battery") - map.value > 20) { - // Springs shades sometimes erroneously report a low battery when rapidly actuated manually. They'll still - // refuse to actuate after one of these reports, but this will limit the bad data that gets surfaced - log.warn "Erroneous battery report dropped from ${device.latestValue("battery")} to $map.value. Not reporting" - } else { - createEvent(map) - } + def map = [ name: "battery", unit: "%" ] + if (cmd.batteryLevel == 0xFF || cmd.batteryLevel == 0) { + map.value = 1 + map.descriptionText = "${device.displayName} has a low battery" + map.isStateChange = true + } else { + map.value = cmd.batteryLevel + } + state.lastbatt = now() + if (map.value <= 1 && device.latestValue("battery") != null && device.latestValue("battery") - map.value > 20) { + // Springs shades sometimes erroneously report a low battery when rapidly actuated manually. They'll still + // refuse to actuate after one of these reports, but this will limit the bad data that gets surfaced + log.warn "Erroneous battery report dropped from ${device.latestValue("battery")} to $map.value. Not reporting" + } else { + createEvent(map) + } } def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) { - // the docs we got said that the device would send a notification report, but we've determined that - // is not true + // the docs we got said that the device would send a notification report, but we've determined that + // is not true } def zwaveEvent(physicalgraph.zwave.Command cmd) { - log.debug "unhandled $cmd" - return [] + log.debug "unhandled $cmd" + return [] } def open() { - log.debug "open()" - def level = switchDirection ? 0 : 99 - levelChangeFollowUp(level) - zwave.basicV1.basicSet(value: level).format() - // zwave.basicV1.basicSet(value: 0xFF).format() + log.debug "open()" + + setShadeLevel(99) // Handle switchDirection in setShadeLevel } def close() { - log.debug "close()" - def level = switchDirection ? 99 : 0 - levelChangeFollowUp(level) - zwave.basicV1.basicSet(value: level).format() - //zwave.basicV1.basicSet(value: 0).format() + log.debug "close()" + + setShadeLevel(0) // Handle switchDirection in setShadeLevel } def setLevel(value, duration = null) { - log.debug "setLevel(${value.inspect()})" - Integer level = value as Integer - level = switchDirection ? 99-level : level - if (level < 0) level = 0 - if (level > 99) level = 99 - levelChangeFollowUp(level) - zwave.basicV1.basicSet(value: level).format() + log.debug "setLevel($value)" + + setShadeLevel(value) +} + +def setShadeLevel(value) { + Integer level = Math.max(Math.min(value as Integer, 99), 0) + + level = switchDirection ? 99-level : level + + log.debug "setShadeLevel($value) -> $level" + + levelChangeFollowUp(level) // Follow up in a few seconds to make sure the shades didn't "forget" to send us level updates + zwave.basicV1.basicSet(value: level).format() } def presetPosition() { - zwave.switchMultilevelV1.switchMultilevelSet(value: 0xFF).format() + zwave.switchMultilevelV1.switchMultilevelSet(value: 0xFF).format() } def pause() { - log.debug "pause()" - stop() + log.debug "pause()" + stop() } def stop() { - log.debug "stop()" - zwave.switchMultilevelV3.switchMultilevelStopLevelChange().format() + log.debug "stop()" + zwave.switchMultilevelV3.switchMultilevelStopLevelChange().format() } def ping() { - zwave.switchMultilevelV1.switchMultilevelGet().format() + zwave.switchMultilevelV1.switchMultilevelGet().format() } def refresh() { - log.debug "refresh()" - delayBetween([ - zwave.switchMultilevelV1.switchMultilevelGet().format(), - zwave.batteryV1.batteryGet().format() - ], 1500) + log.debug "refresh()" + delayBetween([ + zwave.switchMultilevelV1.switchMultilevelGet().format(), + zwave.batteryV1.batteryGet().format() + ], 1500) } def levelChangeFollowUp(expectedLevel) { - state.expectedValue = expectedLevel - state.levelChecks = 0 - runIn(5, "checkLevel", [overwrite: true]) + state.expectedValue = expectedLevel + state.levelChecks = 0 + runIn(5, "checkLevel", [overwrite: true]) } def checkLevelReport(value) { - if (state.expectedValue != null) { - if ((state.expectedValue == 99 && value >= 99) || - (value >= state.expectedValue - 2 && value <= state.expectedValue + 2)) { - unschedule("checkLevel") - } - } + if (state.expectedValue != null) { + if ((state.expectedValue == 99 && value >= 99) || + (value >= state.expectedValue - 2 && value <= state.expectedValue + 2)) { + unschedule("checkLevel") + } + } } def checkLevel() { - if (state.levelChecks != null && state.levelChecks < 5) { - state.levelChecks = state.levelChecks + 1 - runIn(5, "checkLevel", [overwrite: true]) - sendHubCommand(zwave.switchMultilevelV1.switchMultilevelGet()) - } else { - unschedule("checkLevel") - } -} \ No newline at end of file + if (state.levelChecks != null && state.levelChecks < 5) { + state.levelChecks = state.levelChecks + 1 + runIn(5, "checkLevel", [overwrite: true]) + sendHubCommand(zwave.switchMultilevelV1.switchMultilevelGet()) + } else { + unschedule("checkLevel") + } +} diff --git a/devicetypes/smartthings/zigbee-window-shade-battery.src/zigbee-window-shade-battery.groovy b/devicetypes/smartthings/zigbee-window-shade-battery.src/zigbee-window-shade-battery.groovy index 797ddd4f432..f2445c53c9a 100644 --- a/devicetypes/smartthings/zigbee-window-shade-battery.src/zigbee-window-shade-battery.groovy +++ b/devicetypes/smartthings/zigbee-window-shade-battery.src/zigbee-window-shade-battery.groovy @@ -11,6 +11,7 @@ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License * for the specific language governing permissions and limitations under the License. */ + import groovy.json.JsonOutput import physicalgraph.zigbee.zcl.DataType @@ -21,6 +22,7 @@ metadata { capability "Configuration" capability "Refresh" capability "Window Shade" + capability "Window Shade Level" capability "Window Shade Preset" capability "Health Check" capability "Switch Level" @@ -36,13 +38,16 @@ metadata { } tiles(scale: 2) { - multiAttributeTile(name:"windowShade", type: "generic", width: 6, height: 4) { + multiAttributeTile(name:"windowShade", type: "lighting", width: 6, height: 4) { tileAttribute("device.windowShade", key: "PRIMARY_CONTROL") { - attributeState "open", label: 'Open', action: "close", icon: "http://www.ezex.co.kr/img/st/window_open.png", backgroundColor: "#00A0DC", nextState: "closing" - attributeState "closed", label: 'Closed', action: "open", icon: "http://www.ezex.co.kr/img/st/window_close.png", backgroundColor: "#ffffff", nextState: "opening" - attributeState "partially open", label: 'Partially open', action: "close", icon: "http://www.ezex.co.kr/img/st/window_open.png", backgroundColor: "#d45614", nextState: "closing" - attributeState "opening", label: 'Opening', action: "pause", icon: "http://www.ezex.co.kr/img/st/window_open.png", backgroundColor: "#00A0DC", nextState: "partially open" - attributeState "closing", label: 'Closing', action: "pause", icon: "http://www.ezex.co.kr/img/st/window_close.png", backgroundColor: "#ffffff", nextState: "partially open" + attributeState "open", label: 'Open', action: "close", icon: "st.shades.shade-open", backgroundColor: "#00A0DC", nextState: "closing" + attributeState "closed", label: 'Closed', action: "open", icon: "st.shades.shade-closed", backgroundColor: "#ffffff", nextState: "opening" + attributeState "partially open", label: 'Partially open', action: "close", icon: "st.shades.shade-open", backgroundColor: "#00A0DC", nextState: "closing" + attributeState "opening", label: 'Opening', action: "pause", icon: "st.shades.shade-opening", backgroundColor: "#00A0DC", nextState: "partially open" + attributeState "closing", label: 'Closing', action: "pause", icon: "st.shades.shade-closing", backgroundColor: "#ffffff", nextState: "partially open" + } + tileAttribute ("device.windowShadeLevel", key: "SLIDER_CONTROL") { + attributeState "shadeLevel", action:"setShadeLevel" } } standardTile("contPause", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { @@ -54,18 +59,12 @@ metadata { standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 1) { state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" } - valueTile("shadeLevel", "device.level", width: 4, height: 1) { - state "level", label: 'Shade is ${currentValue}% up', defaultState: true - } valueTile("batteryLevel", "device.battery", width: 2, height: 2) { state "battery", label:'${currentValue}% battery', unit:"" } - controlTile("levelSliderControl", "device.level", "slider", width:2, height: 1, inactiveLabel: false) { - state "level", action:"switch level.setLevel" - } main "windowShade" - details(["windowShade", "contPause", "presetPosition", "shadeLevel", "levelSliderControl", "refresh", "batteryLevel"]) + details(["windowShade", "contPause", "presetPosition", "refresh", "batteryLevel"]) } } @@ -87,27 +86,31 @@ private List collectAttributes(Map descMap) { if (descMap.additionalAttrs) { descMaps.addAll(descMap.additionalAttrs) } - return descMaps -} -def installed() { - log.debug "installed" - sendEvent(name: "supportedWindowShadeCommands", value: JsonOutput.toJson(["open", "close", "pause"])) + return descMaps } // Parse incoming device messages to generate events def parse(String description) { log.debug "description:- ${description}" + + if (device.currentValue("shadeLevel") == null && device.currentValue("level") != null) { + sendEvent(name: "shadeLevel", value: device.currentValue("level"), unit: "%") + } + if (description?.startsWith("read attr -")) { Map descMap = zigbee.parseDescriptionAsMap(description) + if (isBindingTableMessage(description)) { parseBindingTableMessage(description) } else if (supportsLiftPercentage() && descMap?.clusterInt == CLUSTER_WINDOW_COVERING && descMap.value) { log.debug "attr: ${descMap?.attrInt}, value: ${descMap?.value}, descValue: ${Integer.parseInt(descMap.value, 16)}, ${device.getDataValue("model")}" List descMaps = collectAttributes(descMap) def liftmap = descMaps.find { it.attrInt == ATTRIBUTE_POSITION_LIFT } + if (liftmap && liftmap.value) { def newLevel = zigbee.convertHexToInt(liftmap.value) + if (shouldInvertLiftPercentage()) { // some devices report % level of being closed (instead of % level of being opened) // inverting that logic is needed here to avoid a code duplication @@ -121,18 +124,23 @@ def parse(String description) { levelEventHandler(valueInt) } else if (reportsBatteryPercentage() && descMap?.clusterInt == zigbee.POWER_CONFIGURATION_CLUSTER && zigbee.convertHexToInt(descMap?.attrId) == BATTERY_PERCENTAGE_REMAINING && descMap.value) { def batteryLevel = zigbee.convertHexToInt(descMap.value) + batteryPercentageEventHandler(batteryLevel) } } } def levelEventHandler(currentLevel) { - def lastLevel = device.currentValue("level") + def lastLevel = device.currentValue("shadeLevel") ?: device.currentValue("level") // Try shadeLevel, if not use level and pass to logic below + log.debug "levelEventHandle - currentLevel: ${currentLevel} lastLevel: ${lastLevel}" + if (lastLevel == "undefined" || currentLevel == lastLevel) { //Ignore invalid reports log.debug "Ignore invalid reports" } else { - sendEvent(name: "level", value: currentLevel) + sendEvent(name: "shadeLevel", value: currentLevel, unit: "%") + sendEvent(name: "level", value: currentLevel, unit: "%", displayed: false) + if (currentLevel == 0 || currentLevel == 100) { sendEvent(name: "windowShade", value: currentLevel == 0 ? "closed" : "open") } else { @@ -147,8 +155,9 @@ def levelEventHandler(currentLevel) { } def updateFinalState() { - def level = device.currentValue("level") + def level = device.currentValue("shadeLevel") log.debug "updateFinalState: ${level}" + if (level > 0 && level < 100) { sendEvent(name: "windowShade", value: "partially open") } @@ -163,28 +172,40 @@ def batteryPercentageEventHandler(batteryLevel) { def close() { log.info "close()" - setLevel(0) + + setShadeLevel(0) } def open() { log.info "open()" - setLevel(100) + + setShadeLevel(100) } -def setLevel(data, rate = null) { - log.info "setLevel()" +def setLevel(value, rate = null) { + log.info "setLevel($value)" + + setShadeLevel(value) +} + +def setShadeLevel(value) { + log.info "setShadeLevel($value)" + + Integer level = Math.max(Math.min(value as Integer, 100), 0) def cmd + if (supportsLiftPercentage()) { if (shouldInvertLiftPercentage()) { // some devices keeps % level of being closed (instead of % level of being opened) // inverting that logic is needed here - data = 100 - data + level = 100 - level } - cmd = zigbee.command(CLUSTER_WINDOW_COVERING, COMMAND_GOTO_LIFT_PERCENTAGE, zigbee.convertToHexString(data, 2)) + cmd = zigbee.command(CLUSTER_WINDOW_COVERING, COMMAND_GOTO_LIFT_PERCENTAGE, zigbee.convertToHexString(level, 2)) } else { - cmd = zigbee.command(zigbee.LEVEL_CONTROL_CLUSTER, COMMAND_MOVE_LEVEL_ONOFF, zigbee.convertToHexString(Math.round(data * 255 / 100), 2)) + cmd = zigbee.command(zigbee.LEVEL_CONTROL_CLUSTER, COMMAND_MOVE_LEVEL_ONOFF, zigbee.convertToHexString(Math.round(level * 255 / 100), 2)) } - cmd + + return cmd } def pause() { @@ -197,7 +218,7 @@ def pause() { } def presetPosition() { - setLevel(preset ?: 50) + setShadeLevel(preset ?: 50) } /** @@ -210,21 +231,32 @@ def ping() { def refresh() { log.info "refresh()" def cmds + if (supportsLiftPercentage()) { cmds = zigbee.readAttribute(CLUSTER_WINDOW_COVERING, ATTRIBUTE_POSITION_LIFT) } else { cmds = zigbee.readAttribute(zigbee.LEVEL_CONTROL_CLUSTER, ATTRIBUTE_CURRENT_LEVEL) } + return cmds } +def installed() { + log.debug "installed" + + sendEvent(name: "supportedWindowShadeCommands", value: JsonOutput.toJson(["open", "close", "pause"]), displayed: false) +} + def configure() { - // Device-Watch allows 2 check-in misses from device + ping (plus 2 min lag time) + def cmds + log.info "configure()" + + // Device-Watch allows 2 check-in misses from device + ping (plus 2 min lag time) sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + log.debug "Configuring Reporting and Bindings." - def cmds if (supportsLiftPercentage()) { cmds = zigbee.configureReporting(CLUSTER_WINDOW_COVERING, ATTRIBUTE_POSITION_LIFT, DataType.UINT8, 2, 600, null) } else { @@ -248,6 +280,7 @@ def usesLocalGroupBinding() { private def parseBindingTableMessage(description) { Integer groupAddr = getGroupAddrFromBindingTable(description) + if (groupAddr) { List cmds = addHubToGroup(groupAddr) cmds?.collect { new physicalgraph.device.HubAction(it) } @@ -258,7 +291,9 @@ private Integer getGroupAddrFromBindingTable(description) { log.info "Parsing binding table - '$description'" def btr = zigbee.parseBindingTableResponse(description) def groupEntry = btr?.table_entries?.find { it.dstAddrMode == 1 } + log.info "Found ${groupEntry}" + !groupEntry?.dstAddr ?: Integer.parseInt(groupEntry.dstAddr, 16) } diff --git a/devicetypes/smartthings/zigbee-window-shade.src/zigbee-window-shade.groovy b/devicetypes/smartthings/zigbee-window-shade.src/zigbee-window-shade.groovy index 98f1a878f1b..7ad3527fa07 100755 --- a/devicetypes/smartthings/zigbee-window-shade.src/zigbee-window-shade.groovy +++ b/devicetypes/smartthings/zigbee-window-shade.src/zigbee-window-shade.groovy @@ -22,6 +22,7 @@ metadata { capability "Configuration" capability "Refresh" capability "Window Shade" + capability "Window Shade Level" capability "Window Shade Preset" capability "Health Check" capability "Switch Level" @@ -41,7 +42,7 @@ metadata { } tiles(scale: 2) { - multiAttributeTile(name:"windowShade", type: "generic", width: 6, height: 4) { + multiAttributeTile(name:"windowShade", type: "lighting", width: 6, height: 4) { tileAttribute("device.windowShade", key: "PRIMARY_CONTROL") { attributeState "open", label: 'Open', action: "close", icon: "http://www.ezex.co.kr/img/st/window_open.png", backgroundColor: "#00A0DC", nextState: "closing" attributeState "closed", label: 'Closed', action: "open", icon: "http://www.ezex.co.kr/img/st/window_close.png", backgroundColor: "#ffffff", nextState: "opening" @@ -49,6 +50,9 @@ metadata { attributeState "opening", label: 'Opening', action: "pause", icon: "http://www.ezex.co.kr/img/st/window_open.png", backgroundColor: "#00A0DC", nextState: "partially open" attributeState "closing", label: 'Closing', action: "pause", icon: "http://www.ezex.co.kr/img/st/window_close.png", backgroundColor: "#ffffff", nextState: "partially open" } + tileAttribute ("device.windowShadeLevel", key: "SLIDER_CONTROL") { + attributeState "shadeLevel", action:"setShadeLevel" + } } standardTile("contPause", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { state "pause", label:"", icon:'st.sonos.pause-btn', action:'pause', backgroundColor:"#cccccc" @@ -59,15 +63,9 @@ metadata { standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 1) { state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" } - valueTile("shadeLevel", "device.level", width: 4, height: 1) { - state "level", label: 'Shade is ${currentValue}% up', defaultState: true - } - controlTile("levelSliderControl", "device.level", "slider", width:2, height: 1, inactiveLabel: false) { - state "level", action:"switch level.setLevel" - } main "windowShade" - details(["windowShade", "contPause", "presetPosition", "shadeLevel", "levelSliderControl", "refresh"]) + details(["windowShade", "contPause", "presetPosition", "refresh"]) } } @@ -89,22 +87,31 @@ private List collectAttributes(Map descMap) { if (descMap.additionalAttrs) { descMaps.addAll(descMap.additionalAttrs) } + return descMaps } // Parse incoming device messages to generate events def parse(String description) { log.debug "description:- ${description}" + + if (device.currentValue("shadeLevel") == null && device.currentValue("level") != null) { + sendEvent(name: "shadeLevel", value: device.currentValue("level"), unit: "%") + } + if (description?.startsWith("read attr -")) { Map descMap = zigbee.parseDescriptionAsMap(description) + if (isBindingTableMessage(description)) { parseBindingTableMessage(description) } else if (supportsLiftPercentage() && descMap?.clusterInt == CLUSTER_WINDOW_COVERING && descMap.value) { log.debug "attr: ${descMap?.attrInt}, value: ${descMap?.value}, descValue: ${Integer.parseInt(descMap.value, 16)}, ${device.getDataValue("model")}" List descMaps = collectAttributes(descMap) def liftmap = descMaps.find { it.attrInt == ATTRIBUTE_POSITION_LIFT } + if (liftmap && liftmap.value) { def newLevel = zigbee.convertHexToInt(liftmap.value) + if (shouldInvertLiftPercentage()) { // some devices report % level of being closed (instead of % level of being opened) // inverting that logic is needed here to avoid a code duplication @@ -118,18 +125,23 @@ def parse(String description) { levelEventHandler(valueInt) } else if (reportsBatteryPercentage() && descMap?.clusterInt == zigbee.POWER_CONFIGURATION_CLUSTER && zigbee.convertHexToInt(descMap?.attrId) == BATTERY_PERCENTAGE_REMAINING && descMap.value) { def batteryLevel = zigbee.convertHexToInt(descMap.value) + batteryPercentageEventHandler(batteryLevel) } } } def levelEventHandler(currentLevel) { - def lastLevel = device.currentValue("level") + def lastLevel = device.currentValue("shadeLevel") ?: device.currentValue("level") // Try shadeLevel, if not use level and pass to logic below + log.debug "levelEventHandle - currentLevel: ${currentLevel} lastLevel: ${lastLevel}" + if (lastLevel == "undefined" || currentLevel == lastLevel) { //Ignore invalid reports log.debug "Ignore invalid reports" } else { - sendEvent(name: "level", value: currentLevel) + sendEvent(name: "shadeLevel", value: currentLevel, unit: "%") + sendEvent(name: "level", value: currentLevel, unit: "%", displayed: false) + if (currentLevel == 0 || currentLevel == 100) { sendEvent(name: "windowShade", value: currentLevel == 0 ? "closed" : "open") } else { @@ -144,8 +156,9 @@ def levelEventHandler(currentLevel) { } def updateFinalState() { - def level = device.currentValue("level") + def level = device.currentValue("shadeLevel") log.debug "updateFinalState: ${level}" + if (level > 0 && level < 100) { sendEvent(name: "windowShade", value: "partially open") } @@ -158,10 +171,6 @@ def batteryPercentageEventHandler(batteryLevel) { } } -def supportsLiftPercentage() { - device.getDataValue("manufacturer") != "Feibit Co.Ltd" -} - def close() { log.info "close()" zigbee.command(CLUSTER_WINDOW_COVERING, COMMAND_CLOSE) @@ -172,19 +181,29 @@ def open() { zigbee.command(CLUSTER_WINDOW_COVERING, COMMAND_OPEN) } -def setLevel(data, rate = null) { - log.info "setLevel()" +def setLevel(value, rate = null) { + log.info "setLevel($value)" + + setShadeLevel(value) +} + +def setShadeLevel(value) { + log.info "setShadeLevel($value)" + + Integer level = Math.max(Math.min(value as Integer, 100), 0) def cmd + if (supportsLiftPercentage()) { if (shouldInvertLiftPercentage()) { // some devices keeps % level of being closed (instead of % level of being opened) // inverting that logic is needed here - data = 100 - data + level = 100 - level } - cmd = zigbee.command(CLUSTER_WINDOW_COVERING, COMMAND_GOTO_LIFT_PERCENTAGE, zigbee.convertToHexString(data, 2)) + cmd = zigbee.command(CLUSTER_WINDOW_COVERING, COMMAND_GOTO_LIFT_PERCENTAGE, zigbee.convertToHexString(level, 2)) } else { - cmd = zigbee.command(zigbee.LEVEL_CONTROL_CLUSTER, COMMAND_MOVE_LEVEL_ONOFF, zigbee.convertToHexString(Math.round(data * 255 / 100), 2)) + cmd = zigbee.command(zigbee.LEVEL_CONTROL_CLUSTER, COMMAND_MOVE_LEVEL_ONOFF, zigbee.convertToHexString(Math.round(level * 255 / 100), 2)) } + return cmd } @@ -194,7 +213,7 @@ def pause() { } def presetPosition() { - setLevel(preset ?: 50) + setShadeLevel(preset ?: 50) } /** @@ -207,25 +226,32 @@ def ping() { def refresh() { log.info "refresh()" def cmds + if (supportsLiftPercentage()) { cmds = zigbee.readAttribute(CLUSTER_WINDOW_COVERING, ATTRIBUTE_POSITION_LIFT) } else { cmds = zigbee.readAttribute(zigbee.LEVEL_CONTROL_CLUSTER, ATTRIBUTE_CURRENT_LEVEL) } + return cmds } def installed() { + log.debug "installed" + sendEvent(name: "supportedWindowShadeCommands", value: JsonOutput.toJson(["open", "close", "pause"]), displayed: false) } def configure() { - // Device-Watch allows 2 check-in misses from device + ping (plus 2 min lag time) + def cmds + log.info "configure()" + + // Device-Watch allows 2 check-in misses from device + ping (plus 2 min lag time) sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + log.debug "Configuring Reporting and Bindings." - def cmds if (supportsLiftPercentage()) { cmds = zigbee.configureReporting(CLUSTER_WINDOW_COVERING, ATTRIBUTE_POSITION_LIFT, DataType.UINT8, 0, 600, null) } else { @@ -249,6 +275,7 @@ def usesLocalGroupBinding() { private def parseBindingTableMessage(description) { Integer groupAddr = getGroupAddrFromBindingTable(description) + if (groupAddr) { List cmds = addHubToGroup(groupAddr) cmds?.collect { new physicalgraph.device.HubAction(it) } @@ -259,7 +286,9 @@ private Integer getGroupAddrFromBindingTable(description) { log.info "Parsing binding table - '$description'" def btr = zigbee.parseBindingTableResponse(description) def groupEntry = btr?.table_entries?.find { it.dstAddrMode == 1 } + log.info "Found ${groupEntry}" + !groupEntry?.dstAddr ?: Integer.parseInt(groupEntry.dstAddr, 16) } @@ -271,6 +300,10 @@ private List readDeviceBindingTable() { ["zdo mgmt-bind 0x${device.deviceNetworkId} 0", "delay 200"] } +def supportsLiftPercentage() { + device.getDataValue("manufacturer") != "Feibit Co.Ltd" +} + def shouldInvertLiftPercentage() { return isIkeaKadrilj() || isIkeaFyrtur() || isSomfyGlydea() } diff --git a/devicetypes/smartthings/zwave-window-shade.src/zwave-window-shade.groovy b/devicetypes/smartthings/zwave-window-shade.src/zwave-window-shade.groovy index c43694d37e9..67554aa4a70 100644 --- a/devicetypes/smartthings/zwave-window-shade.src/zwave-window-shade.groovy +++ b/devicetypes/smartthings/zwave-window-shade.src/zwave-window-shade.groovy @@ -15,252 +15,262 @@ import groovy.json.JsonOutput metadata { - definition (name: "Z-Wave Window Shade", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.blind") { - capability "Window Shade" - capability "Window Shade Preset" - capability "Battery" - capability "Refresh" - capability "Health Check" - capability "Actuator" - capability "Sensor" - - command "stop" - - capability "Switch Level" // until we get a Window Shade Level capability - - // This device handler is specifically for non-SWF position-aware window coverings - // - fingerprint type: "0x1107", cc: "0x5E,0x26", deviceJoinName: "Window Treatment" //Window Shade - fingerprint type: "0x9A00", cc: "0x5E,0x26", deviceJoinName: "Window Treatment" //Window Shade -// fingerprint mfr:"026E", prod:"4353", model:"5A31", deviceJoinName: "Window Blinds" -// fingerprint mfr:"026E", prod:"5253", model:"5A31", deviceJoinName: "Roller Shade" - } - - simulator { - status "open": "command: 2603, payload: FF" - status "closed": "command: 2603, payload: 00" - status "10%": "command: 2603, payload: 0A" - status "66%": "command: 2603, payload: 42" - status "99%": "command: 2603, payload: 63" - status "battery 100%": "command: 8003, payload: 64" - status "battery low": "command: 8003, payload: FF" - - // reply messages - reply "2001FF,delay 1000,2602": "command: 2603, payload: 10 FF FE" - reply "200100,delay 1000,2602": "command: 2603, payload: 60 00 FE" - reply "200142,delay 1000,2602": "command: 2603, payload: 10 42 FE" - reply "200163,delay 1000,2602": "command: 2603, payload: 10 63 FE" - } - - tiles(scale: 2) { - multiAttributeTile(name:"windowShade", type: "lighting", width: 6, height: 4){ - tileAttribute ("device.windowShade", key: "PRIMARY_CONTROL") { - attributeState "open", label:'${name}', action:"close", icon:"st.shades.shade-open", backgroundColor:"#79b821", nextState:"closing" - attributeState "closed", label:'${name}', action:"open", icon:"st.shades.shade-closed", backgroundColor:"#ffffff", nextState:"opening" - attributeState "partially open", label:'Open', action:"close", icon:"st.shades.shade-open", backgroundColor:"#79b821", nextState:"closing" - attributeState "opening", label:'${name}', action:"stop", icon:"st.shades.shade-opening", backgroundColor:"#79b821", nextState:"partially open" - attributeState "closing", label:'${name}', action:"stop", icon:"st.shades.shade-closing", backgroundColor:"#ffffff", nextState:"partially open" - } - tileAttribute ("device.level", key: "SLIDER_CONTROL") { - attributeState "level", action:"setLevel" - } - } - - standardTile("home", "device.level", width: 2, height: 2, decoration: "flat") { - state "default", label: "home", action:"presetPosition", icon:"st.Home.home2" - } - - standardTile("refresh", "device.refresh", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { - state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh", nextState: "disabled" - state "disabled", label:'', action:"", icon:"st.secondary.refresh" - } - - valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) { - state "battery", label:'${currentValue}% battery', unit:"" - } - - preferences { - input "preset", "number", title: "Preset position", description: "Set the window shade preset position", defaultValue: 50, range: "1..100", required: false, displayDuringSetup: false - } - - main(["windowShade"]) - details(["windowShade", "home", "refresh", "battery"]) - - } + definition (name: "Z-Wave Window Shade", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.blind") { + capability "Window Shade" + capability "Window Shade Level" + capability "Window Shade Preset" + capability "Switch Level" + capability "Battery" + capability "Refresh" + capability "Health Check" + capability "Actuator" + capability "Sensor" + + command "stop" + + // This device handler is specifically for non-SWF position-aware window coverings + // + fingerprint type: "0x1107", cc: "0x5E,0x26", deviceJoinName: "Window Treatment" //Window Shade + fingerprint type: "0x9A00", cc: "0x5E,0x26", deviceJoinName: "Window Treatment" //Window Shade +// fingerprint mfr:"026E", prod:"4353", model:"5A31", deviceJoinName: "Window Blinds" +// fingerprint mfr:"026E", prod:"5253", model:"5A31", deviceJoinName: "Roller Shade" + } + + simulator { + status "open": "command: 2603, payload: FF" + status "closed": "command: 2603, payload: 00" + status "10%": "command: 2603, payload: 0A" + status "66%": "command: 2603, payload: 42" + status "99%": "command: 2603, payload: 63" + status "battery 100%": "command: 8003, payload: 64" + status "battery low": "command: 8003, payload: FF" + + // reply messages + reply "2001FF,delay 1000,2602": "command: 2603, payload: 10 FF FE" + reply "200100,delay 1000,2602": "command: 2603, payload: 60 00 FE" + reply "200142,delay 1000,2602": "command: 2603, payload: 10 42 FE" + reply "200163,delay 1000,2602": "command: 2603, payload: 10 63 FE" + } + + tiles(scale: 2) { + multiAttributeTile(name:"windowShade", type: "lighting", width: 6, height: 4){ + tileAttribute ("device.windowShade", key: "PRIMARY_CONTROL") { + attributeState "open", label:'${name}', action:"close", icon:"st.shades.shade-open", backgroundColor:"#00A0DC", nextState:"closing" + attributeState "closed", label:'${name}', action:"open", icon:"st.shades.shade-closed", backgroundColor:"#ffffff", nextState:"opening" + attributeState "partially open", label:'Open', action:"close", icon:"st.shades.shade-open", backgroundColor:"#00A0DC", nextState:"closing" + attributeState "opening", label:'${name}', action:"stop", icon:"st.shades.shade-opening", backgroundColor:"#00A0DC", nextState:"partially open" + attributeState "closing", label:'${name}', action:"stop", icon:"st.shades.shade-closing", backgroundColor:"#ffffff", nextState:"partially open" + } + tileAttribute ("device.windowShadeLevel", key: "SLIDER_CONTROL") { + attributeState "shadeLevel", action:"setShadeLevel" + } + } + + standardTile("home", "device.level", width: 2, height: 2, decoration: "flat") { + state "default", label: "home", action:"presetPosition", icon:"st.Home.home2" + } + + standardTile("refresh", "device.refresh", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh", nextState: "disabled" + state "disabled", label:'', action:"", icon:"st.secondary.refresh" + } + + valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) { + state "battery", label:'${currentValue}% battery', unit:"" + } + + preferences { + input "preset", "number", title: "Preset position", description: "Set the window shade preset position", defaultValue: 50, range: "1..100", required: false, displayDuringSetup: false + } + + main(["windowShade"]) + details(["windowShade", "home", "refresh", "battery"]) + + } } def parse(String description) { - def result = null - //if (description =~ /command: 2603, payload: ([0-9A-Fa-f]{6})/) - // TODO: Workaround manual parsing of v4 multilevel report - def cmd = zwave.parse(description, [0x20: 1, 0x26: 3]) // TODO: switch to SwitchMultilevel v4 and use target value - if (cmd) { - result = zwaveEvent(cmd) - } - log.debug "Parsed '$description' to ${result.inspect()}" - return result + def result = null + + if (device.currentValue("shadeLevel") == null && device.currentValue("level") != null) { + sendEvent(name: "shadeLevel", value: device.currentValue("level"), unit: "%") + } + + //if (description =~ /command: 2603, payload: ([0-9A-Fa-f]{6})/) + // TODO: Workaround manual parsing of v4 multilevel report + def cmd = zwave.parse(description, [0x20: 1, 0x26: 3]) // TODO: switch to SwitchMultilevel v4 and use target value + if (cmd) { + result = zwaveEvent(cmd) + } + log.debug "Parsed '$description' to ${result.inspect()}" + return result } def getCheckInterval() { - // These are battery-powered devices, and it's not very critical - // to know whether they're online or not – 12 hrs - 4 * 60 * 60 + // These are battery-powered devices, and it's not very critical + // to know whether they're online or not – 12 hrs + 4 * 60 * 60 } def installed() { - sendEvent(name: "checkInterval", value: checkInterval, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) - sendEvent(name: "supportedWindowShadeCommands", value: JsonOutput.toJson(["open", "close", "pause"]), displayed: false) - response(refresh()) + sendEvent(name: "checkInterval", value: checkInterval, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) + sendEvent(name: "supportedWindowShadeCommands", value: JsonOutput.toJson(["open", "close", "pause"]), displayed: false) + response(refresh()) } def updated() { - if (device.latestValue("checkInterval") != checkInterval) { - sendEvent(name: "checkInterval", value: checkInterval, displayed: false) - } - if (!device.latestState("battery")) { - response(zwave.batteryV1.batteryGet()) - } + if (device.latestValue("checkInterval") != checkInterval) { + sendEvent(name: "checkInterval", value: checkInterval, displayed: false) + } + if (!device.latestState("battery")) { + response(zwave.batteryV1.batteryGet()) + } } def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { - handleLevelReport(cmd) + handleLevelReport(cmd) } def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) { - handleLevelReport(cmd) + handleLevelReport(cmd) } def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelReport cmd) { - handleLevelReport(cmd) + handleLevelReport(cmd) } private handleLevelReport(physicalgraph.zwave.Command cmd) { - def descriptionText = null - def shadeValue = null - - def level = cmd.value as Integer - if (level >= 99) { - level = 100 - shadeValue = "open" - } else if (level <= 0) { - level = 0 // unlike dimmer switches, the level isn't saved when closed - shadeValue = "closed" - } else { - shadeValue = "partially open" - descriptionText = "${device.displayName} shade is ${level}% open" - } - checkLevelReport(level) - def levelEvent = createEvent(name: "level", value: level, unit: "%", displayed: false) - def stateEvent = createEvent(name: "windowShade", value: shadeValue, descriptionText: descriptionText, isStateChange: levelEvent.isStateChange) - - def result = [stateEvent, levelEvent] - if (!state.lastbatt || now() - state.lastbatt > 24 * 60 * 60 * 1000) { - log.debug "requesting battery" - state.lastbatt = (now() - 23 * 60 * 60 * 1000) // don't queue up multiple battery reqs in a row - result << response(["delay 15000", zwave.batteryV1.batteryGet().format()]) - } - result + def descriptionText = null + def shadeValue = null + + def level = cmd.value as Integer + if (level >= 99) { + level = 100 + shadeValue = "open" + } else if (level <= 0) { + level = 0 // unlike dimmer switches, the level isn't saved when closed + shadeValue = "closed" + } else { + shadeValue = "partially open" + descriptionText = "${device.displayName} shade is ${level}% open" + } + checkLevelReport(level) + + def levelEvent = createEvent(name: "level", value: level, unit: "%", displayed: false) + def shadeLevelEvent = createEvent(name: "shadeLevel", value: level, unit: "%") + def stateEvent = createEvent(name: "windowShade", value: shadeValue, descriptionText: descriptionText, isStateChange: shadeLevelEvent.isStateChange) + + def result = [stateEvent, shadeLevelEvent, levelEvent] + if (!state.lastbatt || now() - state.lastbatt > 24 * 60 * 60 * 1000) { + log.debug "requesting battery" + state.lastbatt = (now() - 23 * 60 * 60 * 1000) // don't queue up multiple battery reqs in a row + result << response(["delay 15000", zwave.batteryV1.batteryGet().format()]) + } + result } def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelStopLevelChange cmd) { - [ createEvent(name: "windowShade", value: "partially open", displayed: false, descriptionText: "$device.displayName shade stopped"), - response(zwave.switchMultilevelV1.switchMultilevelGet().format()) ] + [ createEvent(name: "windowShade", value: "partially open", displayed: false, descriptionText: "$device.displayName shade stopped"), + response(zwave.switchMultilevelV1.switchMultilevelGet().format()) ] } def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { - def map = [ name: "battery", unit: "%" ] - if (cmd.batteryLevel == 0xFF) { - map.value = 1 - map.descriptionText = "${device.displayName} has a low battery" - map.isStateChange = true - } else { - map.value = cmd.batteryLevel - } - state.lastbatt = now() - createEvent(map) + def map = [ name: "battery", unit: "%" ] + + if (cmd.batteryLevel == 0xFF) { + map.value = 1 + map.descriptionText = "${device.displayName} has a low battery" + map.isStateChange = true + } else { + map.value = cmd.batteryLevel + } + + state.lastbatt = now() + + createEvent(map) } def zwaveEvent(physicalgraph.zwave.Command cmd) { - log.debug "unhandled $cmd" - return [] + log.debug "unhandled $cmd" + return [] } def open() { - levelChangeFollowUp(99) - log.debug "open()" - /*delayBetween([ - zwave.basicV1.basicSet(value: 0xFF).format(), - zwave.switchMultilevelV1.switchMultilevelGet().format() - ], 1000)*/ - zwave.basicV1.basicSet(value: 99).format() + log.debug "open()" + + setShadeLevel(99) } def close() { - levelChangeFollowUp(0) - log.debug "close()" - /*delayBetween([ - zwave.basicV1.basicSet(value: 0x00).format(), - zwave.switchMultilevelV1.switchMultilevelGet().format() - ], 1000)*/ - zwave.basicV1.basicSet(value: 0).format() + log.debug "close()" + + setShadeLevel(0) } def setLevel(value, duration = null) { - log.debug "setLevel(${value.inspect()})" - Integer level = value as Integer - if (level < 0) level = 0 - if (level > 99) level = 99 - levelChangeFollowUp(level) - zwave.basicV1.basicSet(value: level).format() + log.debug "setLevel($value)" + + setShadeLevel(value) +} + +def setShadeLevel(value) { + Integer level = Math.max(Math.min(value as Integer, 99), 0) + + log.debug "setShadeLevel($value) -> $level" + + levelChangeFollowUp(level) // Follow up in a few seconds to make sure the shades didn't "forget" to send us level updates + zwave.basicV1.basicSet(value: level).format() } def presetPosition() { - setLevel(preset ?: state.preset ?: 50) + setLevel(preset ?: state.preset ?: 50) } def pause() { - log.debug "pause()" - stop() + log.debug "pause()" + + stop() } def stop() { - log.debug "stop()" - zwave.switchMultilevelV3.switchMultilevelStopLevelChange().format() + log.debug "stop()" + + zwave.switchMultilevelV3.switchMultilevelStopLevelChange().format() } def ping() { - zwave.switchMultilevelV1.switchMultilevelGet().format() + zwave.switchMultilevelV1.switchMultilevelGet().format() } def refresh() { - log.debug "refresh()" - delayBetween([ - zwave.switchMultilevelV1.switchMultilevelGet().format(), - zwave.batteryV1.batteryGet().format() - ], 1500) + log.debug "refresh()" + delayBetween([ + zwave.switchMultilevelV1.switchMultilevelGet().format(), + zwave.batteryV1.batteryGet().format() + ], 1500) } def levelChangeFollowUp(expectedLevel) { - state.expectedValue = expectedLevel - state.levelChecks = 0 - runIn(5, "checkLevel", [overwrite: true]) + state.expectedValue = expectedLevel + state.levelChecks = 0 + runIn(5, "checkLevel", [overwrite: true]) } def checkLevelReport(value) { - if (state.expectedValue != null) { - if ((state.expectedValue == 99 && value >= 99) || - (value >= state.expectedValue - 2 && value <= state.expectedValue + 2)) { - unschedule("checkLevel") - } - } + if (state.expectedValue != null) { + if ((state.expectedValue == 99 && value >= 99) || + (value >= state.expectedValue - 2 && value <= state.expectedValue + 2)) { + unschedule("checkLevel") + } + } } def checkLevel() { - if (state.levelChecks != null && state.levelChecks < 5) { - state.levelChecks = state.levelChecks + 1 - runIn(5, "checkLevel", [overwrite: true]) - sendHubCommand(zwave.switchMultilevelV1.switchMultilevelGet()) - } else { - unschedule("checkLevel") - } + if (state.levelChecks != null && state.levelChecks < 5) { + state.levelChecks = state.levelChecks + 1 + runIn(5, "checkLevel", [overwrite: true]) + sendHubCommand(zwave.switchMultilevelV1.switchMultilevelGet()) + } else { + unschedule("checkLevel") + } }