Tuya Cloud platform and homey scripting

Hi everyone,

im new to this community and i wanted to add my Jacuzzi Heater (Invergo App) to Homey. This was i big struggle but i managed to get it fixed with HomeyScript and would like to share this with everyone who might need it too!

So first i created a Cloud Project on the Tuya Developer Platform. (im not going into detail, if you have question for this step just ask me and i will explain it more detailled)

Then i linked my SmartLife app with the Tuya Cloud Project (again not going in detail)

When this is all setup and you made sure that you can control your devices and not only read then we are half way there.

So now i needed to send a command to the Tuya Cloud Project from my Homey script.
here is the code i used and it gets a token that refreshes every 2 hours by the request.

const clientId = 'Your Cloud Project ClientID';
const accessSecret = 'Your Access Secret (DONT SHARE!)';
const deviceId = 'Your Device ID';

// SHA256 and HMAC-SHA256 functions (your originals)
function sha256(ascii) {
  function rightRotate(value, amount) {
    return (value >>> amount) | (value << (32 - amount));
  }
  const maxWord = Math.pow(2, 32);
  let result = '';
  const words = [];
  const asciiBitLength = ascii.length * 8;
  const hash = sha256.h || [];
  const k = sha256.k || [];
  let primeCounter = k.length;
  const isComposite = {};
  for (let candidate = 2; primeCounter < 64; candidate++) {
    if (!isComposite[candidate]) {
      for (let i = 0; i < 313; i += candidate) {
        isComposite[i] = candidate;
      }
      hash[primeCounter] = (Math.pow(candidate, .5) * maxWord) | 0;
      k[primeCounter++] = (Math.pow(candidate, 1/3) * maxWord) | 0;
    }
  }
  ascii += '\x80';
  while (ascii.length % 64 - 56) ascii += '\x00';
  for (let i = 0; i < ascii.length; i++) {
    const j = ascii.charCodeAt(i);
    if (j >> 8) return;
    words[i >> 2] |= j << ((3 - i) % 4) * 8;
  }
  words[words.length] = (asciiBitLength / maxWord) | 0;
  words[words.length] = (asciiBitLength);
  for (let j = 0; j < words.length;) {
    const w = words.slice(j, j += 16);
    const oldHash = hash.slice(0, 8);
    for (let i = 0; i < 64; i++) {
      const w15 = w[i - 15], w2 = w[i - 2];
      const a = hash[0], e = hash[4];
      const temp1 = hash[7] +
        (rightRotate(e, 6) ^ rightRotate(e, 11) ^ rightRotate(e, 25)) +
        ((e & hash[5]) ^ (~e & hash[6])) +
        k[i] +
        (w[i] = (i < 16) ? w[i] : (
          w[i - 16] +
          (rightRotate(w15, 7) ^ rightRotate(w15, 18) ^ (w15 >>> 3)) +
          w[i - 7] +
          (rightRotate(w2, 17) ^ rightRotate(w2, 19) ^ (w2 >>> 10))
        ) | 0
        );
      const temp2 = (rightRotate(a, 2) ^ rightRotate(a, 13) ^ rightRotate(a, 22)) +
        ((a & hash[1]) ^ (a & hash[2]) ^ (hash[1] & hash[2]));
      hash.unshift((temp1 + temp2) | 0);
      hash[4] = (hash[4] + temp1) | 0;
    }
    for (let i = 0; i < 8; i++) {
      hash[i] = (hash[i] + oldHash[i]) | 0;
    }
  }
  for (let i = 0; i < 8; i++) {
    for (let j = 3; j + 1; j--) {
      const b = (hash[i] >> (j * 8)) & 255;
      result += ((b < 16) ? '0' : '') + b.toString(16);
    }
  }
  return result;
}

function hex2bin(hex) {
  const bytes = [];
  for (let i = 0; i < hex.length; i += 2) {
    bytes.push(String.fromCharCode(parseInt(hex.substr(i, 2), 16)));
  }
  return bytes.join('');
}

function hmacSha256(key, message) {
  const blocksize = 64;
  if (key.length > blocksize) {
    key = hex2bin(sha256(key));
  }
  if (key.length < blocksize) {
    key += '\x00'.repeat(blocksize - key.length);
  }
  let o_key_pad = '', i_key_pad = '';
  for (let i = 0; i < blocksize; i++) {
    o_key_pad += String.fromCharCode(key.charCodeAt(i) ^ 0x5c);
    i_key_pad += String.fromCharCode(key.charCodeAt(i) ^ 0x36);
  }
  const innerHash = sha256(i_key_pad + message);
  const finalHash = sha256(o_key_pad + hex2bin(innerHash));
  return finalHash;
}

// --- SAFE HomeyScript Compatible URL PATH Extractor ---
function extractPathAndQuery(fullUrl) {
  const withoutProtocol = fullUrl.replace(/^https?:\/\//, '');
  const pathIndex = withoutProtocol.indexOf('/');
  if (pathIndex === -1) return '/';
  return withoutProtocol.substring(pathIndex);
}

// --- Signature Generator ---
function generateSignature({ clientId, accessToken = '', secret, method, fullUrl, body = '' }) {
  const t = Date.now().toString();
  const pathAndQuery = extractPathAndQuery(fullUrl); // Safe extraction
  const bodyHash = sha256(body || '');
  const message = clientId + accessToken + t + method.toUpperCase() + "\n" + bodyHash + "\n" + "\n" + pathAndQuery;
  const sign = hmacSha256(secret, message).toUpperCase();
  return { sign, t };
}

// --- Get Access Token ---
async function getAccessToken() {
  const fullUrl = 'https://openapi.tuyaeu.com/v1.0/token?grant_type=1';
  const { sign, t } = generateSignature({
    clientId,
    secret: accessSecret,
    method: 'GET',
    fullUrl
  });
  const headers = {
    'client_id': clientId,
    'sign': sign,
    'sign_method': 'HMAC-SHA256',
    't': t
  };
  const response = await fetch(fullUrl, { method: 'GET', headers });
  const data = await response.json();
  console.log('Access Token Response:', data);
  if (!data.success) {
    throw new Error('Failed to get access token: ' + JSON.stringify(data));
  }
  return data.result.access_token;
}

// --- Get Device Status ---
async function getDeviceStatus() {
  const accessToken = await getAccessToken();
  const fullUrl = `https://openapi.tuyaeu.com/v1.0/devices/${deviceId}/status`;
  const { sign, t } = generateSignature({
    clientId,
    accessToken,
    secret: accessSecret,
    method: 'GET',
    fullUrl
  });
  const headers = {
    "sign_method": "HMAC-SHA256",
    "client_id": clientId,
    "access_token": accessToken,
    "t": t,
    "sign": sign,
    "Content-Type": "application/json"
  };
  const response = await fetch(fullUrl, {
    method: 'GET',
    headers
  });
  const result = await response.json();
  console.log('Device Status Result:', result);
  if (!result.success) {
    throw new Error('Failed to get device status: ' + JSON.stringify(result));
  }
  // Find the "switch" status
  const switchStatus = result.result.find(item => item.code === 'switch');
  if (switchStatus) {
    console.log('Jacuzzi Heater is', switchStatus.value ? 'ON' : 'OFF');
    return switchStatus.value; // true or false
  } else {
    console.error('Switch status not found');
    return false;
  }
}

// Main runner
return (async () => {
  try {
    return await getDeviceStatus(); // <== PURE true or false
  } catch (error) {
    return false; // Failsafe: if error, assume false
  }
})();

This wil get the status of the Device. I also made a script to Turn ON and turn OFF the device.
I only use the Homey Script to start a advanced flow, i dont know if its possible to create a virtual device to turn it on and off?

**Extra information
I used the IoT on the Tuya platform to retrieve the possible commands, for instance in the Smart Life App i can set Mode, Temperature but not with the Cloud Platform. The API only lets me Turn the device ON/OFF. So to make sure what the command is, check the IoT platform of Tuya.

If anyone is interested let me know.

4 Likes

Looks like you might have missed the ‘Raw’ options described in the intro to the Tuya Cloup app forum entry - they provide support for device features not supported by the app natively.

Andy

1 Like

I was just about to say the same.
Nothing wrong with some scripting of course, but with TC it’s not an easier way imho.

I figured this out later too, but thank for pointing it out. I just missed a few devices in the Tuya Cloud app, so i dived into it myself.

1 Like

Yea I figured out you didn’t know about it yet. Nonetheless the script is quite impressive :clap::wink:

Thank you! This is exactly what I have been looking for since using the device can get out of sync when using raw commands. For example if homey looses internet connection or is restarted. Also, for some reason my device has a fault object that does not reset with the raw commands (tuya never sends one).

Anyway, back to the code, from what i can see, this script gets the token and the device status in one go. @Bentley040514 is this everthing I need?

Antohag’s post drew my attention, so I decided to play with the script.
I wanted to have the Tuya device ID’s in variables and I wanted to extract values from the returned JSON per advanced flow.

I used Better Logic variables, because you can export these as JSON per app settings; simply add the device name as variable name, and add the device ID as value.
I imported the JSON again by copy/paste (if import doesn’t respond → JSON fault(s))

To extract the JSON value of my choice, I use the formula
$.result[?(@.code == 'device_function')].value

To pass the device id from a variable as argument, I changed the deviceId constance:
const deviceId = args[0];

Off to fiddle with setting a device switch .

I have got everything working, but since the device is not in the Tuya app, I have created a virtual device with the App Device Capabilities.

First I needed to get the device id and the name of the Capabilities, this can be done by dumping out all devices and capabilities with this code and find the device id and the capabilites (this only needs to be done once):

console.log(await Homey.devices.getDevices());

Then i use the device ID:

const device = await Homey.devices.getDevice({ id: 'whatever the ID is' });

To get the value of the Capability i use the name with this code:

device.capabilitiesObj['whatever the name of the capability is'].value);

Then i compare the value recived with the script and update if it is out of sync:

  if ((device.capabilitiesObj['whatever the name of the capability is'].value) !== (result.result[position].value)) {
    await Homey.devices.setCapabilityValue({
    deviceId:'whatever the ID is', 
    capabilityId:'whatever the name of the capability is',
    value: (result.result[position].value)});
    console.log('På/Av oppdatert med:',(result.result[position].value));
  }

Hope this helps if someone is trying the same. Was a lot of trial and error to get the device and set the value of the capabilities.

1 Like

Yeah DC device is somewhat easier with flowcards.
I used a similar approach for handling unsupported devices with flowcards, which can be extended with a virtual device.