Sending commands to device

Based on the Kaa v1.2.

Time to complete: 8 min.

Overview

Welcome to the third tutorial in the Kaa getting started guide! We assume that you have successfully collected data from your device to Kaa, so make sure to review the second tutorial before proceeding here.

From this tutorial you will learn some additional concepts of the Kaa platform and discover how to:

  • execute commands on your device
  • view command execution history on the Kaa UI

Terms and concepts

Let us start by reviewing some new terms and concepts related to the Kaa command invocation feature.

Command

Command is a short-lived message sent to a connected endpoint from the Kaa platform. With the commands, you can toggle the lights on/off, open a car trunk, or request an immediate endpoint state report.

Any commands may be in either pending or executed state. Pending state means that the command was invoked but no execution result for that command is known yet. Executed state is assigned to the command that has gotten an endpoint response, meaning an endpoint received the command, executed it, and sent the execution result back to the platform.

Consider the following example.

Let’s assume you just sent a command to your Tesla car to park itself in a parking slot. The platform immediately assigns pending to that command, while your Tesla receives the command and starts parking. After finishing with the task, the car reports the execution result of that command back to the platform and Kaa assigns executed to that command.

Every command has an execution status code and reason phrase that endpoints can specify at the moment of reporting the execution result back to the platform. If, for example, your Tesla couldn’t finish the parking because it was taken by another vehicle, it may report the execution result specifying 409 status code and some meaningful reason phrase, which will be displayed for you in the Kaa UI.

Command type

Command type represents the type of command that you want to execute on an endpoint e.g. reboot, close-door, switch-light, or in the case of Tesla—park. An endpoint can handle as many command types as you define in its firmware.

Now, let’s jump into action and do some practice.

Playbook

We assume that you have already created an application, application version, and endpoint with a token while following the “connecting your first device” tutorial. You can reuse them or create new ones.

Start by logging into your Kaa Cloud account.

Invoke a command


When the user invokes a command (e.g., from Kaa UI) on a device that connects to the platform over a synchronous protocol (e.g., HTTP), there is no way for the platform to push such command to the device. Instead, Kaa persists the command and waits until the device requests it for execution. This means that for devices with synchronous protocols it is their responsibility to periodically poll the platform for new commands.

So let’s invoke some command and make it available for execution by an endpoint.

Go to the endpoint’s dashboard and find the “Command execution” widget.

Fill out Command type field with reboot and set Maximum command retention for delivery to 1 hour. Maximum command retention for delivery defines the time of how long the command is available for execution by an endpoint.

Click Run.

command-execution

Once the command is invoked, it appears in a response to the polling during the time specified in the Maximum command retention for delivery field (1 hour in our case).

To poll the platform for new commands with the reboot command type, execute the bellow cURL. The last part of the URL designates the command type, which in our case is reboot.

Replace <app-version-name> with the actual application version used by the endpoint and <endpoint-token> with the endpoint token.

curl --location --request POST 'https://connect.cloud.kaaiot.com:443/kp1/<app-version-name>/cex/<endpoint-token>/command/reboot' \
--data-raw '{}'

You just retrieved the earlier invoked command. Capture the id from the response. It’s an internal command ID used by Kaa to uniquely identify the command.

Note that the command is still in the Pending state in the “Commands history” widget on Kaa UI. Let’s send the command execution result back to Kaa.

Specify the application version for <app-version-name>, the endpoint token for <endpoint-token> and the command ID for <command-ID> from the earlier received response body.

curl --location --request POST 'https://connect.cloud.kaaiot.com:443/kp1/<app-version-name>/cex/<endpoint-token>/result/reboot' \
--data-raw '[{
    "id": <command-ID>,
    "statusCode": 200,
    "reasonPhrase": "OK",
    "payload": "Success"
}]'

When the platform receives the execution result for the command with the specific command ID, it marks the command as executed and stops returning it in a response for the command polling.



To run the below MQTT client on your PC, you will need Python 3 installed. To speed things up a little, you can also just open and run it on Replit.com.

Initialize the ENDPOINT_TOKEN and the APPLICATION_VERSION variables with endpoint token and application version respectively.

# Simple MQTT-based command execution client for the Kaa IoT platform.
# Handles "reboot" and "zero" command types.
# See https://docs.kaaiot.io/KAA/docs/current/Tutorials/sending-commands-to-device/

import json
import paho.mqtt.client as mqtt
import random
import signal
import string
import time

KPC_HOST = "mqtt.cloud.kaaiot.com"  # Kaa Cloud plain MQTT host
KPC_PORT = 1883  # Kaa Cloud plain MQTT port

ENDPOINT_TOKEN = ""         # Paste endpoint token
APPLICATION_VERSION = ""    # Paste application version


class DataCollectionClient:

    def __init__(self, client):
        self.client = client
        self.data_collection_topic = f'kp1/{APPLICATION_VERSION}/dcx/{ENDPOINT_TOKEN}/json/32'

        command_reboot_topic = f'kp1/{APPLICATION_VERSION}/cex/{ENDPOINT_TOKEN}/command/reboot/status'
        self.client.message_callback_add(command_reboot_topic, self.handle_reboot_command)
        self.command_reboot_result_topik = f'kp1/{APPLICATION_VERSION}/cex/{ENDPOINT_TOKEN}/result/reboot'

        command_zero_topic = f'kp1/{APPLICATION_VERSION}/cex/{ENDPOINT_TOKEN}/command/zero/status'
        self.client.message_callback_add(command_zero_topic, self.handle_zero_command)
        self.command_zero_result_topik = f'kp1/{APPLICATION_VERSION}/cex/{ENDPOINT_TOKEN}/result/zero'

    def connect_to_server(self):
        print(f'Connecting to Kaa server at {KPC_HOST}:{KPC_PORT} using application version {APPLICATION_VERSION} and endpoint token {ENDPOINT_TOKEN}')
        self.client.connect(KPC_HOST, KPC_PORT, 60)
        print('Successfully connected')

    def disconnect_from_server(self):
        print(f'Disconnecting from Kaa server at {KPC_HOST}:{KPC_PORT}...')
        self.client.loop_stop()
        self.client.disconnect()
        print('Successfully disconnected')

    def handle_reboot_command(self, client, userdata, message):
        print(f'<--- Received "reboot" command on topic {message.topic} \nRebooting...')
        command_result = self.compose_command_result_payload(message)
        print(f'command result {command_result}')
        client.publish(topic=self.command_reboot_result_topik, payload=command_result)
        # With below approach we don't receive the command confirmation on the server side.
        # self.client.disconnect()
        # time.sleep(5)  # Simulate the reboot
        # self.connect_to_server()

    def handle_zero_command(self, client, userdata, message):
        print(f'<--- Received "zero" command on topic {message.topic} \nSending zero values...')
        command_result = self.compose_command_result_payload(message)
        client.publish(topic=self.data_collection_topic, payload=self.compose_data_sample(0, 0, 0))
        client.publish(topic=self.command_zero_result_topik, payload=command_result)

    def compose_command_result_payload(self, message):
        command_payload = json.loads(str(message.payload.decode("utf-8")))
        print(f'command payload: {command_payload}')
        command_result_list = []
        for command in command_payload:
            commandResult = {"id": command['id'], "statusCode": 200, "reasonPhrase": "OK", "payload": "Success"}
            command_result_list.append(commandResult)
        return json.dumps(
            command_result_list
        )

    def compose_data_sample(self, fuelLevel, minTemp, maxTemp):
        return json.dumps({
            'timestamp': int(round(time.time() * 1000)),
            'fuelLevel': fuelLevel,
            'temperature': random.randint(minTemp, maxTemp),
        })


def on_message(client, userdata, message):
    print(f'Message received: topic {message.topic}\nbody {str(message.payload.decode("utf-8"))}')


def main():
    # Initiate server connection
    client = mqtt.Client(client_id=''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(6)))

    data_collection_client = DataCollectionClient(client)
    data_collection_client.connect_to_server()

    client.on_message = on_message

    # Start the loop
    client.loop_start()

    fuelLevel, minTemp, maxTemp = 100, 95, 100

    # Send data samples in loop
    listener = SignalListener()
    while listener.keepRunning:

        payload = data_collection_client.compose_data_sample(fuelLevel, minTemp, maxTemp)

        result = data_collection_client.client.publish(topic=data_collection_client.data_collection_topic, payload=payload)
        if result.rc != 0:
            print('Server connection lost, attempting to reconnect')
            data_collection_client.connect_to_server()
        else:
            print(f'--> Sent message on topic "{data_collection_client.data_collection_topic}":\n{payload}')

        time.sleep(3)

        fuelLevel = fuelLevel - 0.3
        if fuelLevel < 1:
            fuelLevel = 100

    data_collection_client.disconnect_from_server()


class SignalListener:
    keepRunning = True

    def __init__(self):
        signal.signal(signal.SIGINT, self.stop)
        signal.signal(signal.SIGTERM, self.stop)

    def stop(self, signum, frame):
        print('Shutting down...')
        self.keepRunning = False


if __name__ == '__main__':
    main()

Run the python code.

Now that the simulator is running, go to the endpoint’s dashboard and send a command with type zero to the endpoint.

zero-command-execution

Navigate to the endpoint Device telemetry widget and see that the endpoint responded with zero telemetry data values.

zero-telemetry


Now repeat the above procedure with any other command specifying custom Command type and Command body in JSON.

Browsing command history

You can browse command history using the Commands history widget on Kaa UI. You can view command status (“Pending” or “Executed”), its request and response payloads, and re-run the command.

command-history

Resources

All the tutorial resources are located on GitHub.

Feedback

This tutorial is based on Kaa 1.2 released on July 6-th, 2020. If you, our reader from the future, spot some major discrepancies with your current version of the Kaa platform, or if anything does not work for you, please give us a shout and we will help!

And if the tutorial served you well, we’d still love to hear your feedback, so join the community!