/**
* Candeo C-ZB-DM201-2G Zigbee 2-Gang Dimmer Module
* Supports Momentary Switches
* Via Parent Device:
* Supports on / off / setLevel / startLevelChange / stopLevelChange / flash (operates both gangs)
* Reports switch events (combines child device states into one based on state determination setting)
* Reports level events (combines child device levels into one based on level determination setting)
* Has Setting For Determination Of Master State
* Has Setting For Determination Of Master Level
* Via Child Devices:
* Supports on / off / setLevel / startLevelChange / stopLevelChange / flash
* Reports switch / level events
* Has Setting For Level Transition Time (use device setting, as fast as possible or set an explicit time)
* Has Setting For Level Change Rate (as fast as possible or set an explicit rate)
* Has Setting For Flash Time
* Has Setting For Flash Timeout
* Has Setting For Power On Default
* Has Setting For Power On Default Level
* Has Setting For Default On Level
* Has Setting For Explicit State After Hub Startup
*/
metadata {
definition(name: 'Candeo C-ZB-DM201-2G Zigbee 2-Gang Dimmer Module', namespace: 'Candeo', author: 'Candeo', importUrl: 'https://raw.githubusercontent.com/candeosmart/hubitat-zigbee/refs/heads/main/Candeo%20C-ZB-DM201-2G%20Zigbee%202-Gang%20Dimmer%20Module.groovy', singleThreaded: true) {
capability 'Switch'
capability 'SwitchLevel'
capability 'ChangeLevel'
capability 'Flash'
capability 'Actuator'
capability 'Initialize'
capability 'Refresh'
capability 'Configuration'
command 'resetPreferencesToDefault'
fingerprint profileId: '0104', endpointId: '01', inClusters: '0000,0003,0004,0005,0006,0008,1000', outClusters: '0019', manufacturer: 'Candeo', model: 'C-ZB-DM201-2G', deviceJoinName: 'Candeo C-ZB-DM201-2G Zigbee 2-Gang Dimmer Module'
}
preferences {
input name: 'deviceDriverOptions', type: 'hidden', title: 'Device Driver Options', description: 'The following options change the behaviour of the device driver, they take effect after hitting "Save Preferences below."'
input name: 'masterStateDetermination', type: 'enum', title: 'Choose How Master State Is Determined', description: 'The master on / off control operates both channels so its state can be determined from the following options.
', options: PREFMASTERSTATEDETERMINATION, defaultValue: 'allSame'
input name: 'masterLevelDetermination', type: 'enum', title: 'Choose How Master Level Is Determined', description: 'The master level control operates both channels so its level can be determined from the following options.
', options: PREFMASTERLEVELDETERMINATION, defaultValue: 'allSame'
input name: 'loggingOption', type: 'enum', title: 'Logging Option', description: 'Sets the logging level cumulatively, for example "Driver Trace Logging" will include all logging levels below it.
', options: PREFLOGGING, defaultValue: '5'
input name: 'deviceConfigurationOptions', type: 'hidden', title: 'Device Configuration Options', description: 'The following options change the behaviour of the device itself, they take effect after hitting "Save Preferences below", followed by "Configure" above.
For a battery powered device, you may also need to wake it up manually!'
}
}
import groovy.transform.Field
import com.hubitat.app.ChildDeviceWrapper
import com.hubitat.app.DeviceWrapper
private @Field final String CANDEO = 'Candeo C-ZB-DM201-2G Device Driver'
private @Field final Boolean DEBUG = false
private @Field final Integer LOGSOFF = 1800
private @Field final Integer ZIGBEEDELAY = 1000
private @Field final Integer DEVICEMINLEVEL = 1
private @Field final Integer DEVICEMAXLEVEL = 254
private @Field final Map PREFFALSE = [value: 'false', type: 'bool']
private @Field final Map PREFTRUE = [value: 'true', type: 'bool']
private @Field final Map PREF5 = [value: '5', type: 'enum']
private @Field final Map PREFALLSAME = [value: 'allSame', type: 'enum']
private @Field final Map PREFHUBRESTART = [ 'off': 'Off', 'on': 'On', 'refresh': 'Refresh State Only', 'nothing': 'Do Nothing' ]
private @Field final Map PREFMASTERSTATEDETERMINATION = [ 'anyOn': 'If Any Child Is On, Set Master On', 'anyOff': 'If Any Child Is Off, Set Master Off', 'allSame': 'Only Change The State If All Children Are In The Same State']
private @Field final Map PREFMASTERLEVELDETERMINATION = [ '01': 'Use Level From Child 01 As Master Level', '02': 'Use Level From Child 02 As Master Level', 'allSame': 'Only Change The Level If All Children Are At The Same Level']
private @Field final Map PREFLOGGING = ['0': 'Device Event Logging', '1': 'Driver Informational Logging', '2': 'Driver Warning Logging', '3': 'Driver Error Logging', '4': 'Driver Debug Logging', '5': 'Driver Trace Logging' ]
private @Field final List CHILDEPS = ['01', '02']
void installed() {
logTrace('installed called', true)
resetPreferencesToDefault()
}
void resetPreferencesToDefault() {
logsOn()
logTrace('resetPreferencesToDefault called')
settings.keySet().each { String setting ->
device.removeSetting(setting)
}
device.updateSetting('masterStateDetermination', PREFALLSAME)
logInfo("masterStateDetermination setting is: ${PREFMASTERSTATEDETERMINATION[masterStateDetermination]}")
device.updateSetting('masterLevelDetermination', PREFALLSAME)
logInfo("masterLevelDetermination setting is: ${PREFMASTERLEVELDETERMINATION[masterLevelDetermination]}")
logInfo('logging level is: Driver Trace Logging')
logInfo("logging level will reduce to Driver Error Logging after ${LOGSOFF} seconds")
}
void uninstalled() {
logTrace('uninstalled called')
clearAll()
}
void initialize() {
logTrace('initialize called')
}
List updated() {
logTrace('updated called')
logTrace("settings: ${settings}")
logInfo("masterStateDetermination setting is: ${PREFMASTERSTATEDETERMINATION[masterStateDetermination]}", true)
logInfo("masterLevelDetermination setting is: ${PREFMASTERLEVELDETERMINATION[masterLevelDetermination]}", true)
logInfo("logging level is: ${PREFLOGGING[loggingOption]}", true)
clearAll()
if (logMatch('debug')) {
logInfo("logging level will reduce to Driver Error Logging after ${LOGSOFF} seconds", true)
runIn(LOGSOFF, logsOff)
}
if (checkPreferences()) {
logInfo('Device Configuration Options have been changed, will now configure the device!', true)
return configure()
}
}
void logsOff() {
logTrace('logsOff called')
if (DEBUG) {
logDebug('DEBUG field variable is set, not disabling logging automatically!', true)
}
else {
logInfo('automatically reducing logging level to Driver Error Logging', true)
device.updateSetting('loggingOption', [value: '3', type: 'enum'])
}
}
List configure() {
logTrace('configure called')
List cmds = []
CHILDEPS.each { String childEP ->
String address = "EP${childEP}"
ChildDeviceWrapper childDevice = findChildDevice(address)
if (childDevice) {
logDebug("got child device name: ${childDevice.name} displayName: ${childDevice.displayName}, sending configure")
childDevice.configure()
}
else {
logWarn('could not find child device, skipping command!')
}
}
cmds += refresh()
logDebug("sending ${cmds}")
return cmds
}
List refresh() {
logTrace('refresh called')
List cmds = []
CHILDEPS.each { String childEP ->
String address = "EP${childEP}"
ChildDeviceWrapper childDevice = findChildDevice(address)
if (childDevice) {
logDebug("got child device name: ${childDevice.name} displayName: ${childDevice.displayName}, sending refresh")
childDevice.refresh()
}
else {
logWarn('could not find child device, skipping command!')
}
}
logDebug("sending ${cmds}")
return cmds
}
void on() {
logTrace('on called')
CHILDEPS.each { String childEP ->
String address = "EP${childEP}"
ChildDeviceWrapper childDevice = findChildDevice(address)
if (childDevice) {
logDebug("got child device name: ${childDevice.name} displayName: ${childDevice.displayName}, sending on")
childDevice.on()
}
else {
logWarn('could not find child device, skipping command!')
}
}
}
void off() {
logTrace('off called')
CHILDEPS.each { String childEP ->
String address = "EP${childEP}"
ChildDeviceWrapper childDevice = findChildDevice(address)
if (childDevice) {
logDebug("got child device name: ${childDevice.name} displayName: ${childDevice.displayName}, sending off")
childDevice.off()
}
else {
logWarn('could not find child device, skipping command!')
}
}
}
void flash(BigDecimal rate = null) {
logTrace("flash called rate: ${rate ?: 'no rate specified'}")
CHILDEPS.each { String childEP ->
String address = "EP${childEP}"
ChildDeviceWrapper childDevice = findChildDevice(address)
if (childDevice) {
logDebug("got child device name: ${childDevice.name} displayName: ${childDevice.displayName}, sending flash")
childDevice.flash(rate)
}
else {
logWarn('could not find child device, skipping command!')
}
}
}
void setLevel(BigDecimal level) {
logTrace("setLevel called level: ${level}")
CHILDEPS.each { String childEP ->
String address = "EP${childEP}"
ChildDeviceWrapper childDevice = findChildDevice(address)
if (childDevice) {
logDebug("got child device name: ${childDevice.name} displayName: ${childDevice.displayName}, sending setLevel")
childDevice.setLevel(level)
}
else {
logWarn('could not find child device, skipping command!')
}
}
}
void setLevel(BigDecimal level, BigDecimal rate) {
logTrace("setLevel called level: ${level} rate: ${rate}")
CHILDEPS.each { String childEP ->
String address = "EP${childEP}"
ChildDeviceWrapper childDevice = findChildDevice(address)
if (childDevice) {
logDebug("got child device name: ${childDevice.name} displayName: ${childDevice.displayName}, sending setLevel")
childDevice.setLevel(level, rate)
}
else {
logWarn('could not find child device, skipping command!')
}
}
}
void startLevelChange(String direction) {
logTrace("startLevelChange called direction: ${direction}")
CHILDEPS.each { String childEP ->
String address = "EP${childEP}"
ChildDeviceWrapper childDevice = findChildDevice(address)
if (childDevice) {
logDebug("got child device name: ${childDevice.name} displayName: ${childDevice.displayName}, sending startLevelChange")
childDevice.startLevelChange(direction)
}
else {
logWarn('could not find child device, skipping command!')
}
}
}
void stopLevelChange() {
logTrace('stopLevelChange called')
CHILDEPS.each { String childEP ->
String address = "EP${childEP}"
ChildDeviceWrapper childDevice = findChildDevice(address)
if (childDevice) {
logDebug("got child device name: ${childDevice.name} displayName: ${childDevice.displayName}, sending stopLevelChange")
childDevice.stopLevelChange()
}
else {
logWarn('could not find child device, skipping command!')
}
}
}
void componentOn(DeviceWrapper childDevice) {
logTrace('componentOn called')
logDebug("got childDevice: ${childDevice.displayName}")
String endpoint = childDevice.deviceNetworkId.split('-EP')[1]
List cmds = ["he cmd 0x${device.deviceNetworkId} 0x${endpoint} 0x0006 0x01 {}"]
doZigBeeCommand(cmds)
}
void componentOff(DeviceWrapper childDevice) {
logTrace('componentOff called')
logDebug("got childDevice: ${childDevice.displayName}")
String endpoint = childDevice.deviceNetworkId.split('-EP')[1]
List cmds = ["he cmd 0x${device.deviceNetworkId} 0x${endpoint} 0x0006 0x00 {}"]
doZigBeeCommand(cmds)
}
void componentSetLevel(DeviceWrapper childDevice, Integer scaledLevel, Integer scaledTransition) {
logTrace("componentSetLevel called scaledLevel: ${scaledLevel} scaledTransition: ${scaledTransition}")
logDebug("got childDevice: ${childDevice.displayName}")
String endpoint = childDevice.deviceNetworkId.split('-EP')[1]
List cmds = ["he cmd 0x${device.deviceNetworkId} 0x${endpoint} 0x0008 4 {0x${intTo8bitUnsignedHex(scaledLevel)} 0x${intTo16bitUnsignedHex(scaledTransition)}}"]
doZigBeeCommand(cmds)
}
void componentStartLevelChange(DeviceWrapper childDevice, Integer upDown, Integer scaledRate) {
logTrace("componentStartLevelChange called upDown: ${upDown} scaledRate: ${scaledRate}")
logDebug("got childDevice: ${childDevice.displayName}")
String endpoint = childDevice.deviceNetworkId.split('-EP')[1]
List cmds = ["he cmd 0x${device.deviceNetworkId} 0x${endpoint} 0x0008 5 {0x${intTo8bitUnsignedHex(upDown)} 0x${intTo16bitUnsignedHex(scaledRate)}}"]
doZigBeeCommand(cmds)
}
void componentStopLevelChange(DeviceWrapper childDevice) {
logTrace('componentStopLevelChange called')
logDebug("got childDevice: ${childDevice.displayName}")
String endpoint = childDevice.deviceNetworkId.split('-EP')[1]
List cmds = ["he cmd 0x${device.deviceNetworkId} 0x${endpoint} 0x0008 3 {}"]
doZigBeeCommand(cmds)
}
void componentRefresh(DeviceWrapper childDevice) {
logTrace('componentRefresh called')
logDebug("got childDevice: ${childDevice.displayName}")
String endpoint = childDevice.deviceNetworkId.split('-EP')[1]
List cmds = ["he rattr 0x${device.deviceNetworkId} 0x${endpoint} 0x0006 0x0000 {}", "delay ${ZIGBEEDELAY}",
"he rattr 0x${device.deviceNetworkId} 0x${endpoint} 0x0008 0x0000 {}", "delay ${ZIGBEEDELAY}"]
doZigBeeCommand(cmds)
}
void componentConfigure(DeviceWrapper childDevice) {
logTrace('componentConfigure called')
logDebug("got childDevice: ${childDevice.displayName}")
String endpoint = childDevice.deviceNetworkId.split('-EP')[1]
String deviceConfigDefaultPowerOnBehaviour = getChildDeviceSettingItem("EP${endpoint}", 'deviceConfigDefaultPowerOnBehaviour') ?: 'previous'
logDebug("startup on off is ${deviceConfigDefaultPowerOnBehaviour ?: 'previous'}")
Map startUpOnOff = ['on': 0x01, 'off': 0x0, 'opposite': 0x02, 'previous': 0x03]
logDebug("startUpOnOff: ${startUpOnOff[deviceConfigDefaultPowerOnBehaviour ?: 'previous']}")
String deviceConfigDefaultPowerOnLevel = getChildDeviceSettingItem("EP${endpoint}", 'deviceConfigDefaultPowerOnLevel') ?: 'previous'
logDebug("startup current level is: ${deviceConfigDefaultPowerOnLevel ?: 'previous'}")
Integer startUpLevel = deviceConfigDefaultPowerOnLevel ? deviceConfigDefaultPowerOnLevel == 'previous' ? 255 : percentageValueToLevel(deviceConfigDefaultPowerOnLevel) : 255
logDebug("startUpLevel: ${startUpLevel}")
String deviceConfigDefaultOnLevel = getChildDeviceSettingItem("EP${endpoint}", 'deviceConfigDefaultOnLevel') ?: 'previous'
logDebug("on level is: ${deviceConfigDefaultOnLevel ?: 'previous'}")
Integer onLevel = deviceConfigDefaultOnLevel ? deviceConfigDefaultOnLevel == 'previous' ? 255 : percentageValueToLevel(deviceConfigDefaultOnLevel) : 255
logDebug("onLevel: ${onLevel}")
List cmds = ["zdo bind 0x${device.deviceNetworkId} 0x${endpoint} 0x01 0x0006 {${device.zigbeeId}} {}", "delay ${ZIGBEEDELAY}",
"he cr 0x${device.deviceNetworkId} 0x${endpoint} 0x0006 0x0000 ${DataType.BOOLEAN} 0 3600 {}", "delay ${ZIGBEEDELAY}",
"he rattr 0x${device.deviceNetworkId} 0x${endpoint} 0x0006 0x0000 {}", "delay ${ZIGBEEDELAY}",
"zdo bind 0x${device.deviceNetworkId} 0x${endpoint} 0x01 0x0008 {${device.zigbeeId}} {}", "delay ${ZIGBEEDELAY}",
"he cr 0x${device.deviceNetworkId} 0x${endpoint} 0x0008 0x0000 ${DataType.UINT8} 1 3600 {01}", "delay ${ZIGBEEDELAY}",
"he rattr 0x${device.deviceNetworkId} 0x${endpoint} 0x0008 0x0000 {}", "delay ${ZIGBEEDELAY}",
"he rattr 0x${device.deviceNetworkId} 0x${endpoint} 0x0006 0x4003 {}", "delay ${ZIGBEEDELAY}",
"he wattr 0x${device.deviceNetworkId} 0x${endpoint} 0x0006 0x4003 ${convertToHexString(DataType.ENUM8)} {${startUpOnOff[deviceConfigDefaultPowerOnBehaviour ?: 'previous']}} {}", "delay ${ZIGBEEDELAY}",
"he rattr 0x${device.deviceNetworkId} 0x${endpoint} 0x0006 0x4003 {}", "delay ${ZIGBEEDELAY}",
"he rattr 0x${device.deviceNetworkId} 0x${endpoint} 0x0008 0x0011 {}", "delay ${ZIGBEEDELAY}",
"he wattr 0x${device.deviceNetworkId} 0x${endpoint} 0x0008 0x0011 ${convertToHexString(DataType.UINT8)} {${convertToHexString(onLevel)}} {}", "delay ${ZIGBEEDELAY}",
"he rattr 0x${device.deviceNetworkId} 0x${endpoint} 0x0008 0x0011 {}", "delay ${ZIGBEEDELAY}",
"he rattr 0x${device.deviceNetworkId} 0x${endpoint} 0x0008 0x4000 {}", "delay ${ZIGBEEDELAY}",
"he wattr 0x${device.deviceNetworkId} 0x${endpoint} 0x0008 0x4000 ${convertToHexString(DataType.UINT8)} {${convertToHexString(startUpLevel)}} {}", "delay ${ZIGBEEDELAY}",
"he rattr 0x${device.deviceNetworkId} 0x${endpoint} 0x0008 0x4000 {}", "delay ${ZIGBEEDELAY}"]
doZigBeeCommand(cmds)
}
void checkAndSetMasterLevel() {
logTrace('checkAndSetMasterLevel called')
List childLevels = []
CHILDEPS.each { String childEP ->
String address = "EP${childEP}"
Integer childDeviceLevel = getChildDeviceCurrentValue(address, 'level') == 'unknown' ? 9999 : getChildDeviceCurrentValue(address, 'level').toInteger()
childLevels.add(childDeviceLevel)
}
logDebug("childLevels: ${childLevels}")
childLevels = childLevels.toUnique()
Boolean levelsMatch = false
if (childLevels.size() == 1) {
levelsMatch = true
}
logDebug("levelsMatch: ${levelsMatch}")
logDebug("masterLevelDetermination setting is: ${masterLevelDetermination}")
Integer masterLevel = 9999
if (levelsMatch && masterLevelDetermination == 'allSame') {
Integer levelValue = childLevels.pop()
masterLevel = levelValue
logDebug("masterLevel set to levelValue: ${levelValue}")
}
else if (masterLevelDetermination != 'allSame') {
String address = "EP${masterLevelDetermination}"
Integer childDeviceLevel = getChildDeviceCurrentValue(address, 'level') == 'unknown' ? 9999 : getChildDeviceCurrentValue(address, 'level').toInteger()
masterLevel = childDeviceLevel
logDebug("masterLevel set to childDeviceLevel: ${childDeviceLevel}")
}
if (masterLevel == 9999) {
logDebug('masterLevel could not be determined, skipping this event!')
}
else {
logDebug("masterLevel determined to be ${masterLevel}")
String descriptionText = "${device.displayName} was set to ${masterLevel}%"
logEvent(descriptionText)
sendEvent(processEvent([name: 'level', value: masterLevel, unit: '%', type: 'digital', descriptionText: descriptionText]))
}
}
List