[DEPRECATED]TuyaHubitat - (jinvoo, smart life, tuya smart - switches only)

I had this thought as well, going to give it a shot. I was also thinking of making it user defined - so it could be whatever the user defined. I have been thinking about this, I have to figure out how to report back on the status of all the switches to make the status work right. Another case of lacking the core knowledge I suppose.

So I think I caught what you meant and came up with this. I also think I convoluted it somewhere along the line, it however is working as expected.

import groovy.json.JsonSlurper
/*
TuyAPI SmartPlug Device Handler

Derived from
	TP-Link HS Series Device Handler
	Copyright 2017 Dave Gutheinz
Original smartthings work and node server created by Ben Lawson


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.

Supported models and functions:  This device supports smart plugs that use the Tuya Smart Life app

Update History
05-23-2018 - Updated to control all powerstrip plugs from one device. Fixed status response - should now be accurate. Node script update with changes by Dave Gutheinz.
05-22-2018 - Updated to make on off commands work and make compatible with Hubitat
01-04-2018	- Initial release
*/
metadata {
	definition (name: "TuyAPI Smart Plug", namespace: "cwwilson08", author: "Chris Wilson") {
		capability "Refresh"
		capability "Actuator"
       // command "togglePower", ["NUMBER"]
       // attribute "Plug_1", "string"
       // attribute "Plug_2", "string"
       // attribute "Plug_3", "string"
      //  attribute "Plug_4", "string"
      //  attribute "Plug_5", "string"
     //   attribute "Plug_6", "string"
        
        command "USB"
        attribute "USB", "5"
        command "Plug_1"
        attribute "Plug_1", "1"
        command "Plug_2"
        attribute "Plug_2", "2"
        command "Plug_3"
        attribute "Plug_3", "3"
        command "Plug_4"
        attribute "Plug_4", "4"
        attribute "numberOfButtons", "number"
	}
}
preferences {
	input(name: "deviceIP", type: "text", title: "Device IP", required: true, displayDuringSetup: true)
	input(name: "gatewayIP", type: "text", title: "Gateway IP", required: true, displayDuringSetup: true)
	input(name: "deviceID", type: "text", title: "Device ID", required: true, displayDuringSetup: true)
	input(name: "localKey", type: "text", title: "Local Key", required: true, displayDuringSetup: true)
   // input(name: "dps", type: "text", title: "dps", required: true, displayDuringSetup: true)
}

def installed() {
	updated()
    sendEvent(name: "numberOfButtons", value: 30)
}

def updated() {
	unschedule()
	runEvery15Minutes(refresh)
	runIn(2, refresh)
}
//	----- BASIC PLUG COMMANDS ------------------------------------
def togglePower(deviceNo) {
    deviceNo = deviceNo.toString()
//	deleted the cmds on and off.  Replace with this.
//	assume that states are on or off.  Offline will try to turn on.
//	added deviceNo.  This will pass through the system to the response method and allow proper parsing.
    def plugState = device.currentValue("Plug_${deviceNo}")
    if (plugState == "on") {
		sendCmdtoServer("off", deviceNo, "deviceCommand", "onOffResponse")
    } else {
		sendCmdtoServer("on", deviceNo, "deviceCommand", "onOffResponse")
    }
}

def Plug_1(deviceNo) {
    deviceNo = "Plug_1"
//	deleted the cmds on and off.  Replace with this.
//	assume that states are on or off.  Offline will try to turn on.
//	added deviceNo.  This will pass through the system to the response method and allow proper parsing.
    def plugState = device.currentValue(deviceNo)
    log.debug "${plugState} LINE 75"
    if (plugState == "on") {
		sendCmdtoServer("off", deviceNo, "deviceCommand", "onOffResponse")
    } else {
		sendCmdtoServer("on", deviceNo, "deviceCommand", "onOffResponse")
    }
}
def Plug_2(deviceNo) {
    deviceNo = "Plug_2"
//	deleted the cmds on and off.  Replace with this.
//	assume that states are on or off.  Offline will try to turn on.
//	added deviceNo.  This will pass through the system to the response method and allow proper parsing.
    def plugState = device.currentValue(deviceNo)
    log.debug "${plugState} LINE 75"
    if (plugState == "on") {
		sendCmdtoServer("off", deviceNo, "deviceCommand", "onOffResponse")
    } else {
		sendCmdtoServer("on", deviceNo, "deviceCommand", "onOffResponse")
    }
}
def Plug_3(deviceNo) {
    deviceNo = "Plug_3"
//	deleted the cmds on and off.  Replace with this.
//	assume that states are on or off.  Offline will try to turn on.
//	added deviceNo.  This will pass through the system to the response method and allow proper parsing.
    def plugState = device.currentValue(deviceNo)
    log.debug "${plugState} LINE 75"
    if (plugState == "on") {
		sendCmdtoServer("off", deviceNo, "deviceCommand", "onOffResponse")
    } else {
		sendCmdtoServer("on", deviceNo, "deviceCommand", "onOffResponse")
    }
}
def Plug_4(deviceNo) {
    deviceNo = "Plug_4"
//	deleted the cmds on and off.  Replace with this.
//	assume that states are on or off.  Offline will try to turn on.
//	added deviceNo.  This will pass through the system to the response method and allow proper parsing.
    def plugState = device.currentValue(deviceNo)
    log.debug "${plugState} LINE 75"
    if (plugState == "on") {
		sendCmdtoServer("off", deviceNo, "deviceCommand", "onOffResponse")
    } else {
		sendCmdtoServer("on", deviceNo, "deviceCommand", "onOffResponse")
    }
}
def USB(deviceNo) {
    deviceNo = "USB"
//	deleted the cmds on and off.  Replace with this.
//	assume that states are on or off.  Offline will try to turn on.
//	added deviceNo.  This will pass through the system to the response method and allow proper parsing.
    def plugState = device.currentValue(deviceNo)
    log.debug "${plugState} LINE 75"
    if (plugState == "on") {
		sendCmdtoServer("off", deviceNo, "deviceCommand", "onOffResponse")
    } else {
		sendCmdtoServer("on", deviceNo, "deviceCommand", "onOffResponse")
    }
}

def onOffResponse(response, deviceNo){
    log.debug "line 147 ${deviceNo}"
//    deviceNo = deviceNo.toString()
//	Changed switch to plug_$(deviceNo}.  device no comes from the original toggle (carried
//	all commands and responses.
	if (cmdResponse == "TcpTimeout") {
		log.error "$device.name $device.label: Communications Error"
        sendEvent(name: "{deviceNo}", value: "offline", descriptionText: "ERROR - OffLine - mod onOffResponse")
	
	} else {

		log.info "${device.name} ${device.label}: Power: ${response}"
		sendEvent(name: "${deviceNo}", value: response)
	}
}
//	----- REFRESH ------------------------------------------------
def refresh(){
//	Will do all six switches on refresh, one second apart.
    def rfrsh = []
	rfrsh << sendCmdtoServer("status", "USB", "deviceCommand", "refreshResponse")
	rfrsh << pauseExecution(500)
	rfrsh << sendCmdtoServer("status", "Plug_1", "deviceCommand", "refreshResponse")
	rfrsh << pauseExecution(500)
	rfrsh << sendCmdtoServer("status", "Plug_2", "deviceCommand", "refreshResponse")
	rfrsh << pauseExecution(500)
	rfrsh << sendCmdtoServer("status", "Plug_3", "deviceCommand", "refreshResponse")
    rfrsh << pauseExecution(500)
    rfrsh << sendCmdtoServer("status", "Plug_4", "deviceCommand", "refreshResponse")
    rfrsh << pauseExecution(500)
   // rfrsh << sendCmdtoServer("status", "6", "deviceCommand", "refreshResponse")
    rfrsh
}
def refreshResponse(cmdResponse, deviceNo){
    log.debug "cmdresponse ${cmdResponse}"
    def status
	if (cmdResponse == "TcpTimeout") {
		log.error "$device.name $device.label: Communications Error"
		sendEvent(name: "Plug_${deviceNo}", value: "offline", descriptionText: "ERROR - OffLine - mod onOffResponse")
	} else {
        if (cmdResponse == true){
            status = "on"
        }else{
            status = "off"
		}
        
		log.info "${device.name} ${device.label}: Power: ${status}"
		sendEvent(name: "${deviceNo}", value: status)
	}
}
//	----- SEND COMMAND DATA TO THE SERVER -------------------------------------

private sendCmdtoServer(command, deviceNo, hubCommand, action){
//	added deviceNo to items put in header.
//	Will add to the node.js script to extract then add to response.
   // def deviceIP
   // def deviceID
    def dps 
    switch(deviceNo) {
        case "Plug_1":
    		deviceIP = deviceIP
    		deviceID = deviceID
            dps = 1
        	break
        case "Plug_2":
    		deviceIP = deviceIP
    		deviceID = deviceID
            dps = 2
        	break
        case "Plug_3":
    		deviceIP = deviceIP
    		deviceID = deviceID
            dps = 3
        	break
        case "Plug_4":
    		deviceIP = deviceIP
    		deviceID = deviceID
            dps = 4
        	break
         case "USB":
    		deviceIP = deviceIP
    		deviceID = deviceID
            dps = 5
        log.debug "${deviceNo} line 228 devicno"
        	break
         case "6":
    		deviceIP = deviceIP
    		deviceID = deviceID
            dps = deviceNo
        	break
        default:
            break
    }
	def headers = [:] 
	headers.put("HOST", "$gatewayIP:8083")	//	SET TO VALUE IN JAVA SCRIPT PKG.
	headers.put("tuyapi-ip", deviceIP)
	headers.put("tuyapi-devid", deviceID)
	headers.put("tuyapi-localkey", localKey)
	headers.put("tuyapi-command", command)
    headers.put("action", action)
	headers.put("command", hubCommand)
    headers.put("deviceNo", deviceNo)
    headers.put("dps", dps)
   // log.debug "${hubCommand}"
	def hubCmd = new hubitat.device.HubAction([
        method: "GET",
		headers: headers]
    
	)
    log.debug "${hubCmd}"
    hubCmd
    
}

def parse(response) {
//	extract also deviceNo from header to pass to parse methods.
	def resp = parseLanMessage(response)
	def action = resp.headers["action"]
    def deviceNo = resp.headers["deviceNo"]
	def jsonSlurper = new JsonSlurper()
	def cmdResponse = jsonSlurper.parseText(resp.headers["cmd-response"])
    def onoff = resp.headers["onoff"]
log.error onoff
log.error deviceNo
log.error cmdResponse
    
    if (cmdResponse == "TcpTimeout") {
		log.error "$device.name $device.label: Communications Error"
		sendEvent(name: "switch", value: "offline", descriptionText: "ERROR at hubResponseParse TCP Timeout")
		sendEvent(name: "deviceError", value: "TCP Timeout in Hub")
	} else {
        log.debug "line 175 ${action} and ${cmdResponse}"
		actionDirector(action, cmdResponse, onoff, deviceNo)
		sendEvent(name: "deviceError", value: "OK")
	}
}
def actionDirector(action, cmdResponse, onoff, deviceNo) {
	switch(action) {
		case "onOffResponse":
        onOffResponse(onoff, deviceNo)
			break

		case "refreshResponse":
        log.debug "line 188 ${cmdResponse}"
			refreshResponse(cmdResponse, deviceNo)
			break

		default:
			log.debug "at default"
	}
}

The preferred method to expose multiple switches from a single device is to use a Composite Device Driver model. The main Driver is considered the Parent device, and it can create individual Child devices, each one with its own "Capability Switch". Then, each child can be independently controlled from Rule Machine, or any other Application.

@ogiewon

Hi yes my reading and looking at driver code has been hinting to me that I need to figure this part of it out. I suppose I will start looking into this as well.

The more I think about this and consider the wide range of devices that can be controlled, I am seeing use cases to have a few different drivers depending on device type. However I suppose figuring out the parent/child device thing is going to be the next step.

Further investigating of the development on the Tuya api shows that have made great progress on figuring out how to register devices with the cloud without the original apps. It may be possible to set this whole thing up to discovery / setup / find the needed id / keys and then create the devices.

Then again it also sounds like lots more reading and studying for me and my "projects" seem to keep piling up and getting left undone these days.

Depending on the integration, it may make sense to use a Service Manager App that creates Child Devices. Just depends on what makes the most sense.

How does the dashboard handle the individual switches for a composite device? Getting to the switches within the dashboard would be a concern.

So I think I have everything working the way I want it to.

The ALLon seems to update appropriately.
I added in a user defined switch for easy control from other apps.
Other switches can be controlled through custom commands in RM.
Everything seems to stay synced up ok. Although keeping that ALLon switch accurate requires a lot of refreshes that wold not be necessary otherwise.

Short of going down the child device route, I can not think of anything else I would like to do with this.

If anyone out there has any of these devices and the patience to get it all setup please test and let me know what you think.

I posted this in a test branch for now. I will update the main branch once I have let this run awhile.

As always many many thanks to @djgutheinz....

1 Like

Each and every child device can be considered a standalone entity. They would be able to be selected in the dashboard like any other device.

1 Like

Hi guys,

Just a quick update.

I figured out how to call a refresh on all the devices at once. This serious reduces the amount of calls being made. Lots of changes made in the code.

I started figuring how to handle the errors in the node server - the code there is still really messy - but I feel like I am making progress, lol I have to figure out exactly what to do once I catch the errors. Thinking still the answer is to setup retries.

Posted in the test branch again.

1 Like

So I apparently overestimated the interest in this... I am not sure anyone else has even tried it.

That being said, I have had this running for a couple of months and it seems to be solid.

I figured I would try to take this to the next level and try the Composite Device Drive model. I however do not know where to begin. I am hoping some kind soul can point me in the right direction.

I believe I would need to modify the driver to have only a single switch. The only difference between the each switch is one value the DPS setting. Having a value of 1-6 - 6 being a call for status of all switches.

That part I think I can manage to get done.

The parent / child thing I do not even know where to begin. If anyone can kick or nudge me in the proper direction to get started I would appreciate it. If not I suppose I will poke at it awhile to see if I can figure it out. Is there a SIMPLE example somewhere I can take a look at?

Great strides have been made in setting up and configuring these devices without use of of the android apps. I hope to eventually maybe, be able to make a smartapp that goes from beginning to end of setup. That however is way down the road.

Hi @cwwilson08

I have 2 switches that uses the Smart Life app which I will in due course try to use your code with & give feedback on. No timeline unfortunately... I am very much a newbie to this, but did manage to set up my own node.js server on a Pi to run some of Dave Gutheinz' TP-Link stuff local (and managed to get it all functional!!) I'll reach out when I start deep diving into this.

Thanks
J

Hey @GatVlieg

I am happy you are willing to gibe this a shot. This is very similar to the tplinl k node.js setup. If you can make that work you can likely get this going too.

Let me know if you need any help.

interested as well. I have some of those Tuya power strips with USB and one in-wall switch and some other regular smart plugs from Tuya.

I have tried it running the node server on a RP3B+ alongside the other node (TP-Link). I had to get an old S3 rooted to find the key/dev ids. However it fails and punts as soon as the first command is sent from Hubitat. Haven't had the time to dig deeper except I might today

something like this:
Command sending to IP: 192.168.0.118 Command: status
/home/pi/node_modules/node-forge/lib/aes.js:203
var len = tmp.length();
^

TypeError: tmp.length is not a function
at forge.aes.Algorithm.initialize (/home/pi/node_modules/node-forge/lib/aes.js:203:19)
at new forge.cipher.BlockCipher (/home/pi/node_modules/node-forge/lib/cipher.js:118:18)
at Object.forge.cipher.createCipher (/home/pi/node_modules/node-forge/lib/cipher.js:42:10)
at new TuyaCipher (/home/pi/node_modules/tuyapi/lib/cipher.js:14:30)
at new TuyaDevice (/home/pi/node_modules/tuyapi/index.js:54:26)
at processDeviceCommand (/home/pi/TuyaHub/TuyAPI_Server_v1.js:87:13)
at Server.onRequest (/home/pi/TuyaHub/TuyAPI_Server_v1.js:53:4)
at Server.emit (events.js:182:13)
at parserOnIncoming (_http_server.js:657:12)
at HTTPParser.parserOnHeadersComplete (_http_common.js:109:17)

Did you npm install the codetheweb/tuyapi?

They have made leaps and bounds since I made this.

I set this up again the other day so I am sure we can still make it work. I simply replaced the contents of the node server with what is in my forked repo.

Use the driver in the test branch. I currently only have the one power strip with 4 switches and the USB group.

Single switch devices may need a different driver. I'd be willing to make something if we can get you going.

Let me try and update and install the codetheweb/tuyapi again. I have two strips of the same you got (4 switches with USB group) so hope to get those working first.

Hi man... good to see you spreading your coding wings!!!

I just picked up 6 of these plugs for a bargain price and would love to try your code. Just need to carve out some time. It will be a good use of my Hubitat hub since I moved all my lights back to ST.

1 Like

It is too bad you've been having so much trouble on this side ken.

What sort of plugs did you get? Are they single outlets?

It has been awhile since I worked on it. The guys who mad this a pi have taken it quite a bit further. I believe you can do everything from discovery and registration right from your pi and never use any of the tuya apps. I have not played with that yet.

They also have made changes to the a pi which do not work with my driver.
I was able to get this going again though with the files in the test branch of my repo.

I have a feeling we may have issue with a single plug device. I ordered one this morning. Hopefully I can make a driver specific for it.

I appreciate the support. Hit me up when you go to give it a shot.

Yea - my troubles with Hubitat all stem from erratic behavior of my GE Zwave switches. Everything else is pretty good.

On the Tuya stuff I bought 6 single plugs. They were dirt cheap and work great using the SmartLife app. IFTTT is my default path to linking them into Hubitat or ST but your code will be much better.

1 Like

So my single plug arrived today. I just confirmed that with the code as is I am able to control the device and get status. It should not be much trouble to scale the back to have it be for a single outlet. On my days off I will see if I can make that happen.

If and when you do go to install use the files in my test branch, until I get everything cleaned up and presentable. I will try and post clear instructions to install as well. As I am a using a version that is FAR behind the master repo we need to make sure it is installed.

I'm suffering from the Outlet rebooting issue. When sending some commands, it performs the command requested and then reboots (I guess) and returns to the previous state. So if it's Off and you try to power on, it will sometimes (50 50 chance) turn on then right back off. I bought about 8 of these during a good sale and it happens on all of them.

I also have an Outdoor Outlet that this happens to.

I am not sure I have ever come across this. Are you using my driver and node server when this happens?