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:
Method | Description | Signature | Returns |
---|---|---|---|
setProperty | Write given value into device property | setProperty(<property name>, <data-point>) | Promise of property value as data point object |
getProperty | Gets given property object | getProperty(<property name>) | Promise of Property * object |
log | Write into device log | log(<log type>, <message>) | Promise of undefined |
readData | Read historical data | readData(<property name>, <query object**>) | Promise of Query Result object*** |
findDevices | Get 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 |
executeDeviceMethod | Executes a device method from gateway device method. | executeDeviceMethod(<device id>, <method name>, <argument>) | Promise of {"result": <value> } |
setDeviceProperty | Set device property from gateway device method | setDeviceProperty(<device id>, <property name>, <data-point>) | Promise of property value as data point object |
getDeviceProperty | Get device property from gateway device method | getDeviceProperty(<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 passsetProperty()
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:
Method | Description | Signature | Returns |
---|---|---|---|
setProperty | Write given value into app property | setProperty(<property name>, <data-point>) | Promise of property value as data point object |
getProperty | Gets given property object | getProperty(<property name>) | Promise of Property object |
log | Write into app log | log(<log type>, <message>) | Promise of undefined |
readData | Read historical data | readData(<property name>, <query object>) | Promise of Query Result object |
findDevices | Get devices* by given filter | findDevices(<query object>*) | Promise of device id list |
executeDeviceMethod | Executes a device method from app method | executeDeviceMethod(<device id>, <method name>, <argument>) | Promise of {"result": <value> } |
setDeviceProperty | Set device property from app method | setDeviceProperty(<device id>, <property name>, <data-point>) | Promise of property value as data point object |
getDeviceProperty | Get device property from app method | getDeviceProperty(<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
Field | Description | Mandatory |
---|---|---|
url | string | yes |
headers | map of string key and values | no |
Example:
let headers = {
'header-1': 'value-1'
}
return http.get("http://jsonplaceholder.typicode.com/todos/1", headers).then(response => response.data)
POST
Field | Description | Mandatory |
---|---|---|
url | string | yes |
payload | json object | yes |
headers | map of string key and values | no |
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
Field | Description | Mandatory |
---|---|---|
url | string | yes |
payload | json object | yes |
headers | map of string key and values | no |
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
Field | Description | Mandatory |
---|---|---|
url | string | yes |
headers | map of string key and values | no |
Example:
let headers = {
'header-1': 'value-1'
}
return http.delete("http://jsonplaceholder.typicode.com/todos/1", headers).then(response => response.data)
Response Object
Field | Description |
---|---|
status | http status |
data | body 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 returnedundefined
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.
Updated over 4 years ago