Skip to content

Status Transformers

Matej Sychra edited this page Mar 22, 2018 · 16 revisions

This is internal feature in development. Similarly to AWS Lambda, it's a cloud-based JavaScript function, executed when your device updates its status attribute.

As a user, you manage such functions and attach them to your device.

Prerequisite: Your function must be declared as transformer(status, device) in order to be called. Function must return processed first parameter status and optionaly can use device parameter with provided device details.

Description

Status transformer is a JavaScript function, that takes at least one parameter (status as string), and is expected to process it and return a human-readable value. Optionally, the function can receive a limited device descriptor, that should help you identifying the device using UDID or Alias. In case the transformer will not return anything, status will not be transformed, but the function will executed. And one more thing... keep in mind, that you can also perform URL requests as well as a webhook to integrate any other service outside THiNX.

Status Transformers are not expected to be executed inside thinx-device-api. There will be separate execution environment dedicated using Docker, or possibly using AWS Lambda Integration.

1. Minimal Example

// Minimal `no-op` Transformer (always start with comment!)

var transformer = function(status, device) {
    return status;
};

2. SimpleCell Example

This shows one of SimpleCell device events converted to string.

// SimpleCell
var transformer = function(status, device) {
  if (status == "52") {
    return "Device moved.";
  } else {
    return status;
  }
};

3. Battery Example

Device

IoT device is sending its battery voltage. The value is prefixed by 0xba (battery) and encoded to a float value. Keeping in mind, that the device has an ARM CPU while the cloud is running on x86 machine, there's a need to change endianness of the incoming value.

See the device code at https://github.com/suculent/thinx-example-battery-uart-wifi

Status Transformer Code

//
// Sample tag & byte/float Transformer
// Expected value is 0xbaXXXXXXXX where XXXXXXXX are 4 bytes float battery voltage
// (You may decide on your own tag length or structure, this is just an example.)
//

var transformer = function(status, device) {

    // Check the prefix for 'ba'
    const tag = ststr.substr(0, 2); // start, length
    var icon = "";
    if (tag == "ba") {
      const hex_voltage = ststr.substr(2, ststr.length - 2);
      const voltage = hexToFloat(flipHexString("0x" + hex_voltage, 8));
      if (voltage < 3.0) {
        icon = "#e"; // show red error sign when battery is empty
      } else if (voltage > 3.0 && voltage < 3.4) {
        icon = "#w"; // show yellow warning sign when battery is bellow specified level
      }
      return "Battery " + voltage + " V" + icon;
    } else {
      return status; // in case return value is undefined, original status will be displayed
    }

    //
    // Convenience functions
    //

    // Converts between endians
    var flipHexString = function(hexValue, hexDigits) {
      var h = hexValue.substr(0, 2);
      for (var i = 0; i < hexDigits; ++i) {
        h += hexValue.substr(2 + (hexDigits - 1 - i) * 2, 2);
      }
      return h;
    };

    // Converts hexadecimal value to float
    var hexToFloat = function(hex) {
      var s = hex >> 31 ? -1 : 1;
      var e = (hex >> 23) & 0xFF;
      return s * (hex & 0x7fffff | 0x800000) * 1.0 / Math.pow(2, 23) * Math.pow(2, (e - 127));
    };
};

4. Slack Webhook Example

This example does not transform status in any way, returns input value as it, while it splits the data flow and forwards each message with a webhook to Slack. And yes, you can easily fry it, if you'll shell your MQTT queue too much.

//
// THiNX2Slack Webhook Status Transformer
//

var transformer = function(status, device) {

	var https = require("https");
	var slack_path = "/services/T6V3DBVUN/B9V7F7XAT/MvvMI72kBi1S5bqcFXYZ1Ad2"

	// Takes non-transformer `status` as a body of the message.
	// Transform/format its value here, if required.

	var message = (typeof(status) === "undefined") ? "Hello, World!" : status;

	var options = {
		strictSSL: false,
		rejectUnauthorized: false, // TODO: remove this dirty hack! needs out public cert chain
		hostname: 'hooks.slack.com',
		port: 443,
		path: slack_path,
		method: 'POST',
		headers: {
			'Accept': 'application/json',
			'Content-type': 'application/json'
		}
	};

	var body = {
		username: device.alias,
		text: message
	}

	var number = parseFloat(message);
	var color = "good";

	if (number > 3.4) {
		color = "good";
	} else if (3.4 > number > 3.0) {
		color = "warning";
	} else if (3.0 > number) {
		color = "danger";
	}

	body.attachments = [
        {
            author_name: device.alias,
            author_icon: "https://rtm.thinx.cloud/assets/thinx/img/ioticons/b_small_" + device.icon + ".png",
            fallback: device.status,
            pretext: "_Your_ device just posted new status.",
            footer: "This message has been sent by THiNX Status Transformer.",
            image_url: "https://rtm.thinx.cloud/assets/thinx/img/ioticons/b_large_" + device.icon + ".png",
            thumb_url: "https://rtm.thinx.cloud/assets/thinx/img/ioticons/w_medium_" + device.icon + ".png",
            mrkdwn_in: ["pretext"],
            color: color,
            fields: [{
                        title: device.status,
                        short: false
                    }]
            }
        ];


	if ((typeof(device) !== "undefined") && device !== null) {
		body.fields = device;
	}

	var req = https.request(options, (res) => {
		var chunks = [];
		if (typeof(res) === "undefined") {
			console.log("SlackHook: No response.");
		}
		res.on('data', function(chunk) {
			chunks.push(chunk);
		}).on('end', function() {
			console.log("Slackhook Response: " + Buffer.concat(chunks).toString());
		});
	});

	req.on('error', (e) => {
		console.error("SlackHook Error: "+e);
	});
	req.write(JSON.stringify(body));
	req.end();

	return status;
}

5. Data Storage and Visualisation

This example saves data directly into optional InfluxDB/Grafana docker container. Contact THiNX administrators for database and access while the database creation and access to logs is not implemented.

//
// THiNX2InfluxDB Status Transformer
//

var transformer = function(status, device) {

  const owner = device.owner;
  const udid = device.udid;
  const alias = device.alias;

  // Transform status value
  var battery = parseFloat(status.replace("Battery ", "").replace("V", ""));

  // Make async POST to InfluxDB (should be Status Transformer API method)
  function influx(data, completion) {
    var influx_result;
    var http = require('http');
    var options = {
      host: 'thinx.cloud',
      port: '8086',
      path: '/write?db='+owner,
      method: 'POST'
    };
    var req = http.request(options, function(response) {
      var body = '';
      response.on('data', function (chunk) {
        body += chunk;
      });
      response.on('error', function (error) {
        completion(false, error);
      });
      response.on('end', function () {
        completion(true, body);
      });
    });
    req.write(data);
    req.end();
  }

  // Prepare async POST to InfluxDB
  var alias_no_spaces = alias.replace(" ", "_");
  var data = 'battery,udid='+udid+',alias='+alias_no_spaces+' value='+battery;
  influx(data, function(success, result) {
    /* Status Transformers SHOULD support callback completion to return error status here. */
    /* Meanwhile, results can be parsed at least into audit log. */
    console.log("[OID:"+owner+"] [STLOG] "+result);
  });

  return battery; // return processed status (purely float value)
}