/*
 * 
 *  Tuya Switch Module Driver
 *	
 *  Supports: TS0001 / TS0011 / TS0012
 *  
 */


@Field String driverVersion = "v1.09 (7th December 2023)"
@Field boolean debugMode = false


#include BirdsLikeWires.library
import groovy.transform.Field

@Field String deviceMan = "Tuya"
@Field String deviceType = "Switch Module"

@Field int reportIntervalMinutes = 10
@Field int checkEveryMinutes = 4


metadata {

	definition (name: "$deviceMan $deviceType", namespace: "BirdsLikeWires", author: "Andrew Davison", importUrl: "https://raw.githubusercontent.com/birdslikewires/hubitat/master/tuya/drivers/tuya_switch_module_ts0011_ts0012.groovy") {

		capability "Actuator"
		capability "Configuration"
		capability "HealthCheck"
		capability "Refresh"
		capability "Switch"

		command "flash"

		attribute "healthStatus", "enum", ["offline", "online"]
		attribute "mode", "string"

		if (debugMode) {
			command "testCommand"
		}

		fingerprint profileId: "0104", inClusters: "0000,0003,0006", outClusters: "0003,0006,0004", manufacturer: "_TYZB01_phjeraqq", model: "TS0001", deviceJoinName: "$deviceMan $deviceType TS0001", application: "43"
		fingerprint profileId: "0104", inClusters: "0003,0004,0005,0006,E000,E001,0000", outClusters: "0019,000A", manufacturer: "_TZ3000_qmi1cfuq", model: "TS0011", deviceJoinName: "$deviceMan $deviceType TS0011", application: "43"
		fingerprint profileId: "0104", inClusters: "0003,0004,0005,0006,E000,E001,0000", outClusters: "0019,000A", manufacturer: "_TZ3000_jl7qyupf", model: "TS0012", deviceJoinName: "$deviceMan $deviceType TS0012", application: "43"

	}

}


preferences {
	
	input name: "flashEnabled", type: "bool", title: "Enable flash", defaultValue: false
	input name: "flashRate", type: "number", title: "Flash rate (ms)", range: "500..5000", defaultValue: 1000

	if ("${getDeviceDataByName('model')}" == "TS0012") {
		input name: "flashRelays", type: "enum", title: "Flash relay", options:[["FF":"Both"],["01":"Relay 1"],["02":"Relay 2"]]
	}

	input name: "infoLogging", type: "bool", title: "Enable logging", defaultValue: true
	input name: "debugLogging", type: "bool", title: "Enable debug logging", defaultValue: true
	input name: "traceLogging", type: "bool", title: "Enable trace logging", defaultValue: true	

}


void testCommand() {

	logging("${device} : Test Command", "info")

}


void configureSpecifics() {
	// Called by main configure() method in BirdsLikeWires.library

	requestBasic()

	// Set device name.
	String deviceModel = getDeviceDataByName('model')
	device.name = "$deviceMan $deviceType $deviceModel"

	// Store relay count and create children.
	state.relayCount = ("${getDeviceDataByName('model')}" == "TS0012") ? 2 : 1
	if (state.relayCount > 1) {
		for (int i = 1; i <= state.relayCount; i++) {
			fetchChild("hubitat", "Generic Component Switch", "0$i")
		}
	} else {
		deleteChildren()
	}

	// Always set to 'static' to ensure we're never stuck in 'flashing' mode.
	sendEvent(name: "mode", value: "static", isStateChange: false)

	// Reporting

	/// Sadly it would appear that these modules don't support binding to 00 or FF as an endpoint.
	/// On other modules it's possible to do a configureReporting((0x0006, 0x0000...) which results in all
	/// endpoint states being reported. Instead we just have to poll, ironically using FF as the endpoint.

	sendZigbeeCommands(zigbee.onOffConfig())
	randomSixty = Math.abs(new Random().nextInt() % 60)
	schedule("${randomSixty} 0/${reportIntervalMinutes} * * * ? *", refresh)

}


void updateSpecifics() {
	// Called by updated() method in BirdsLikeWires.library

	return

}


void ping() {

	logging("${device} : Ping", "info")
	refresh()

}


void refresh() {

	sendZigbeeCommands(["he rattr 0x${device.deviceNetworkId} 0xFF 0x0006 0x00 {}"])
	logging("${device} : Refreshed", "info")

}


void off() {

	sendZigbeeCommands(["he cmd 0x${device.deviceNetworkId} 0xFF 0x0006 0x00 {}"])
	sendEvent(name: "mode", value: "static")

}


void on() {

	sendZigbeeCommands(["he cmd 0x${device.deviceNetworkId} 0xFF 0x0006 0x01 {}"])
	sendEvent(name: "mode", value: "static")

}


void flash() {

	if (!flashEnabled) {
		logging("${device} : Flash : Disabled", "warn")
		return
	}

	if (!flashRelays && "${getDeviceDataByName('model')}" == "TS0012") {
		logging("${device} : Flash : No relay chosen in preferences.", "warn")
		return
	}

	logging("${device} : Flash : Rate of ${flashRate ?: 1000} ms", "info")
	sendEvent(name: "mode", value: "flashing")
	pauseExecution 200
    flashOn()

}


void flashOn() {

	String mode = device.currentState("mode").value
	logging("${device} : flashOn : Mode is ${mode}", "debug")

    if (mode != "flashing") return
    runInMillis((flashRate ?: 1000).toInteger(), flashOff)

	String flashEndpoint = "FF"
	if (flashRelays) flashEndpoint = flashRelays

	sendZigbeeCommands(["he cmd 0x${device.deviceNetworkId} 0x${flashEndpoint} 0x0006 0x01 {}"])

}


void flashOff() {

	String mode = device.currentState("mode").value
	logging("${device} : flashOn : Mode is ${mode}", "debug")

    if (mode != "flashing") return
	runInMillis((flashRate ?: 1000).toInteger(), flashOn)

	String flashEndpoint = "FF"
	if (flashRelays) flashEndpoint = flashRelays

	sendZigbeeCommands(["he cmd 0x${device.deviceNetworkId} 0x${flashEndpoint} 0x0006 0x00 {}"])

}


void parse(String description) {

	updateHealthStatus()
	checkDriver()

	Map descriptionMap = zigbee.parseDescriptionAsMap(description)

	if (descriptionMap) {

		logging("${device} : Parse : ${descriptionMap}", "debug")
		processMap(descriptionMap)

	} else {
		
		logging("${device} : Parse : Failed to parse received data. Please report these messages to the developer.", "warn")
		logging("${device} : Parse : ${description}", "error")

	}

}


void processMap(Map map) {

	logging("${device} : processMap() : ${map}", "trace")

	if (map.cluster == "0001") {
		// Power configuration.

		if (map.value) {

			logging("${device} : Switch ${map.endpoint} : On", "warn")

		} else {

			filterThis(map)

		}

	} else if (map.cluster == "0006" || map.clusterId == "0006") {
		// Relay configuration and response handling.
		// State confirmation and command receipts.

		if (map.command == "01") {
			// Relay States (Refresh)

			if (map.value == "00") {

				if (state.relayCount > 1) {

					def childDevice = fetchChild("hubitat", "Generic Component Switch", "${map.endpoint}")
					childDevice.parse([[name:"switch", value:"off"]])
					def currentChildStates = fetchChildStates("switch","${childDevice.id}")

					if (currentChildStates.every{it == "off"}) {
						logging("${device} : Switch : All Off", "info")
						sendEvent(name: "switch", value: "off")
					}

				} else {

					sendEvent(name: "switch", value: "off")

				}

				logging("${device} : Switch ${map.endpoint} : Off", "info")

			} else {

				if (state.relayCount > 1) {
					def childDevice = fetchChild("hubitat", "Generic Component Switch", "${map.endpoint}")
					childDevice.parse([[name:"switch", value:"on"]])
				}

				sendEvent(name: "switch", value: "on")
				logging("${device} : Switch ${map.endpoint} : On", "info")

			}

		} else if (map.command == "07") {
			// Relay Configuration

			logging("${device} : Relay Configuration : Successful", "info")

		} else if (map.command == "0A" || map.command == "0B") {
			// Relay States

			// Command "0A" is local actuation, command "0B" is remote actuation.
			String relayActuation = (map.command == "0A") ? "local" : "remote"
			
			// These appear to always report on both 0A and 0B when remote controlled, so I'm skipping the local message for now.

			// I'll investigate this with a momentary switch when I get chance.

			// Temporary Skipper
			if (map.command == "0A") {
				logging("${device} : Skipping $relayActuation actuation message", "debug")
				return
			}

			String relayActuated = (map.command == "0A") ? map.endpoint : map.sourceEndpoint
			String relayState = (map.command == "0A") ? map.value : map.data[0]
			String relayOnOff = (relayState == "00") ? "off" : "on"

			if (state.relayCount > 1) {

				def childDevice = fetchChild("hubitat", "Generic Component Switch", "$relayActuated")
				childDevice.parse([[name:"switch", value:"${relayOnOff}"]])
				def currentChildStates = fetchChildStates("switch","${childDevice.id}")

				// You need all of them off to be off, but only one to be on to be on. ;)
				if (relayOnOff == "off" && currentChildStates.every{it == "off"}) {

					debounceParentState("switch", "${relayOnOff}", "All Devices ${relayOnOff.capitalize()}", "info", 300)
					sendEvent(name: "switch", value: "${relayOnOff}")

				} else if (relayOnOff == "on") {

					sendEvent(name: "switch", value: "${relayOnOff}")

				}

			} else {

				sendEvent(name: "switch", value: "${relayOnOff}")

			}

			logging("${device} : ${relayActuation.capitalize()}ly Switched $relayActuated : ${relayOnOff.capitalize()}", "info")

		} else if (map.command == "00") {

			logging("${device} : Skipped : State Counter Message", "debug")

		} else {

			filterThis(map)

		}

	} else {

		filterThis(map)

	}
	
}
