Websocket

Guide to websocket communications with the Ardexa API

Getting started

The Ardexa API uses socket.io (https://socket.io/) for websocket requests. There are client libraries available for a variety of programming languages. The examples listed here will use NodeJS.

To get started, we will issue a simple REMOTE SHELL request. You will need:

  • An API token with the Control Devices privilege

    • For testing, you can simply use your current session token: (Your name) -> My Profile -> Version -> Copy to clipboard

  • The workgroup ID and device ID of the device

    • These can be found under Manual Config -> General on the Devices page for the target device

Connect and authenticate

Here is an example connect function:

import io from 'socket.io-client'
const workgroupId = 'your-workgroup-id'

function connect () {
  return new Promise((resolve, reject) => {
    let pending = true
    const websocket = io(`https://app.ardexa.com`, {
      query: `token=${process.env.TOKEN}&orgName=${workgroupId}`,
      path: '/socket.io-client',
      forceNew: true,
      transports: ['websocket'],
    })
    websocket.once('connect', () => {
      console.log('SOCKET CONNECTED')
      if (pending) {
        resolve(websocket)
        pending = false
      }
    })
    websocket.once('connect_error', err => {
      console.error('Socket error', err, err.data)
      if (pending) {
        reject(err)
        pending = false
      }
    })
  })
}

All operations using socket.io are event driven, so in this example we have wrapped the connection events in a Promise to make things a little easier. The important parts to notice are:

  • The API token is read from an environment variable called TOKEN

  • The cloud is app.ardexa.com. Please update this to match your cloud URL.

  • The workgroupId is passed in at connection time. You must use a separate connection for each workgroup

  • The websocket will emit connect when successfully connected, or connect_error if there was a problem

Now that we're connected, we can issue a REMOTE SHELL by emitting (sending) the correct event type and listening for the responses

async function main () {
  const deviceId = 'your-device-id'
  const cmd = 'uptime'
  const websocket = await connect()
  websocket.once('device:cmd:reply', (item) => {
    console.log('REPLY:', item)
    disconnect(websocket)
  })
  websocket.once('device:cmd:rpc', (item) => {
    console.log('RPC:', item)
    if (item.error) disconnect(websocket)
  })
  websocket.emit('device:cmd', { deviceId, cmd })
}
  • device:cmd is emitted with the following parameters

    • deviceId: the ID of the target device

    • cmd: the command we want to run on that device

    • requestId: optional. Will be returned in any subsequent replies. Useful for matching requset and response objects if you are running many concurrent requests

  • device:cmd:rpc is received first. This confirms that the API has received the request and it has been successfully processed. If there are errors, such as a permission problem, they will be delivered here and there will be no reply. Successful requests will be assigned a correlationId which can be used to match the reply/replies to the initial request

  • device:cmd:reply is received once the request has been fully processed by the edge device. For device:cmd, this includes the output of the command (stdout and stderr) as well as the return code.

Here is a sample of the output when the script is run from the command line:

SOCKET CONNECTED
RPC: { correlationId: '8c1c1b05-8c98-4010-af92-747511102415' }
REPLY: {
  stdout: ' 05:33:14 up 474 days,  1:21,  0 users,  load average: 0.00, 0.00, 0.00\n',
  stderr: '',
  returnCode: 0,
  correlationId: '8c1c1b05-8c98-4010-af92-747511102415'
}

Emitted events

device:stateChange

Sent when a device's online state changes (online/offline). Contains the cn (Common name), vhost (aka workgroup ID) and the latest online status

{
    "cn": "CN=3dcf4324-3c0a-40bc-8d13-5d5bbbcf229e,O=dev",
    "vhost": "dev",
    "online": false
}

device:new

Sent when a new device is created. Contains the id (aka Device ID), the cn and it's online status (which is always false)

{
    "id": "102b5359-eb59-479d-a471-552f7ca2c093",
    "cn": "CN=102b5359-eb59-479d-a471-552f7ca2c093,O=dev",
    "online": false
}

device:metaChange

Sent when there is a change to the agent configuration. Contains the device ID as the id property. The workgroup ID is the one used to connect the current socket.

{
    "id": "666170e2-d3b2-49b7-88fc-542e1a2c9732"
}

RPC events

Live data feed

device:feed

Get a live feed of data directly from the broker. The deviceId, table and source are mandatory. For CSV scenarios you can optionally include a specific field to only stream a single value (the default is to stream an array of values)

{
    "requestId": "b329c143-46fe-4654-a970-8c25aee2d34d",
    "deviceId": "666170e2-d3b2-49b7-88fc-542e1a2c9732",
    "table": "solar",
    "source": "inverter-01",
    "field": "AC Power"
}

The device:feed:rpc will include a feedId:

{
    "requestId": "b329c143-46fe-4654-a970-8c25aee2d34d",
    "feedId": "4cbe686d-303a-4081-84aa-aecff0f07c80"
}

The value prop will contain the value(s) requested:

{
    "feedId": "4cbe686d-303a-4081-84aa-aecff0f07c80",
    "event_time": "2023-08-09T03:55:37.000Z",
    "table": "solar",
    "source": "inverter-01",
    "device": "666170e2-d3b2-49b7-88fc-542e1a2c9732",
    "messageId": "71391",
    "value": 66920,
    "requestId": "b329c143-46fe-4654-a970-8c25aee2d34d"
}

device:feed:stop

Will stop an existing feed. Simply pass in the feedId received in the :rpc response

{
    "feedId": "4cbe686d-303a-4081-84aa-aecff0f07c80"
}

device:feed:stopAll

Takes no arguments, will stop all feeds associated with the current socket connection.

Agent configuration

device:testConfig

Send a config object (JSON) to the specified device ID for testing and validation. Since the destination format is YAML, but the request is using JSON, the API will pre-process the request to look for "anchors" in the config, specifically in the fields property. In the example below, example_fields is used as an anchor by the tail scenario.

{
    "requestId": "ed758e3c-d37c-48e5-a3f2-fe187904f64c",
    "deviceId": "666170e2-d3b2-49b7-88fc-542e1a2c9732",
    "config": {
        "example_fields": [
            {
                "meta": { "units": "$" },
                "name": "one",
                "expect": "integer"
            },
            {
                "meta": { "units": "@" },
                "name": "two",
                "expect": "integer"
            },
            {
                "meta": { "units": "!" },
                "name": "other one",
                "expect": "keyword"
            },
            {
                "name": "baleReady",
                "expect": "bool"
            }
        ],
        "amqp": {
            "cacertfile": "/etc/ardexa/keys/Ardexa CA chain - amqps.crt",
            "certfile": "/etc/ardexa/keys/client.crt",
            "keyfile": "/etc/ardexa/keys/client.key",
            "hostname": "minikube.local",
            "port": 30671
        },
        "debug": 1,
        "run": [
            {
                "command": "top -b -n2 | grep \"Cpu(s)\"|tail -n 1 | awk '{print $2 + $4}'",
                "frequency": 30,
                "expect": "decimal",
                "table": "data",
                "source": "cpu_usage",
                "meta": {
                    "units": "percentage",
                    "comment": "measures cpu usage"
                }
            }
        ],
        "tail": [
            {
                "file": "/tmp/junk.log",
                "table": "junk",
                "source": "file1",
                "expect": "csv",
                "fields": "example_fields"
            }
        ],
        "stdout": true,
    }
}

As the request is fairly determinate in terms of time taken, there is only a single :rpc response indicating success:

{
  "requestId": "ab76bc81-78d6-41db-bef3-1204240aeaf1",
  "result": "success"
}

Or failure:

{
    "requestId": "79da4cc6-7081-4595-84e9-82d9d05c807c",
    "result": "error",
    "error": {
        "message": "ERROR. 2147. Parse error: Failed to parse 'frequency': yaml-cpp: error at line 50, column 16: bad conversion LC: 2\nERROR. 2147. Parse error: Failed to parse 'frequency': 2156. Bad frequency LC: 2\n"
    }
}

device:applyConfig

Just like device:testConfig, except the configuration will be saved and the agent will be restarted

device:applyYamlConfig

Just like device:applyConfig, except the configuration is a String instead of an Object, and the string must be a YAML document.

Other

device:refreshMeta

Dynamically mapped scenarios (read: anything logged to /opt/ardexa/logs), can occasionally get out of sync with the cloud. To force a re-read and re-sync event, use this RPC request

{
    "requestId": "bd1ed0dc-0e46-4f2a-b466-9514222aa81a",
    "deviceId": "666170e2-d3b2-49b7-88fc-542e1a2c9732"
}

Will always succeed

{
    "requestId": "bd1ed0dc-0e46-4f2a-b466-9514222aa81a",
    "complete": true
}

Will be followed by a device:stateChange as the agent is restarted, and a device:metaChange as the metadata is re-synced

device:doUpgrade

Upgade the agent to the latest version

{
    "requestId": "71089965-c53d-4e66-a3f7-a3de03aa76a9",
    "deviceId": "666170e2-d3b2-49b7-88fc-542e1a2c9732"
}

Will emit a series of device:doUpgrade:reply events at each step of the process

{
    "status": "Arch detected: amd64",
    "complete": false,
    "requestId": "71089965-c53d-4e66-a3f7-a3de03aa76a9"
}

The final message in a successful series will set complete: true

{
    "status": "Complete",
    "complete": true,
    "requestId": "71089965-c53d-4e66-a3f7-a3de03aa76a9"
}

If there is an error, it will be reported and no further replies will be forthcoming

{
    "error": "Internal error occurred",
    "requestId": "71089965-c53d-4e66-a3f7-a3de03aa76a9"
}

device:sendArdexactl

Send a cooy of the ardexactl control script to the target device. This normally happens automatically if the script is required, e.g. device:doUpgrade, but there are situations where that's not the case, such as agent API logins.

{
    "requestId": "96f021d3-f866-4dec-819f-9a2035e91b12",
    "deviceId": "666170e2-d3b2-49b7-88fc-542e1a2c9732"
}

And the device:sendArdexactl:rpc response

{
    "requestId": "96f021d3-f866-4dec-819f-9a2035e91b12",
    "complete": true
}

device:sendArdpkg

Send a cooy of the ardpkg plugin management app to the target device. This normally happens automatically if the script is required, e.g. installing a plugin. This call can also be used to upgrade ardpkg to the latest version

{
    "requestId": "96f021d3-f866-4dec-819f-9a2035e91b12",
    "deviceId": "666170e2-d3b2-49b7-88fc-542e1a2c9732"
}

And the device:sendArdpkg:rpc response

{
    "requestId": "96f021d3-f866-4dec-819f-9a2035e91b12",
    "complete": true
}

Last updated