-
Notifications
You must be signed in to change notification settings - Fork 88.5k
ICP-10949: Moved IKEA shades to a new DTH (based on zigbee-window-shade), because of adding the "Battery" capability. #11364
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
275 changes: 275 additions & 0 deletions
275
devicetypes/smartthings/zigbee-window-shade-battery.src/zigbee-window-shade-battery.groovy
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,275 @@ | ||
| /** | ||
| * | ||
| * Copyright 2019 SmartThings | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except | ||
| * in compliance with the License. You may obtain a copy of the License at: | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed | ||
| * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License | ||
| * for the specific language governing permissions and limitations under the License. | ||
| */ | ||
| import groovy.json.JsonOutput | ||
| import physicalgraph.zigbee.zcl.DataType | ||
|
|
||
| metadata { | ||
| definition(name: "ZigBee Window Shade Battery", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.blind", mnmn: "SmartThings", vid: "generic-shade-2") { | ||
| capability "Actuator" | ||
| capability "Battery" | ||
| capability "Configuration" | ||
| capability "Refresh" | ||
| capability "Window Shade" | ||
| capability "Health Check" | ||
| capability "Switch Level" | ||
|
|
||
| command "pause" | ||
|
|
||
| fingerprint manufacturer: "IKEA of Sweden", model: "KADRILJ roller blind", deviceJoinName: "IKEA KADRILJ Blinds" // raw description 01 0104 0202 00 09 0000 0001 0003 0004 0005 0020 0102 1000 FC7C 02 0019 1000 | ||
| fingerprint manufacturer: "IKEA of Sweden", model: "FYRTUR block-out roller blind", deviceJoinName: "IKEA FYRTUR Blinds" // raw description 01 0104 0202 01 09 0000 0001 0003 0004 0005 0020 0102 1000 FC7C 02 0019 1000 | ||
| } | ||
|
|
||
| tiles(scale: 2) { | ||
| multiAttributeTile(name:"windowShade", type: "generic", 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" | ||
| } | ||
| } | ||
| standardTile("contPause", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { | ||
| state "pause", label:"", icon:'st.sonos.pause-btn', action:'pause', backgroundColor:"#cccccc" | ||
| } | ||
| 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", "shadeLevel", "levelSliderControl", "refresh", "batteryLevel"]) | ||
| } | ||
| } | ||
|
|
||
| private getCLUSTER_WINDOW_COVERING() { 0x0102 } | ||
| private getCOMMAND_OPEN() { 0x00 } | ||
| private getCOMMAND_CLOSE() { 0x01 } | ||
| private getCOMMAND_PAUSE() { 0x02 } | ||
| private getCOMMAND_GOTO_LIFT_PERCENTAGE() { 0x05 } | ||
| private getATTRIBUTE_POSITION_LIFT() { 0x0008 } | ||
| private getATTRIBUTE_CURRENT_LEVEL() { 0x0000 } | ||
| private getCOMMAND_MOVE_LEVEL_ONOFF() { 0x04 } | ||
| private getBATTERY_PERCENTAGE_REMAINING() { 0x0021 } | ||
|
|
||
| private List<Map> collectAttributes(Map descMap) { | ||
| List<Map> descMaps = new ArrayList<Map>() | ||
|
|
||
| descMaps.add(descMap) | ||
|
|
||
| if (descMap.additionalAttrs) { | ||
| descMaps.addAll(descMap.additionalAttrs) | ||
| } | ||
| return descMaps | ||
| } | ||
|
|
||
| def installed() { | ||
| log.debug "installed" | ||
| sendEvent(name: "supportedWindowShadeCommands", value: JsonOutput.toJson(["open", "close", "pause"])) | ||
| } | ||
|
|
||
| // Parse incoming device messages to generate events | ||
| def parse(String description) { | ||
| log.debug "description:- ${description}" | ||
| 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<Map> 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 | ||
| newLevel = 100 - newLevel | ||
| } | ||
| levelEventHandler(newLevel) | ||
| } | ||
| } else if (!supportsLiftPercentage() && descMap?.clusterInt == zigbee.LEVEL_CONTROL_CLUSTER && descMap.value) { | ||
| def valueInt = Math.round((zigbee.convertHexToInt(descMap.value)) / 255 * 100) | ||
|
|
||
| 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") | ||
| 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) | ||
| if (currentLevel == 0 || currentLevel == 100) { | ||
| sendEvent(name: "windowShade", value: currentLevel == 0 ? "closed" : "open") | ||
| } else { | ||
| if (lastLevel < currentLevel) { | ||
| sendEvent([name:"windowShade", value: "opening"]) | ||
| } else if (lastLevel > currentLevel) { | ||
| sendEvent([name:"windowShade", value: "closing"]) | ||
| } | ||
| runIn(1, "updateFinalState", [overwrite:true]) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| def updateFinalState() { | ||
| def level = device.currentValue("level") | ||
| log.debug "updateFinalState: ${level}" | ||
| if (level > 0 && level < 100) { | ||
| sendEvent(name: "windowShade", value: "partially open") | ||
| } | ||
| } | ||
|
|
||
| def batteryPercentageEventHandler(batteryLevel) { | ||
| if (batteryLevel != null) { | ||
| batteryLevel = Math.min(100, Math.max(0, batteryLevel)) | ||
| sendEvent([name: "battery", value: batteryLevel, unit: "%", descriptionText: "{{ device.displayName }} battery was {{ value }}%"]) | ||
| } | ||
| } | ||
|
|
||
| def close() { | ||
| log.info "close()" | ||
| zigbee.command(CLUSTER_WINDOW_COVERING, COMMAND_CLOSE) | ||
| } | ||
|
|
||
| def open() { | ||
| log.info "open()" | ||
| zigbee.command(CLUSTER_WINDOW_COVERING, COMMAND_OPEN) | ||
| } | ||
|
|
||
| def setLevel(data, rate = null) { | ||
| log.info "setLevel()" | ||
| 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 | ||
| } | ||
| cmd = zigbee.command(CLUSTER_WINDOW_COVERING, COMMAND_GOTO_LIFT_PERCENTAGE, zigbee.convertToHexString(data, 2)) | ||
| } else { | ||
| cmd = zigbee.command(zigbee.LEVEL_CONTROL_CLUSTER, COMMAND_MOVE_LEVEL_ONOFF, zigbee.convertToHexString(Math.round(data * 255 / 100), 2)) | ||
| } | ||
| return cmd | ||
| } | ||
|
|
||
| def pause() { | ||
| log.info "pause()" | ||
| zigbee.command(CLUSTER_WINDOW_COVERING, COMMAND_PAUSE) | ||
| } | ||
|
|
||
| /** | ||
| * PING is used by Device-Watch in attempt to reach the Device | ||
| * */ | ||
| def ping() { | ||
| return refresh() | ||
| } | ||
|
|
||
| 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 configure() { | ||
| // Device-Watch allows 2 check-in misses from device + ping (plus 2 min lag time) | ||
| log.info "configure()" | ||
| 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 { | ||
| cmds = zigbee.levelConfig() | ||
| } | ||
|
|
||
| if (usesLocalGroupBinding()) { | ||
| cmds += readDeviceBindingTable() | ||
| } | ||
|
|
||
| if (reportsBatteryPercentage()) { | ||
| cmds += zigbee.configureReporting(zigbee.POWER_CONFIGURATION_CLUSTER, BATTERY_PERCENTAGE_REMAINING, DataType.UINT8, 30, 21600, 0x01) | ||
| } | ||
|
|
||
| return refresh() + cmds | ||
| } | ||
|
|
||
| def usesLocalGroupBinding() { | ||
| isIkeaKadrilj() || isIkeaFyrtur() | ||
| } | ||
|
|
||
| private def parseBindingTableMessage(description) { | ||
| Integer groupAddr = getGroupAddrFromBindingTable(description) | ||
| if (groupAddr) { | ||
| List cmds = addHubToGroup(groupAddr) | ||
| cmds?.collect { new physicalgraph.device.HubAction(it) } | ||
| } | ||
| } | ||
|
|
||
| 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) | ||
| } | ||
|
|
||
| private List addHubToGroup(Integer groupAddr) { | ||
| ["st cmd 0x0000 0x01 ${CLUSTER_GROUPS} 0x00 {${zigbee.swapEndianHex(zigbee.convertToHexString(groupAddr,4))} 00}", "delay 200"] | ||
| } | ||
|
|
||
| private List readDeviceBindingTable() { | ||
| ["zdo mgmt-bind 0x${device.deviceNetworkId} 0", "delay 200"] | ||
| } | ||
|
|
||
| def supportsLiftPercentage() { | ||
| isIkeaKadrilj() || isIkeaFyrtur() | ||
| } | ||
|
|
||
| def shouldInvertLiftPercentage() { | ||
| return isIkeaKadrilj() || isIkeaFyrtur() | ||
| } | ||
|
|
||
| def reportsBatteryPercentage() { | ||
| return isIkeaKadrilj() || isIkeaFyrtur() | ||
| } | ||
|
|
||
| def isIkeaKadrilj() { | ||
| device.getDataValue("model") == "KADRILJ roller blind" | ||
| } | ||
|
|
||
| def isIkeaFyrtur() { | ||
| device.getDataValue("model") == "FYRTUR block-out roller blind" | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think for this PR we should just remove the fingerprints but leave everything else as it is. We'll need to migrate existing devices to the new DTH and then we can remove the unnecessary code in a follow up PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's right, i only removed ikea fingerprints and changed whitespaces (4 spaces -> tab) in zigbee-window-shade.groovy
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are still a bunch of changes in this file that need to be left as is. Maybe you forgot to push your updates?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@tpmanley Anything specific ? I've just removed all the code related do ikea shades (including any supporting methods and other conditions).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's what I'm referring to. Ikea devices joined already will continue using this DTH so we can't remove the Ikea specific code until we run a script to migrate existing devices to the new DTH. The only changes to the existing DTH should be the removal of the fingerprints for now. In the future we can remove the Ikea specific stuff once the migration has been completed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok, thanks for clarification.