Tuya Switch Control (Jinvoo, Smart Life)

If you are trying to add the command directly to the command string then it gets a bit more complicated since you need to scape everything since there is an outer set of quotes that need escaping. The spaces don’t matter. I don’t have any devices that require dps settings and just added support at the request of others. So I only really tested it as a command line tool and haven’t ever tested against a rule before. The rules are a bit a of a mess but I have was working on getting cloud support and considering just making a proper binding. The previous control script was very unreliable so lots of the crap in the rules was aimed at that, but I should update them with nice clean rules.

I would recommend that you pass the argument to the rule, it helps having to escape everything. So if you pass -set '{"dps":1, "set":true}' to item testTuya it should work fine.

rule "Test dps"
when
    Item tesTuya received command
then
    var setCommand="node /etc/openhab2/scripts/node_modules/njstuya -ip 10.xx -id xx -key xx " + receivedCommand.toString
    var resp = executeCommandLine(setCommand, 5000)
    if(db) { logInfo("Tuya", "New broadcast {} resp {}", setCommand, resp)  }
end

I would also suggest you pass the item ip, key, id as well or have it as some kind of variable than fixed into separate rules.

But if you want to test it directly or hardcoded then if you escape everything any of the following should work.
As a variable

    var setCommand = "'{\"dps\":1,\"set\":true}'"
    var resp = executeCommandLine("node /etc/openhab2/scripts/node_modules/njstuya -ip 192.168.xx -id xx -key xx -set " + setCommand, 5000)

Or hardcoded

    executeCommandLine("node /etc/openhab2/scripts/node_modules/njstuya -ip 192.168.xx -id xx -key xx -set " + '\'{"dps": 1, "set":true}\'', 5000)
    executeCommandLine("node /etc/openhab2/scripts/node_modules/njstuya -ip 192.168.xx -id xx -key xx -set '{\"dps\": 1, \"set\":true}'", 5000)

Edit: I’ve redone the items and rules.
Items

//Tuya items and thermo
//  Complete ip/id/key

// Tuya Group Devices 
Group gTuyaActions "Tuya Action to run"
Group:Switch gTuyaStates "Tuya devices"
// Tuya devices
Switch LivingRoomHeater	"Heater state"  (gTuyaStates, gSwitch)  [ "Switchable" ]
Switch BedroomHeater	"Heater state"  (gTuyaStates, gSwitch)  [ "Switchable" ]
Number KitchenThermostatCurrentTemp "Kitchen Thermostat Current Temperature" (gKitchenThermostat)  //[ "CurrentTemperature" ]
Number KitchenThermostatTargetTemp "Kitchen Thermostat Target Temperature" (gKitchenThermostat) {autoupdate="true"}//[ "TargetTemperature" ]
// Send current time to refresh to be able to query when previous refresh was
String TuyaRefresh "Refresh"

// Tuya Devices with sensistive data edit before sharing. 
String LivingRoomHeaterCommand "-ip 10.x.x.x -id xx -key xx" (gTuyaActions)
String BedroomHeaterCommand "-ip 10.x.x.x -id xx -key xx" (gTuyaActions)

Rules

var _DEBUG = false
var db = false
var Timer statusTimer = null
var Timer errorTimer = null

rule "Execute Tuya Action"
when
    Member of gTuyaActions received command
then
    if(db) logInfo("TUYA", "Group trigger {} command", triggeringItem, receivedCommand)

    var setCommand="node /etc/openhab2/scripts/node_modules/njstuya " + triggeringItem.label + " " + receivedCommand.toString
    var tuyaDevice = triggeringItem.name.replace('Command','')
    
    // Runs command vis njstuya
    var resp = executeCommandLine(setCommand, 5000)
    
    if(resp === null)   resp = "Error null"
    if(resp.contains("Error") || resp.contains("Warning") || resp.length < 2){
        logInfo("Tuya", "Error in response [{}] ", resp)
        postUpdate(tuyaDevice, "UNDEF")
        
        // refresh on Error only once after good state. 
        if (errorTimer === null) {
            errorTimer = createTimer(now.plusMillis(1500),  [ | 
                triggeringItem.sendCommand("STATUS")
                errorTimer.cancel()
                errorTimer = null
            ])
        }
    }
    else {
        if(db) logInfo("Tuya", "Updating state [{}] ", resp)   
        postUpdate(tuyaDevice, resp)
        // Reset errorTimer to null if state is good
        if (errorTimer !== null) {
            errorTimer.cancel()
            errorTimer = null
        } 
    }
    if(db) { logInfo("Tuya", "New broadcast {} resp {}", setCommand, resp)  }
end

rule "Tuya Refresh"
when
    Item TuyaRefresh received command
    or
    Time cron "13 0/2 * * * ?"
then
    gTuyaActions.allMembers.forEach[ device | sendCommand(device, "status")]
end


rule "Update Thermostat Value"
when
    Item HueKitchenTemp changed
then
    if(db) { logInfo("Thermostat", "update kitchen temp {}", HueKitchenTemp)}
    KitchenThermostatCurrentTemp.postUpdate(Float::parseFloat(String::format("%s",HueKitchenTemp.state).replace(' ','')))
end

rule "Control Temp"
when
    Item KitchenThermostatCurrentTemp changed
    or 
    Item KitchenThermostatTargetTemp changed
then
    logInfo("Thermostat", "Set {} change temperature from {} to {}", LivingRoomHeater ,KitchenThermostatCurrentTemp.state, KitchenThermostatTargetTemp.state)
    var isCold =  (KitchenThermostatCurrentTemp.state as Number) < (KitchenThermostatTargetTemp.state as Number)
    var setCommand = if(isCold) 'ON' else 'OFF'


    //When using slider you can get lots of changes, this should prevent lots of commands in a row
    if(!LivingRoomHeater.state.toString.contains(setCommand) && statusTimer === null) {
        if(db) { logInfo("Thermostat", " logic {} state {} cmd{}", LivingRoomHeater.state.toString.contains(setCommand), LivingRoomHeater.state.toString, setCommand)}
        if(db) { logInfo("Thermostat", " is cold {} set command {}", isCold, setCommand)}
        LivingRoomHeaterCommand.sendCommand(setCommand)


        // Update state
        if (statusTimer !== null) {
            statusTimer.cancel()
            statusTimer = null
        } 

        statusTimer = createTimer(now.plusMillis(1500),  [ | 
            LivingRoomHeaterCommand.sendCommand("STATUS")
            statusTimer.cancel()
            statusTimer = null
        ])
    } else if (statusTimer !== null) {
        statusTimer.cancel()
        statusTimer = null
    } 

end