Method Implementation

Connio methods are analog to methods in Object Oriented Programming. They provide behaviour to Device and App objects. All methods gets single argument called value that can be any primitive or object value. Object's private, protected and public methods can be called from other methods of the same object using object type specific prefix (ie. this.myMethod(), or this.myMethod()).

Each method is executed within its own context. While a method is executing, it can interact with the rest of the system through its context which is constructed during run-time. The information within the context depends on whether the method is part of a Device or an App. For both cases, the context will contain all properties and methods of the parent object and allow local access to them. The context also contains a set of built-in methods (see below).

In order to maintain a highly performant script execution environment, all script calls are asynchronous in nature. As a result, all internal API calls are promise-based (i.e. they return Promise object).

Device Methods

Device methods can call other user defined and built-in device methods. The following private built-in methods can be called from user defined methods:

MethodDescriptionSignatureReturns
setPropertyWrite given value into device propertysetProperty(<property name>, <data-point>)Promise of property value as data point object
getPropertyGets given property objectgetProperty(<property name>)Promise of Property* object
logWrite into device loglog(<log type>, <message>)Promise of undefined
readDataRead historical datareadData(<property name>, <query object**>)Promise of Query Result object***
findDevicesGet devices by given filter****findDevices(<query object>). Works only for devices inherited from Gateway profile to locate devices that are linked to this gateway.Promise of device id list
executeDeviceMethodExecutes a device method from gateway device method.executeDeviceMethod(<device id>, <method name>, <argument>)Promise of {"result": <value> }
setDevicePropertySet device property from gateway device methodsetDeviceProperty(<device id>, <property name>, <data-point>)Promise of property value as data point object
getDevicePropertyGet device property from gateway device methodgetDeviceProperty(<device id>, <property name>)Promise of property value
{
  "meta": {
    "name": "identifier",
    "dateModified": "2019-10-21T19:35:06.779Z",
    "friendlyName": "identifier",
    "locked": false,
    "accountId": "_acc_741403117096528031",
    "publish": "never",
    "retention": {
      "condition": {
        "when": "always"
      },
      "lifetime": "3months",
      "type": "historical",
      "capacity": 100000
    },
    "id": "_prp_742037026752907103",
    "inherited": false,
    "qualifiedName": "test_device$identifier",
    "dateCreated": "2019-10-21T19:35:06.779Z",
    "ownerId": "_dpf_741453409226596860",
    "type": "string",
    "access": "public"
  },
  "time": "2019-10-21T19:36:04.696Z",
  "value": "FJ-093490-DNNDL"
}
{
  value: <property value>,
  time: <value capture time>
}

** See Query Object format.

resultSet:
    {
        "sampleSize": <number>,
        "results": [
            {
                "ref": {
                    "id": <property-id>,
                    "qname": <property-qualified-name>,
                    "objectId": <object-id>
                },
                "values": [
                    {
                        "t": <iso-time>,
                        "v": <value>
                    }
                  ....
                ],
                "groupBy": [
                    {
                        "GroupByType": {
                            "type": "number",
                            "group": []
                        }
                    }
                ],
                "attributes": {
                    "protocol": [
                        ....
                    ],
                    "source": [
                        ....
                    ]
                }
            }
        ]
    }

****See Device Filters format.

// This method converts the given temperature 
// to Kelvin and stores in `temperature` property.
function setTemperature(value) {
  // convert the value from Fahrenheit to Kelvin
  let kelvin = ((value-32)/1.8)+273.15;
  
  return this.setProperty("temperature", {
    value: kelvin,
    time: new Date().toISOString() // now
  }).then(prop => prop.value);
}
// This method returns `temperature` property value 
// in Fahrenheit. 
function getTemperature(value) {
  return this.getProperty("temperature").then(prop => {
    // convert the value from Kelvin to Fahrenheit
    return ((prop.value-273.15)*1.8)+32
  });
}
// You can create a Puclic method for your devices to log incoming 
// value in device log.
function logMsg(value) {
  // Possible log types are:
  //  error, debug, info, warning
  return this.log('info', value);
}
// Get 10 days average of device temperature
function get10DayAvg(value) {
  let query = {
    "startRelative": {
      "value": 10,
      "unit": "days"
    },
    "aggregators": [
      {
        "name": "avg",
        "sampling": {
        "value": 10,
        "unit": "days"
      }
     }
    ]
  };
  
  return this.readData("temperature", query).then(resultSet => {
    // See above for resultSet format
    return resultSet.results[0].values[0].v;
  });
}
// Assuming that this device has a private method called 'toKelvin'
//function toKelvin(value) {
//  return ((value-32)/1.8)+273.15;
//}

function setTemperature(value) {
  return this.setProperty("temperature", {
    value: this.toKelvin(value),
    time: new Date().toISOString() // now
  }).then(prop => prop.value);
}
function getDeviceInfoSummary(value) {
  async function f() {
      // Read `identifier` property and populate the custom id list
      this.customIds["imei"] = await this.getProperty("identifier")
        .then(p => p.value || "-");

      let deviceInfoSum = {
          id: this.id,
          name: this.name,
          friendlyName: this.friendlyName,
          customIds: this.customIds,
      };
      return deviceInfoSum;
  }
  return f();
}

🚧

PITFALL WARNING

One common mistake is to pass a primitive value directly into the setProperty() method. This results with an error. You must always pass setProperty() a data point object.

The following device attributes can be accessed from user defined methods:

function getDeviceInfoSummary(value) {
  async function f() {    
      let deviceInfoSum = {
          id: this.id,
          accountID: this.accountId,
          name: this.name,
          friendlyName: this.friendlyName,
          customIds: this.customIds,
          status: this.status,
          appIds: this.apps,
          location: this.location,
          profileID: this.profileId
      };
      return deviceInfoSum;
  }
  return f();
}

📘

IMPORTANT

Not that you cannot set device's optional built-in properties (e.g. customIds, location, tags, description, etc) in the methods if they are not already created.

For example the following code will fail if imei has not been created.

async function f() {    
    this.customIds["imei"] = await this.getProperty("identifier")
    ....

App Methods

App methods can call other user defined methods, device methods and built-in app methods. The following private built-in methods can be called from user defined methods:

MethodDescriptionSignatureReturns
setPropertyWrite given value into app propertysetProperty(<property name>, <data-point>)Promise of property value as data point object
getPropertyGets given property objectgetProperty(<property name>)Promise of Property object
logWrite into app loglog(<log type>, <message>)Promise of undefined
readDataRead historical datareadData(<property name>, <query object>)Promise of Query Result object
findDevicesGet devices* by given filterfindDevices(<query object>*)Promise of device id list
executeDeviceMethodExecutes a device method from app methodexecuteDeviceMethod(<device id>, <method name>, <argument>)Promise of {"result": <value> }
setDevicePropertySet device property from app methodsetDeviceProperty(<device id>, <property name>, <data-point>)Promise of property value as data point object
getDevicePropertyGet device property from app methodgetDeviceProperty(<device id>, <property name>)Promise of property value

*See Device Filters format.

// Get all devices linked to this app and call their
// `setTemperature` methods.
function setDeviceTemperatures(value) {
  return this.findDevices({})
    .then(results => {
      // `results` is the list of matching device ids (or empty list)
      let deviceId = results[0];        
      // Execute device method
      return this.executeDeviceMethod(deviceId, 'setTemperature', value);
    });
}
// Get all devices linked to this app and set their
// `temperature` properties directly.
function setDeviceTemperatures(value) {
  return this.findDevices({})
    .then(results => {
      // `results` is the list of matching device ids (or empty list)
      let deviceId = results[0];        
      
      // write into device property
      let dp = { 
        value: value,
        time: new Date().toISOString()
      };
    
      return this.setDeviceProperty(deviceId, "temperature", dp);
    });
}
// Get device by serial number and read its
// `temperature` property value.
// value is the query param (e.g. {'sn':'SN-001'})
function getDeviceTemperature(value) {
  return this.findDevices(value)
    .then(results => {
      // `results` is the list of matching device ids (or empty list)
      let deviceId = results[0];
    
      return this.getDeviceProperty(deviceId, "temperature")
        .then(prop => prop.value);
    });
}
function getDeviceLocations(value) {
  const extractResultField = ({ result }) => result;

  const getDevLocation = d => this.executeDeviceMethod(d, 'getLocation', value);

  const getDevices = async (devices) => {
      const requests = devices.map(getDevLocation);
      const result = await Promise.all(requests);

      return result.map(extractResultField);
  }

  // Get all online devices linked to this app, 
  // then return their locations
  let q = {
    'prop': [ 
      {'property': 'connectionStatus', 'operation': 'eq', value: 'online'} 
    ]};
  return this.findDevices(q)
    .then(getDevices)
    .then(_ => _);
}

The following app attributes can be accessed from user defined methods:

function getAppInfoSummary(value) {
  async function f() {    
      let appSummaryInfo = {
          id: this.id,
          accountID: this.accountId,
          name: this.name,
          friendlyName: this.friendlyName,
          status: this.status,
          profileID: this.profileId
      };
      return appSummaryInfo;
  }
  return f();
}

📘

IMPORTANT

Not that you cannot set app's optional built-in properties (e.g. tags, description, etc) in the methods if they are not already created.

Querying devices by property value

As with the Rest API, you can query devices by their property values from methods. This mechanism allows you to not only use the query results in your logic, but also build dynamic query methods (i.e. views) for your solutions.

Below is an example for such call:

function getRunningMachines(value) {
  // Get all devices linked to this app 
  // with `engine` property in `running` state, and
  // temperature is greater than 34.0 degrees.
  // Alternatively we can use `value` as a query param.
  let q = {
    'prop': [ 
      {'property': 'engine', 'operation': 'eq', value: 'running'},
      {'property': 'temperature', 'operation': 'gt', value: 34.0}
    ]};
  return this.findDevices(q)
    .then(results);
}

Promises

A promise is an object that wraps an asynchronous operation and notifies us when it’s done. This sounds exactly like callbacks, but the improvement is in how we use promises. Instead of providing a callback, a promise has its own methods which you call to tell the promise what will happen when it is successful or when it fails. The standard methods which a promise provides are the then() method for when a successful result is available and the catch() method for when something goes wrong. Using promises, a readFile function becomes:

readFile('foo.txt')
.then(function(result) {
  doSomethingWithData(data);
})
.catch(function(error) {
  console.log("An error has happened!");
});

However, the improvement from using promises becomes more apparent when we look at our nested callback example:

readFile('foo.txt')
.then(function(data) {
  return doSomethingWithData(data);
})
.then(function(result) {
  return writeFile('result.txt', result);
})
.then(function() {
  console.log('success');
})
.catch(function(error) {
  console.log("An error has happened!");
});

Instead of nesting callbacks inside callbacks inside callbacks, you chain then methods together making it more readable and easier to follow. Every then method should either return a new Promise or just a value or object which will be passed to the next then() method in the chain. Another important thing to notice is that even though we are doing two different asynchronous requests we only have one catch() method where we handle our errors. That’s because any error that occurs in the Promise chain will stop further execution and an error will end up in the next catch() method in the chain.

It is important to note that, just like with callbacks, these are still asynchronous operations. The code that is executed when the request has completed — that is the subsequent then() method calls — is put on the event loop just like a callback function would be. This means you cannot access any variables passed to or declared in the Promise chain outside the Promise. The same goes for errors thrown in the Promise chain. You must also have at least one catch() method at the end of your Promise chain for you to be able to handle errors that occur. If you do not have a catch() method, any errors will silently pass and you will have no idea why your Promise does not behave as expected:

try {
  readFile('foo.txt')
  .then(function(result) {
    doSomethingWithData(data);
  });
} catch(error) {
  console.log("An error has happened!"); // You will never reach here even if an error is thrown inside the Promise chain
}

Parallel Execution of Promises

// Device Context
// function body

const PRESSURE_PROPERTY = 'pressure';
const TEMPERATURE_PROPERTY = 'temperature';
const VIEW1_PROPERTY = 'view1';

async function compose(context) {
    Object.assign(context, {
        costOfkWh: value,
    });

    // Acquire temperature and pressure values in parallel
    let [pressureProp, temperatureProp] = await Promise.all([
       this.getProperty(PRESSURE_PROPERTY),
       this.getProperty(TEMPERATURE_PROPERTY)
    ]);

    context.pressure.value = pressureProp.value || 0;
    context.pressure.unit = pressureProp.meta.measurement && pressureProp.meta.measurement.unit && pressureProp.meta.measurement.unit.symbol;

    if (pressureProp.meta.boundaries) {
        context.pressure.range = {
            min: pressureProp.meta.boundaries.min,
            max: pressureProp.meta.boundaries.max,
        };
    }
    
    context.temperature.value = temperatureProp.value || 0;
    context.temperature.unit = temperatureProp.meta.measurement && temperatureProp.meta.measurement.unit && temperatureProp.meta.measurement.unit.symbol;

    if (temperatureProp.meta.boundaries) {
        context.temperature.range = {
            min: temperatureProp.meta.boundaries.min,
            max: temperatureProp.meta.boundaries.max,
        };
    }

    // Call the methods below in parallel.
    let results = await Promise.all([
        this.getCompressorInfo(),
        this.queryWarningAlarmSummary(context),
        this.getTimeToMaintenance(context),
        this.hasInverter(context),
    ]);
    
    // Merge results into context
    results.forEach(result => Object.assign(context, result));

    return context;
};

return this.getProperty(VIEW1_PROPERTY)
 .then(property => property.value || this.getEmptyView())
 .then(compose)
 .then(context => this.setProperty(VIEW1_PROPERTY, { value: context, time: new Date().toISOString() }))
 .then(property => property.value);

Querying Historical Data

You can access to time-series database from the device and app methods. This will allow you to use the historical data in your logic or create custom view for your systems. Below is an example of querying average of a property for 4 different pierods. Please see Reading historical data section for query details.

// Device Context
// function body

/**
 * @private
 * @param {{ results: { values: { v: any }[] }[] }} response
 * @returns {any}
 */
function extractValue(response) {
  if (
    response.results &&
    response.results.length &&
    response.results[0].values &&
    response.results[0].values.length
  ) {
    return response.results[0].values[0].v;
  }

  return void 0;
}

function buildQueryWithAggregate(agg) {
  /**
    Builds time-series queries to collect 4 different stats for given property
  */
  return [
        { startRelative: { value: 24, unit: 'hours' }, aggregators: [ {name: agg, sampling: { value: 24, unit: 'hours'} } ] },
        { startRelative: { value: 7, unit: 'days' }, aggregators: [ {name: agg, sampling: { value: 7, unit: 'days'} } ] },
        { startRelative: { value: 30, unit: 'days' }, aggregators: [ {name: agg, sampling: { value: 30, unit: 'days'} } ] },
        { startRelative: { value: 12, unit: 'months' }, aggregators: [ {name: agg, sampling: { value: 12, unit: 'months'} } ] }
    ];
}

/**
 * @async
 * @param {{ agg: string, pname: string }} value
 * @returns {Promise<Array>}
 */
async function compose(value) {
  const queries = buildQueryWithAggregate(value.agg);
  const requests = queries.map((q) => this.readData(value.pname, q));
  const responses = await Promise.all(requests);

  return responses.map(extractValue);
}

return compose(value);

Making HTTP Requests to External URLs

❗️

CURRENTLY IN ALPHA

Please note that this feature is currently in Alpha version. Please report issues if you find any problems.

You can make Get, Post, Put and Delete HTTP requests using JSON payload to interact with external REST services from your Device and App methods.

You can allow your system methods to make requests to any domain, or you can limit access with only certain domains.

// `value` is the name of the city e.g. Reykjavik
let url = `https://api.openweathermap.org/data/2.5/weather?q=${value}&APPID=98c1dfa23cc036a0879ed7e11a8b5283&units=metric`;

return http.get(url).then(response => { 
    this.setProperty("MaxTemp", {
        value: response.data.main.temp_max,
        time: new Date().toISOString()
    }).then(_ => _);
});

GET

FieldDescriptionMandatory
urlstringyes
headersmap of string key and valuesno

Example:

let headers = {
    'header-1': 'value-1'
}
return http.get("http://jsonplaceholder.typicode.com/todos/1", headers).then(response => response.data)

POST

FieldDescriptionMandatory
urlstringyes
payloadjson objectyes
headersmap of string key and valuesno

Example:

let payload = {
    "userId": 1,
    "title": "test-connio",
    "completed": false
}

let headers = {
    'header-1': 'value-1'
}
return http.post("http://jsonplaceholder.typicode.com/todos", payload, headers).then(response => response.data)

PUT

FieldDescriptionMandatory
urlstringyes
payloadjson objectyes
headersmap of string key and valuesno

Example:

let payload = {
    "userId": 1,
    "title": "test-connio",
    "completed": false
}

let headers = {
    'header-1': 'value-1'
}

return http.put("http://jsonplaceholder.typicode.com/todos/350", payload, headers).then(response => response.data)

DELETE

FieldDescriptionMandatory
urlstringyes
headersmap of string key and valuesno

Example:

let headers = {
    'header-1': 'value-1'
}

return http.delete("http://jsonplaceholder.typicode.com/todos/1", headers).then(response => response.data)

Response Object

FieldDescription
statushttp status
databody data in json or text depending response content type

Using setTimeout / cancelTimeout in App Methods

🚧

EXPERIMENTAL

This feature is experimental and can be changed in the future.

You may decide to execute a function not right now, but at a certain time later. That’s called “scheduling a call”. setTimeout allows to run a function once after the interval of time; cancelTimeout allows to delete previously created timer.

// @return a promise containing timer id 
setTimeout(<app method name>, <timeout in milliseconds>, <method arguments>)

// @return undefined
cancelTimeout(<timer id>)
// Call checkHumidity method after 15 seconds using zone1 as argument
return setTimeout("checkHumidity", 15000, "zone1")
  .then( timerId => {
    // store timer id in a property 
    this.activeTimer = timerId;
});

// Deleting timer in another method
cancelTimeout(this.activeTimer);

Best Practices

  • Always explicitly return from your Method implementation
    Non returning functions are considered returned undefined by JavaScript engine. This is fine if your method is side effecting and doesn't need to return any value. However, in such case, if your method contains promises, they are not guaranteed to be executed. It is always good practice to return your promise from the method to guarantee that it is resolved before the method execution is complete.

  • Write idempotent functions
    Try to write scripts which produce the same result even if they are called multiple times. This lets you retry a call if the previous call fails partway through your code.