设备配置
用例描述
作为设备制造商或固件开发人员,我希望我的设备能够在 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 将首先检查提供的ProvisionDeviceKey和ProvisionDeviceSecret以查找相应的Device Profile。找到配置文件后,平台将使用配置的配置策略来验证设备名称。有两种可用的供应策略:
- 允许创建新设备 - 检查同名设备尚未在 YiCONNECT 中注册。当您在制造过程中不知道唯一设备名称(MAC 地址等)列表,但设备本身可以访问固件中的此信息时,此策略非常有用。它更容易实现,但安全性不如第二种策略。
- 检查预配置的设备 - 检查是否已在 YiCONNECT 中创建同名设备,但尚未配置。当您只想允许配置特定设备列表时,此策略非常有用。假设您在制造过程中收集了唯一 IDS(MAC 地址等)列表。您可以使用[批量配置将此列表上传到 YiCONNECT。现在,列表中的设备可以发出配置请求,并且其他设备将无法自行配置。
配置完成后,YiCONNECT 将更新设备的配置状态服务器属性并将其设置为配置值。
设备配置文件配置
您应该配置设备配置文件以启用配置功能、收集配置设备密钥和配置设备密钥。
- 您可以创建新的设备配置文件或打开现有的设备配置文件。要创建新的,您应该打开设备配置文件页面并单击表标题中的“+”图标。
- 输入新设备配置文件的名称,然后单击“添加设备配置文件”向导的步骤 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())