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


#1

Hi all, I just wanted to take a minute to share something I have been working on. Lots of research and poking has led me to a local control solution for these wifi devices. At this time support is for outlets only, however seems it is possible to expand and include most products controlled with this api.

Install can be a bit technical but not overly challenging.
UPDATE - 5/23/18 - With Dave's guidance, got all plugs working from one hubitat device. Refresh now refreshes all devices at once and should be accurate.

UPDATE - 5/23/18 - Modified one line of the codetheweb/tuyapi index.js file to allow for proper status reporting when refresh is called on multiple plug devices. I do no think this modified version is needed if your device has a single plug.

This requires:

  1. A node.js server (running modified version of the tuyapi server linked below if using multiplug device) running on same network as your devices.

  2. Your devices to already be setup using the official app (official app can be deleted once install is completed).

  3. Have the required device id and key for your key. (there are many ways to do this noted in the codetheweb/tuyapi repo. I used a rooted android device and it was painless).

  4. The TuyaHubitat device handler installed.

I did not create anything new. I just took all the pieces and got them working together.
Credits:
Blawson327: Original work on smartthings driver and node script.
codetheweb: All the work RE the tuya api to make this work.
@djgutheinz: Much of this code derived from his work on the Tp-Link interface.

There is still work to be done. The node script throws and unhandledpromiserejection from time to time. I have no idea how to catch this error properly. If anyone has any thoughts / knows how to correct I would appreciate any guidance.

Currently I can one device per switch - In my case I have the 4 plug surge protector with usb ports as well. I need to figure out how to get all the buttons in one device handler - Any help with this would be appreciated as well.

I pushed my minimal knowledge to the max to make this work, so I put this out for the community to look at / help with.


#2

OK. I made a quick cut. See

This provides for two commands

TogglePower (deviceNo) - Enter device number and it will toggle the power state.

Refresh - Will refresh the state of all the plugs.

Design is for four switches. To be able to use in dashboard, a PushableSwitch (5 each) would be required. 1 - 4 would TogglePower. 5 would Refresh.


#3

@djgutheinz

Hey thank you again for your assistance. This did not work out of the box. After staring at it a little while I saw what was going on and got it going.

Which led me to the next problem of the code always reporting back only status of device 1. LOL after lots more staring I ran down the problem in one line of api code. I have finally sorted this out.

Next step is to get the switching / reporting all working via your examples.
Then to figure out how to handle some of the errors the node server throws - The device seems to reject requests from time to time. I think that retries may be the solution.

At that point i think this will be pretty darn functional.


#4

OP updated.

Refresh now all calls all devices in a row.
Device control for multiple plugs from one hubitat device.


#5

So I am trying to figure out how to make this all controllable through RM. How can I get multiple on/off switches in the device? I am able to add tiles that work through the hubitat interface, I just can't seem to access them from RM. I am likely missing something simple.

Anybody out there who can guide me in the right direction?


#6

I am not familiar with the Rule Machine. I do know that you can only have one switch under capability switch per device driver, and the only switch commands are on() and off() (without parameters).

Homework on Rule Machine
a. Does the Rule Machine use capabilities to identify potential devices? (I believe it does).
b. Will it accept Capability Refresh?
c. Once connected to a device, can the rule machine read any exposed attribute? (i.e., the state for each of your plugs)?
d. Once connected to a device, can the rule machine execute any exposed command? (i.e., togglePower(1), etc).

If these are all yes, then you have a path:
Look for devices with Capability Refresh
Select your power strip
get the state attributes for the individual plugs (or specific plug of interest)
send the togglePower(n) command to turn on/off.


#7

Of course, you could have four instances of your device driver, using Switch and commands on / off. Each could then act independently in the rule machine. That is back where you started, but it does what you want!


#8

Lol yeah these are the same thoughts i was having. Thanks for the confirmtion.

Upon your advice i looked a little closer at RM. I was able to create a custom command that activated the actuator capability.

Moving forward just Need to figure out how to properly update Allon (switch6) switch. It always reports back 0. If all other switches 1-5 are on it should be on. Otherwise it should be off. It controls all the switches on the device at once. Shouldn't be too hard i hope lol.a

And then make switch 5 report back as USB Instead of plug 5.

Then to clean up the node script a little.


#9

A thought. If you go for the single driver, the AllOn would be the "Capability Switch".

You can also use attributes to identify the individual switches. i.e.,

attribute "Waffle Iron" for toggle 1
attribute "Microwave" for toggle 2
attribute "USB" for toggle 5

I do this in my wifi speaker driver for presets. Then I set the value to the title of the preset station. It then displays (on the main page for the device):

Preset_1: News Radio 1080 KRLD
Preset_2: 60's Folk Radio
Preset_3: All My Music

As I change preset definitions, the title changes automatically by updating the attribute.


#10

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"
	}
}

#11

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.


#12

@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.


#13

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.


#14

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


#15

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....


#16

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.


#17

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.


#18

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.


#19

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


#20

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.