跳转至

设备配置

用例描述

作为设备制造商或固件开发人员,我希望我的设备能够在 YiCONNECT 中自动配置。在自动供应期间,设备可以生成唯一凭证或者要求服务器为设备提供唯一凭证。

YiCONNECT 允许使用X.509 证书链通过 MQTT 进行身份验证自动配置新设备。

怎么运行的?

设备可以向 YiCONNECT 发送设备配置请求 (Request)。请求应始终包含配置密钥和秘密。该请求可以可选地包括设备名称和设备生成的凭证。如果这些凭据不存在,服务器将生成供设备使用的访问令牌。

供应请求示例:

{
  "deviceName":"DEVICE_NAME",
  "provisionDeviceKey":"YOUR_PROVISION_KEY_HERE",
  "provisionDeviceSecret":"YOUR_PROVISION_SECRET_HERE"
}

YiCONNECT

YiCONNECT 验证请求并回复设备配置响应 (Response)。成功的响应包含设备 ID、凭证类型和正文。如果验证不成功,响应将仅包含状态。

配置响应示例:

{
  "provisionDeviceStatus":"SUCCESS",
  "credentialsType":"ACCESS_TOKEN",
  "accessToken":"sLzc0gDAZPkGMzFVTyUY"
}

YiCONNECT

在验证请求期间,YiCONNECT 将首先检查提供的ProvisionDeviceKeyProvisionDeviceSecret以查找相应的Device Profile。找到配置文件后,平台将使用配置的配置策略来验证设备名称。有两种可用的供应策略:

  • 允许创建新设备 - 检查同名设备尚未在 YiCONNECT 中注册。当您在制造过程中不知道唯一设备名称(MAC 地址等)列表,但设备本身可以访问固件中的此信息时,此策略非常有用。它更容易实现,但安全性不如第二种策略。
  • 检查预配置的设备 - 检查是否已在 YiCONNECT 中创建同名设备,但尚未配置。当您只想允许配置特定设备列表时,此策略非常有用。假设您在制造过程中收集了唯一 IDS(MAC 地址等)列表。您可以使用[批量配置将此列表上传到 YiCONNECT。现在,列表中的设备可以发出配置请求,并且其他设备将无法自行配置。

配置完成后,YiCONNECT 将更新设备的配置状态服务器属性并将其设置为配置值。

设备配置文件配置

您应该配置设备配置文件以启用配置功能、收集配置设备密钥和配置设备密钥。

  • 您可以创建新的设备配置文件或打开现有的设备配置文件。要创建新的,您应该打开设备配置文件页面并单击表标题中的“+”图标。
  • 输入新设备配置文件的名称,然后单击“添加设备配置文件”向导的步骤 4。在此示例中,我们将使用名称“Device Provisioning Test”。但是,通常这应该是您的设备型号或类似型号。
  • 选择一种配置策略,复制配置密钥和密码,最后单击“添加”。

输入新设备配置文件的名称,然后单击“添加设备配置文件”向导的步骤 4。 在此示例中,我们将使用名称“Device Provisioning Test”。 但是,通常这应该是您的设备型号或类似型号。

选择一种配置策略,复制配置密钥和密码,最后单击“添加”。

提供设备 API

MQTT 设备 API

您可以使用MQTT API参考来开发将执行配置请求的设备固件。如前所述,设备可以在注册过程中请求服务器生成凭据或提供自己的凭据。请参阅下面每个选项的请求/响应和代码示例:

配置请求数据示例:

{
  "deviceName":"DEVICE_NAME",
  "provisionDeviceKey":"PUT_PROVISION_KEY_HERE",
  "provisionDeviceSecret":"PUT_PROVISION_SECRET_HERE"
}

配置响应示例:

{
  "status":"SUCCESS",
  "credentialsType":"ACCESS_TOKEN",
  "credentialsValue":"sLzc0gDAZPkGMzFVTyUY"
}

示例脚本为了与 YiCONNECT 通信,我们将使用 Paho MQTT 模块,因此我们应该安装它:

pip3 install paho-mqtt --user

脚本源代码如下。您可以将其复制粘贴到文件中,例如:

device-provision-example.py

现在您应该运行该脚本并按照其中的步骤进行操作。
您可以使用 python 3 启动脚本:

python3 device-provision-example.py

脚本源代码:

from paho.mqtt.client import Client
from json import dumps, loads

RESULT_CODES = {
    1: "incorrect protocol version",
    2: "invalid client identifier",
    3: "server unavailable",
    4: "bad username or password",
    5: "not authorised",
    }



def collect_required_data():
    config = {}
    print("\n\n", "="*80, sep="")
    print(" "*10, "\033[1m\033[94mYiCONNECT device provisioning with basic authorization example script.\033[0m", sep="")
    print("="*80, "\n\n", sep="")
    host = input("Please write your YiCONNECT \033[93mhost\033[0m or leave it blank to use default (yiqisoft.cloud): ")
    config["host"] = host if host else "mqtt.yiqisoft.cloud"
    port = input("Please write your YiCONNECT \033[93mport\033[0m or leave it blank to use default (1883): ")
    config["port"] = int(port) if port else 1883
    config["provision_device_key"] = input("Please write \033[93mprovision device key\033[0m: ")
    config["provision_device_secret"] = input("Please write \033[93mprovision device secret\033[0m: ")
    device_name = input("Please write \033[93mdevice name\033[0m or leave it blank to generate: ")
    if device_name:
        config["device_name"] = device_name
    print("\n", "="*80, "\n", sep="")
    return config


class ProvisionClient(Client):
    PROVISION_REQUEST_TOPIC = "/provision/request"
    PROVISION_RESPONSE_TOPIC = "/provision/response"

    def __init__(self, host, port, provision_request):
        super().__init__()
        self._host = host
        self._port = port
        self._username = "provision"
        self.on_connect = self.__on_connect
        self.on_message = self.__on_message
        self.__provision_request = provision_request

    def __on_connect(self, client, userdata, flags, rc):  # Callback for connect
        if rc == 0:
            print("[Provisioning client] Connected to YiCONNECT ")
            client.subscribe(self.PROVISION_RESPONSE_TOPIC)  # Subscribe to provisioning response topic
            provision_request = dumps(self.__provision_request)
            print("[Provisioning client] Sending provisioning request %s" % provision_request)
            client.publish(self.PROVISION_REQUEST_TOPIC, provision_request)  # Publishing provisioning request topic
        else:
            print("[Provisioning client] Cannot connect to YiCONNECT!, result: %s" % RESULT_CODES[rc])

    def __on_message(self, client, userdata, msg):
        decoded_payload = msg.payload.decode("UTF-8")
        print("[Provisioning client] Received data from YiCONNECT: %s" % decoded_payload)
        decoded_message = loads(decoded_payload)
        provision_device_status = decoded_message.get("status")
        if provision_device_status == "SUCCESS":
            self.__save_credentials(decoded_message["credentialsValue"])
        else:
            print("[Provisioning client] Provisioning was unsuccessful with status %s and message: %s" % (provision_device_status, decoded_message["errorMsg"]))
        self.disconnect()

    def provision(self):
        print("[Provisioning client] Connecting to YiCONNECT (provisioning client)")
        self.__clean_credentials()
        self.connect(self._host, self._port, 60)
        self.loop_forever()

    def get_new_client(self):
        client_credentials = self.__get_credentials()
        new_client = None
        if client_credentials:
            new_client = Client()
            new_client.username_pw_set(client_credentials)
            print("[Provisioning client] Read credentials from file.")
        else:
            print("[Provisioning client] Cannot read credentials from file!")
        return new_client

    @staticmethod
    def __get_credentials():
        new_credentials = None
        try:
            with open("credentials", "r") as credentials_file:
                new_credentials = credentials_file.read()
        except Exception as e:
            print(e)
        return new_credentials

    @staticmethod
    def __save_credentials(credentials):
        with open("credentials", "w") as credentials_file:
            credentials_file.write(credentials)

    @staticmethod
    def __clean_credentials():
        open("credentials", "w").close()


def on_tb_connected(client, userdata, flags, rc):  # Callback for connect with received credentials
    if rc == 0:
        print("[YiCONNECT client] Connected to YiCONNECT with credentials: %s" % client._username.decode())
    else:
        print("[YiCONNECT client] Cannot connect to YiCONNECT!, result: %s" % RESULT_CODES[rc])


if __name__ == '__main__':

    config = collect_required_data()

    YICONNECT_HOST = config["host"]  # YiCONNECT instance host
    YICONNECT_PORT = config["port"]  # YiCONNECT instance MQTT port

    PROVISION_REQUEST = {"provisionDeviceKey": config["provision_device_key"],  # Provision device key, replace this value with your value from device profile.
                         "provisionDeviceSecret": config["provision_device_secret"],  # Provision device secret, replace this value with your value from device profile.
                         }
    if config.get("device_name") is not None:
        PROVISION_REQUEST["deviceName"] = config["device_name"]
    provision_client = ProvisionClient(YICONNECT_HOST, YICONNECT_PORT, PROVISION_REQUEST)
    provision_client.provision()  # Request provisioned data
    tb_client = provision_client.get_new_client()  # Getting client with provisioned data
    if tb_client:
        tb_client.on_connect = on_tb_connected  # Setting callback for connect
        tb_client.connect(YICONNECT_HOST, YICONNECT_PORT, 60)
        tb_client.loop_forever()  # Starting infinity loop
    else:
        print("Client was not created!")

HTTP 设备 API

您可以使用HTTP API参考来开发将执行配置请求的设备固件。如前所述,设备可以在注册过程中请求服务器生成凭证或提供其自己的凭证。请参阅下面每个选项的请求/响应和代码示例:

配置请求数据示例:

{
  "deviceName":"DEVICE_NAME",
  "provisionDeviceKey":"PUT_PROVISION_KEY_HERE",
  "provisionDeviceSecret":"PUT_PROVISION_SECRET_HERE"
}

配置响应示例:

{
  "status":"SUCCESS",
  "credentialsType":"ACCESS_TOKEN",
  "credentialsValue":"sLzc0gDAZPkGMzFVTyUY"
}

示例脚本脚本源代码如下。您可以将其复制粘贴到文件中,例如:

device-provision-example.py

现在您应该运行该脚本并按照其中的步骤进行操作。
您可以使用 python 3 启动脚本:

python3 device-provision-example.py

脚本源代码:

from requests import post
from json import dumps


def collect_required_data():
    config = {}
    print("\n\n", "="*80, sep="")
    print(" "*10, "\033[1m\033[94mYiCONNECT device provisioning without authorization example script. HTTP API\033[0m", sep="")
    print("="*80, "\n\n", sep="")
    host = input("Please write your YiCONNECT \033[93mhost\033[0m or leave it blank to use default (https://yiqisoft.cloud): ")
    config["host"] = host if host else "https://yiqisoft.cloud"
    port = input("Please write your YiCONNECT \033[93mHTTP port\033[0m or leave it blank to use default (443): ")
    config["port"] = int(port) if port else 443
    config["provision_device_key"] = input("Please write \033[93mprovision device key\033[0m: ")
    config["provision_device_secret"] = input("Please write \033[93mprovision device secret\033[0m: ")
    device_name = input("Please write \033[93mdevice name\033[0m or leave it blank to generate: ")
    if device_name:
        config["device_name"] = device_name
    print("\n", "="*80, "\n", sep="")
    return config


# Example for message to YiCONNECT
to_publish = {
  "stringKey": "value1",
  "booleanKey": True,
  "doubleKey": 42.0,
  "longKey": 73,
  "jsonKey": {
    "someNumber": 42,
    "someArray": [1, 2, 3],
    "someNestedObject": {"key": "value"}
  }
}

if __name__ == '__main__':

    config = collect_required_data()

    YICONNECT_HOST = config["host"]  # YiCONNECT instance host
    YICONNECT_PORT = config["port"]  # YiCONNECT instance MQTT port

    PROVISION_REQUEST = {"provisionDeviceKey": config["provision_device_key"],  # Provision device key, replace this value with your value from device profile.
                         "provisionDeviceSecret": config["provision_device_secret"],  # Provision device secret, replace this value with your value from device profile.
                         }
    if config.get("device_name") is not None:
        PROVISION_REQUEST["deviceName"] = config["device_name"]
    response = post("%s:%i/api/v1/provision" % (YICONNECT_HOST, YICONNECT_PORT), json=PROVISION_REQUEST)
    decoded_response = response.json()
    print("Received response: ")
    print(decoded_response)
    received_token = decoded_response.get("credentialsValue")
    if received_token is not None:
        response = post('%s:%i/api/v1/%s/telemetry' % (YICONNECT_HOST, YICONNECT_PORT, received_token,), dumps(to_publish))
        print("[YICONNECT CLIENT] Response code from YiCONNECT.")
        print(response.status_code)
    else:
        print("Failed to get access token from response.")
        print(decoded_response.get("errorMsg"))

CoAP 设备 API

您可以使用CoAP API参考来开发将执行配置请求的设备固件。 如前所述,设备可以在注册过程中请求服务器生成凭证或提供其自己的凭证。请参阅下面每个选项的请求/响应和代码示例:

配置请求数据示例:

{
  "deviceName":"DEVICE_NAME",
  "provisionDeviceKey":"PUT_PROVISION_KEY_HERE",
  "provisionDeviceSecret":"PUT_PROVISION_SECRET_HERE"
}

配置响应示例:

{
  "status":"SUCCESS",
  "credentialsType":"ACCESS_TOKEN",
  "credentialsValue":"sLzc0gDAZPkGMzFVTyUY"
}

示例脚本为了与 YiCONNECT 进行通信,我们将使用 asyncio 和 aiocoap 模块,因此我们应该安装它:

pip3 install asyncio aiocoap --user

脚本源代码如下。您可以将其复制粘贴到文件中,例如:

device-provision-example.py

现在您应该运行该脚本并按照其中的步骤进行操作。
您可以使用 python 3 启动脚本:

python3 device-provision-example.py

脚本源代码:

import logging
import asyncio

from aiocoap import Context, Message, Code
from json import loads, dumps

YICONNECT_HOST = ""
YICONNECT_PORT = ""

logging.basicConfig(level=logging.INFO)


def collect_required_data():
    config = {}
    print("\n\n", "="*80, sep="")
    print(" "*10, "\033[1m\033[94mYiCONNECT device provisioning without authorization example script. CoAP API\033[0m", sep="")
    print("="*80, "\n\n", sep="")
    host = input("Please write your YiCONNECT \033[93mhost\033[0m or leave it blank to use default (yiqisoft.cloud): ")
    config["host"] = host if host else "coap.yiqisoft.cloud"
    port = input("Please write your YiCONNECT \033[93mCoAP port\033[0m or leave it blank to use default (5683): ")
    config["port"] = int(port) if port else 5683
    config["provision_device_key"] = input("Please write \033[93mprovision device key\033[0m: ")
    config["provision_device_secret"] = input("Please write \033[93mprovision device secret\033[0m: ")
    device_name = input("Please write \033[93mdevice name\033[0m or leave it blank to generate: ")
    if device_name:
        config["device_name"] = device_name
    print("\n", "="*80, "\n", sep="")
    return config


# Example for message to YiCONNECT
to_publish = {
  "stringKey": "value1",
  "booleanKey": True,
  "doubleKey": 42.0,
  "longKey": 73,
  "jsonKey": {
    "someNumber": 42,
    "someArray": [1, 2, 3],
    "someNestedObject": {"key": "value"}
  }
}


async def process():
    server_address = "coap://" + YICONNECT_HOST + ":" + str(YICONNECT_PORT)

    client_context = await Context.create_client_context()
    await asyncio.sleep(2)
    try:
        msg = Message(code=Code.POST, payload=str.encode(dumps(PROVISION_REQUEST)), uri=server_address+'/api/v1/provision')
        request = client_context.request(msg)
        try:
            response = await asyncio.wait_for(request.response, 60000)
        except asyncio.TimeoutError:
            raise Exception("Request timed out!")

        if response is None:
            raise Exception("Response is empty!")

        decoded_response = loads(response.payload)
        logging.info("Received response: %s", decoded_response)
        received_token = decoded_response.get("credentialsValue")
        if received_token is not None:
            msg = Message(code=Code.POST, payload=str.encode(dumps(to_publish)),
                          uri=server_address+('/api/v1/%s/telemetry' % received_token))
            request = client_context.request(msg)
            try:
                response = await asyncio.wait_for(request.response, 60000)
            except asyncio.TimeoutError:
                raise Exception("Request timed out!")

            if response:
                logging.info("[YICONNECT CLIENT] Response from YiCONNECT.")
                logging.info(response)
            else:
                raise Exception("[YICONNECT CLIENT] Cannot save telemetry with received credentials!")
        else:
            logging.error("Failed to get access token from response.")
            logging.error(decoded_response.get("errorMsg"))
    except Exception as e:
        logging.error(e)
    finally:
        await client_context.shutdown()

if __name__ == '__main__':

    config = collect_required_data()

    YICONNECT_HOST = config["host"]  # YiCONNECT instance host
    YICONNECT_PORT = config["port"]  # YiCONNECT instance port

    PROVISION_REQUEST = {"provisionDeviceKey": config["provision_device_key"],  # Provision device key, replace this value with your value from device profile.
                         "provisionDeviceSecret": config["provision_device_secret"],  # Provision device secret, replace this value with your value from device profile.
                         }
    if config.get("device_name") is not None:
        PROVISION_REQUEST["deviceName"] = config["device_name"]

    asyncio.run(process())