OH3 use JavaScript code and run POST api call

Hi all,

what is the best way of using JavaScript code in preparation of a POST Api call to an external cloud api?

I’d like to create an element on the UI that will call an API to control to call power relay. As I can’t add it natively to OH I’d like to use an existing Api.

For the call I required some calculations in JavaScript for parameter in the call. Can I include the JavaScript code somewhere?

Thanks.

There are far too little details here so all I can suggest is to use the HTTP binding to make the calls and you can use a JavaScript Transformation to do the calculations after the data is pulled.

I would like to control Milfra Smart Module TB41 via https://openapi.tuyaeu.com

First I need to get an access token:
4 header parameters required:

  • client_id
    *sign
    *timestamp
    *sign_method

two are dynamic and within postman those functions are used.

(function () {
var timestamp = getTime();
pm.environment.set(“timestamp”,timestamp);
var clientId = pm.environment.get(“client_id”);
var secret = pm.environment.get(“secret”);
var sign = calcSign(clientId,secret,timestamp);
pm.environment.set(‘easy_sign’, sign);
})();

function getTime(){
var timestamp = new Date().getTime();
return timestamp;
}

function calcSign(clientId,secret,timestamp){
var str = clientId + timestamp;
var hash = CryptoJS.HmacSHA256(str, secret);
var hashInBase64 = hash.toString();
var signUp = hashInBase64.toUpperCase();
return signUp;
}

The body of the command send call is quite simple:

{
“commands”:[
{
“code”: “switch_1”,
“value”:false
}
]
}

I want to combine both and use a OH3 UI button.

That’s my idea and want to check how this is feasible within OH3.

Are you aware that you can connect to Tuya devices over MQTT? It’s a bit of a chore, but lots of folks here have done it.

By the same token, if you get this working and it’s much simpler than MQTT, I think a lot of people would be interested in a tutorial. I know I would be. :wink:

1 Like

This must involve an Item. UI interaction always involves sending commands to an Item.
Assuming you just want a "Go!"button, this will probably be convenient as a switch.

Then you can set up a rule that listens for a command to that Item, and acts on it.
The rule can build a URL with parameters or headers or whatever you need, and make an HTTP call. Then it can do whatever with the results,maybe update an Item.
You can write rules in javascript though this is not as popular in OH as other rule languages.

Thanks for the hint. I’ve tried flashing with Tasmota, but firmware on the device is too new.
I’ve seen the article, but for me there a too many point of failures and really custom.

Will keep that in mind. Once I’ve made progress I’ll update here.

Regards

Yes that was my idea having a simple button like a switch.

The approach sounds interesting. I’ll dig deeper to it.

Have not used the HTTP binding so far, it is additional or just making it more complicated?
Regards.

I hear you. I bought a ceiling lamp that can’t be flashed with Tuya-convert. I took a shot at flashing via the serial port, but it didn’t work and I’m not sure I want to bother trying again.

Since I only have the one device (my other three Tuyas all flashed easily), I’ve been playing around with using IFTTT webhooks to send commands to it. However, it’s a one-way command and I haven’t thought that it to completion.

Jep same to me. My Teckin power-outlets got flashed easily. This one is left. I’ll try the serial way this or next week. Friend of mine has deeper knowledge in flashing. Don’t believe it will be successful, but it’s worth to give it a try and update here if this an option.

Your requirement is complicated, with a number of fiddly dynamic bits.
That probably won’t be convenient to use with HTTP binding thing and channel.
You can use the HTTP binding Action in a rule that builds the fiddly bits.

1 Like

I’m currently doing some research and proof of concept.

Currently I’m struggling to add headers to the http call.

First observation it seems not to be possible to use the import command in scripts like:

import org.eclipse.xtext.xbase.lib.Functions

Error:

2021-03-29 23:57:19.395 [ERROR] [internal.handler.ScriptActionHandler] - Script execution of rule with UID 'c56a75f85a' failed: <eval>:1:0 Expected an operand but found import
import org.eclipse.xtext.xbase.lib.Functions;
^ in <eval> at line number 1 at column number 0

I’ve tried to use sendHttpGetRequest and HttpUtil.executeUrl. With second I succeeded in an http call. But didn’t find a way to use headers:

var logger = Java.type('org.slf4j.LoggerFactory').getLogger('org.openhab.rule.' + ctx.ruleUID);
var newHashMap = Java.type("java.util.HashMap");
var dateT = new Date();
var headers = new newHashMap();
var HttpUtil = Java.type("org.openhab.core.io.net.http.HttpUtil");

var Url = "https://openapi.tuyaeu.com/v1.0/token?grant_type=1"
//Get timestamp
var timeStamp=dateT.getTime();
logger.info("dateT "+t.toString());

//Define Headers
headers.put("client_id","12345678");
headers.put("t",timeStamp);
logger.info("headers"+headers);
//Get HTTP Call
var output = HttpUtil.executeUrl("GET", Url, headers,  1000);

logger.info("HTTP response:"+output);

Error:

2021-03-30 00:16:13.417 [INFO ] [org.openhab.rule.c56a75f85a         ] - Timestamp: 1617056173382
2021-03-30 00:16:13.441 [INFO ] [org.openhab.rule.c56a75f85a         ] - headers{t=1.617056173382E12, client_id=12345678}
2021-03-30 00:16:13.460 [ERROR] [internal.handler.ScriptActionHandler] - Script execution of rule with UID 'c56a75f85a' failed: TypeError: Can not invoke method [jdk.dynalink.beans.OverloadedDynamicMethod
 String org.openhab.core.io.net.http.HttpUtil.executeUrl(String,String,InputStream,String,int)
 String org.openhab.core.io.net.http.HttpUtil.executeUrl(String,String,int)
 String org.openhab.core.io.net.http.HttpUtil.executeUrl(String,String,Properties,InputStream,String,int,String,Integer,String,String,String)
 String org.openhab.core.io.net.http.HttpUtil.executeUrl(String,String,Properties,InputStream,String,int)
] with the passed arguments; they do not match any of its method signatures. in <eval> at line number 29

Any suggestion on the header topic?

Regards

That seems to be the case with Rules DSL Script Actions. I have a theory for why which I don’t need to go into (unless you are curious).

You can work around it by using the fully qualified class name.

Furthermore, you don’t need to import Functions or Procedures. To create a Timer it’s as simple as:

val myFunction = [ Type arg, Type arg2 |
    // code goes here
]

Where Type gets replaced with the type of the argument. If there is no meaningful return value a Procedure will be created. Otherwise if the last line to execute in the code returns anything other than void a Function will be returned.

So you probably don’t need this import anyway.

In other cases where you do need an import, instead you’ll need use the full class name when you use it. Instead of import java.util.Map and then lower down in the code you can just use Map you’d remove the import and everywhere you use Map you’d have to use java.util.Map instead.

This is only for Rules DSL.

JavaScript doesn’t have an import key word. Instead, for Nashorn JavaScript to “import” Java classes you use Java.type, just as you are doing in the code you posted.

One further comment, org.eclipse.xtext.xbase.lib imports have no business being in a JavaScript, Python or any other script condition or script action language. Those are specific to and only applicable to Rules DSL. They won’t work in other languages.

As for your second error. Why use HttpUtil? Use the sendHttpXRequest Actions. See http://www.openhab.org/docs/configuration/actions.html#http-actions

The error is because there is no executeUrl method of HttpUtil that takes those four arguments. See http://www.openhab.org/javadoc/latest/org/openhab/core/io/net/http/httputil. But there is a sendHttpGetRequest Action that takes those four arguments.

1 Like

@rlkoshak thanks for the valuable input.
With that I could achieve a valid api call, but the “sign” is invalid. The base64 encoded HMAC is not correctly generated and I’m not able to figure out why now.

With the given example the generated hash should have the result:
78eec2264c0633e67222c231cd956aedd835405b6e44d3a4961ea17d2b015dd2

But the Hmac and SecretKeySpec code generates a hash:
EO7CJKWGM+ZYISIXZZVQ7DG1QFTURNOKLH6HFSSBXDI=

I’ve checked with several HMAC generators that my hash is incorrect, and I’ve no idea why. Any suggestions?

The output of below script when I run it is:

2021-04-03 00:09:19.614 [INFO ] [org.openhab.rule.f6df09624c         ] - Timestamp: 1617395205009
2021-04-03 00:09:19.617 [INFO ] [org.openhab.rule.f6df09624c         ] - str: 12345ucwaf51nm8qbvwu1617395205009
2021-04-03 00:09:19.641 [INFO ] [org.openhab.rule.f6df09624c         ] - sha256_HMAC: javax.crypto.Mac@c772bd
2021-04-03 00:09:19.667 [INFO ] [org.openhab.rule.f6df09624c         ] - secret_key javax.crypto.spec.SecretKeySpec@fa775bf3
2021-04-03 00:09:19.691 [INFO ] [org.openhab.rule.f6df09624c         ] - sha256_HMAC: javax.crypto.Mac@c772bd
2021-04-03 00:09:19.819 [INFO ] [org.openhab.rule.f6df09624c         ] - sign: EO7CJKWGM+ZYISIXZZVQ7DG1QFTURNOKLH6HFSSBXDI=
2021-04-03 00:09:19.821 [INFO ] [org.openhab.rule.f6df09624c         ] - hash2test: [B@1864f0f
2021-04-03 00:09:19.855 [INFO ] [org.openhab.rule.f6df09624c         ] - headers: {sign_Method=HMAC-SHA256, t=1617395205009, sign=EO7CJKWGM+ZYISIXZZVQ7DG1QFTURNOKLH6HFSSBXDI=, client_id=12345ucwaf51nm8qbvwu}
2021-04-03 00:09:19.864 [INFO ] [org.openhab.rule.f6df09624c         ] - Headertest: {sign_Method=HMAC-SHA256, t=1617395205009, sign=EO7CJKWGM+ZYISIXZZVQ7DG1QFTURNOKLH6HFSSBXDI=, client_id=12345ucwaf51nm8qbvwu}
2021-04-03 00:09:19.855 [INFO ] [org.openhab.rule.f6df09624c         ] - HTTP response:{"code":1004,"msg":"sign invalid","success":false,"t":1617395205009}

This is script for testing purpose. Real ids and secreted have been replaced. But the HMAC hash issue is persistent in this code as well.

//Test Purpose: Secrets and client id replaced by dummies
var logger = Java.type('org.slf4j.LoggerFactory').getLogger('org.openhab.rule.' + ctx.ruleUID);
var newHashMap = Java.type("java.util.HashMap"); //Hashmap for header entries
var testProperty = Java.type("java.util.Properties"); //alternate way of defining header
var Mac = Java.type("javax.crypto.Mac");
var SecretKeySpec = Java.type ("javax.crypto.spec.SecretKeySpec");
var hashing = Java.type("java.util.Base64.Encoder");
var dateT = new Date();  // required for getting timestamp
var HttpUtil = Java.type("org.openhab.core.io.net.http.HttpUtil");  //required for executing HTTP call
var vclientID = "12345ucwaf51nm8qbvwu"; //Client ID for API call
var secret = "5364d0751b3d123457f2ced5e9738d24"; //Secret for creating hash based message authentication code HMAC
var signMethod = "HMAC-SHA256";  //Sign Method for API call
var Url = "https://openapi.tuyaeu.com/v1.0/token?grant_type=1"  //Tuya API URL

//Get timestamp
//var timeStamp=dateT.getTime();  corret setup
var timeStamp="1617395205009"; //testing
logger.info("Timestamp: "+timeStamp.toString());


//calc sign
//var str = vclientID + timeStamp.toString(); correct setup
var str = "12345ucwaf51nm8qbvwu1617395205009";
logger.info("str: "+str);
var sha256_HMAC = Mac.getInstance("HmacSHA256");
logger.info("sha256_HMAC: "+sha256_HMAC.toString());
var secret_key = new SecretKeySpec(secret.getBytes("UTF-8"),"HmacSHA256");
logger.info("secret_key "+secret_key.toString());
sha256_HMAC.init(secret_key);
logger.info("sha256_HMAC: "+sha256_HMAC);
var hash = java.util.Base64.encoder.encodeToString(sha256_HMAC.doFinal(str.getBytes("UTF-8"))).toString();
var hash2test = sha256_HMAC.doFinal(str.getBytes("UTF-8"));
hash = hash.toUpperCase();

logger.info("sign: "+hash);
logger.info("hash2test: "+hash2test);

//hash can be generated online for validation
//https://www.freeformatter.com/hmac-generator.html#ad-output
//https://codebeautify.org/hmac-generator?__cf_chl_captcha_tk__=f408d7ac52b42aaa5f151788489582ba85aa08c9-1617394342-0-AbRrY2T5r9t2goLd-DTR1_D8mJ2rGslNCyyC0SZ0g_5vdCyjUJo7gbwsIjZHtweoD5GRswehvl36PZvIb9EWgdNO01H9o-ZMzVCBxW7miKs8PYP2epnjk8O-EBjncARMkn_r9PZ3yp7TV12H9mDuhtFRmfTWP_sxC2ucBnanCUyMtahkwbTAwLNWpIVA1YcmiqL0mt63sTk_osgrwIDiYxV-VQHV8JChZEhRL5iqsSdBydu3885PymeFBP1iP6LdIIAtvy-ak0sqrcR77v-7jZY11u8SzfVVtNxVpI_RQlUIqzrd95cKnwITHnZ5ixXAgKTnouMLiEiJGYSz65Zfo6lwxaCfdAn8QT4onnyVAFM7nSrEqdLMuUgPoG58UsY3xHa9g6Xq3AyhPn1tDaegB9y3JOWHMcWilgg5QoNp2L5nPIvfTrwkZ1lDNgnulFdcbrZGBifVXtvFmMoIIU6juE0NEGJHNDY4r2x2BYRKz1flSHZ1efwwbmR0IUcMZZM3dntzVLcqJb-zLgriyD-LhJ_tyfAjMS6MX9E486WIWpUcL6vX-jsILmSDMH_eGETCacHUbEF6NQ90QK2sEwXjZ5IgKmDIVIyCWalneyBgyb9g


//Define Headers Option1 via Hashmap

var headers = new newHashMap();
headers.put("client_id",vclientID);
headers.put("t",timeStamp.toString());
headers.put("sign_Method", signMethod);
headers.put("sign", hash);
logger.info("headers: "+headers);

//Define Headers Option2 via Proterty
var headerTest = new testProperty();
headerTest.put("client_id",vclientID);
headerTest.put("t",timeStamp);
headerTest.put("sign_Method", signMethod);
headerTest.put("sign", hash);
logger.info("Headertest: "+headerTest);

var output = HttpUtil.executeUrl("GET", Url, headerTest,null,null, 1000);

logger.info("HTTP response:"+output);

Regards,
becksen

I don’t know anything about HMAC so can’t comment on what is wrong. The code looks ok on a quick scan.

Hi everybody,

I’m presenting my solution. It works and serves the desired purpose. It can definitely be improved but it contains all required to build your own solution in OH3.

I’m open to any corrections or improvements. If someone knows a better and more performant HMAC library you’re highly welcome to advise.

Find my solution attached.
Control tuya devices via oh3v1.0.pdf (962.5 KB)

this.OPENHAB_CONF = (this.OPENHAB_CONF === undefined) ? java.lang.System.getProperty("openhab.conf") : this.OPENHAB_CONF;
load(OPENHAB_CONF + "/libraries/rollups/hmac-sha256.js");
//load("/etc/openhab/libraries/rollups/hmac-sha256.js");

var logger = Java.type('org.slf4j.LoggerFactory').getLogger('org.openhab.rule.' + ctx.ruleUID);
var newHashMap = Java.type("java.util.HashMap"); //Hashmap for header entries
var testProperty = Java.type("java.util.Properties"); //alternate way of defining header
var Mac = Java.type("javax.crypto.Mac");
var SecretKeySpec = Java.type ("javax.crypto.spec.SecretKeySpec");
var hashing = Java.type("java.util.Base64.Encoder");
var dateT = new Date();  // required for getting timestamp
var HttpUtil = Java.type("org.openhab.core.io.net.http.HttpUtil");  //required for executing HTTP call
var vclientID = "<client id>" //Client ID for API call
var secret = "<secret>";  //Secret for creating hash based message authentication code HMAC
var signMethod = "HMAC-SHA256";  //Sign Method for API call
var Url = "https://openapi.tuyaeu.com/v1.0/token?grant_type=1";  //Tuya API URL
var deviceID = "<device id>";
var UrlPost = "https://openapi.tuyaeu.com/v1.0/devices/"+deviceID+"/commands"
//var easyAccessToken = Java.type("org.openhab.core.transform.actions.Transformation"); //JSON Transofrmation HTTP response
//https://community.openhab.org/t/oh3-how-to-use-map-transform-in-ecma-rules/111908/7


//Get timestamp
var timeStamp=dateT.getTime();  //corret setup
//var timeStamp="1617225789665"; //testing
logger.info("Timestamp: "+timeStamp.toString());


//calc sign
var str = vclientID + timeStamp.toString(); //correct setup
//var str = "12345ucwaf51nm8qbvwu1617395205009";
logger.info("str: "+str);
/*var sha256_HMAC = Mac.getInstance("HmacSHA256");
logger.info("sha256_HMAC: "+sha256_HMAC.toString());
var secret_key = new SecretKeySpec(secret.getBytes("UTF-8"),"HmacSHA256");
logger.info("secret_key "+secret_key.toString());
sha256_HMAC.init(secret_key);

logger.info("sha256_HMAC: "+sha256_HMAC);
var hash = java.util.Base64.encoder.encodeToString(sha256_HMAC.doFinal(str.getBytes("UTF-8"))).toString();
var hash2 = sha256_HMAC.doFinal(str.getBytes("UTF-8")); */
var hash = CryptoJS.HmacSHA256(str, secret)
var hashInt64 = hash.toString();
hashInt64 = hashInt64.toUpperCase();
//var hash = hashing.encode(str.getBytes());
//hash.encode(str.getBytes());
logger.info("sign: "+hashInt64);
//logger.info("hash2 "+hash2);

//hash can be generate online for validation
//https://www.freeformatter.com/hmac-generator.html#ad-output
//https://codebeautify.org/hmac-generator?__cf_chl_captcha_tk__=f408d7ac52b42aaa5f151788489582ba85aa08c9-1617394342-0-AbRrY2T5r9t2goLd-DTR1_D8mJ2rGslNCyyC0SZ0g_5vdCyjUJo7gbwsIjZHtweoD5GRswehvl36PZvIb9EWgdNO01H9o-ZMzVCBxW7miKs8PYP2epnjk8O-EBjncARMkn_r9PZ3yp7TV12H9mDuhtFRmfTWP_sxC2ucBnanCUyMtahkwbTAwLNWpIVA1YcmiqL0mt63sTk_osgrwIDiYxV-VQHV8JChZEhRL5iqsSdBydu3885PymeFBP1iP6LdIIAtvy-ak0sqrcR77v-7jZY11u8SzfVVtNxVpI_RQlUIqzrd95cKnwITHnZ5ixXAgKTnouMLiEiJGYSz65Zfo6lwxaCfdAn8QT4onnyVAFM7nSrEqdLMuUgPoG58UsY3xHa9g6Xq3AyhPn1tDaegB9y3JOWHMcWilgg5QoNp2L5nPIvfTrwkZ1lDNgnulFdcbrZGBifVXtvFmMoIIU6juE0NEGJHNDY4r2x2BYRKz1flSHZ1efwwbmR0IUcMZZM3dntzVLcqJb-zLgriyD-LhJ_tyfAjMS6MX9E486WIWpUcL6vX-jsILmSDMH_eGETCacHUbEF6NQ90QK2sEwXjZ5IgKmDIVIyCWalneyBgyb9g


//Define Headers Option1 via Hashmap
//headers.put("client_id","xp49eucwaf51nm8qbvwu"); Real don't share
var headers = new newHashMap();
headers.put("client_id",vclientID);
headers.put("t",timeStamp.toString());
//headers.put("sign", signUp);
headers.put("sign_Method", signMethod);
headers.put("sign", hashInt64);
logger.info("headers: "+headers);

//Define Headers Option2 via Proterty
var headerTest = new testProperty();
headerTest.put("client_id",vclientID);
headerTest.put("t",timeStamp.toString());
//headerTest.put("sign", signUp);
headerTest.put("sign_Method", signMethod);
headerTest.put("sign", hashInt64);
logger.info("Headertest: "+headerTest);
  //.Base64.encoder.encode(sha256_HMAC.init(secret_key));

    //Base64.encodeBase64String(sha256_HMAC.doFinal(message.getBytes()));

//var hash = CryptoJS.HmacSHA256 (str,secret);
//var hash = new Mac.getInstance();


//var hashInBase64 = hash.toString();
//var signUp = hashInBase64.toUpperCase();

var httpResponse = HttpUtil.executeUrl("GET", Url, headerTest,null,null, 1000);

logger.info("HTTP response:"+httpResponse);

//example output:
// {"result":{"access_token":"f5197771b54c7350b6161b34bed8fdeb","expire_time":5302,"refresh_token":"da8ec9d910b027f73f7adc74020d6cab","uid":"bay16158430523737yAT"},"success":true,"t":1617478354451}

var tempTransform = JSON.parse(httpResponse);
//https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse
var easyAccessToken = (tempTransform.result.access_token); //get correct element string
logger.info("accessToken: "+easyAccessToken);

//re-calculate Sign:
var timeStamp=dateT.getTime();
var str = vclientID+easyAccessToken+timeStamp.toString();
var hash = CryptoJS.HmacSHA256(str, secret)
var hashInt64 = hash.toString();
hashInt64 = hashInt64.toUpperCase();

//headers.put("access_token", easyAccessToken);
headerTest.put("access_token", easyAccessToken);
headerTest.put("t",timeStamp.toString());
//headerTest.put("sign", signUp);
headerTest.put("sign", hashInt64);
logger.info("Headertest: "+headerTest);


//executeUrl (String httpMethod, String url, Properties httpHeaders, InputStream content, String contentType, int timeout)

//value TRUE/FALSE defines switching light on or off
var contentPost = JSON.stringify({commands:[{code: 'switch_1', value: true}]});
logger.info("contentPost "+contentPost);
var input = org.apache.commons.io.IOUtils.toInputStream(contentPost, "UTF-8");
var httpPostResponse = HttpUtil.executeUrl("POST", UrlPost, headerTest,input,"application/JSON",2000);
logger.info("httpPostResponse: "+httpPostResponse);






//https://community.openhab.org/t/icloud-device-data-integration-in-openhab/32329


//x = new Date();
//h = x.getHours();
//logger.info("Current hour: Test Tuya"+h);

//var url = "https://openapi.tuyaeu.com/v1.0/token?grant_type=1";
//var response = sendHttpGetRequest(url);

//var Exec = Java.type("org.openhab.core.model.script.actions.Exec");
//var test = Exec.sendHttpGetRequest("https://google.com");
//logger.info("Test"+test);



//headers.put("Cache-control","no-cache");
//var output =  Exec.sendHttpGetRequest("https://google.com");
//var output = test.sendHttpGetRequest("https://google.com");
//logger.info("HRRP sendHttpGetRequest"+output);


//var timeRightNow = new dtime();



//var output = HttpUtil.executeUrl("GET", "https://google.com", 2000) WORKS