/** * * Fibaro Dimmer 2 (US) * * github: Eric Maycock (erocm123) * email: erocmail@gmail.com * Date: 2016-07-31 8:03PM * Copyright Eric Maycock * * * 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. * * added button to the current state so Hubitat apps can pick up the 3 buttons and added doubletap and releasable button: borristhecat 24/5/19 * added basicSet zwave event so physical button presses are handled by the hub. /eriktack 20190812 * added physical and digital send events. Split logging for info and debug. Added the ability to hide the configure settings. borristhecat 26/8/19 */ metadata { definition (name: "Fibaro Dimmer 2", namespace: "erocm123", author: "Eric Maycock", importUrl: "https://raw.githubusercontent.com/erocm123/Hubitat/master/Drivers/fibaro-dimmer-2.src/fibaro-dimmer-2.groovy") { capability "Actuator" capability "Switch" capability "Switch Level" capability "Refresh" capability "Configuration" capability "Sensor" // capability "Polling" capability "Energy Meter" capability "Power Meter" capability "PushableButton" capability "HoldableButton" capability "ReleasableButton" capability "DoubleTapableButton" capability "Health Check" attribute "needUpdate", "string" attribute "firmware", "String" fingerprint mfr: "010F", prod: "0102", model: "2000", deviceJoinName: "Fibaro Dimmer 2" fingerprint deviceId: "0x1101", inClusters: "0x72,0x86,0x70,0x85,0x8E,0x26,0x7A,0x27,0x73,0xEF,0x26,0x2B" fingerprint deviceId: "0x1101", inClusters: "0x5E,0x20,0x86,0x72,0x26,0x5A,0x59,0x85,0x73,0x98,0x7A,0x56,0x70,0x31,0x32,0x8E,0x60,0x75,0x71,0x27" } preferences { input name: "settingEnable", type: "bool", title: "Enable setting", defaultValue: false input name: "enableDebugging", type: "bool", title: "Enable Debug Logging?", defaultValue: false input name: "enableInfo", type: "bool", title: "Enable Info Logging?", defaultValue: true if (settingEnable) input description: "Once you change values on this page, the corner of the \"configuration\" icon will change orange until all configuration parameters are updated.", title: "Settings", displayDuringSetup: false, type: "paragraph", element: "paragraph" if (settingEnable) generate_preferences(configuration_model()) } } private getCommandClassVersions() { [ 0x20: 1, // Basic 0x25: 1, // Switch Binary 0x70: 1, // Configuration 0x98: 1, // Security 0x60: 3, // Multi Channel 0x8E: 2, // Multi Channel Association 0x26: 1, // Switch Multilevel 0x87: 1, // Indicator 0x72: 2, // Manufacturer Specific 0x5B: 1, // Central Scene 0x32: 3, // Meter 0x85: 2, // Association 0x86: 1, // Version 0x75: 2 // Protection ] } def parse(description) { def result = null if (description.startsWith("Err 106")) { state.sec = 0 result = createEvent(descriptionText: description, isStateChange: true) } else if (description != "updated") { def cmd = zwave.parse(description, commandClassVersions) if (cmd) { result = zwaveEvent(cmd) //log.debug("'$cmd' parsed to $result") } else { log.debug "Couldn't zwave.parse '$description'" } } result } def zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd) { logging(cmd) def request = update_needed_settings() if(request != []){ return [response(commands(request))] } else { return null } } def zwaveEvent(hubitat.zwave.commands.basicv1.BasicSet cmd, ep=null) { logging("$cmd : Endpoint: $ep") /*def event if (!ep) { event = [createEvent([name: "switch", value: cmd.value? "on":"off"])] } return event*/ } def zwaveEvent(hubitat.zwave.commands.sceneactivationv1.SceneActivationSet cmd) { logging(cmd) logging("sceneId: $cmd.sceneId") logging("dimmingDuration: $cmd.dimmingDuration") logging("Configuration for preference \"Switch Type\" is set to ${settings."20"}") if (settings."20" == "2") { Info("Switch configured as Roller blinds") switch (cmd.sceneId) { // Roller blinds S1 case 10: // Turn On (1x click) buttonEvent(1, "pushed") break case 13: // Release buttonEvent(1, "released") break case 14: // 2x click buttonEvent(1, "doubleTapped") break case 17: // Brightening buttonEvent(1, "held") break // Roller blinds S2 case 11: // Turn Off buttonEvent(2, "pushed") break case 13: // Release buttonEvent(2, "released") break case 14: // 2x click buttonEvent(2, "doubleTapped") break case 15: // 3x click buttonEvent(3, "pushed") break case 18: // Dimming buttonEvent(2, "held") break default: logging("Unhandled SceneActivationSet: ${cmd}") break } } else if (settings."20" == "1") { Info("Switch configured as Toggle") switch (cmd.sceneId) { // Toggle S1 case 10: // Off to On buttonEvent(1, "held") break case 11: // On to Off buttonEvent(1, "released") break case 14: // 2x click buttonEvent(1, "doubleTapped") break // Toggle S2 case 20: // Off to On buttonEvent(2, "held") break case 21: // On to Off buttonEvent(2, "released") break case 24: // 2x click buttonEvent(2, "doubleTapped") break case 25: // 3x click buttonEvent(3, "pushed") break default: logging("Unhandled SceneActivationSet: ${cmd}") break } } else { if (settings."20" == "0") Info("Switch configured as Momentary") else logging("Switch type not configured") switch (cmd.sceneId) { // Momentary S1 case 16: // 1x click buttonEvent(1, "pushed") break case 14: // 2x click buttonEvent(1, "doubleTapped") break case 12: // held buttonEvent(1, "held") break case 13: // release buttonEvent(1, "released") break // Momentary S2 case 26: // 1x click buttonEvent(2, "pushed") break case 24: // 2x click buttonEvent(2, "doubleTapped") break case 25: // 3x click buttonEvent(3, "pushed") break case 22: // held buttonEvent(2, "held") break case 23: // release buttonEvent(2, "released") break default: logging("Unhandled SceneActivationSet: ${cmd}") break } } } def buttonEvent(button, value) { Info("buttonEvent() Button:$button, Value:$value") sendEvent(name: value, value: button, isStateChange:true, type: "physical") } def zwaveEvent(hubitat.zwave.commands.switchmultilevelv1.SwitchMultilevelReport cmd) { logging(cmd) dimmerEvents(cmd, (!state.lastRan || now() <= state.lastRan + 2000)?"digital":"physical") } def dimmerEvents(hubitat.zwave.Command cmd, source = null) { def result = [] def value = (cmd.value ? "on" : "off") def switchEvent = createEvent(name: "switch", value: value, descriptionText: "$device.displayName was turned $value [${source?source:'physical'}]", type: source?source:"physical") result << switchEvent if (cmd.value) { result << createEvent(name: "level", value: cmd.value, unit: "%", descriptionText: "$device.displayName was set to ${cmd.value}% [${source?source:'physical'}]", type: source?source:"physical") } if (switchEvent.isStateChange) { result << response(["delay 3000", zwave.meterV2.meterGet(scale: 2).format()]) } return result } def zwaveEvent(hubitat.zwave.commands.associationv2.AssociationReport cmd) { logging(cmd) state."association${cmd.groupingIdentifier}" = cmd.nodeId[0] } def zwaveEvent(hubitat.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { def encapsulatedCommand = cmd.encapsulatedCommand(commandClassVersions) if (encapsulatedCommand) { state.sec = 1 def result = zwaveEvent(encapsulatedCommand) result = result.collect { if (it instanceof hubitat.device.HubAction && !it.toString().startsWith("9881")) { response(cmd.CMD + "00" + it.toString()) } else { it } } result } } def zwaveEvent(hubitat.zwave.commands.crc16encapv1.Crc16Encap cmd) { def encapsulatedCommand = zwave.getCommand(cmd.commandClass, cmd.command, cmd.data,1) if (encapsulatedCommand) { zwaveEvent(encapsulatedCommand) } else { log.warn "Unable to extract CRC16 command from ${cmd}" } } def zwaveEvent(hubitat.zwave.Command cmd) { logging("Unhandled Z-Wave Event: $cmd") } def zwaveEvent(hubitat.zwave.commands.meterv3.MeterReport cmd) { logging(cmd) if (cmd.meterType == 1) { if (cmd.scale == 0) { sendEvent(name: "energy", value: cmd.scaledMeterValue, unit: "kWh", type: "digital") } else if (cmd.scale == 1) { sendEvent(name: "energy", value: cmd.scaledMeterValue, unit: "kVAh", type: "digital") } else if (cmd.scale == 2) { sendEvent(name: "power", value: Math.round(cmd.scaledMeterValue), unit: "W", type: "digital") } else { sendEvent(name: "electric", value: cmd.scaledMeterValue, unit: ["pulses", "V", "A", "R/Z", ""][cmd.scale - 3], type: "digital") } } } def zwaveEvent(hubitat.zwave.commands.sensormultilevelv1.SensorMultilevelReport cmd){ logging(cmd) def map = [:] switch (cmd.sensorType) { case 4: map.name = "power" map.value = cmd.scaledSensorValue.toInteger().toString() map.unit = cmd.scale == 1 ? "Btu/h" : "W" break default: map.descriptionText = cmd.toString() } createEvent(map) } def on() { state.lastRan = now() commands([zwave.basicV1.basicSet(value: 0xFF), zwave.basicV1.basicGet()]) } def off() { state.lastRan = now() commands([zwave.basicV1.basicSet(value: 0x00), zwave.basicV1.basicGet()]) } def refresh() { logging("$device.displayName refresh()") def cmds = [] if (state.lastRefresh != null && now() - state.lastRefresh < 5000) { logging("Refresh Double Press") def configuration = new XmlSlurper().parseText(configuration_model()) configuration.Value.each { if ( "${it.@setting_type}" == "zwave" ) { cmds << zwave.configurationV1.configurationGet(parameterNumber: "${it.@index}".toInteger()) } } cmds << zwave.firmwareUpdateMdV2.firmwareMdGet() } else { cmds << zwave.meterV2.meterGet(scale: 0) cmds << zwave.meterV2.meterGet(scale: 2) cmds << zwave.basicV1.basicGet() } state.lastRefresh = now() commands(cmds) } def ping() { logging("$device.displayName ping()") def cmds = [] cmds << zwave.meterV2.meterGet(scale: 0) cmds << zwave.meterV2.meterGet(scale: 2) cmds << zwave.basicV1.basicGet() commands(cmds) } def setLevel(level, duration) { state.lastRan = now() logging("setLevel value:$level, duration:$duration") def dimmingDuration = duration < 128 ? duration : 128 + Math.round(duration / 60) logging("dimmingDuration: $dimmingDuration") commands([ zwave.switchMultilevelV2.switchMultilevelSet(value: level < 100 ? level : 99, dimmingDuration: dimmingDuration), zwave.switchMultilevelV1.switchMultilevelGet() ]) } def setLevel(level) { state.lastRan = now() logging("setLevel value:$level") if(level > 99) level = 99 if(level < 1) level = 1 def cmds = [] cmds << zwave.basicV1.basicSet(value: level) cmds << zwave.switchMultilevelV1.switchMultilevelGet() commands(cmds) } def updated() { sendEvent(name: "numberOfButtons", value: 3) state.enableDebugging = settings.enableDebugging state.enableInfo = settings.enableInfo logging("updated() is being called") sendEvent(name: "checkInterval", value: 2 * 30 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) state.needfwUpdate = "" def cmds = update_needed_settings() sendEvent(name:"needUpdate", value: device.currentValue("needUpdate"), displayed:false, isStateChange: true) commands(cmds) } def installed(){ log.warn "installed..." sendEvent(name: "numberOfButtons", value: 3) } private command(hubitat.zwave.Command cmd) { if (getDataValue("zwaveSecurePairingComplete") == "true") { zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() } else { cmd.format() } } private commands(commands, delay=1500) { delayBetween(commands.collect{ command(it) }, delay) } def generate_preferences(configuration_model) { def configuration = new XmlSlurper().parseText(configuration_model) configuration.Value.each { switch(it.@type) { case ["byte","short","four"]: input "${it.@index}", "number", title:"${it.@label}\n" + "${it.Help}", range: "${it.@min}..${it.@max}", defaultValue: "${it.@value}", displayDuringSetup: "${it.@displayDuringSetup}" break case "list": def items = [] it.Item.each { items << ["${it.@value}":"${it.@label}"] } input "${it.@index}", "enum", title:"${it.@label}\n" + "${it.Help}", defaultValue: "${it.@value}", displayDuringSetup: "${it.@displayDuringSetup}", options: items break case "decimal": input "${it.@index}", "decimal", title:"${it.@label}\n" + "${it.Help}", range: "${it.@min}..${it.@max}", defaultValue: "${it.@value}", displayDuringSetup: "${it.@displayDuringSetup}" break case "boolean": input "${it.@index}", "bool", title:"${it.@label}\n" + "${it.Help}", defaultValue: "${it.@value}", displayDuringSetup: "${it.@displayDuringSetup}" break } } } def update_current_properties(cmd) { def currentProperties = state.currentProperties ?: [:] currentProperties."${cmd.parameterNumber}" = cmd.configurationValue if (settings."${cmd.parameterNumber}" != null) { if (settings."${cmd.parameterNumber}".toInteger() == convertParam("${cmd.parameterNumber}".toInteger(), cmd2Integer(cmd.configurationValue))) { sendEvent(name:"needUpdate", value:"NO", displayed:false, isStateChange: true) } else { sendEvent(name:"needUpdate", value:"YES", displayed:false, isStateChange: true) } } state.currentProperties = currentProperties } def update_needed_settings() { def cmds = [] def currentProperties = state.currentProperties ?: [:] def configuration = new XmlSlurper().parseText(configuration_model()) def isUpdateNeeded = "NO" if(!state.needfwUpdate || state.needfwUpdate == ""){ logging("Requesting device firmware version") cmds << zwave.versionV1.versionGet() } if(!state.association1 || state.association1 == "" || state.association1 == "1"){ logging("Setting association group 1") cmds << zwave.associationV2.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId) cmds << zwave.associationV2.associationGet(groupingIdentifier:1) } if(!state.association2 || state.association2 == "" || state.association1 == "2"){ logging("Setting association group 2") cmds << zwave.associationV2.associationSet(groupingIdentifier:2, nodeId:zwaveHubNodeId) cmds << zwave.associationV2.associationGet(groupingIdentifier:2) } configuration.Value.each { if ("${it.@setting_type}" == "zwave"){ if (currentProperties."${it.@index}" == null) { if (device.currentValue("firmware") == null || it.@fw == "" || "${it.@fw}".indexOf(device.currentValue("firmware")) >= 0){ isUpdateNeeded = "YES" logging("Current value of parameter ${it.@index} is unknown") cmds << zwave.configurationV1.configurationGet(parameterNumber: it.@index.toInteger()) } } else if (settings."${it.@index}" != null && convertParam(it.@index.toInteger(), cmd2Integer(currentProperties."${it.@index}")) != settings."${it.@index}".toInteger()) { if (device.currentValue("firmware") == null || it.@fw == "" || "${it.@fw}".indexOf(device.currentValue("firmware")) >= 0){ isUpdateNeeded = "YES" logging("Parameter ${it.@index} will be updated to " + settings."${it.@index}") def convertedConfigurationValue = convertParam(it.@index.toInteger(), settings."${it.@index}".toInteger()) cmds << zwave.configurationV1.configurationSet(configurationValue: integer2Cmd(convertedConfigurationValue, it.@byteSize.toInteger()), parameterNumber: it.@index.toInteger(), size: it.@byteSize.toInteger()) cmds << zwave.configurationV1.configurationGet(parameterNumber: it.@index.toInteger()) } } } } sendEvent(name:"needUpdate", value: isUpdateNeeded, displayed:false, isStateChange: true) return cmds } /** * Convert 1 and 2 bytes values to integer */ def cmd2Integer(array) { switch(array.size()) { case 1: array[0] break case 2: ((array[0] & 0xFF) << 8) | (array[1] & 0xFF) break case 3: ((array[0] & 0xFF) << 16) | ((array[1] & 0xFF) << 8) | (array[2] & 0xFF) break case 4: ((array[0] & 0xFF) << 24) | ((array[1] & 0xFF) << 16) | ((array[2] & 0xFF) << 8) | (array[3] & 0xFF) break } } def integer2Cmd(value, size) { switch(size) { case 1: [value] break case 2: def short value1 = value & 0xFF def short value2 = (value >> 8) & 0xFF [value2, value1] break case 3: def short value1 = value & 0xFF def short value2 = (value >> 8) & 0xFF def short value3 = (value >> 16) & 0xFF [value3, value2, value1] break case 4: def short value1 = value & 0xFF def short value2 = (value >> 8) & 0xFF def short value3 = (value >> 16) & 0xFF def short value4 = (value >> 24) & 0xFF [value4, value3, value2, value1] break } } def zwaveEvent(hubitat.zwave.commands.configurationv1.ConfigurationReport cmd) { update_current_properties(cmd) logging("${device.displayName} parameter '${cmd.parameterNumber}' with a byte size of '${cmd.size}' is set to '${cmd2Integer(cmd.configurationValue)}'") return null } def zwaveEvent(hubitat.zwave.commands.firmwareupdatemdv2.FirmwareMdReport cmd){ logging("Firmware Report ${cmd.toString()}") def firmwareVersion switch(cmd.checksum){ case "3281": firmwareVersion = "3.08" break; default: firmwareVersion = cmd.checksum } state.needfwUpdate = "false" updateDataValue("firmware", firmwareVersion.toString()) createEvent(name: "currentFirmware", value: firmwareVersion) } def zwaveEvent(hubitat.zwave.commands.versionv1.VersionReport cmd) { logging("${device.label?device.label:device.name}: ${cmd}") if(cmd.applicationVersion != null && cmd.applicationSubVersion != null) { def firmware = "${cmd.applicationVersion}.${cmd.applicationSubVersion.toString().padLeft(2,'0')}" logging("${device.label?device.label:device.name}: Firmware report received: ${firmware}") state.needfwUpdate = "false" createEvent(name: "firmware", value: "${firmware}") } else if(cmd.firmware0Version != null && cmd.firmware0SubVersion != null) { def firmware = "${cmd.firmware0Version}.${cmd.firmware0SubVersion.toString().padLeft(2,'0')}" logging("${device.label?device.label:device.name}: Firmware report received: ${firmware}") state.needfwUpdate = "false" createEvent(name: "firmware", value: "${firmware}") } } def configure() { state.enableDebugging = settings.enableDebugging state.enableInfo = settings.enableInfo logging("Configuring Device For Hubitat Use") def cmds = [] cmds = update_needed_settings() if (cmds != []) commands(cmds) } def convertParam(number, value) { switch (number){ case 201: if (value < 0) 256 + value else if (value > 100) value - 256 else value break case 202: if (value < 0) 256 + value else if (value > 100) value - 256 else value break case 203: if (value < 0) 65536 + value else if (value > 1000) value - 65536 else value break case 204: if (value < 0) 256 + value else if (value > 100) value - 256 else value break default: value break } } private def logging(message) { if (state.enableDebugging == null || state.enableDebugging == true) log.debug "$message" } private def Info(message) { if (state.enableInfo == null || state.enableInfo == true) log.info "$message" } def configuration_model() { ''' (parameter is set automatically during the calibration process) The parameter can be changed manually after the calibration. Range: 1~98 Default: 1 (parameter is set automatically during the calibration process) The parameter can be changed manually after the calibration. Range: 2~99 Default: 99 This parameter defines the percentage value of dimming step during the automatic control. Range: 1~99 Default: 1 This parameter defines the time of single dimming step set in parameter 5 during the automatic control. Range: 0~255 Default: 1 This parameter defines the percentage value of dimming step during the manual control. Range: 1~99 Default: 1 This parameter defines the time of single dimming step set in parameter 7 during the manual control. Range: 0~255 Default: 5 The Dimmer 2 will return to the last state before power failure. 0 - the Dimmer 2 does not save the state before a power failure, it returns to the "off" position 1 - the Dimmer 2 restores its state before power failure Range: 0~1 Default: 1 This parameter allows to automatically switch off the device after specified time (seconds) from switching on the light source. Range: 1~32767 Default: 0 If the parameter is active, switching on the Dimmer 2 (S1 single click) will always set this brightness level. Range: 0~99 Default: 0 Choose between momentary, toggle and roller blind switch. Range: 0~2 Default: 0 By default each change of toggle switch position results in action of Dimmer 2 (switch on/off) regardless the physical connection of contacts. 0 - device changes status on switch status change 1 - device status is synchronized with switch status Range: 0~1 Default: 0 set the brightness level to MAX Range: 0~1 Default: 1 Switch no. 2 controls the Dimmer 2 additionally (in 3-way switch mode). Function disabled for parameter 20 set to 2 (roller blind switch). Range: 0~1 Default: 0 SCENE ID depends on the switch type configurations. Range: 0~1 Default: 0 This parameter allows for switching the role of keys connected to S1 and S2 without changes in connection. Range: 0~1 Default: 0 This parameter determines the trigger of auto-calibration procedure, e.g. power on, load error, etc. 0 - No auto-calibration of the load after power on 1 - Auto-calibration performed after first power on 2 - Auto-calibration performed after each power on 3 - Auto-calibration performed after first power on or after each LOAD ERROR alarm (no load, load failure, burnt out bulb), if parameter 37 is set to 1 also after alarms: SURGE (Dimmer 2 output overvoltage) and OVERCURRENT (Dimmer 2 output overcurrent) 4 - Auto-calibration performed after each power on or after each LOAD ERROR alarm (no load, load failure, burnt out bulb), if parameter 37 is set to 1 also after alarms: SURGE (Dimmer 2 output overvoltage) and OVERCURRENT (Dimmer 2 output overcurrent) Range: 0~4 Default: 1 This parameter defines the maximum load for a dimmer. Range: 0~350 Default: 250 This parameter determines how the device will react to General Alarm frame. Range: 0~3 Default: 3 (Flash) This parameter determines how the device will react to Flood Alarm frame. Range: 0~3 Default: 2 (OFF) This parameter determines how the device will react to CO, CO2 or Smoke frame. Range: 0~3 Default: 3 (Flash) This parameter determines how the device will react to Heat Alarm frame. Range: 0~3 Default: 1 (ON) This parameter allows to set duration of flashing alarm mode. Range: 1~32000 (1s-32000s) Default: 600 (10 min) The parameter defines the power level change that will result in a new power report being sent. The value is a percentage of the previous report. Range: 0~100 Default: 10 Parameter 52 defines a time period between consecutive reports. Timer is reset and counted from zero after each report. Range: 0~32767 Default: 3600 Energy level change which will result in sending a new energy report. Range: 0~255 Default: 10 The Dimmer 2 may include active power and energy consumed by itself in reports sent to the main controller. Range: 0~1 Default: 0 ''' // // // // }