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

Any chance you can point me at exatxtly what I should purchase? Are all node MCU created equal?

What sort of wire leads should I purchase? Anything else that may be needed?

@cwwilson08 These are the ones I use

Bread board.

FTDI USB UART

Jumper Pin assortment M-M / M-F / F-F (The breadboard comes with some but these are better...

Ty sir. All ordered.

1 Like

@cwwilson08 make ABSOLUTLY sure that the slider on the UART is at 3.3v If you hook it up at 5v whatever you have conected is a gonner.

@JulesT I got a new bulb in today. Got it flashed and Can get White Red and Blue. No Green...

I have tried putting PWM3 everywhere else but no go. When I flashed the bulb it was plashing green so I know the color works..... Any thoughts?

Update. I have RGBW mapped out now

Gosund RGB Bulb A19

D2 GPIO4 - PWM1 (37) White
D6 GPIO12 - PWM2 (38) Red
D7 GPIO13 - PWM4 (40) Blue
D5 GPIO14 - PWM3 (39) Green

Looks like each bulb is different :confused:

What are the requirements of your device handler?

Yeah. Looking at the the Tuya dev thing... they've basically got an app builder - you tell it what things are on what GPIO pins, and it configures the app accordingly. Seems a bit arse-about-face, but what I do know?; )

The requirements are pretty non-existent, to be honest - As long as you can go to the console and get color and 'white ' to work, then install the device handler... and point it at the IP address of the bulb. That should do the trick.

The good thing about it, basically, is that it's up to the tasmota firmware to know how to make the bulb do it's thing :wink:

Hopefully that'll work. If not... let me know what it's up to, and I'll try to fix it up.

-- Jules

@JulesT Link to app builder?

Hi Damon.

No app... just a device handler. No discovery, either - so you'll have to add them by hand and then configure them.

Install the following DH:

Then add device... select the driver... and configure it. Set the DNI to something that'll end up being unique - tasmota_attic_lamp_3 or whatever :wink:

-- Jules

@JulesT It does absolutely nothing for me? I installed the handler. Added the device and configured the device. Is there something I have to do in the DH?

I have configured the ip in the device

This is the log
dev:3692019-01-26 01:23:54.302 am errororg.apache.http.conn.HttpHostConnectException: Connect to null:80 [null/127.0.0.1] failed: Connection refused (Connection refused) on line 282 (on)

dev:3692019-01-26 01:23:54.267 am debughttp://null:80/ax?c1=Power%20On

dev:3692019-01-26 01:23:54.241 am debugon()

dev:3692019-01-26 01:23:53.538 am errororg.apache.http.conn.HttpHostConnectException: Connect to null:80 [null/127.0.0.1] failed: Connection refused (Connection refused) on line 282 (off)

dev:3692019-01-26 01:23:53.392 am debughttp://null:80/ax?c1=Power%20Off

dev:3692019-01-26 01:23:53.366 am debugoff()

Did you enter your IP in the driver?

OK. It's not got the IP address for some reason.

Let me create a new device at my end, and check if my code was buggered up :wink:

-- Jules

OK. It's definately not your fault, Damon.

I moved some bits around to make it less hacky, and part of that was butchering out some more sophisticated preferences code that was unnecessary. Apparently my much simpler replacement is a load of arse :wink:

Gimme 2 mins, and I'll get a fix bodged in.

-- Jules

Hi Damon.

OK. I'm an idiot. 'ip' and 'ipAddress' aren't the same variable - who'd have thought it?

My fault - because I was bodging the driver about - the IP address was set right in the state of the device I was working on anyway.

I've re-uploaded it - fetch the changed driver from the same place, it should magically start working. If it doesn't, re-set the IP Address, and all should be well.

Sorry about that, and thanks for your patience.

-- Jules

2 Likes

Everything should be here today. I've done some reading and think I'm ready. The socket I have seems to be no way of opening without destroying it. So I am putting that A-side for now.

Hopefully can ressurect the strip and get a driver going. Then flash the other.

1 Like

@JulesT Thank you!!

ok I now have it controlling the bulb. Here is the issues:

Set Color hue:97 sat:100 level:89 Bulb goes white instead of red
Set Color hue:27 sat:97 level:89 Bulb goes "yellowish white" instead of green
Set Color hue:62 sat:100 level:100 Bulb goes "blueish white" instead of blue

So Set Color isn't working for me

Setting color Temp turns my light to green no matter the setting

Set Level Does Nothing

Set Hue 30 is RED
Set Hue 50 is Pink
Set Hue 60 is Blue

So, I'm very grateful for the control of these bulbs at least I can turn them on and off again. Thank you!
Hopefully the color settings can be fixed too :slight_smile:

Hmmm. looks like you've got the settings wrong, then.

If you go to the console on the tasmota web interface, and type 'white 100' - what colour do you get?
Similarly, if you do 'rgbcolor' commands on the web console, do the following do what you expect:

rgbcolor ff0000 - should be red.
rgbcolor 00ff00 - should be green.
rgbcolor 0000ff - should be blue.

If those aren't right, then you've got the GPIO settings wrong.

Tasmota assumes the following, I think, from playing with it:

PWM1 - Red
PWM2 - Green
PWM3 - Blue
PWM4 - White

I'd start with - do a 'white 100' - and whatever color it is - you should set PWM4 to the GPIO pin on your current setup that corresponds to the color above. Repeat for other colours until it all makes sense.

Of course... if the console does the right thing, and the driver is wrong, then it's my fault, and I'll have to work out a way to look at it :wink:

-- Jules

@JulesT Ok so I did have my pins configured wrong. You have given the BEST explanation that I have seen on this subject. Thank you.

Now only one more issue to fix.... I cant Dim. Other than that it's ready to go.

Adjusted the code to fix dimming.

/**
 *  Copyright 2016 Eric Maycock & Jules Taplin
 *
 *  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.
 *
 *  Sonoff Wifi Switch
 *
 *  Author: Eric Maycock (erocm123)
 *  Date: 2016-06-02
 */

import groovy.json.JsonSlurper
import groovy.util.XmlSlurper

metadata {
	definition (name: "Tasmota RGBW Bulb", namespace: "uk.org.ourhouse", author: "Jules Taplin") {
        //capability "Actuator"
		capability "Switch"
		//capability "Refresh"
		capability "ColorControl"
		capability "ColorMode"
		capability "ColorTemperature"
		capability "Light"
		capability "Switch"
		capability "SwitchLevel"
		//capability "Sensor"
        //capability "Configuration"
        //capability "Health Check"

        //command "reboot"

        attribute   "needUpdate", "string"
        attribute   "uptime", "string"
        attribute   "ip", "string"
		attribute	"level", "number" //added by damondins
	}

	simulator {
	}
    /*
    preferences {
        input description: "Once you change values on this page, the corner of the \"configuration\" icon will change orange until all configuration parameters are updated.", title: "Settings", displayDuringSetup: false, type: "paragraph", element: "paragraph"
		generate_preferences(configuration_model())
	}
*/
	preferences {		section("Sonoff Host") {
            input(name: "ipAddress", type: "string", title: "IP Address", displayDuringSetup: true, required: true)
	}}
	
	//tileAttribute ("device.level", key: "SLIDER_CONTROL") {
    //    attributeState "level", action:"setLevel"
    //}
/*
 	tiles (scale: 2){
		multiAttributeTile(name:"switch", type: "generic", width: 6, height: 4, canChangeIcon: true){
			tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
				attributeState "on", label:'${name}', action:"switch.off", backgroundColor:"#00a0dc", icon: "st.switches.switch.on", nextState:"turningOff"
				attributeState "off", label:'${name}', action:"switch.on", backgroundColor:"#ffffff", icon: "st.switches.switch.off", nextState:"turningOn"
				attributeState "turningOn", label:'${name}', action:"switch.off", backgroundColor:"#00a0dc", icon: "st.switches.switch.off", nextState:"turningOff"
				attributeState "turningOff", label:'${name}', action:"switch.on", backgroundColor:"#ffffff", icon: "st.switches.switch.on", nextState:"turningOn"
			}
        }

		standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
			state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
		}
        valueTile("ip", "ip", width: 2, height: 1) {
    		state "ip", label:'IP Address\r\n${currentValue}'
		}
        valueTile("uptime", "uptime", width: 2, height: 1) {
    		state "uptime", label:'Uptime ${currentValue}'
		}

    }
*/

	main(["switch"])
	details(["switch",
             "refresh","configure","reboot",
             "ip", "uptime"])
}

def installed() {
	log.debug "installed()"
	configure()
}

def configure() {
    logging("configure()", 1)
    def cmds = []
    cmds = update_needed_settings()
    cmds << getAction("/info")
    if (cmds != []) cmds
}

def updated()
{
    logging("updated()", 1)
    def cmds = []
    cmds = update_needed_settings()
    sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "lan", hubHardwareId: device.hub.hardwareID])
    sendEvent(name:"needUpdate", value: device.currentValue("needUpdate"), displayed:false, isStateChange: true)
    if (cmds != []) cmds
}

private def logging(message, level) {
    if (logLevel != "0"){
    switch (logLevel) {
       case "1":
          if (level > 1)
             log.debug "$message"
       break
       case "99":
          log.debug "$message"
       break
    }
    }
}

def parse(description) {
    def events = []
    def msg = parseLanMessage(description)
    def descMap = parseDescriptionAsMap(description)
    def body

    if (descMap["mac"] != null && (!state.mac || state.mac != descMap["mac"])) {
		log.debug "Mac address of device found ${descMap["mac"]}"
        state.mac = descMap["mac"]
	}

    if (state.mac != null && state.dni != state.mac) state.dni = setDeviceNetworkId(state.mac)
    if (descMap["body"]) body = new String(descMap["body"].decodeBase64())

    if (body && body != "") {

    if(body.startsWith("{") || body.startsWith("[")) {
    def slurper = new JsonSlurper()
    def result = slurper.parseText(body)

    log.debug "result: ${result}"

    if (result.containsKey("type")) {
        if (result.type == "configuration")
            events << update_current_properties(result)
    }
    if (result.containsKey("power")) {
        events << createEvent(name: "switch", value: result.power)
    }
    if (result.containsKey("uptime")) {
        events << createEvent(name: "uptime", value: result.uptime, displayed: false)
    }
    if (result.containsKey("deviceType")) {
        state.type = result.deviceType
    }
    } else {
        //log.debug "Response is not JSON: $body"
    }
    }

    if (!device.currentValue("ip") || (device.currentValue("ip") != getDataValue("ip"))) events << createEvent(name: 'ip', value: getDataValue("ip"))

    return events
}

def parseDescriptionAsMap(description) {
	description.split(",").inject([:]) { map, param ->
		def nameAndValue = param.split(":")

        if (nameAndValue.length == 2) map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
        else map += [(nameAndValue[0].trim()):""]
	}
}

def on() {
	log.debug "on()"
    def cmds = []
	sendEvent(name: "switch", value: "on");
    cmds << getAction("Power%20On")
    return cmds
}

def off() {
    log.debug "off()"
	def cmds = []
	sendEvent(name: "switch", value: "off");
    cmds << getAction("Power%20Off")
    return cmds
}

def refresh() {
	log.debug "refresh()"
    def cmds = []
    cmds << getAction("info")
    cmds << getAction("status")
    return cmds
}

def ping() {
    log.debug "ping()"
    refresh()
}


def setColorTemperature(value)
{
	state.colorMode = "CT"
	sendEvent(name: "colorMode", value: "CT");
	setLevel(state.level,0)
}

//added by damondins
def setLevel(value) {
	sendEvent(name: "level", value: value);
	cmds << getAction("Dimmer%20" + value);
}

def setColor(value) {
	log.debug "HSVColor = "+value
	   if (value instanceof Map) {
        def h = value.containsKey("hue") ? value.hue : null
        def s = value.containsKey("saturation") ? value.saturation : null
        def b = value.containsKey("level") ? value.level : null
    	setHsb(h, s, b)
    } else {
        log.warn "Invalid argument for setColor: ${value}"
    }
}

def setHsb(h,s,b)
{
	log.debug("setHsb - ${h},${s},${b}")
	myh = h*4
	if( myh > 360 ) { myh = 360 }
	hsbcmd = "hsbcolor+${myh},${s},${b}"
	log.debug "Cmd = ${hsbcmd}"
	state.hue = h
	state.saturation = s
	state.level = b
	state.colorMode = "RGB"
	sendEvent(name: "hue", value: h);
    sendEvent(name: "saturation", value: s);
	sendEvent(name: "level", value: b);
	sendEvent(name: "colorMode", value: "RGB");
	getAction(hsbcmd)

}

def setHue(h)
{
    setHsb(h,state.saturation,state.level)
}

def setLevel(v,duration)
{
	if(state.colorMode == "RGB") {
		setHsb(state.hue,state.saturation,v)
	}
	else
	{
		state.level = v
		sendEvent(name: "level", value: v);
		getAction("white+${v}")
	}
}

def setSaturation(s)
{
	setHsb(state.hue,s,state.level)
}


private getAction(uri){
  updateDNI()
  def userpass
  def response
  if(password != null && password != "")
      userpass = encodeCredentials("admin", password)

  def headers = getHeader(userpass)

  def params = [
    uri: "http://${getHostAddress()}/ax?c1=${uri}",
    headers: headers
  ]
	log.debug "http://${getHostAddress()}/ax?c1=${uri}"
  httpGet(params) { resp ->
    response = resp.data
  }

  return parseResponse(response)
}

private postAction(uri, data){
  updateDNI()
  def userpass
  def response
  if(password != null && password != "")
      userpass = encodeCredentials("admin", password)

  def headers = getHeader(userpass)

  def params = [
    uri: "http://${getHostAddress()}${uri}",
    headers: headers
  ]

  httpPost(params) { resp ->
    response = resp.data
  }

  parseResponse(response)
}

private setDeviceNetworkId(ip, port = null){
    def myDNI
    if (port == null) {
        myDNI = ip
    } else {
  	    def iphex = convertIPtoHex(ip)
  	    def porthex = convertPortToHex(port)
        myDNI = "$iphex:$porthex"
    }
    log.debug "Device Network Id set to ${myDNI}"
    return myDNI
}

private updateDNI() {
    if (state.dni != null && state.dni != "" && device.deviceNetworkId != state.dni) {
       device.deviceNetworkId = state.dni
    }
}

private getHostAddress() {
    if (override == "true" && ipAddress != null && ipAddress != ""){
        return "${ipAddress}:80"
    }
    else if(getDeviceDataByName("ipAddress") && getDeviceDataByName("port")){
        return "${getDeviceDataByName("ipAddress")}:${getDeviceDataByName("port")}"
    }else{
	    return "${ipAddress}:80"
    }
}

private String convertIPtoHex(ipAddress) {
    String hex = ipAddress.tokenize( '.' ).collect {  String.format( '%02x', it.toInteger() ) }.join()
    return hex
}

private String convertPortToHex(port) {
	String hexport = port.toString().format( '%04x', port.toInteger() )
    return hexport
}

private encodeCredentials(username, password){
	def userpassascii = "${username}:${password}"
    def userpass = "Basic " + userpassascii.bytes.encodeBase64().toString()
    return userpass
}

private getHeader(userpass = null){
    def headers = [:]
    headers.put("Host", getHostAddress())
    headers.put("Content-Type", "application/x-www-form-urlencoded")
    if (userpass != null)
       headers.put("Authorization", userpass)
    return headers
}

def reboot() {
	log.debug "reboot()"
    def uri = "/reboot"
    getAction(uri)
}

def sync(ip, port) {
    def existingIp = getDataValue("ip")
    def existingPort = getDataValue("port")
    if (ip && ip != existingIp) {
        updateDataValue("ip", ip)
        sendEvent(name: 'ip', value: ip)
    }
    if (port && port != existingPort) {
        updateDataValue("port", port)
    }
}

def generate_preferences(configuration_model)
{
    def configuration = new XmlSlurper().parseText(configuration_model)

    configuration.Value.each
    {
        if(it.@hidden != "true" && it.@disabled != "true"){
        switch(it.@type)
        {
            case ["number"]:
                input "${it.@index}", "number",
                    title:"${it.@label}\n" + "${it.Help}",
                    range: "${it.@min}..${it.@max}",
                    defaultValue: "${it.@value}",
                    displayDuringSetup: "${it.@displayDuringSetup}"
            break
            case "list":
                def items = []
                it.Item.each { items << ["${it.@value}":"${it.@label}"] }
                input "${it.@index}", "enum",
                    title:"${it.@label}\n" + "${it.Help}",
                    defaultValue: "${it.@value}",
                    displayDuringSetup: "${it.@displayDuringSetup}",
                    options: items
            break
            case ["password"]:
                input "${it.@index}", "password",
                    title:"${it.@label}\n" + "${it.Help}",
                    displayDuringSetup: "${it.@displayDuringSetup}"
            break
            case "decimal":
               input "${it.@index}", "decimal",
                    title:"${it.@label}\n" + "${it.Help}",
                    range: "${it.@min}..${it.@max}",
                    defaultValue: "${it.@value}",
                    displayDuringSetup: "${it.@displayDuringSetup}"
            break
            case "boolean":
               input "${it.@index}", "boolean",
                    title:"${it.@label}\n" + "${it.Help}",
                    defaultValue: "${it.@value}",
                    displayDuringSetup: "${it.@displayDuringSetup}"
            break
        }
        }
    }
}

 /*  Code has elements from other community source @CyrilPeponnet (Z-Wave Parameter Sync). */

def update_current_properties(cmd)
{
    def currentProperties = state.currentProperties ?: [:]
    currentProperties."${cmd.name}" = cmd.value

    if (settings."${cmd.name}" != null)
    {
        if (settings."${cmd.name}".toString() == cmd.value)
        {
            sendEvent(name:"needUpdate", value:"NO", displayed:false, isStateChange: true)
        }
        else
        {
            sendEvent(name:"needUpdate", value:"YES", displayed:false, isStateChange: true)
        }
    }
    state.currentProperties = currentProperties
}


def update_needed_settings()
{
    def cmds = []
    def currentProperties = state.currentProperties ?: [:]

    def configuration = new XmlSlurper().parseText(configuration_model())
    def isUpdateNeeded = "NO"

    cmds << getAction("/configSet?name=haip&value=${device.hub.getDataValue("localIP")}")
    cmds << getAction("/configSet?name=haport&value=${device.hub.getDataValue("localSrvPortTCP")}")

    configuration.Value.each
    {
        if ("${it.@setting_type}" == "lan" && it.@disabled != "true"){
            if (currentProperties."${it.@index}" == null)
            {
                if (it.@setonly == "true"){
                    logging("Setting ${it.@index} will be updated to ${it.@value}", 2)
                    cmds << getAction("/configSet?name=${it.@index}&value=${it.@value}")
                } else {
                    if (it.@index == "externaltype") {
                        if(state.type != "Sonoff S20") {
                            isUpdateNeeded = "YES"
                            logging("Current value of setting ${it.@index} is unknown", 2)
                            cmds << getAction("/configGet?name=${it.@index}")
                        } else {
                            log.debug "Sonoff S20 does not support externatype configuration"
                        }
                    } else {
                        isUpdateNeeded = "YES"
                        logging("Current value of setting ${it.@index} is unknown", 2)
                        cmds << getAction("/configGet?name=${it.@index}")
                    }
                }
            }
            else if ((settings."${it.@index}" != null || it.@hidden == "true") && currentProperties."${it.@index}" != (settings."${it.@index}"? settings."${it.@index}".toString() : "${it.@value}"))
            {
                isUpdateNeeded = "YES"
                logging("Setting ${it.@index} will be updated to ${settings."${it.@index}"}", 2)
                cmds << getAction("/configSet?name=${it.@index}&value=${settings."${it.@index}"}")
            }
        }
    }

    sendEvent(name:"needUpdate", value: isUpdateNeeded, displayed:false, isStateChange: true)
    return cmds
}

def parseResponse(description) {
	//log.debug "Parsing: ${description}"
	return 				// DISABLED FOR NOW!
    def events = []
    def result = description
    log.debug "result: ${result}"
    if (result != []){
    if (result.containsKey("type")) {
        if (result.type == "configuration")
            events << update_current_properties(result)
    }
    if (result.containsKey("power")) {
        events << createEvent(name: "switch", value: result.power)
    }
    if (result.containsKey("uptime")) {
        events << createEvent(name: "uptime", value: result.uptime, displayed: false)
    }
    if (result.containsKey("deviceType")) {
        state.type = result.deviceType
    }
    }


    if (!device.currentValue("ip") || (device.currentValue("ip") != getDataValue("ip"))) events << createEvent(name: 'ip', value: getDataValue("ip"))
    events.each {
        sendEvent(it)
    }
    return events
}

def configuration_model()
{
'''
<configuration>
<Value type="text" index="ip" label="IP Address" setting_type="preference"></Value>
<Value type="list" index="logLevel" label="Debug Logging Level?" value="0" setting_type="preference" fw="">
<Help>
</Help>
    <Item label="None" value="0" />
    <Item label="Reports" value="1" />
    <Item label="All" value="99" />
</Value>
</configuration>
'''
}`Preformatted text`

Well guys. I got close. But in the end fail. I managed to flash my strip. Connected to it once over IP. Undid all my soldering plugged into mains and nothing. Looks like I mangled original connections

Lol I think I have to tap out.

Oh... what a bastard. Sorry to hear that. :frowning:

Ah well. Your dedication to trying can't be faulted!

-- Jules