/** * Candeo C-ZB-SM205-2G Zigbee 2-Gang Switch Module (L+N) Child Switch * Supports Momentary & Toggle Switches * Supports on / off / flash * Reports switch events * Has Setting For Flash Time * Has Setting For Flash Timeout * Has Setting For Power On Default * Has Setting For Explicit State After Hub Startup */ metadata { definition(name: 'Candeo C-ZB-SM205-2G Zigbee 2-Gang Switch Module (L+N) Child Switch', namespace: 'Candeo', author: 'Candeo', component: true, importUrl: 'https://raw.githubusercontent.com/candeosmart/hubitat-zigbee/main/Candeo%20C-ZB-SM205-2G%20Zigbee%202-Gang%20Switch%20Module%20(L%2BN)%20Child%20Switch.groovy', singleThreaded: true) { capability 'Switch' capability 'Flash' capability 'Actuator' capability 'Initialize' capability 'Refresh' capability 'Configuration' command 'resetPreferencesToDefault' } 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: 'flashTime', type: 'enum', title: 'Flash Time (ms)', description: 'When carrying out a flash command, use this as the on and off time.

', options: PREFFLASHTIME, defaultValue: '750' input name: 'flashTimeout', type: 'enum', title: 'Flash Timeout (m)', description: 'When carrying out a flash command, automatically cancel after this amount of time.

', options: PREFFLASHTIMEOUT, defaultValue: '10' input name: 'hubStartupDefaultCommand', type: 'enum', title: 'Explicit Command After Hub Has Restarted', description: 'After the hub restarts, carry out this command on the device.

', options: PREFHUBRESTART, defaultValue: 'refresh' 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!
' input name: 'deviceConfigDefaultPowerOnBehaviour', type: 'enum', title: 'Default State After Return From Power Failure', description: 'After a power failure, set the device to this state when the power is restored.

', options: PREFPOWERON, defaultValue: 'previous' input name: 'platformOptions', type: 'hidden', title: 'Platform Options', description: 'The following options are relevant to the Hubitat platform and UI itself.' } } import groovy.transform.Field private @Field final String CANDEO = 'Candeo C-ZB-SM205-2G Device Driver' private @Field final Boolean DEBUG = false private @Field final Integer LOGSOFF = 1800 private @Field final Map PREFFALSE = [value: 'false', type: 'bool'] private @Field final Map PREFTRUE = [value: 'true', type: 'bool'] private @Field final Map PREFPREVIOUS = [value: 'previous', type: 'enum'] private @Field final Map PREF10 = [value: '10', type: 'enum'] private @Field final Map PREF5 = [value: '5', type: 'enum'] private @Field final Map PREFPOWERON = [ 'off': 'Off', 'on': 'On', 'opposite': 'Opposite', 'previous': 'Previous' ] private @Field final Map PREFHUBRESTART = [ 'off': 'Off', 'on': 'On', 'refresh': 'Refresh State Only', 'nothing': 'Do Nothing' ] private @Field final Map PREFFLASHTIME = ['500': '500ms', '750': '750ms', '1000': '1000ms', '1500': '1500ms', '2000': '2000ms', '2500': '2500ms', '3000': '3000ms', '4000': '4000ms', '5000': '5000ms'] private @Field final Map PREFFLASHTIMEOUT = ['0': 'never', '1': '1m', '2': '2m', '3': '3m', '4': '4m', '5': '5m', '10': '10m', '15': '15m', '30': '30m', '60': '60m', '90': '90m', '120': '120m', '180': '180m'] 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' ] void installed() { logTrace('installed called', true) resetPreferencesToDefault() } void resetPreferencesToDefault() { logsOn() logTrace('resetPreferencesToDefault called') settings.keySet().each { String setting -> device.removeSetting(setting) } device.updateSetting('deviceConfigDefaultPowerOnBehaviour', PREFPREVIOUS) logInfo("deviceConfigDefaultPowerOnBehaviour setting is: ${PREFPOWERON[deviceConfigDefaultPowerOnBehaviour]}") device.updateSetting('hubStartupDefaultCommand', [value: 'refresh', type: 'enum']) logInfo("hubStartupDefaultCommand setting is: ${PREFHUBRESTART[hubStartupDefaultCommand]}") device.updateSetting('flashTime', [value: '750', type: 'enum']) logInfo("flashTime setting is: ${PREFFLASHTIME[flashTime]}") device.updateSetting('flashTimeout', PREF10) logInfo("flashTimeout setting is: ${PREFFLASHTIMEOUT[flashTimeout]}") 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') String startupDefaultCommand = hubStartupDefaultCommand ?: 'refresh' switch (startupDefaultCommand) { case 'off': off() break case 'on': on() break case 'refresh': refresh() break default: break } } void updated() { logTrace('updated called') logTrace("settings: ${settings}") logInfo("deviceConfigDefaultPowerOnBehaviour setting is: ${PREFPOWERON[deviceConfigDefaultPowerOnBehaviour ?: 'previous']}", true) logInfo("hubStartupDefaultCommand setting is: ${PREFHUBRESTART[hubStartupDefaultCommand ?: 'refresh']}", true) logInfo("flashTime setting is: ${PREFFLASHTIME[flashTime ?: '750']}", true) logInfo("flashTimeout setting is: ${PREFFLASHTIMEOUT[flashTimeout ?: '10']}", 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) 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']) } } void configure() { logTrace('configure called') parent?.componentConfigure(this.device) } void refresh() { logTrace('refresh called') parent?.componentRefresh(this.device) } void on() { logTrace('on called') flashStop(false) parent?.componentOn(this.device) state['action'] = 'digitalon' } void off() { logTrace('off called') flashStop(false) parent?.componentOff(this.device) state['action'] = 'digitaloff' } void flash(BigDecimal rate = null) { logTrace("flash called rate: ${rate ?: 'no rate specified'}") if (state['flashing']) { logDebug('state["flashing"] is true, stopping flasher') state['flashing'] = false flashStop() } else { logDebug('state["flashing"] is false, starting flasher') state['flashing'] = true String currentState = device.currentValue('switch') logDebug("device state is currently: ${currentState}") state['flashPrevious'] = currentState == 'on' ?: false Integer flashRate = rate if (!flashRate) { logDebug("no rate specified, using flashTime: ${flashTime ?: '750'}") flashRate = flashTime ? flashTime.toInteger() : 750 } logDebug("flashRate: ${flashRate}") if (flashRate > 5000 || flashRate < 500) { flashRate = flashRate > 5000 ? 5000 : flashRate < 500 ? 500 : flashRate logWarn('flashRate outside safe range (500 - 5000), resetting to safe value!') } runInMillis(flashRate, flasher, [data: ['on': !(state['flashPrevious']), 'rate': flashRate]]) logDebug("flashTimeout: ${flashTimeout ?: '10'}") Integer flashEnd = (flashTimeout ? flashTimeout.toInteger() : 10) * 60 * 1000 logDebug("flashEnd: ${flashEnd}") if (flashEnd > 0) { logDebug('setting flashing timeout') runInMillis(flashEnd, flashStop) } else { logDebug('no timeout requested') } } } void flasher(Map data) { logTrace("flasher called data: ${data}") if (state['flashing']) { String action = 'digitalon' if (data.on) { logDebug('turning on') parent?.componentOn(this.device) } else { logDebug('turning off') parent?.componentOff(this.device) action = 'digitaloff' } state['action'] = action runInMillis(data.rate, flasher, [data: ['on': !(data.on), 'rate': data.rate]]) } else { logDebug('state["flashing"] is false, skipping!') } } void flashStop(Boolean reinstate = true) { logTrace('flashStop called') state['flashing'] = false unschedule('flasher') unschedule('flashStop') if (reinstate) { logDebug("reinstate is true, reinstating device previous state: ${state['flashPrevious'] ? 'on' : 'off'}") String action = 'digitalon' if (state['flashPrevious']) { logDebug('reinstate device to on') parent?.componentOn(this.device) } else { logDebug('reinstate device to off') parent?.componentOff(this.device) action = 'digitaloff' } state['action'] = action } } void parse(String description) { logTrace('parse called') logWarn("parse(String description) not implemented description: ${description}") } void parse(Map event) { logTrace('parse called') if (event) { logDebug("got event: ${event}") if (event.name == 'switch') { String descriptionText = "was turned ${event.value}" String currentValue = device.currentValue('switch') if (event.value == currentValue) { descriptionText = "is ${event.value}" } String type = 'physical' String action = state['action'] ?: 'standby' if (action == 'digitalon' || action == 'digitaloff') { logDebug("action is ${action}") type = 'digital' state['action'] = 'standby' logDebug('action set to standby') } logEvent(descriptionText) sendEvent(processEvent([name: 'switch', value: event.value, type: type, descriptionText: descriptionText])) } else if (event.name == 'startupBehaviour') { Map startUpOnOff = ['00': 'off', '01': 'on', '02': 'opposite', '03': 'previous', 'FF': 'previous'] String startupBehaviour = PREFPOWERON[startUpOnOff[event.value]] ?: "${event.value} (unknown}" logDebug("deviceConfigDefaultPowerOnBehaviour is currently set to: ${PREFPOWERON[deviceConfigDefaultPowerOnBehaviour ?: 'previous']} and device reports it is set to: ${startupBehaviour}") } else { logWarn('unexpected event type!') } } else { logWarn('empty event!') } } String lookupData(String dataType, String dataName) { logTrace("lookupData called dataType: ${dataType} dataName: ${dataName}") String lookupDataValue = null switch (dataType) { case 'state': lookupDataValue = state[dataName] break case 'setting': lookupDataValue = settings[dataName] break case 'data': lookupDataValue = getDataValue(dataName) break default: break } logDebug("${dataType} ${dataName}: ${lookupDataValue}") return lookupDataValue } private Map processEvent(Map event) { logTrace("processEvent called data: ${event}") return createEvent(event) } private Boolean logMatch(String logLevel) { Map logLevels = ['event': '0', 'info': '1', 'warn': '2', 'error': '3', 'debug': '4', 'trace': '5' ] return loggingOption ? loggingOption.toInteger() >= logLevels[logLevel].toInteger() : true } private String logTrace(String msg, Boolean override = false) { if (logMatch('trace') || override) { log.trace(logMsg(msg)) } } private String logDebug(String msg, Boolean override = false) { if (logMatch('debug') || override) { log.debug(logMsg(msg)) } } private String logError(String msg, Boolean override = false) { if (logMatch('error') || override) { log.error(logMsg(msg)) } } private String logWarn(String msg, Boolean override = false) { if (logMatch('warn') || override) { log.warn(logMsg(msg)) } } private String logInfo(String msg, Boolean override = false) { if (logMatch('info') || override) { log.info(logMsg(msg)) } } private String logEvent(String msg, Boolean override = false) { if (logMatch('event') || override) { log.info(logMsg(msg)) } } private String logMsg(String msg) { String log = "candeo logging for ${CANDEO} -- " log += msg return log } private void logsOn() { logTrace('logsOn called', true) device.updateSetting('loggingOption', PREF5) runIn(LOGSOFF, logsOff) } private void clearAll() { logTrace('clearAll called') state.clear() atomicState.clear() unschedule() } private Boolean checkPreference(String preference) { logTrace("checkPreference called preference: ${preference}") String oldPreference = preference + '_OLD' String newPreferenceValue = settings.containsKey(preference) ? settings[preference].toString() : 'unknown' logDebug("newPreferenceValue: ${newPreferenceValue}") String oldPreferenceValue = settings.containsKey(oldPreference) ? settings[oldPreference].toString() : 'unknown' logDebug("oldPreferenceValue: ${oldPreferenceValue}") if (oldPreferenceValue != newPreferenceValue) { device.updateSetting(oldPreference, newPreferenceValue) return true } return false } private Boolean checkPreferences() { logTrace('checkPreferences called') Set deviceConfig = settings.keySet().findAll { String preference -> preference.startsWith('deviceConfig') && preference.indexOf('_OLD') == -1 && checkPreference(preference) } return (deviceConfig.size() > 0) }