diff --git a/api/provisioningapi.yaml b/api/provisioningapi.yaml new file mode 100644 index 0000000..e9ed6cf --- /dev/null +++ b/api/provisioningapi.yaml @@ -0,0 +1,88 @@ +openapi: "3.0.2" +info: + title: Provisioning REST API + description: Provisioning REST API + contact: + email: info@openremote.io + license: + name: AGPL 3.0 + url: https://www.gnu.org/licenses/agpl-3.0.en.html + version: "1.0" +servers: + - url: "https://demo.openremote.io/api/{realm}" + variables: + realm: + default: master +security: + - bearerAuth: [] +paths: + /device: + post: + operationId: provisionDevice + summary: Provisions a new device for the user + description: Provisions a new device, creating both an Asset to represent it and a Service Account for the device to connect over MQTT. The asset type is selected based on the provided modelName. If none matches, an error is reported. + requestBody: + $ref: "#/components/requestBodies/DeviceProvisioning" + responses: + '200': + $ref: "#/components/responses/DeviceProvisioningResponse" + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' +components: + securitySchemes: + bearerAuth: # arbitrary name for the security scheme + type: http + scheme: bearer + requestBodies: + DeviceProvisioning: + content: + application/json: + schema: + type: object + required: + - deviceId + - password + - modelName + properties: + deviceId: + type: string + description: ID of device + example: "123456789ABC" + password: + type: string + description: Password for service account + example: "s3cr3t" + modelName: + type: string + description: Model of device to provision + example: "orbattery" + responses: + UnauthorizedError: + description: Access token is missing or invalid + ForbiddenError: + description: Principal is not allowed to perform this operation + DeviceProvisioningResponse: + description: Information about a successfully provisioned device + content: + application/json: + schema: + type: object + required: + - assetId + - assetCreated + - serviceUserCreated + properties: + assetId: + type: string + description: OpenRemote Asset ID + example: "6tXbmMvQLd2e7UrY5xLviR" + assetCreated: + type: boolean + description: Indicates if the battery asset was created by this call (true) or already existed (false) + example: true + serviceUserCreated: + type: boolean + description: Indicates if the service user was created by this call (true) or already existed (false) + example: true \ No newline at end of file diff --git a/docs/architecture/apps-and-consoles.md b/docs/architecture/apps-and-consoles.md index 92ff4cd..baccea8 100644 --- a/docs/architecture/apps-and-consoles.md +++ b/docs/architecture/apps-and-consoles.md @@ -327,3 +327,322 @@ Start the camera and scan a QR code. } } ``` + +### ESP Provision (provider: "espprovision") +Allows provisioning an ESP32 device in the system via a 3-step workflow: +1. discover the device and establish a secure communication to the device over BLE +2. discover Wi-Fi networks and configure the device to connect to it +3. provision the device in the backend and configure the device to connect to the backend + +This is based on Espressif [Unified Provisioning](https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/provisioning/provisioning.html) +and uses their [esp-idf-provisioning-ios](https://github.com/espressif/esp-idf-provisioning-ios) and [esp-idf-provisioning-android](https://github.com/espressif/esp-idf-provisioning-android) libraries. + +#### Start BLE scan (App -> Console) + +Starts a BLE scans without any timeout. + +```json +{ + "provider": "espprovision", + "action": "START_BLE_SCAN", + "prefix": "PROV_" +} +``` + +The prefix value is optional, if not specified a default empty value is used. + +The returned devices are filtered based on their service name having the given prefix. + +#### BLE scan error response (Console -> App) + +If there’s an error starting or during the scan, the following message is sent + +```json +{ + "provider": "espprovision", + "action": "STOP_BLE_SCAN", + "errorCode": , + "errorMessage": "An optional detail message about the error, not meant for end-user" +} +``` + +All errors will be reported to the web application and not handled by the native code. + +| Error | errorCode | Reason | +|----------------|-----------|-------------------------------------------------------------------------------------------------------| +| Timeout | 600 | A timeout (120s) occurred during device search (even if some devices were already found and reported) | +| Generic error | 10000 | A non specific error has occurred | + +#### BLE scan response (Console -> App) + +Periodically during the scan, if the provider has found BLE devices, it will send the complete list to the web app using the below structure + +```json +{ + "provider": "espprovision", + "action": "START_BLE_SCAN", + "devices": [ + { + "id": "", + "name": "", + "signalStrength": "" + }, ... + ] +} +``` +signalStrength is optional, it will not be present in a first version. + +#### Stop BLE scan (App -> Console) + +Stops on-going BLE scans, calling this if none is underway is not an error. + +```json +{ + "provider": "espprovision", + "action": "STOP_BLE_SCAN" +} +``` + +#### BLE scan stop response (Console -> App) + +Always sends back a confirmation message, even if no scan was underway. +This message is also sent when the scan is stopped upon connection to a device. + +```json +{ + "provider": "espprovision", + "action": "STOP_BLE_SCAN" +} +``` + +#### Connect to device (App -> Console) + +Establishes a secure connection to the device with the given id. + +This also stops any BLE scan that was in progress. + +```json +{ + "provider": "espprovision", + "action": "CONNECT_TO_DEVICE", + "id": "", + "pop": "xyz" +} +``` + +The pop (Proof of Possession) is used for establishing the security layer of the device communication channel. +If not provided, a default value is used. + +#### Device connection status (Console -> App) + +Connection status information can be sent at any time (and multiple times), +e.g. if at any point the BLE connection is lost, message with status `disconnected` is sent. + +```json +{ + "provider": "espprovision", + "action": "CONNECT_TO_DEVICE", + "id": "", + "status": "connected" | "disconnected" | "connectionError", + "errorCode": , + "errorMessage": "An optional detail message about the error, not meant for end-user" +} +``` + +Possible error codes +| Error | errorCode | Reason | +|----------------------|-----------|---------------------------------------------------------------------------| +| Unknown device | 100 | The provided id was not discovered in the previous search | +| BLE connection error | 200 | Error establishing a BLE connection with the device | +| Communication error | 301 | Error establishing a connection with the device (on top of BLE connection)| +| Security error | 400 | Error while handling security (includes invalid credentials) | +| Generic error | 10000 | A non specific error has occurred | + +#### Disconnect from device (App -> Console) + +```json +{ + "provider": "espprovision", + "action": "DISCONNECT_FROM_DEVICE" +} +``` + +#### Start Wi-Fi scan (App -> Console) + +Asks the connected device to start a Wi-Fi scan without any timeout. + +```json +{ + "provider": "espprovision", + "action": "START_WIFI_SCAN" +} +``` + +#### Wi-Fi scan error response (Console -> App) + +If there’s an error starting or during the scan, the following message is sent +```json +{ + "provider": "espprovision", + "action": "STOP_WIFI_SCAN", + "errorCode": , + "errorMessage": "An optional detail message about the error, not meant for end-user" +} +``` + +| Error | errorCode | Reason | +|---------------------|-----------|-----------------------------------------------------------------------------------------------------| +| Not connected | 300 | There is no communication channel with the device | +| Communication error | 301 | Error in communication with device to start scan or receive information back | +| Timeout | 600 | A timeout (120s) occurred during Wi-Fi scan (even if some networks were already found and reported) | + +#### Wi-Fi scan response (Console -> App) + +Periodically during the scan, if the provider has found SSIDs, it will send the complete list to the web app using the below structure + +```json +{ + "provider": "espprovision", + "action": "START_WIFI_SCAN", + "networks": [ + { + "ssid": "", + "signalStrength": -12 + }, ... + ] +} +``` + +#### Stop Wi-Fi scan (App -> Console) + +Stops on-going Wi-Fi scans, calling this if none is underway is not an error. + +```json +{ + "provider": "espprovision", + "action": "STOP_WIFI_SCAN" +} +``` + +#### Stop Wi-Fi scan response (Console -> App) + +Always sends back a confirmation message, even if no scan was underway. +```json +{ + "provider": "espprovision", + "action": "STOP_WIFI_SCAN" +} +``` +Implementation note: there is no command to stop the Wi-Fi scan on the device, but it only does it for a limited amount of time. +The provider is the one implementing a loop to scan “indefinitely”. +When the STOP_WIFI_SCAN command is sent, the provider stops this loop. + +#### Send Wi-Fi configuration (App -> Console) + +Sends SSID and password to the device for it to configure its Wi-Fi network. + +This also stops any Wi-Fi scan that was in progress. + +```json +{ + "provider": "espprovision", + "action": "SEND_WIFI_CONFIGURATION", + "ssid": "", + "password": "" +} +``` + +#### Wi-Fi configuration response (Console -> App) + +Once device has reported its status, the following information is sent by the provider + +```json +{ + "provider": "espprovision", + "action": "SEND_WIFI_CONFIGURATION", + "connected": true | false, + "errorCode": , + "errorMessage": "An optional detail message about the error, not meant for end-user" +} +``` +| Error | errorCode | Reason | +|---------------------------|-----------|--------------------------------------------------------------------------| +| Not connected | 300 | There is no communication channel with the device | +| Communication error | 301 | Error in communication with device to start scan or receive information back | +| Wi-Fi configuration error | 500 | Error in applying the provided Wi-Fi configuration | +| Wi-Fi communication error | 501 | Could not determine the status of the Wi-Fi network | +| Wi-Fi authentication error | 502 | Wrong Wi-Fi credentials | +| Wi-Fi network not found | 503 | Could not find given Wi-Fi network | +| Generic error | 10000 | A non specific error has occurred | + +#### Provision device (App -> Console) +```json +{ + "provider": "espprovision", + "action": "PROVISION_DEVICE", + "userToken": "" +} +``` + +Provisions the device with the backend. A valid authentication token towards the backend, +as the user logged in to the app, must be provided in `userToken`. + +The provider will perform all the required steps to accomplish this and notify back the status to the caller. + +This includes: +- Getting the model name and device id from the device +- Generating a random password +- Provisioning the device with the backend, creating the required asset and service account. + The provider performs a `POST` on `/rest/device` to trigger this. +- Linked the created asset to the end user account (corresponding to userToken) +- Sending the required configuration to the device +- Waiting for the device to connect + +#### Provision device response (Console -> App) + +Once the device is connected to the backend or if connection failed, the following information is sent by the provider + +```json +{ + "provider": "espprovision", + "action": "PROVISION_DEVICE", + "connected": true | false, + "errorCode": , + "errorMessage": "An optional detail message about the error, not meant for end-user" +} +``` + +| Error | errorCode | Reason | +|---------------------|-----------|------------------------------------------------------------------------------| +| Not connected | 300 | There is no communication channel with the device | +| Communication error | 301 | Error in communication with device to start scan or receive information back | + +#### Exit provisioning (App -> Console) + +```json +{ + "provider": "espprovision", + "action": "EXIT_PROVISIONING" +} +``` +Asks the device to exit provisioning mode. + +#### Exit provisioning response (Console -> App) + +Once the device is out of provisioning mode, the following information is sent by the provider + +```json +{ + "provider": "espprovision", + "action": "EXIT_PROVISIONING", + "exit": true | false, + "errorCode": , + "errorMessage": "An optional detail message about the error, not meant for end-user" +} +``` + +| Error | errorCode | Reason | +|---------------------|-----------|------------------------------------------------------------------------------| +| Not connected | 300 | There is no communication channel with the device | +| Communication error | 301 | Error in communication with device to start scan or receive information back | +| Generic error | 10000 | A non specific error has occurred | diff --git a/docs/architecture/esp32-device.md b/docs/architecture/esp32-device.md new file mode 100644 index 0000000..41eccdc --- /dev/null +++ b/docs/architecture/esp32-device.md @@ -0,0 +1,107 @@ +--- +sidebar_position: 5 +--- + +# ESP32 devices + +Many IoT devices are or can be based on an ESP32 microcontroller. +A typical setup for integrating such a device in the OpenRemote ecosystem includes 3 components: +- a firmware running on the ESP32 MCU in the device +- a mobile app to configure the device and connect to the backend +- the OpenRemote backend + +```mermaid +graph LR + %% Styling Definitions + classDef greenStyle fill:#d4edda,stroke:#28a745,stroke-width:2px,color:#000; + classDef innerGreenStyle fill:#e8f5e9,stroke:#28a745,stroke-width:1px,color:#000; + classDef orangeStyle fill:#ffe0b2,stroke:#f57c00,stroke-width:2px,color:#000; + classDef redStyle fill:#ffcdd2,stroke:#c62828,stroke-width:2px,color:#000; + classDef purpleStyle fill:#e1bee7,stroke:#7b1fa2,stroke-width:2px,color:#000; + + App[App]:::orangeStyle + + OR[Backend]:::greenStyle + + ESP[Device]:::purpleStyle + + %% Connections + App <--> OR + App <--> ESP + ESP <--> OR +``` + +We offer software elements to support the development of all 3 components. + +A device is represented in the OpenRemote backend by an asset of a specific type. +The device communicates with OpenRemote over MQTTS, authenticated with a dedicated service user. + +In this typical use case, the device uses Wi-Fi for its internet connectivity. + +To integrate a new device into the system, it needs to be provisioned. +This can either be done automatically, see [User Guide Auto provisioning](/user-guide/gateways-and-devices/auto-provisioning.md) +or through a manual process performed by the end-user. + +For the latter case, the workflow is as follows + +```mermaid +sequenceDiagram + autonumber + + Note over User,Backend: User login + + User->>App: Login + App->>Backend: Authenticate user + Backend-->>App: Authenticated + + Note over User,Backend: Device Wi-Fi provisioning + + User->>Device: Put in discovery mode + App->>App: Start device scan + Device-->>App: Device name + User->>App: select device + App->>App: Stop device scan + App->>Device: Connect + Device->>App: Get PoP + App-->>Device: PoP + App->>Device: Start Wi-Fi scan + Device-->>App: Wi-Fi information + User->>App: Select Wi-Fi (or join other) + User->>App: Enter Wi-Fi password + App->>Device: Wi-Fi configuration + Device->>App: Wi-Fi connection status + + Note over User,Backend: Device provisioning + + App->>Device: Get Device Id (DeviceInfo Request) + Device-->>App: Device Id (DeviceInfo Response) + + App->>App: Create password + App->>Backend: Provision device + Note right of App: App provides Device Id and password + + Backend->>Backend: Create Device Asset + Note right of Backend: Asset is linked to user account + + Backend->>Backend: Create Service Account + Note right of Backend: Service Account is restricted
and only has rights on the Device Asset.
username is Device Id + + Backend-->>App: Confirmed + Note right of Backend: Returns Asset Id + + App->>Device: Send configuration (OpenRemoteConfig Request) + Device->>Backend: connect + Note right of Device: Over MQTTS using username/password
(Service Account) + + Device-->>App: Status (OpenRemoteConfig Response) + App->>Device: Get connection status (BackendConnectionStatus Request) + Device-->>App: connection status (BackendConnectionStatus Response) + Note right of Device: Repeat to poll for connected status +``` + +Communication between the Mobile Application and the Device is based on Espressif [Unified Provisioning](https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/provisioning/provisioning.html). +This mechanism is used to discover the device, then establish a secure communication channel over BLE. +Communication on this channel uses Protocol Buffer payloads, in addition to the messages defined by Espressif, OpenRemote uses messages defined in the following ProtoBuf spec: [ORConfigChannelProtocol](https://github.com/openremote/console-ios-lib/blob/7212bc905c7df34c2f3d62f801f0e4df7529a2f0/ORLib/ORConfigChannelProtocol.proto) +OpenRemote includes the [ESP Provision provider](apps-and-consoles.md#esp-provision-provider-espprovision) to support the implementation of the mobile application side. + +On the backend, the project must implement a single `/rest/device` endpoint, see [Provision Device API](../provisioning-api/provision-device.api.mdx) for more details. diff --git a/docs/provisioning-api/provision-device.api.mdx b/docs/provisioning-api/provision-device.api.mdx new file mode 100644 index 0000000..a962596 --- /dev/null +++ b/docs/provisioning-api/provision-device.api.mdx @@ -0,0 +1,71 @@ +--- +id: provision-device +title: "Provisions a new device for the user" +description: "Provisions a new device, creating both an Asset to represent it and a Service Account for the device to connect over MQTT. The asset type is selected based on the provided modelName. If none matches, an error is reported." +sidebar_label: "Provisions a new device for the user" +hide_title: true +hide_table_of_contents: true +api: eJzdVk1v20YQ/SuLOSUAQ8lWPlqeqthBIcBuFFsGWgg+jMiRuMlyl5ldShYE/fdiltSX1dRAkVN1oqjZmbfvzbzRBlxNjEE7Oyogg5rdUnvt7DUtdU6QQEE+Z11LBGQw3v3uFSpLK1XEuETlTBi0XaiZC6VCq4beU1DBKaaayZMNSgeFtlCo7onllBrmuWtsUHPHKpTUJZNDubOW8qDckljdfplMUjUpSWGbdF2T0l55MpQHKtQMPRXK2ZgkXqGgQlWuIPMHVpSq0VxZZ0lVGPKSfCIAidmxpGGqHQcqUkiA6XtDPnx0xRqyDeTOBrJBHrGujc4jU72vXsjYgM9LqlCeBBJk4GZfKQ9dHs1UQDaF9lajAhKo0fuVY3nco4PHRGiviYMmL8n2Bw6JfWBtF2dyjK6Vm3e0QQL0hFVtJP7icvD23fsPv/w6/HgF26PCL6Ycd5FRFN8Jha1QJyX8IOdBkOSHq7yY/VZCD5hF6n3LnWR3PMMQiNewjR+h1NfO+paiy36/ZeqEDTt3XEWJFM5cExQq3+Q5eT9vjFkfSlFxIO2naBwbM0ocn65kGki+dgw+eOLdyzO9d4dfZO9zTfaOKheoG6/R9Qlp78Ofs+p2+eWmuKQPD/zXu6ebpb4TiU5QHerMnDOE9ryvbCE8kFd6HoeqE6MbwBX6duBl9tYqlNqrHI1RrwI39Fo5VmiYsFgretJewl7N0Xh6fQxXYrf/yNB/ALjr1MYT/0x8XfO97V+c99swtpYK7htZMZJKey8WKLZil2h0AfHo4PzomLXNdY1GzlkXFBrjVlTEgSCWPm5h7+0ZBEZFoXTRp50P0U9CCRn09s0sNBB7yKYbaNhABmUItc96vYIql7qaLMcGSrXrYa17GyY01RYSWCJrnJm2JePbFvYcGxMggwp9IJZxfJQ6ecM6rGOhGSETDxvBMn08/vlehqfNeBy0l1fACeoYJnLHoHhVmb+7gxt/2qlybI+nNnfscjt3OjGnY1NJQNu5i1B0iMOzX20i4d2n+4kajkc/XoDPosRFMI8uQhVqYV4K/HbCuJQ1Oifr40Vsi2r4+/hGDdI+JM8kW61W6cI2qeNFrzvme7iozZtB2k/JpmWojOQUyVt4F2k/2r3zoUJ7VOQHi3u/emVunl/2aP/97xd/15CBnkKvNqit0Bjl2HRTtlvk4t+ljF82hc1GEDyw2W7l9fdGeiubPh6mKU5EAiVhQRyn5RutIYOrltk3E6kr4aaR+mcbaJvsTojb1OFfYx+PHGL8+X4CCcy6fzLCCWTAuJLNhSvIABJwUel24uXdBgzaRYMLiW1zyudvH2aGvg== +sidebar_class_name: "post api-method" +info_path: docs/provisioning-api/provisioning-rest-api +custom_edit_url: null +--- + +import MethodEndpoint from "@theme/ApiExplorer/MethodEndpoint"; +import ParamsDetails from "@theme/ParamsDetails"; +import RequestSchema from "@theme/RequestSchema"; +import StatusCodes from "@theme/StatusCodes"; +import OperationTabs from "@theme/OperationTabs"; +import TabItem from "@theme/TabItem"; +import Heading from "@theme/Heading"; + + + + + + + + + + +Provisions a new device, creating both an Asset to represent it and a Service Account for the device to connect over MQTT. The asset type is selected based on the provided modelName. If none matches, an error is reported. + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/provisioning-api/provisioning-rest-api.info.mdx b/docs/provisioning-api/provisioning-rest-api.info.mdx new file mode 100644 index 0000000..04ceea1 --- /dev/null +++ b/docs/provisioning-api/provisioning-rest-api.info.mdx @@ -0,0 +1,98 @@ +--- +id: provisioning-rest-api +title: "Provisioning REST API" +description: "Provisioning REST API" +sidebar_label: Introduction +sidebar_position: 0 +hide_title: true +custom_edit_url: null +--- + +import ApiLogo from "@theme/ApiLogo"; +import Heading from "@theme/Heading"; +import SchemaTabs from "@theme/SchemaTabs"; +import TabItem from "@theme/TabItem"; +import Export from "@theme/ApiExplorer/Export"; + + + + + + + + + +Provisioning REST API + +
+ + + + + + + +
+ + + + + + + + +
+ Security Scheme Type: + + http +
+ HTTP Authorization Scheme: + + bearer +
+
+
+
+
+

+ Contact +

+ [info@openremote.io](mailto:info@openremote.io) + +
+

+ License +

+ AGPL 3.0 + +
+ \ No newline at end of file diff --git a/docs/provisioning-api/sidebar.ts b/docs/provisioning-api/sidebar.ts new file mode 100644 index 0000000..41b6979 --- /dev/null +++ b/docs/provisioning-api/sidebar.ts @@ -0,0 +1,24 @@ +import type { SidebarsConfig } from "@docusaurus/plugin-content-docs"; + +const sidebar: SidebarsConfig = { + apisidebar: [ + { + type: "doc", + id: "provisioning-api/provisioning-rest-api", + }, + { + type: "category", + label: "UNTAGGED", + items: [ + { + type: "doc", + id: "provisioning-api/provision-device", + label: "Provisions a new device for the user", + className: "api-method post", + }, + ], + }, + ], +}; + +export default sidebar.apisidebar; diff --git a/docusaurus.config.ts b/docusaurus.config.ts index 916020e..7264770 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -77,6 +77,16 @@ const config: Config = { hideSendButton: false, showSchemas: true, } satisfies OpenApiPlugin.Options, + provisioningapi: { + specPath: "api/provisioningapi.yaml", + outputDir: "docs/provisioning-api", + sidebarOptions: { + groupPathsBy: "tag", + categoryLinkSource: "tag", + }, + hideSendButton: false, + showSchemas: true, + } satisfies OpenApiPlugin.Options, } satisfies Plugin.PluginOptions, }, ],