Skip to content

Instantly share code, notes, and snippets.

@MorningZ
Created June 22, 2020 15:50
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save MorningZ/87f29c57bd4b6d45df8df33b65187eb0 to your computer and use it in GitHub Desktop.
Save MorningZ/87f29c57bd4b6d45df8df33b65187eb0 to your computer and use it in GitHub Desktop.
Hubitat Driver - IKEA Furtyr
/**
* Added to HE on April 24, 2020
*
* 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.
*
*
* first release for IKEA smart window blinds for hubitat adapted from the driver for ST by Wayne Man
*
* 2020/03/31/ Josh Sutinen
* - Inverted level to match expectation of Google Assistant and Alexa where 0 = closed and 100 = open
*
* 2020/04/13 Philippe Charette
* - Made the level inversion configurable
* - Added option to adjust the "Open" Level
*/
import hubitat.zigbee.zcl.DataType
metadata {
definition(name: "IKEA Window Blinds", namespace: "ryan780", author: "ryan780", ocfDeviceType: "oic.d.blind", mnmn: "SmartThings", vid: "generic-shade") {
capability "Actuator"
capability "Configuration"
capability "Refresh"
capability "Window Shade"
capability "Health Check"
capability "Switch Level"
capability "Battery"
command "pause"
attribute "lastCheckin", "String"
attribute "lastOpened", "String"
fingerprint inClusters: "0000,0001,0003,0004", manufacturer: "IKEA of Sweden", model: "FYRTUR block-out roller blind"
preferences {
input name: "invert", type: "bool", defaultValue: false, title: "Invert level values",
description: "Disabled: 0 = Opened, 100 = Closed.\n" +
"Enabled: 100 = Opened, 0 = Closed."
input name: "adjustOpenLevel", type: "bool", defaultValue: false, title: "Adjust open level",
description: "Activate to set the \"Open\" position."
if(adjustOpenLevel) input name: "openLevel", type: "number", defaultValue: 0, range: "0..100", title: "Adjusted open level",
description: "Set to keep the blind from completely open and let it dangle.\n" +
"The value set will be the new \"0\" (\"100\" if the invert option is checked)."
}
}
}
private getCLUSTER_BATTERY_LEVEL() { 0x0001 }
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 List<Map> collectAttributes(Map descMap) {
List<Map> descMaps = new ArrayList<Map>()
descMaps.add(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}"
def now = new Date().format("yyyy MMM dd EEE h:mm:ss a", location.timeZone)
// send event for heartbeat
sendEvent(name: "lastCheckin", value: now)
if (description?.startsWith("read attr -")) {
Map descMap = zigbee.parseDescriptionAsMap(description)
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)
levelEventHandler(calculatePosition_FromDevice(newLevel))
}
} else if (!supportsLiftPercentage() && descMap?.clusterInt == zigbee.LEVEL_CONTROL_CLUSTER && descMap.value) {
def valueInt = calculatePosition_FromDevice(Math.round((zigbee.convertHexToInt(descMap.value)) / 255 * 100))
levelEventHandler(valueInt)
}
if (descMap?.clusterInt == CLUSTER_BATTERY_LEVEL && descMap.value) {
log.debug "attr: ${descMap?.attrInt}, value: ${descMap?.value}, descValue: ${Integer.parseInt(descMap.value, 16)}"
sendEvent(name: "battery", value: Integer.parseInt(descMap.value, 16))
}
}
}
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) == invert ? "closed" : "open")
} else {
if ((lastLevel > currentLevel) == invert) {
sendEvent([name:"windowShade", value: "closing"])
} else if ((lastLevel < currentLevel) == invert) {
sendEvent([name:"windowShade", value: "opening"])
}
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 supportsLiftPercentage() {
device.getDataValue("manufacturer") != "Feibit Co.Ltd"
}
def close() {
log.info "close()"
zigbee.command(CLUSTER_WINDOW_COVERING, COMMAND_CLOSE)
}
def open() {
log.info "open()"
if (adjustOpenLevel) {
setLevel(invert ? 100 : 0)
} else {
zigbee.command(CLUSTER_WINDOW_COVERING, COMMAND_OPEN)
}
}
def setLevel(data, rate = null) {
log.info "setLevel()"
def cmd
data = data.toInteger()
if (supportsLiftPercentage()) {
cmd = zigbee.command(CLUSTER_WINDOW_COVERING, COMMAND_GOTO_LIFT_PERCENTAGE, zigbee.convertToHexString(calculatePosition_ToDevice(data), 2))
} else {
cmd = zigbee.command(zigbee.LEVEL_CONTROL_CLUSTER, COMMAND_MOVE_LEVEL_ONOFF, zigbee.convertToHexString(calculatePosition_ToDevice(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 zigbee.readAttribute(CLUSTER_BATTERY_LEVEL, 0x0021) // Read the Battery Level
}
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()
}
return refresh() + cmds
}
def setPosition(value){
setLevel(value)
}
Integer calculatePosition_FromDevice(value) {
if (adjustOpenLevel) {
def adjOpenLevel = invert ? 100 - openLevel : openLevel
def a = 100 / (100 - adjOpenLevel)
def b = 100 - (100 * a)
def ret = a * value + b
return Math.round(invert ? 100 - ret : ret)
} else {
return Math.round(invert ? 100 - value : value)
}
}
Integer calculatePosition_ToDevice(value) {
def input = invert ? 100 - value : value
if (adjustOpenLevel) {
def adjOpenLevel = invert ? 100 - openLevel : openLevel
def a = (100 - adjOpenLevel) / 100
def b = 100 - (100 * a)
def ret = a * input + b
return Math.round(ret)
} else {
return Math.round(input)
}
}
@roficas
Copy link

roficas commented Aug 11, 2021

add to line 26 (definition)

importUrl: "https://gist.githubusercontent.com/MorningZ/87f29c57bd4b6d45df8df33b65187eb0/raw/0a12d352bc06199fa413e7005c18d9d23cbe468f/hibitat_ikea_furtyr.groovy"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment