/** * Clipsal / Schneider Electric Wiser Smoke Alarm 755WSA Zigbee Driver */ import hubitat.zigbee.clusters.iaszone.ZoneStatus metadata { definition (name: "Clipsal/Schneider Wiser Smoke Alarm 755WSA", namespace: "custom", author: "Hermes") { capability "SmokeDetector" capability "Battery" capability "Refresh" capability "Configuration" capability "TamperAlert" capability "Sensor" attribute "lifetime", "number" attribute "heat", "string" attribute "batteryVoltage", "number" attribute "batteryLow", "string" attribute "testMode", "string" attribute "silenceMode", "string" attribute "ledBrightness", "string" attribute "lastStatus", "string" fingerprint profileId: "0104", inClusters: "0000,0001,0003,0020,0402,0500,0B05,FC04", outClusters: "0003,0019", manufacturer: "Schneider Electric", model: "755WSA", deviceJoinName: "Clipsal Wiser Smoke Alarm (755WSA)" } preferences { input name: "logEnable", type: "bool", title: "Enable debug logging", defaultValue: true input name: "txtEnable", type: "bool", title: "Enable descriptionText logging", defaultValue: true } } def parse(String description) { if (settings.logEnable != false) log.debug "parse description: ${description}" def descMap = [:] if (description?.startsWith('zone status')) { return parseIasMessage(description) } else if (description?.startsWith('enroll request')) { if (settings.logEnable != false) log.debug "Sending enroll response" return zigbee.enrollResponse() } else if (description?.startsWith('catchall') || description?.startsWith('read attr')) { descMap = zigbee.parseDescriptionAsMap(description) if (settings.logEnable != false) log.debug "Parsed description map: ${descMap}" if (descMap?.isClusterSpecific == false && (descMap?.command == "01" || descMap?.command == "0A")) { if (descMap?.cluster == "0500" && descMap.attrInt == 0x0002) { // Zone Status return parseZoneStatusHex(descMap.value) } else if (descMap?.cluster == "0001" && descMap.attrInt == 0x0020) { // Battery Voltage return parseBatteryVoltage(descMap.value) } else if (descMap?.cluster == "0001" && descMap.attrInt == 0x0021) { // Battery % return parseBatteryPercent(descMap.value) } else if (descMap?.cluster == "FC04") { // Custom Schneider Cluster return parseCustomCluster(descMap.attrInt, descMap.value) } } } return [] } private parseIasMessage(String description) { ZoneStatus zs = zigbee.parseZoneStatus(description) if (settings.logEnable != false) log.debug "Parsed ZoneStatus: Alarm1=${zs.isAlarm1Set()}, Alarm2=${zs.isAlarm2Set()}, Tamper=${zs.isTamperSet()}, Battery=${zs.isBatterySet()}" return translateZoneStatus(zs) } private parseZoneStatusHex(String hexValue) { int val = Integer.parseInt(hexValue, 16) ZoneStatus zs = new ZoneStatus(val) if (settings.logEnable != false) log.debug "Parsed hex ZoneStatus: Alarm1=${zs.isAlarm1Set()}, Alarm2=${zs.isAlarm2Set()}, Tamper=${zs.isTamperSet()}, Battery=${zs.isBatterySet()}" return translateZoneStatus(zs) } private translateZoneStatus(ZoneStatus zs) { def results = [] def smokeVal = zs.isAlarm1Set() ? "detected" : "clear" results << createEvent(name: "smoke", value: smokeVal, descriptionText: "${device.displayName} smoke is ${smokeVal}") if (settings.txtEnable != false) log.info "${device.displayName} smoke is ${smokeVal}" def heatVal = zs.isAlarm2Set() ? "detected" : "clear" results << createEvent(name: "heat", value: heatVal, descriptionText: "${device.displayName} heat is ${heatVal}") if (settings.txtEnable != false) log.info "${device.displayName} heat is ${heatVal}" def tamperVal = zs.isTamperSet() ? "detected" : "clear" results << createEvent(name: "tamper", value: tamperVal, descriptionText: "${device.displayName} tamper is ${tamperVal}") if (settings.txtEnable != false) log.info "${device.displayName} tamper is ${tamperVal}" def battLowVal = zs.isBatterySet() ? "detected" : "clear" results << createEvent(name: "batteryLow", value: battLowVal, descriptionText: "${device.displayName} batteryLow is ${battLowVal}") if (settings.txtEnable != false) log.info "${device.displayName} batteryLow is ${battLowVal}" sendEvent(name: "lastStatus", value: new Date().toString()) return results } private parseBatteryVoltage(String hexValue) { int val = Integer.parseInt(hexValue, 16) BigDecimal volts = val / 10.0 if (settings.txtEnable != false) log.info "${device.displayName} Battery Voltage is ${volts}V" BigDecimal maxVolts = 3.0 BigDecimal minVolts = 2.5 BigDecimal pct = Math.min(100, Math.max(0, Math.round((volts - minVolts) / (maxVolts - minVolts) * 100))) if (settings.txtEnable != false) log.info "${device.displayName} Calculated Battery % from voltage: ${pct}%" return [ createEvent(name: "battery", value: pct, unit: "%", descriptionText: "${device.displayName} battery is ${pct}%"), createEvent(name: "batteryVoltage", value: volts, unit: "V", descriptionText: "${device.displayName} battery voltage is ${volts}V") ] } private parseBatteryPercent(String hexValue) { int val = Integer.parseInt(hexValue, 16) int pct = Math.round(val / 2.0) if (settings.txtEnable != false) log.info "${device.displayName} Battery Percent from device: ${pct}%" return [createEvent(name: "battery", value: pct, unit: "%", descriptionText: "${device.displayName} battery is ${pct}%")] } private parseCustomCluster(int attrInt, String hexValue) { int val = Integer.parseInt(hexValue, 16) def results = [] if (attrInt == 0x0000) { // se_led_brightness def brightness = (val == 1) ? "Max" : "Min" results << createEvent(name: "ledBrightness", value: brightness, descriptionText: "${device.displayName} LED brightness is ${brightness}") if (settings.txtEnable != false) log.info "${device.displayName} LED brightness is ${brightness}" } else if (attrInt == 0x0003) { // se_lifetime BigDecimal lifetime = val * 0.5 results << createEvent(name: "lifetime", value: lifetime, unit: "y", descriptionText: "${device.displayName} remaining lifetime is ${lifetime} years") if (settings.txtEnable != false) log.info "${device.displayName} Remaining lifetime is ${lifetime} years" } else if (attrInt == 0x0005) { // se_test_mode def testMode = (val == 1) ? "active" : "inactive" results << createEvent(name: "testMode", value: testMode, descriptionText: "${device.displayName} test mode is ${testMode}") if (settings.txtEnable != false) log.info "${device.displayName} Test mode is ${testMode}" } else if (attrInt == 0x0006) { // se_silence_alarm def silenceMode = (val == 1) ? "active" : "inactive" results << createEvent(name: "silenceMode", value: silenceMode, descriptionText: "${device.displayName} silence mode is ${silenceMode}") if (settings.txtEnable != false) log.info "${device.displayName} Silence mode is ${silenceMode}" } return results } def refresh() { if (settings.logEnable != false) log.debug "refresh()..." def cmds = [] cmds += zigbee.readAttribute(0x0500, 0x0002) // IAS Zone Status cmds += zigbee.readAttribute(0x0001, 0x0020) // Battery Voltage cmds += zigbee.readAttribute(0x0001, 0x0021) // Battery % int ep = device.endpointId ? Integer.parseInt(device.endpointId, 16) : 20 cmds += zigbee.readAttribute(0xFC04, 0x0000, [mfgCode: 0x105E, destEndpoint: ep]) // LED Brightness cmds += zigbee.readAttribute(0xFC04, 0x0001, [mfgCode: 0x105E, destEndpoint: ep]) // Alarm Sound Level cmds += zigbee.readAttribute(0xFC04, 0x0002, [mfgCode: 0x105E, destEndpoint: ep]) // Alarm Sound Mode cmds += zigbee.readAttribute(0xFC04, 0x0003, [mfgCode: 0x105E, destEndpoint: ep]) // Remaining Lifetime cmds += zigbee.readAttribute(0xFC04, 0x0004, [mfgCode: 0x105E, destEndpoint: ep]) // Hush Duration cmds += zigbee.readAttribute(0xFC04, 0x0005, [mfgCode: 0x105E, destEndpoint: ep]) // Test Mode cmds += zigbee.readAttribute(0xFC04, 0x0006, [mfgCode: 0x105E, destEndpoint: ep]) // Silence Mode return cmds } def configure() { if (settings.txtEnable != false) log.info "configure()..." def cmds = [] cmds += zigbee.enrollResponse() def epStr = device.endpointId ?: "14" cmds += "zdo bind 0x${device.deviceNetworkId} 0x${epStr} 0x01 0x0500 {${device.zigbeeId}} {}" cmds += "zdo bind 0x${device.deviceNetworkId} 0x${epStr} 0x01 0x0001 {${device.zigbeeId}} {}" cmds += "zdo bind 0x${device.deviceNetworkId} 0x${epStr} 0x01 0xFC04 {${device.zigbeeId}} {}" cmds += zigbee.configureReporting(0x0500, 0x0002, 0x19, 0, 3600, 0x00) // IAS ZoneStatus change cmds += zigbee.configureReporting(0x0001, 0x0020, 0x20, 3600, 21600, 0x01) // Battery Volts cmds += zigbee.configureReporting(0x0001, 0x0021, 0x20, 3600, 21600, 0x01) // Battery % cmds += refresh() return cmds } def updated() { if (settings.logEnable != false) log.debug "updated()..." if (device.currentValue("heat") == null) { sendEvent(name: "heat", value: "clear", descriptionText: "Initializing heat alarm state to clear") } if (device.currentValue("batteryVoltage") == null) { sendEvent(name: "batteryVoltage", value: 3.0, unit: "V", descriptionText: "Initializing batteryVoltage state to 3.0V") } if (device.currentValue("tamper") == null) { sendEvent(name: "tamper", value: "clear", descriptionText: "Initializing tamper state to clear") } if (settings.logEnable == true) { if (settings.logEnable != false) log.info "Debug logging will automatically turn off in 30 minutes (1800s)." runIn(1800, "logsOff") } } def logsOff() { log.warn "debug logging disabled..." device.updateSetting("logEnable", [value: "false", type: "bool"]) }