/**
* Tuya Smart Siren Zigbee driver for Hubitat
*
* https://community.hubitat.com/t/tuya-smart-siren-zigbee-driver-doesnt-work/73624/19
*
* 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.
*
* ver. 1.0.0 2022-04-02 kkossev - First published version
* ver. 1.1.0 2022-11-05 kkossev - Alarm events are registered upon confirmation from the device only; added switch capability; added Tone capability (beep command); combined Tuya commands; default settings are restored after the beep command
* added capability 'Chime'; setVolume; volumeUp, volumeDown; playSound; beepVolume; playSoundVolume; playSoundDuration; unschedule() is called when preferences are updated.
* ver. 1.1.1 2022-12-27 kkossev - bug fix: playing a sound from RM rule without specifying the volume level was making the device freeze; debug logs cleanup; sounds titles improvements;
*
* TODO: preferences for the beep() command
*
*/
def version() { "1.1.1" }
def timeStamp() {"2022/12/27 10:27 PM"}
import groovy.json.*
import groovy.transform.Field
import hubitat.zigbee.zcl.DataType
import hubitat.device.HubAction
import hubitat.device.Protocol
@Field static final Boolean debug = false
metadata {
definition (name: "Tuya Smart Siren Zigbee", namespace: "kkossev", author: "Krassimir Kossev", importUrl: "https://raw.githubusercontent.com/kkossev/Hubitat/main/Drivers/Tuya%20Smart%20Siren%20Zigbee/Tuya%20Smart%20Siren%20Zigbee.groovy", singleThreaded: true ) {
capability "Actuator"
capability "Battery"
capability "Configuration"
capability "Switch"
capability "Alarm" // alarm - ENUM ["strobe", "off", "both", "siren"]; Commands: both() off() siren() strobe()
capability "Tone" // Commands: beep()
capability "Chime" // soundEffects - JSON_OBJECT; soundName - STRING; status - ENUM ["playing", "stopped"]; Commands: playSound(soundnumber); soundnumber required (NUMBER) - Sound number to play; stop()
capability "AudioVolume" //Attributes: mute - ENUM ["unmuted", "muted"] volume - NUMBER, unit:%; Commands: mute() setVolume(volumelevel) volumelevel required (NUMBER) - Volume level (0 to 100) unmute() volumeDown() volumeUp()
attribute "duration", "number"
command "setAlarmMelody", [[name:"Set alarm melody type", type: "ENUM", description: "set alarm type", constraints: melodiesOptions]]
command "setAlarmDuration", [[name:"Length", type: "NUMBER", description: "0..180 = set alarm length in seconds. 0 = no audible alarm"]]
command "setAlarmVolume", [[name:"Volume", type: "ENUM", description: "set alarm volume", constraints: volumeOptions ]]
command "playSound", [
[name:"soundNumber", type: "NUMBER", description: "Melody number, 1..18", isRequired: true],
[name:"volumeLevel", type: "NUMBER", description: "sound volume level, 0..100 % "],
[name:"duration", type: "NUMBER", description: "duration is seconds"]
]
if (debug==true) {
command "test"
}
fingerprint profileId:"0104", endpointId:"01", inClusters:"0004,0005,EF00,0000", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_t1blo2bj", deviceJoinName: "Tuya NEO Smart Siren" // vendor: 'Neo', model: 'NAS-AB02B2'
// https://github.com/zigpy/zha-device-handlers/issues/1379#issuecomment-1077772021
}
preferences {
input (name: "logEnable", type: "bool", title: "Debug logging", description: "Debug information, useful for troubleshooting. Recommended value is false", defaultValue: true)
input (name: "txtEnable", type: "bool", title: "Description text logging", description: "Display sensor states in HE log page. Recommended value is true", defaultValue: true)
input (name: "beepVolume", type: "enum", title: "Beep Volume", description:"Select Beep Volume", defaultValue: "low", options: volumeOptions)
input (name: "playSoundVolume", type: "enum", title: "Play Sound (Chime) command default volume", description:"Select playSound default Volume", defaultValue: "medium", options: volumeOptions)
input (name: "playSoundDuration", type: "number", title: "Play Sound Duration, seconds", description: "The duration of the PlaySound command in seconds", range: "1..180", defaultValue: 10)
}
}
@Field static final List volumeOptions = [
// '---select---',
// 'muted',
'low',
'medium',
'high'
]
@Field static final LinkedHashMap volumeMapping = [
// 'muted' : [ volume: '0', tuya: '-'],
'low' : [ volume: '33', tuya: '0'],
'medium' : [ volume: '66', tuya: '1'],
'high' : [ volume: '100', tuya: '2']
]// as ConfigObject
@Field static final String defaultVolume = "medium"
@Field static final List melodiesOptions = [
'1=Doorbell 1',
'2=Fur Elise',
'3=Westminster',
'4=4 Key Chime',
'5=William Tell',
'6=Mozart Piano',
'7=Space Alarm',
'8=Klaxon',
'9=meep meep',
'10=Wheep',
'11=Barking dog',
'12=Alarm Siren',
'13=Doorbell 2',
'14=Old Phone',
'15=Police Siren',
'16=Evacuation bell',
'17=Clock alarm',
'18=Fire alarm'
] //as String[]
private findVolumeByTuyaValue( fncmd ) {
def volumeName = 'unknown'
def volumePct = -1
volumeMapping.each{ k, v ->
if (v.tuya.value as String == fncmd.toString()) {
volumeName = k
volumePct = v.volume
}
}
return [volumeName, volumePct]
}
private findVolumeByPct( pct ) {
def volumeName = 'unknown'
def volumeTuya = -1
volumeMapping.each{ k, v ->
if (v.volume.value as String == pct.toString()) {
volumeName = k
volumeTuya = v.tuya
}
}
return [volumeName, volumeTuya]
}
private findVolumeByName( name ) {
def volumeTuya = -1
def volumePct = -1
volumeMapping.each{ k, v ->
if (k as String == name as String) {
volumeTuya = v.tuya
volumePct = v.volume
}
}
return [volumeTuya, volumePct]
}
// Constants
@Field static final Integer TUYA_DP_VOLUME = 5
@Field static final Integer TUYA_DP_DURATION = 7
@Field static final Integer TUYA_DP_ALARM = 13
@Field static final Integer TUYA_DP_BATTERY = 15
@Field static final Integer TUYA_DP_MELODY = 21
private getCLUSTER_TUYA() { 0xEF00 }
private getSETDATA() { 0x00 }
private getSETTIME() { 0x24 }
// Tuya Commands
private getTUYA_REQUEST() { 0x00 }
private getTUYA_REPORTING() { 0x01 }
private getTUYA_QUERY() { 0x02 }
private getTUYA_STATUS_SEARCH() { 0x06 }
private getTUYA_TIME_SYNCHRONISATION() { 0x24 }
// tuya DP type
private getDP_TYPE_RAW() { "01" } // [ bytes ]
private getDP_TYPE_BOOL() { "01" } // [ 0/1 ]
private getDP_TYPE_VALUE() { "02" } // [ 4 byte value ]
private getDP_TYPE_STRING() { "03" } // [ N byte string ]
private getDP_TYPE_ENUM() { "04" } // [ 0-255 ]
private getDP_TYPE_BITMAP() { "05" } // [ 1,2,4 bytes ] as bits
// Parse incoming device messages to generate events
def parse(String description) {
checkDriverVersion()
if (state.rxCounter != null) state.rxCounter = state.rxCounter + 1
//if (settings?.logEnable) log.debug "${device.displayName} parse() descMap = ${zigbee.parseDescriptionAsMap(description)}"
if (description?.startsWith('catchall:') || description?.startsWith('read attr -')) {
Map descMap = zigbee.parseDescriptionAsMap(description)
if (descMap.clusterInt == 0x0001 && descMap.commandInt != 0x07 && descMap?.value) {
if (descMap.attrInt == 0x0021) {
getBatteryPercentageResult(Integer.parseInt(descMap.value,16))
} else if (descMap.attrInt == 0x0020){
getBatteryResult(Integer.parseInt(descMap.value, 16))
}
else {
logWarn "unparesed attrint ${descMap.attrInt}"
}
}
else if (descMap?.clusterInt == CLUSTER_TUYA) {
processTuyaCluster( descMap )
}
else if (descMap?.clusterId == "0013") { // device announcement, profileId:0000
if (settings?.logEnable) log.debug "${device.displayName} device announcement"
}
else if (descMap?.cluster == "0000" && descMap?.attrId == "0001") {
if (settings?.logEnable) log.debug "${device.displayName} application version is ${descMap?.value}"
}
else if (descMap?.cluster == "0000" && descMap?.attrId == "FFDF") {
if (settings?.logEnable) log.debug "${device.displayName} Tuya check-in"
}
else if (descMap?.cluster == "0000" && descMap?.attrId == "FFE2") {
if (settings?.logEnable) log.debug "${device.displayName} Tuya AppVersion is ${descMap?.value}"
}
else if (descMap?.cluster == "0000" && descMap?.attrId == "FFE4") {
if (settings?.logEnable) log.debug "${device.displayName} Tuya UNKNOWN attribute FFE4 value is ${descMap?.value}"
}
else if (descMap?.cluster == "0000" && descMap?.attrId == "FFFE") {
if (settings?.logEnable) log.debug "${device.displayName} Tuya UNKNOWN attribute FFFE value is ${descMap?.value}"
}
else if (descMap?.cluster == "0000") {
if (settings?.logEnable) log.debug "${device.displayName} basic cluster report : descMap = ${descMap}"
}
else {
if (settings?.logEnable) log.debug "${device.displayName} NOT PARSED : descMap = ${descMap}"
}
} // if 'catchall:' or 'read attr -'
else {
if (settings?.logEnable) log.debug "${device.displayName} UNPROCESSED description = ${description} descMap = ${zigbee.parseDescriptionAsMap(description)}"
}
}
def processTuyaCluster( descMap ) {
if (descMap?.clusterInt==CLUSTER_TUYA && descMap?.command == "24") { //getSETTIME
if (settings?.logEnable) log.debug "${device.displayName} time synchronization request from device, descMap = ${descMap}"
def offset = 0
try {
offset = location.getTimeZone().getOffset(new Date().getTime())
//if (settings?.logEnable) log.debug "${device.displayName} timezone offset of current location is ${offset}"
}
catch(e) {
if (settings?.logEnable) log.error "${device.displayName} cannot resolve current location. please set location in Hubitat location setting. Setting timezone offset to zero"
}
def cmds = zigbee.command(CLUSTER_TUYA, SETTIME, "0008" +zigbee.convertToHexString((int)(now()/1000),8) + zigbee.convertToHexString((int)((now()+offset)/1000), 8))
if (settings?.logEnable) log.debug "${device.displayName} sending time data : ${cmds}"
cmds.each{ sendHubCommand(new hubitat.device.HubAction(it, hubitat.device.Protocol.ZIGBEE)) }
if (state.txCounter != null) state.txCounter = state.txCounter + 1
}
else if (descMap?.clusterInt==CLUSTER_TUYA && descMap?.command == "0B") { // ZCL Command Default Response
String clusterCmd = descMap?.data[0]
def status = descMap?.data[1]
if (settings?.logEnable) log.debug "${device.displayName} device has received Tuya cluster ZCL command 0x${clusterCmd} response 0x${status} data = ${descMap?.data}"
if (status != "00") {
logWarn "ATTENTION! manufacturer = ${device.getDataValue("manufacturer")} unsupported Tuya cluster ZCL command 0x${clusterCmd} response 0x${status} data = ${descMap?.data} !!!"
}
}
else if ((descMap?.clusterInt==CLUSTER_TUYA) && (descMap?.command == "01" || descMap?.command == "02"))
{
def transid = zigbee.convertHexToInt(descMap?.data[1]) // "transid" is just a "counter", a response will have the same transid as the command
def dp = zigbee.convertHexToInt(descMap?.data[2]) // "dp" field describes the action/message of a command frame
def dp_id = zigbee.convertHexToInt(descMap?.data[3]) // "dp_identifier" is device dependant
def fncmd = getTuyaAttributeValue(descMap?.data) //
//if (settings?.logEnable) log.trace "${device.displayName} dp_id=${dp_id} dp=${dp} fncmd=${fncmd}"
switch (dp) {
case TUYA_DP_VOLUME : // (05) volume [ENUM] 0:low 1: mid 2:high
def volumeName = 'unknown'
def volumePct = -1
(volumeName, volumePct) = findVolumeByTuyaValue( fncmd )
if (volumeName != 'unknown') {
if (settings?.txtEnable) log.debug "${device.displayName} volume received is ${volumeName} ${volumePct}% (${fncmd})"
sendVolumeEvent( volumePct )
}
break
case TUYA_DP_DURATION : // (07) duration [VALUE] in seconds
if (settings?.txtEnable) log.info "${device.displayName} duration is ${fncmd} s"
sendEvent(name: "duration", value: fncmd, descriptionText: descriptionText )
break
case TUYA_DP_ALARM : // (13) alarm [BOOL]
def value = fncmd == 0 ? "off" : fncmd == 1 ? state.lastCommand : "unknown"
if (settings?.logEnable) log.info "${device.displayName} alarm state received is ${value} (${fncmd})"
if (value == "off") {
sendEvent(name: "status", value: "stopped")
if (device.currentValue("alarm", true) in ["beep", "playSound"]) {
runIn( 7, restoreDefaultSettings, [overwrite: true])
//restoreDefaultSettings()
}
}
else {
unschedule(restoreDefaultSettings)
sendEvent(name: "status", value: "playing")
}
sendAlarmEvent(value)
break
case TUYA_DP_BATTERY : // (15) battery [VALUE] percentage
getBatteryPercentageResult( fncmd * 2)
break
case TUYA_DP_MELODY : // (21) melody [enum] 0..17
if (settings?.txtEnable) log.info "${device.displayName} melody is ${melodiesOptions[fncmd]} (${fncmd})"
sendEvent(name: "soundName", value: melodiesOptions[fncmd], descriptionText: descriptionText )
break
default :
logWarn "NOT PROCESSED Tuya cmd: dp=${dp} value=${fncmd} descMap.data = ${descMap?.data}"
break
}
}
}
private int getTuyaAttributeValue(ArrayList _data) {
int retValue = 0
if (_data.size() >= 6) {
int dataLength = _data[5] as Integer
int power = 1;
for (i in dataLength..1) {
retValue = retValue + power * zigbee.convertHexToInt(_data[i+5])
power = power * 256
}
}
return retValue
}
def off() {
sendTuyaAlarm("off")
}
def on() {
sendTuyaAlarm("on")
}
def both() {
sendTuyaAlarm("both")
}
def strobe() {
sendTuyaAlarm("strobe")
}
def siren() {
sendTuyaAlarm( "siren")
}
def sendTuyaAlarm( commandName ) {
wakeUpTuya()
String cmds = ""
logDebug "swithing alarm ${commandName}()"
state.lastCommand = commandName
if (commandName != "off") {
// volume
def volumeName; def volumeTuya;
(volumeName, volumeTuya) = findVolumeByPct( state.setVolume )
if (volumeTuya >= 0 ) {
cmds += appendTuyaCommand( TUYA_DP_VOLUME, DP_TYPE_ENUM, safeToInt(volumeTuya) )
}
// duration
cmds += appendTuyaCommand( TUYA_DP_DURATION, DP_TYPE_VALUE, safeToInt(state.setDuration) )
// melody
def melodyNumber = safeToInt(melodiesOptions.indexOf(state.setMelody))
cmds += appendTuyaCommand( TUYA_DP_MELODY, DP_TYPE_ENUM, melodyNumber )
// play it
unschedule(restoreDefaultSettings)
cmds += appendTuyaCommand( TUYA_DP_ALARM, DP_TYPE_BOOL, 1 )
sendZigbeeCommands( combinedTuyaCommands(cmds) )
}
else {
unschedule(restoreDefaultSettings)
sendZigbeeCommands( sendTuyaCommand(zigbee.convertToHexString(TUYA_DP_ALARM, 2), DP_TYPE_BOOL, "00"))
}
}
// capability "Tone"
def beep() {
if ( true ) {
wakeUpTuya()
String cmds = ""
state.lastCommand = "beep"
logDebug "sending beep() beepVolume = ${settings?.beepVolume}"
def volumeTuya; def volumePct
(volumeTuya, volumePct) = findVolumeByName(settings?.beepVolume )
if (volumeTuya >= 0 ) {
cmds += appendTuyaCommand( TUYA_DP_VOLUME, DP_TYPE_ENUM, safeToInt(volumeTuya) )
}
cmds += appendTuyaCommand( TUYA_DP_DURATION, DP_TYPE_VALUE, 1 )
cmds += appendTuyaCommand( TUYA_DP_MELODY, DP_TYPE_ENUM, 2 )
unschedule(restoreDefaultSettings)
cmds += appendTuyaCommand( TUYA_DP_ALARM, DP_TYPE_BOOL, 1 )
sendZigbeeCommands( combinedTuyaCommands(cmds) )
}
else {
ArrayList cmds = []
state.lastCommand = "beep"
cmds += sendTuyaCommand( zigbee.convertToHexString(TUYA_DP_VOLUME,2), DP_TYPE_ENUM, "01", delay=50)
cmds += sendTuyaCommand( zigbee.convertToHexString(TUYA_DP_DURATION,2), DP_TYPE_VALUE, "00000001", delay=50 )
cmds += sendTuyaCommand( zigbee.convertToHexString(TUYA_DP_MELODY,2), DP_TYPE_ENUM, "02", delay=100 )
unschedule(restoreDefaultSettings)
cmds += sendTuyaCommand( zigbee.convertToHexString(TUYA_DP_ALARM,2), DP_TYPE_BOOL, "01" , delay=200)
sendZigbeeCommands( cmds )
}
}
def restoreDefaultSettings() {
String cmds = ""
def volumeName
def volumeTuya
(volumeName, volumeTuya) = findVolumeByPct( state.setVolume )
if (volumeTuya >= 0) {
cmds += appendTuyaCommand( TUYA_DP_VOLUME, DP_TYPE_ENUM, safeToInt(volumeTuya) )
}
cmds += appendTuyaCommand( TUYA_DP_DURATION, DP_TYPE_VALUE, safeToInt(state.setDuration) )
cmds += appendTuyaCommand( TUYA_DP_MELODY, DP_TYPE_ENUM, safeToInt(melodiesOptions.indexOf(state.setMelody)))
logDebug "restoring default settings volume=${volumeName}, duration=${state.setDuration}, melody=${state.setMelody}"
sendZigbeeCommands( combinedTuyaCommands(cmds) )
}
//capability "AudioVolume" //Attributes: mute - ENUM ["unmuted", "muted"] volume - NUMBER, unit:%; Commands: mute() setVolume(volumelevel) volumelevel required (NUMBER) - Volume level (0 to 100) unmute() volumeDown() volumeUp()
def mute() {
sendEvent(name: "mute", value: "muted")
}
def unmute() {
sendEvent(name: "mute", value: "unmuted")
}
def getNearestTuyaVolumeLevel( volumelevel ) {
def nearestlevel = 0
//if (volumelevel <= 0 ) level = 0
/*else*/ if (volumelevel <= 33) nearestlevel = 33
else if (volumelevel <= 66) nearestlevel = 66
else nearestlevel = 100
return nearestlevel
}
def setVolume(volumelevel) {
// - Volume level (0 to 100)
String cmds = ""
def nearestlevel = getNearestTuyaVolumeLevel( volumelevel )
if (nearestlevel == 0 && device.currentValue("mute", true) == "unmuted") mute()
else if (nearestlevel != 0 && device.currentValue("mute", true) == "muted") unmute()
state.volume = nearestlevel
def volumeName
def volumeTuya
(volumeName, volumeTuya) = findVolumeByPct( nearestlevel )
logDebug "matched volumelevel=${volumelevel} to nearestLlevel=${nearestlevel} (volumeTuya=${volumeTuya})"
if (volumeTuya >= 0) {
cmds += appendTuyaCommand( TUYA_DP_VOLUME, DP_TYPE_ENUM, safeToInt(volumeTuya) )
}
if (settings?.logEnable) log.debug "${device.displayName} setting volume=${volumeName}"
sendZigbeeCommands( combinedTuyaCommands(cmds) )
}
def volumeDown() {
setVolume( state.volume - 34)
}
def volumeUp() {
setVolume( state.volume + 33)
}
// capability "Chime" // soundEffects - JSON_OBJECT; soundName - STRING; status - ENUM ["playing", "stopped"]; Commands: playSound(soundnumber); soundnumber required (NUMBER) - Sound number to play; stop()
// command "playSound", [
// [name:"soundNumber", type: "NUMBER", description: "Melody number, 1..18", isRequired: true],
// [name:"duration", type: "NUMBER", description: "duration is seconds"],
// [name:"volume", type: "NUMBER", description: "sound volume, %"]
// ]
def playSound(soundnumber, volumeLevel=null, duration=null) {
wakeUpTuya()
String cmds = ""
def volumeName; def volumeTuya; def volumePct
int soundNumberIndex = safeToInt(soundnumber)
soundNumberIndex = soundNumberIndex < 1 ? 1 : soundNumberIndex > 18 ? 18 : soundNumberIndex; soundNumberIndex -= 1
if (volumeLevel == null) {
// use the default playSoundVolume
volumeName = settings?.playSoundVolume ?: defaultVolume
(volumeTuya, volumePct) = findVolumeByName( volumeName )
logDebug "volumeLevel is null, volumeTuya = ${volumeTuya}"
}
else {
def nearestVolume = getNearestTuyaVolumeLevel( volumeLevel )
(volumeName, volumeTuya) = findVolumeByPct( nearestVolume )
}
if (duration == null) {
duration = settings?.playSoundDuration ?: 10 as int
}
else {
duration = duration <1 ? 1 : duration > 180 ? 180 : duration as int
}
state.lastCommand = "playSound"
cmds += appendTuyaCommand( TUYA_DP_VOLUME, DP_TYPE_ENUM, safeToInt(volumeTuya))
cmds += appendTuyaCommand( TUYA_DP_DURATION, DP_TYPE_VALUE, safeToInt(duration) )
cmds += appendTuyaCommand( TUYA_DP_MELODY, DP_TYPE_ENUM, soundNumberIndex)
unschedule(restoreDefaultSettings)
cmds += appendTuyaCommand( TUYA_DP_ALARM, DP_TYPE_BOOL, 1 )
logDebug "playSound ${soundnumber} (${melodiesOptions.get(soundNumberIndex)}) index=${soundNumberIndex}, duration=${duration}, volume=${volumeName}(${volumeTuya})"
sendZigbeeCommands( combinedTuyaCommands(cmds) )
}
def stop() {
off()
}
// capability "MusicPlayer"
def pause() {
}
def play() {
}
def sendVolumeEvent( volume, isDigital=false ) {
def map = [:]
map.name = "volume"
map.value = volume
map.unit = "%"
map.type = isDigital == true ? "digital" : "physical"
map.descriptionText = "${map.name} is ${map.value}"
if (txtEnable) {log.info "${device.displayName} ${map.descriptionText}"}
sendEvent(map)
}
def sendAlarmEvent( mode, isDigital=false ) {
def map = [:]
map.name = "alarm"
map.value = mode
//map.unit = "Hz"
map.type = isDigital == true ? "digital" : "physical"
map.descriptionText = "${map.name} is ${map.value}"
if (txtEnable) {log.info "${device.displayName} ${map.descriptionText}"}
sendEvent(map)
sendEvent(name: "switch", value: mode=="off"?"off":"on", descriptionText: map.descriptionText, type:"digital")
}
void setAlarmMelody( melodyName ) {
def melodyIndex = melodiesOptions.indexOf(melodyName)
if (melodyIndex <0) {
logWarn "setMelody invalid ${melodyName}"
return
}
logDebug "setMelody $melodyName ($melodyIndex)"
state.setMelody = melodyName
sendZigbeeCommands( sendTuyaCommand(zigbee.convertToHexString(TUYA_DP_MELODY, 2), DP_TYPE_ENUM, zigbee.convertToHexString(melodyIndex, 2)))
}
void setAlarmDuration(BigDecimal length) {
int duration = length > 255 ? 255 : length < 0 ? 0 : length
if (settings?.logEnable) log.debug "${device.displayName} setDuration ${duration}"
state.setDuration = duration
sendZigbeeCommands( sendTuyaCommand(zigbee.convertToHexString(TUYA_DP_DURATION, 2), DP_TYPE_VALUE, zigbee.convertToHexString(duration, 8)))
}
void setAlarmVolume(String volumeOption) {
def index = volumeMapping.findIndexOf{it.key==volumeOption}
if (index < 0) {
logWarn "setVolume not supported parameter ${volume}"
return
}
//log.trace "volumeMapping[${volumeOption}] = ${volumeMapping[volumeOption]}"
def volumePct = volumeMapping[volumeOption].find{it.key=='volume'}.value
def tuyaValue = volumeMapping[volumeOption].find{it.key=='tuya'}.value
//log.trace "volume=${volumePct}, tuya=${tuyaValue} "
switch (volumeOption) {
case "muted" :
mute()
return
case "low" :
case "medium" :
case "high" :
sendZigbeeCommands( sendTuyaCommand(zigbee.convertToHexString(TUYA_DP_VOLUME, 2), DP_TYPE_ENUM, zigbee.convertToHexString(tuyaValue as int, 2)))
break
default :
logWarn "setVolume not supported parameter ${volume}"
return
}
unmute()
if (settings?.logEnable) log.debug "${device.displayName} setVolume ${volumeOption} ${volumePct}% (Tuya:${tuyaValue})"
state.setVolume = volumePct
}
// called on initial install of device during discovery
// also called from initialize() in this driver!
def installed() {
log.info "${device.displayName} installed()"
unschedule()
unmute()
}
// called when preferences are saved
// runs when save is clicked in the preferences section
def updated() {
if (settings?.txtEnable) log.info "${device.displayName} Updating ${device.getLabel()} (${device.getName()}) model ${device.getDataValue('model')} manufacturer ${device.getDataValue('manufacturer')}"
if (settings?.txtEnable) log.info "${device.displayName} Debug logging is ${logEnable}; Description text logging is ${txtEnable}"
unschedule()
if (logEnable==true) {
runIn(86400, logsOff, [overwrite: true, misfire: "ignore"]) // turn off debug logging after 24 hours
if (settings?.txtEnable) log.info "${device.displayName} Debug logging is will be turned off after 24 hours"
}
else {
unschedule(logsOff)
}
}
def refresh() {
if (settings?.logEnable) {log.debug "${device.displayName} refresh()..."}
zigbee.readAttribute(0, 1)
}
def driverVersionAndTimeStamp() {version()+' '+timeStamp()}
def checkDriverVersion() {
if (state.driverVersion != null && driverVersionAndTimeStamp() == state.driverVersion) {
}
else {
if (txtEnable==true) log.debug "${device.displayName} updating the settings from the current driver version ${state.driverVersion} to the new version ${driverVersionAndTimeStamp()}"
initializeVars( fullInit = false )
state.driverVersion = driverVersionAndTimeStamp()
}
}
def logInitializeRezults() {
if (settings?.txtEnable) log.info "${device.displayName} manufacturer = ${device.getDataValue("manufacturer")}"
if (settings?.txtEnable) log.info "${device.displayName} Initialization finished\r version=${version()} (Timestamp: ${timeStamp()})"
}
// called by initialize() button
void initializeVars(boolean fullInit = true ) {
if (settings?.txtEnable) log.info "${device.displayName} InitializeVars()... fullInit = ${fullInit}"
if (fullInit == true ) {
state.clear()
state.driverVersion = driverVersionAndTimeStamp()
sendEvent(name: "soundEffects", value: JsonOutput.toJson(melodiesOptions), isStateChange: true)
}
//
state.packetID = 0
state.rxCounter = 0
state.txCounter = 0
state.lastCommand = "unknown"
state.setMelody = "1=Doorbell"
state.setDuration = 10
state.setVolume = 66
if (fullInit == true || device.getDataValue("logEnable") == null) device.updateSetting("logEnable", true)
if (fullInit == true || device.getDataValue("txtEnable") == null) device.updateSetting("txtEnable", true)
if (fullInit == true || settings?.beepVolume == null) device.updateSetting("beepVolume", [value:"low", type:"enum"])
if (fullInit == true || settings?.playSoundVolume == null) device.updateSetting("playSoundVolume", [value:"medium", type:"enum"])
if (fullInit == true || settings?.playSoundDuration == null) device.updateSetting("playSoundDuration", [value:10, type:"number"])
}
def tuyaBlackMagic() {
List cmds = []
cmds += zigbee.readAttribute(0x0000, [0x0004, 0x000, 0x0001, 0x0005, 0x0007, 0xfffe], [:], delay=200) // Cluster: Basic, attributes: Man.name, ZLC ver, App ver, Model Id, Power Source, attributeReportingStatus
cmds += zigbee.writeAttribute(0x0000, 0xffde, 0x20, 0x13, [:], delay=200)
return cmds
}
// called when used with capability "Configuration" is called when the configure button is pressed on the device page.
// Runs when driver is installed, after installed() is run. if capability Configuration exists, a Configure command is added to the ui
// It is also called on initial install after discovery.
def configure() {
if (settings?.txtEnable) log.info "${device.displayName} configure().."
List cmds = []
cmds += tuyaBlackMagic()
sendZigbeeCommands(cmds)
}
// called when used with capability "Initialize" it will call this method every time the hub boots up. So for things that need refreshing or re-connecting (LAN integrations come to mind here) ..
// runs first time driver loads, ie system startup
// when capability Initialize exists, a Initialize command is added to the ui.
def initialize() {
log.info "${device.displayName} Initialize()..."
unschedule()
initializeVars()
installed()
updated()
configure()
runIn( 3, logInitializeRezults)
}
private sendTuyaCommand(dp, dp_type, fncmd, delay=200) {
ArrayList cmds = []
cmds += zigbee.command(CLUSTER_TUYA, SETDATA, [:], delay, PACKET_ID + dp + dp_type + zigbee.convertToHexString((int)(fncmd.length()/2), 4) + fncmd )
logDebug "sendTuyaCommand = ${cmds}"
state.txCounter = state.txCounter ?: 0 + 1
return cmds
}
private wakeUpTuya() {
sendZigbeeCommands(zigbee.readAttribute(0x0000, 0x0004, [:], delay=50) )
}
private combinedTuyaCommands(String cmds) {
state.txCounter = state.txCounter ?: 0 + 1
return zigbee.command(CLUSTER_TUYA, SETDATA, [:], delay=200, PACKET_ID + cmds )
}
private appendTuyaCommand(Integer dp, String dp_type, Integer fncmd) {
Integer fncmdLen = dp_type== DP_TYPE_VALUE? 8 : 2
String cmds = zigbee.convertToHexString(dp, 2) + dp_type + zigbee.convertToHexString((int)(fncmdLen/2), 4) + zigbee.convertToHexString(fncmd, fncmdLen)
//logDebug "appendTuyaCommand = ${cmds}"
return cmds
}
void sendZigbeeCommands(ArrayList cmd) {
logDebug "sendZigbeeCommands(cmd=$cmd)"
hubitat.device.HubMultiAction allActions = new hubitat.device.HubMultiAction()
cmd.each {
allActions.add(new hubitat.device.HubAction(it, hubitat.device.Protocol.ZIGBEE))
state.txCounter = state.txCounter ?: 0 + 1
}
sendHubCommand(allActions)
}
private getPACKET_ID() {
// state.packetID = ((state.packetID ?: 0) + 1 ) % 65536
// return zigbee.convertToHexString(state.packetID, 4)
return zigbee.convertToHexString(Math.abs(new Random().nextInt() % 65536), 4)
}
private getDescriptionText(msg) {
def descriptionText = "${device.displayName} ${msg}"
if (settings?.txtEnable) log.info "${descriptionText}"
return descriptionText
}
def logsOff(){
log.info "${device.displayName} debug logging disabled..."
device.updateSetting("logEnable",[value:"false",type:"bool"])
}
def getBatteryPercentageResult(rawValue) {
if (settings?.logEnable) log.debug "${device.displayName} Battery Percentage rawValue = ${rawValue} -> ${rawValue / 2}%"
def result = [:]
if (0 <= rawValue && rawValue <= 200) {
result.name = 'battery'
result.translatable = true
result.value = Math.round(rawValue / 2)
result.descriptionText = "${device.displayName} battery is ${result.value}%"
result.isStateChange = true
result.type == "physical"
result.unit = '%'
sendEvent(result)
if (settings?.txtEnable) log.info "${result.descriptionText}"
}
else {
logWarn "ignoring BatteryPercentageResult(${rawValue})"
}
}
private Map getBatteryResult(rawValue) {
if (settings?.logEnable) log.debug "${device.displayName} batteryVoltage = ${(double)rawValue / 10.0} V"
def result = [:]
def volts = rawValue / 10
if (!(rawValue == 0 || rawValue == 255)) {
def minVolts = 2.1
def maxVolts = 3.0
def pct = (volts - minVolts) / (maxVolts - minVolts)
def roundedPct = Math.round(pct * 100)
if (roundedPct <= 0)
roundedPct = 1
result.value = Math.min(100, roundedPct)
result.descriptionText = "${device.displayName} battery is ${result.value}% (${volts} V)"
result.name = 'battery'
result.unit = '%'
result.isStateChange = true
result.type == "physical"
if (settings?.txtEnable) log.info "${result.descriptionText}"
sendEvent(result)
}
else {
logWarn "ignoring BatteryResult(${rawValue})"
}
}
Integer safeToInt(val, Integer defaultVal=0) {
return "${val}"?.isInteger() ? "${val}".toInteger() : defaultVal
}
Double safeToDouble(val, Double defaultVal=0.0) {
return "${val}"?.isDouble() ? "${val}".toDouble() : defaultVal
}
def logDebug(msg) {
if (settings?.logEnable) {
log.debug "${device.displayName} " + msg
}
}
def logInfo(msg) {
if (settings?.txtEnable) {
log.info "${device.displayName} " + msg
}
}
def logWarn(msg) {
if (settings?.logEnable) {
log.warn "${device.displayName} " + msg
}
}
def test( str ) {
sendEvent(name: "soundEffects", value: JsonOutput.toJson(melodiesOptions), isStateChange: true)
}