├── .github └── workflows │ └── publish.yml ├── CONFIGURE.md ├── DEPLOY.md ├── LICENSE ├── README.md ├── Release_notes.md ├── USE.md ├── azureiotdevice ├── azureiotdevice.html ├── azureiotdevice.js ├── examples │ └── azure-iot-device-example.json └── icons │ └── azureiotdevice.png ├── images ├── IoTC-plugandplay-mobile-flow.png ├── IoTC-plugandplay-mobile-template.png ├── custom-provisioning-tab-00.png ├── device-identity-tab-00.png ├── direct-methods-tab-00.png ├── example-flow.png ├── iot-edge-gateway-tab-00.png └── node.png └── package.json /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to NPM 2 | on: 3 | workflow_dispatch 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Checkout 9 | uses: actions/checkout@v2 10 | - name: Setup Node 11 | uses: actions/setup-node@v2 12 | with: 13 | node-version: '14.x' 14 | registry-url: 'https://registry.npmjs.org' 15 | - run: yarn install 16 | - run: npm publish --access public 17 | env: 18 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 19 | -------------------------------------------------------------------------------- /CONFIGURE.md: -------------------------------------------------------------------------------- 1 | # Configure the Azure IoT Device Node 2 | In this document we describe how to configure the Node-RED Azure IoT Device node for first use. 3 | 4 | ## Good to know 5 | The Azure IoT Device Node-RED node can be configured as an Azure IoT Device using multiple attestation methods and provisioning. In this document we will not explain the details of Azure IoT provisioning, attestation methods, and using X.509 certificates, but we will explain what you need to use as node settings to enable them. 6 | 7 | >For more information on the subjects above read: 8 | >* Manual provisioning with [Azure IoT Hub](https://docs.microsoft.com/en-us/azure/iot-hub/). 9 | > * Using the Azure Portal, Azure CLI, Visual Studio Code, Powershell or the Azure IoT Services SDK. 10 | >* Automated provisioning using [Azure IoT Device Provisioning Services](https://docs.microsoft.com/en-us/azure/iot-dps/). 11 | >* [Device Authentication using X.509 CA Certificates](https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-x509ca-overview). 12 | >* [Tutorial: Using Microsoft-supplied scripts to create test certificates](https://docs.microsoft.com/en-us/azure/iot-hub/tutorial-x509-scripts) 13 | 14 | The Azure IoT Device node can either use a symmetric key or a X.509 certificate as the attestation mechanism. More information on using symmetric key or X.509 certificates can be found in the Azure IoT Hub and Device Provisioning Services documentation online. 15 | 16 | ## Create and configure an Azure IoT Device Node-RED node 17 | Once you installed Node-RED and the Azure IoT Device Node-RED node, run Node-RED and browse to the Node-RED website on your machine, f.i. <node-red-machine>:1880. 18 | In the nodes section on the left-hand side, scroll down to the bottom where you will find the Azure IoT Device node: 19 | 20 | ![Azure IoT Device node](images/node.png) 21 | 22 | ### Steps to configure the node 23 | Like any other Node-RED node you can create an instance of this node by dragging it onto a flow. Once you've dragged it into the flow you have to configure the node as a specific Azure IoT device. In this section we will explain the different setting-tabs and how you can use them to define the behavior of the Azure IoT device. 24 | 25 | ### Device Identity 26 | You need to use the Device Identity tab to define the device identity and the way it connects to Azure IoT. Depending on the choices you make in the drop-down boxes you will see additional fields and options. 27 | 28 | ![Device identity tab](images/device-identity-tab-00.png) 29 | 30 | ##### Fields and options 31 | The following table contains explanation of the fields and options on the **Device Indentity** tab. Some fileds will only be visible depending on selection choices made in other fields. 32 | | Field/option | Description | Depends on | 33 | | --- | --- | --- | 34 | | Registration/Device ID | This field will contain the registration/device ID of the Azure IoT device as it is/will be on IoT Hub or IoT Central. When using Azure DPS and individual enrollment, this field must contain the registraton ID in DPS.| - | 35 | | PnP Model ID | This field will contain the PnP Model ID for the Azure IoT device. The PnP Model ID will be used to setup the device as a [PnP Device](https://docs.microsoft.com/en-us/azure/iot-pnp/overview-iot-plug-and-play). What the reported properties or telemetry should look like when sending, can be found [here](https://docs.microsoft.com/en-us/azure/iot-pnp/concepts-convention). | - | 36 | | Connection Type | This option indicates whether the device will use a preset connection string or use [Device Provisioning Service](https://docs.microsoft.com/en-us/azure/iot-dps/). Depending on the option you select different fields will be shown to fill. | - | 37 | | Authentication Method | Azure IoT supports two authentication types, SAS token-based authentication and X.509 certificate authentication (individual and CA based). | - | 38 | | IoT Hub Hostname | The [Azure IoT Hub](https://docs.microsoft.com/en-us/azure/iot-hub/) Hostname '%iothubname%.azure-device.net'. The name can be found in the Azure portal on the overview page of the IoT Hub. | Connection Type | 39 | | IoT Central Device | Option to indicate whether the device is an [Azure IoT Central](https://docs.microsoft.com/en-us/azure/iot-central/) device. | Connection Type | 40 | | Scope ID | The device provisioning Scope ID. This ID identifies the specific Azure Device Provisioning service to use. Azure IoT Central devices can only be provisioned using Device Provisioning. | Connection Type | 41 | | Enrollment type | Selection to indicate whether the device provisioning enrollment is an individual or a group enrollment. | Connection Type | 42 | | Authentication Method | Selection to indicate whether the attestation method used is shared access key (SAS) or certificate (X.509). | - | 43 | | SAS Key | The [symmetric key](https://docs.microsoft.com/en-us/azure/iot-dps/concepts-symmetric-key-attestation) to authenticate with the Device Provisioning Service (DPS) instance or Azure IOT Central. If the connection type is "connecting string", this is the SAS in the device properties. If the connection type is "dps", this is the SAS found in the enrollment properties. | Authentication Method | 44 | | X.509 Certificate | The [X.509](https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-x509ca-overview) certifate, containing the full chain of the certificate tree for the IoT device. The certificate must be uploaded. | Authentication Method | 45 | | X.509 Key | The Azure IoT device X.509 key. The key must be uploaded. | Authentication Method | 46 | | Protocol | The Azure IoT platform supports three communication [protocols](https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-protocols): HTTPS, MQTT and AMQP. AMQP and MQTT can also be used over websockets. For this node you can only select the MQTT and AMQP, because these protocols support direct communicaton between device and cloud. If you need to use the 443 port outbound from the device, you can use the websockets options. | - | 47 | 48 | > NB: Currently only CA based X.509 certificates (self-signed or from a certificate authority) are supported. Individual device certificates are not. Both individual and group SAS Keys are supported. 49 | 50 | ### Custom Provisioning 51 | The Custom Provisioning tab can be used to define additional payload data for Azure Device Provisioning Services. This [additonal data](https://docs.microsoft.com/en-us/azure/iot-dps/how-to-send-additional-data) can be used for custom provisioning using Azure Device Provisioning Services. The payload fields on this tab will only be visible if the Connection Type is set to "Device provisioning service". The data needs to be a valid JSON structure. 52 | 53 | ![Custom Provisioning tab](images/custom-provisioning-tab-00.png) 54 | 55 | ### Direct Methods 56 | The [Direct Methods](https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-direct-methods) tab is used to indicate which methods are supported by the Azure IoT Device node. This will also require to create processing flows fror each method supported. This tab contain a table where direct methods can be added or deleted. 57 | 58 | ![Direct Methods tab](images/direct-methods-tab-00.png) 59 | 60 | Direct methods can be used from the cloud side to trigger an activity on the device, which requires a request-response pattern. 61 | 62 | ##### Fields and options 63 | Direct Methods items can be added or deleted in this tab. Each item will require: 64 | - Direct Method name 65 | 66 | ### Iot Edge Gateway 67 | The **Iot Edge Gateway** tab enables you to setup your device as a downstream devcie to an IoT Edge device. More information on how to setup an [IoT Edge as a transparant gateway](https://docs.microsoft.com/en-us/azure/iot-edge/how-to-create-transparent-gateway) and use a device as a [downstream device](https://docs.microsoft.com/en-us/azure/iot-edge/how-to-authenticate-downstream-device). All code to enable the downstream device is already implemented in the node. The node just needs to right hostname and certificate to enable to connection between the device and the IoT Edge. 68 | 69 | ![Iot Edge Gateway tab](images/iot-edge-gateway-tab-00.png) 70 | 71 | ##### Fields 72 | The following table contains explanation of the fields and options on the **IoT Edge Gateway** tab. 73 | 74 | | Field | Description | 75 | | --- | --- | 76 | | Hostname | This field will contain the IoT Edge hostname. The hostname identifies a specific IoT Edge. | 77 | | X.509 Gateway CA Certificate | The IoT Edge X.509 CA certificate. This certificate will be used to enable encrypted and trusted communication between the device and the IoT Edge. The certificate must be uploaded. | 78 | 79 | ## Finalize 80 | Once you've configured the device, you need to deploy the node in Node-RED. If the setup was correct, the device will connect to the Azure IoT platform. In the debug window you can see whether the configuration was correct and the device was able to provision and connect to the Azure IoT platform. How to use the node can be found [here](./USE.md). 81 | -------------------------------------------------------------------------------- /DEPLOY.md: -------------------------------------------------------------------------------- 1 | # Deploy the Azure IoT Device node 2 | In the document we describe how to deploy the Node-RED Azure IoT Device node. 3 | 4 | ## Prerequisites 5 | - Node-RED needs to be installed on your machine to use this node 6 | - Git needs to be installed for the manual setup approach. 7 | - NPM needs to be installed 8 | 9 | ## Manually deploy the node to your Node-RED instance 10 | The node can be manually deployed using the Github repository code and 'npm install <folder>' command. How to get started with Node-RED can be found here: https://nodered.org/docs/getting-started/. 11 | 12 | Steps to install this node: 13 | 14 | 1. Clone this repository to a local directory on your machine running Node-RED: 15 | 16 | ``` 17 | git clone https://github.com/iotblackbelt/node-red-contrib-azure-iot-device.git 18 | ``` 19 | 20 | 1. In your Node-RED user directory, typically ~/.node-red, run: 21 | 22 | ``` 23 | npm install 24 | ``` 25 | 26 | For example, on Mac OS or Linux, if the node is located at ~/my-nodes/node-red-contrib-azure-iot-device you would do the following: 27 | 28 | ``` 29 | cd ~/.node-red 30 | npm install ~/my-nodes/node-red-contrib-azure-iot-device 31 | ``` 32 | 33 | On Windows you would do: 34 | 35 | ``` 36 | cd C:\Users\\.node_red 37 | npm install \node-red-contrib-azure-iot-device 38 | ``` 39 | 40 | This creates a symbolic link to your node module project directory in ~/.node-red/node_modules so that Node-RED will discover the node when it starts. Any changes to the node’s file can be picked up by simply restarting Node-RED. On Windows, use npm 5.x or greater. 41 | 42 | 1. Restart Node-RED. The Azure IoT Device node will be available in the Azure IoT nodes section. 43 | 44 | >Note : npm will automatically add an entry for your module in the package.json file located in your user directory. If you don't want it to do this, use the --no-save option to the npm install command. 45 | 46 | ## Deploy the node using npm 47 | Run command in Node-RED installation directory. 48 | 49 | ``` 50 | npm install node-red-contrib-azure-iot-device 51 | ``` 52 | 53 | or run command for global installation. 54 | 55 | ``` 56 | npm install -g node-red-contrib-azure-iot-device 57 | ``` 58 | 59 | ## Install in Node-Red by managing the palette 60 | 61 | Node-RED lets users manage their pallete by installing, removing, disabling or upgrading modules. 62 | 63 | In Node-RED, open the menu and select 'Manage Pallete'. In the new window, select the 'Install' tab. Search for the module by typing in 'azure-iot-edge' in 'search modules'. The 'node-red-contrib-azure-iot-device' module will appear in the list. Select 'Install'. Confirm the installation. 64 | 65 | Once installed, the 'Device' node can be found in the section named 'Azure IoT'. 66 | 67 | ## Next step 68 | The next step is to [configure](./CONFIGURE.md) the node to represent a specific Azure IoT device. 69 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Eric van Uum 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ARCHIVED - REPOSITORY IS NOT MAINTAINED 2 | 3 | # Azure IoT Device Node-RED node 4 | The Azure IoT Device Node-RED node is a node that can be used to connect Node-RED to the Azure IoT platform. It can connect to Azure IoT Hub, Azure IoT Central, and use Azure IoT Edge as a transparant gateway. The node has been created to support the different attestation methods (SAS, X.509) as well as use Azure Device Provisioning Service. The node has been developed using the [Azure IoT Node.js SDK](https://github.com/Azure/azure-iot-sdk-node/). 5 | 6 | The Azure IoT Device node represents a **single device** on the Azure IoT platform. 7 | 8 | > NB: It is our assumption that you have a basic understanding of [Node-RED](https://nodered.org/) and the [Azure IoT platform](https://azure.microsoft.com/en-us/product-categories/iot/). 9 | 10 | ## Deploy the Azure Device node 11 | In the [deploy](https://github.com/iotblackbelt/node-red-contrib-azure-iot-device/blob/master/DEPLOY.md) document we describe how to deploy the node to Node-RED. 12 | 13 | ## Configure an Azure IoT Device node 14 | In the [configure](https://github.com/iotblackbelt/node-red-contrib-azure-iot-device/blob/master/CONFIGURE.md) document we describe how to setup an individual Azure IoT Device node. 15 | 16 | ## Use an Azure IoT Device node 17 | In the [use](https://github.com/iotblackbelt/node-red-contrib-azure-iot-device/blob/master/USE.md) document we describe how to use the Azure IoT Device node to interact with the Azure IoT platform. 18 | - sending telemetry 19 | - receiving and responding to commands 20 | - receiving desired properties 21 | - updating reported properties 22 | - receiving C2D messages 23 | 24 | ## Resilience 25 | The node is developed to resist network loss. If there is no connectivity, the node will try to re-establish a device connection based on the set retry interval. The node doesn't store and forward. All message and properties sent during a loss of connectivity, will be lost. 26 | -------------------------------------------------------------------------------- /Release_notes.md: -------------------------------------------------------------------------------- 1 | # Release notes 2 | 3 | ## Release 0.2.6: 4 | - Updates: 5 | - Updated to latest version of Azure IOT SDK (1.9.0 for device) 6 | 7 | ## Release 0.2.5: 8 | - Bug fixes: 9 | - [#27](https://github.com/iotblackbelt/node-red-contrib-azure-iot-device/issues/27): Incorrect azure-iot-device-amqp version in 0.2.4? 10 | - Wrong version in package.json, has been fixed 11 | 12 | ## Release 0.2.4: 13 | - Bug fixes: 14 | - [#26](https://github.com/iotblackbelt/node-red-contrib-azure-iot-device/issues/26): Why is SAS key visible in logging bug? 15 | - Connection string logging removed. 16 | - [#25](https://github.com/iotblackbelt/node-red-contrib-azure-iot-device/issues/25): New device on another flow: message "leakage" between devices? 17 | - Bug in node code, has been fixed 18 | - [#24](https://github.com/iotblackbelt/node-red-contrib-azure-iot-device/issues/24): TypeError exception with desired properties message 19 | - Bug in node code, has been fixed 20 | - [#21](https://github.com/iotblackbelt/node-red-contrib-azure-iot-device/issues/21): Node-Red device fails provisioning using CA certificates 21 | - Bug in node code, has been fixed. Be aware only CA based X.509 certificates (self-signed or from a certificate authority) are supported. 22 | 23 | - Updates: 24 | - Automatic response on desired property changes for IoT Central and PnP devices has been removed. It is up to the flow-logic in Node-Red to confirm receipt. The change has been made because the logic in Node-Red should determine whether action is taken based on desired property updates. Similar to the command (direct method) approach. Examples of root and component the confirmation messages are added to the [use](https://github.com/iotblackbelt/node-red-contrib-azure-iot-device/blob/master/USE.md) document. 25 | - Documentation: 26 | - CONFIGURE.md added: "Currently only CA based X.509 (group) certificates are supported. Individual device certificates are not. Both individual and group SAS Keys are supported." 27 | - USE.md added: description of "Azure IoT Central and PnP require a specific reported property" 28 | -------------------------------------------------------------------------------- /USE.md: -------------------------------------------------------------------------------- 1 | # Use the Azure IoT Device Node 2 | In this document we describe how to use the Azure IoT Device node for D2C and C2D communication. 3 | 4 | ## Supported interactions 5 | The Azure Device node can be used for: 6 | - sending telemetry 7 | - receiving and responding to commands 8 | - receiving desired properties 9 | - updating reported properties 10 | - receiving C2D messages 11 | 12 | Each interaction will be described in detail in the next sections. The node has a single input and a single output. The node determines the actions to take, based on the topic of Node-RED message. 13 | 14 | ## Output topics 15 | The following output topics are supported: 16 | - 'provisioning' - the outcome of the provisioning process 17 | - 'property' - desired properties received 18 | - 'command' - a received direct method 19 | - 'message' - a received C2D message 20 | 21 | ### Output provisioning message 22 | When the node is deployed using Device Provisioning a node message in the following format will be received on the output, when the provisioning is successful: 23 | 24 | ``` 25 | { 26 | 'topic':'provisioning', 27 | 'deviceId':'', 28 | 'payload': { 29 | 'registrationId':'', 30 | 'createdDateTimeUtc':'UTC_timestamp>', 31 | 'assignedHub':'.azure-devices.net', 32 | 'deviceId':'', 33 | 'status':'', 34 | 'substatus':'', 35 | 'lastUpdatedDateTimeUtc':'', 36 | 'etag':'', 37 | 'payload': { } /* optional */ 38 | } 39 | } 40 | ``` 41 | 42 | You can use this provisioning message to do additional processing on the device side. If you use [custom allocation policies](https://docs.microsoft.com/en-us/azure/iot-dps/how-to-use-custom-allocation-policies) you can return a [payload to the device](https://docs.microsoft.com/en-us/azure/iot-dps/how-to-send-additional-data) that can be used for processing. 43 | 44 | ### Output property message 45 | When you update the desired properties on a device twin in Azure, the node will send a node message to the output containing the updated desired properties: 46 | 47 | ``` 48 | { 49 | 'topic':'property', 50 | 'deviceId':'device_id', 51 | 'payload': { 52 | '': , 53 | ... 54 | '$version': 55 | } 56 | } 57 | ``` 58 | 59 | You can use this message to do additional processing on the device. 60 | 61 | > Azure IoT Central and PnP expect a response on a desired property change. See [Input reported properties](./USE.md#input-reported-properties) for the message structure. 62 | 63 | ### Output command message 64 | When you send a direct method to the device from Azure, the node will send a node message to the output containing the command and its parameters: 65 | 66 | ``` 67 | { 68 | 'topic':'command', 69 | 'deviceId':'device_id', 70 | 'requestId': '', 71 | 'methodName': '', 72 | 'payload': { 73 | '': , 74 | ... 75 | } 76 | } 77 | ``` 78 | 79 | You can use this message to do additional processing on the device. 80 | > The Azure IoT Platform requires a response on a direct method. To send this response you can use the [Input a command response](./USE.md#input-a-command-response). Make sure you use the same request_id, to ensure command and response correlation. 81 | 82 | ### Output C2D message 83 | When you send a C2D message to the device from Azure, the node will send a node message to the output containing the message and its parameters: 84 | 85 | ``` 86 | { 87 | "topic":"message", 88 | "deviceId":"node-red-21", 89 | "messageId":"", 90 | "data":"", 91 | "properties": { 92 | "propertyList": 93 | [] 94 | } 95 | } 96 | ``` 97 | 98 | You can use this message to do additional processing on the device. A message doesn't require a response to be send back. 99 | 100 | ## Input topics 101 | The following input topics are supported: 102 | - 'telemetry' - telemetry to be send 103 | - 'property' - properties to report 104 | - 'response' - a response to a received direct method 105 | 106 | 107 | ### Input telemetry 108 | Sending telemetry, including (optional) properties, requires you to create a connection to the node input and send a node message in the following format: 109 | 110 | ``` 111 | { 112 | 'topic': 'telemetry', 113 | 'payload': { 114 | '': , 115 | ... 116 | }, 117 | 'properties': [ 118 | {'key':'','value':}, 119 | ... 120 | ] 121 | } 122 | ``` 123 | 124 | ### Input reported properties 125 | Sending reported properties requires you to create a connection to the node input and send a node message in the following format: 126 | 127 | ``` 128 | { 129 | 'topic': 'property', 130 | 'payload': { 131 | '': , 132 | ... 133 | } 134 | } 135 | ``` 136 | 137 | #### Azure IoT Central and PnP require a specific reported property format to be send. 138 | 139 | The format for a root level desired property change reponse is: 140 | ``` 141 | { 142 | 'topic': 'property', 143 | 'payload': { 144 | '': { 145 | 'value': , 146 | 'ac': , 147 | 'ad': '', 148 | 'av': 149 | } 150 | } 151 | } 152 | ``` 153 | 154 | The format for a component level desired property change reponse is: 155 | ``` 156 | { 157 | 'topic': 'property', 158 | 'payload': { 159 | '': { 160 | '': , 161 | '__t': 'c' 162 | } 163 | } 164 | } 165 | ``` 166 | 167 | ### Input a command response 168 | Sending a command response requires you to create a connection to the node input and send a node message in the following format: 169 | 170 | ``` 171 | { 172 | 'topic': 'response', 173 | 'payload': { 174 | 'requestId': '', 175 | 'methodName': '', 176 | 'status': , 177 | 'payload': 178 | } 179 | } 180 | ``` 181 | 182 | The request_id is provided by the received direct method, and will need to be returned to the Azure IoT Device node to establish the connection between command and response. 183 | The status property is the device-supplied status of method execution. The payload can be anything that is considered an expexted response for the actual direct method. 184 | 185 | ## Catch errors 186 | All node errors are catchable. See [Handling errors](https://nodered.org/docs/user-guide/handling-errors#catchable-errors) to understand catchable errors and how to use them. 187 | 188 | ## Example flow 189 | Here is an example flow that implements all input topics and an automated response to a direct method. You only have to [configure](./CONFIGURE.md) your node to represent a device. Once you've configured the node and deployed the flow, the telemetry will be send every 5 seconds and the reported properties can be send by clicking the **Trigger reported properties**. 190 | 191 | ![Azure IoT Device node example](images/example-flow.png) 192 | 193 | Paste the following code into the "Import nodes" dialog. 194 | ```json 195 | [{"id":"3f287933.370c26","type":"azureiotdevice","z":"8b925e1d.d4bae","deviceid":"","pnpModelid":"","connectiontype":"","authenticationmethod":"","iothub":"","isIotcentral":false,"scopeid":"","enrollmenttype":"","saskey":"","certname":"","keyname":"","protocol":"mqtt","methods":[{"name":"blink"}],"DPSpayload":"","gatewayHostname":"","caname":"","cert":"","key":"","ca":"","x":740,"y":180,"wires":[["3b324938.562456","91745865.1dcbf8"]]},{"id":"3b324938.562456","type":"debug","z":"8b925e1d.d4bae","name":"Debug output","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":980,"y":140,"wires":[]},{"id":"d25d0bac.bc1648","type":"inject","z":"8b925e1d.d4bae","name":"Trigger telemetry","topic":"","payload":"","payloadType":"date","repeat":"5","crontab":"","once":true,"onceDelay":0.1,"x":240,"y":100,"wires":[["222c0397.643f0c"]]},{"id":"222c0397.643f0c","type":"function","z":"8b925e1d.d4bae","name":"telemetry","func":"msg = {\n 'topic': 'telemetry',\n 'payload': {'humidity': Math.round(10000*Math.random())/100,\n 'temperature': 20 + (Math.round(2500*Math.random())/100),\n 'pressure': 850 + (Math.round(35000*Math.random())/100)\n }\n}\nreturn msg;","outputs":1,"noerr":0,"x":470,"y":100,"wires":[["3f287933.370c26"]]},{"id":"9973fed2.6ba9b","type":"inject","z":"8b925e1d.d4bae","name":"Trigger reported properties","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":220,"y":180,"wires":[["3a505702.e59f38"]]},{"id":"3a505702.e59f38","type":"function","z":"8b925e1d.d4bae","name":"properties","func":"msg = {\n 'topic': 'property',\n 'payload': { 'fanSpeed': {'value':120},\n 'voltage': {'value':5},\n 'current': {'value':55},\n 'irSwitch': {'value':true}\n }\n}\nreturn msg;","outputs":1,"noerr":0,"x":470,"y":180,"wires":[["3f287933.370c26"]]},{"id":"91745865.1dcbf8","type":"function","z":"8b925e1d.d4bae","name":"command response","func":"if (msg.topic == \"command\") {\n var responseMsg = {\n \"topic\": \"response\",\n \"payload\": {\n \"requestId\": msg.payload.requestId,\n \"methodName\": msg.payload.methodName,\n \"status\": 200,\n \"payload\": msg.payload.payload\n }\n };\n return responseMsg;\n}\n","outputs":1,"noerr":0,"x":710,"y":300,"wires":[["3f287933.370c26"]]}] 196 | ``` 197 | 198 | ## Example flow: Azure IoT Central - IoT Plug and Play mobile template 199 | Here is an example that implements the Azure IoT Central - IoT Plug and Play mobile template. You have to [configure](./CONFIGURE.md) your node to represent an Azure IoT Central device that is based on the IoT Plug and Play mobile template. 200 | 201 | ![Azure IoT Central - IoT Plug and Play mobile template](images/IoTC-plugandplay-mobile-template.png) 202 | 203 | The flow will look like this: 204 | 205 | ![Azure IoT Central - IoT Plug and Play mobile flow](images/IoTC-plugandplay-mobile-flow.png) 206 | 207 | Paste the following code into the "Import nodes" dialog. 208 | ```json 209 | [{"id":"32776004.8f91f","type":"azureiotdevice","z":"52c072a8.81e16c","deviceid":"","pnpModelid":"","connectiontype":"dps","authenticationmethod":"sas","iothub":"","isIotcentral":true,"scopeid":"","enrollmenttype":"group","saskey":"","certname":"","keyname":"","passphrase":"","protocol":"mqtt","retryInterval":"4","methods":[{"name":"lightOn"},{"name":"sensors*enableSensors"},{"name":"sensors*changeInterval"}],"DPSpayload":"","gatewayHostname":"","caname":"","cert":"","key":"","ca":"","x":760,"y":200,"wires":[["c614bf9a.bfe9","12c63560.b421bb"]]},{"id":"c614bf9a.bfe9","type":"debug","z":"52c072a8.81e16c","name":"Debug output","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1040,"y":160,"wires":[]},{"id":"c5cbee63.8acc","type":"inject","z":"52c072a8.81e16c","name":"Trigger sensor telemetry","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"5","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":280,"y":120,"wires":[["98321138.42501"]]},{"id":"98321138.42501","type":"function","z":"52c072a8.81e16c","name":"sensor telemetry","func":"msg = {\n 'topic': 'telemetry',\n 'payload': {\n 'battery': Math.floor(Math.random() * 101),\n 'accelerometer': {\n 'x': Math.round((Math.random()*101) * 100000)/100000,\n 'y': Math.round((Math.random()*101) * 100000)/100000,\n 'z': Math.round((Math.random()*101) * 100000)/100000\n },\n 'gyroscope': {\n 'x': Math.round((Math.random()*101) * 100000)/100000,\n 'y': Math.round((Math.random()*101) * 100000)/100000,\n 'z': Math.round((Math.random()*101) * 100000)/100000\n },\n 'magnetometer': {\n 'x': Math.round((Math.random()*101) * 100000)/100000,\n 'y': Math.round((Math.random()*101) * 100000)/100000,\n 'z': Math.round((Math.random()*101) * 100000)/100000\n },\n 'barometer': 850 + (Math.round(35000*Math.random())/100),\n 'geolocation': {\n \"lat\": Math.round((Math.random()*.1 + 47.6) * 10000)/10000,\n \"lon\": Math.round((Math.random()*.1 - 122.1) * 10000)/10000,\n \"alt\": Math.floor(Math.random() * 10001)\n }\n },\n 'properties': [\n {'key':'$.sub', 'value': 'sensors'}\n ]\n}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":510,"y":120,"wires":[["32776004.8f91f"]]},{"id":"31337ada.642326","type":"inject","z":"52c072a8.81e16c","name":"Trigger root properties","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":230,"y":200,"wires":[["c5d662d9.e8e51"]]},{"id":"c5d662d9.e8e51","type":"function","z":"52c072a8.81e16c","name":"\"readOnlyProp\" property","func":"msg = {\n 'topic': 'property',\n 'payload': { 'readOnlyProp': 'Node-Red ReadOnlyProp'}\n}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":490,"y":200,"wires":[["32776004.8f91f"]]},{"id":"f10e080b.e5c688","type":"function","z":"52c072a8.81e16c","name":"command response","func":"if (msg.topic == \"command\") {\n var responseMsg = {\n \"topic\": \"response\",\n \"payload\": {\n \"requestId\": msg.payload.requestId,\n \"methodName\": msg.payload.methodName,\n \"status\": 200,\n \"payload\": msg.payload.payload\n }\n };\n return responseMsg;\n}\n","outputs":1,"noerr":0,"initialize":"","finalize":"","x":850,"y":380,"wires":[["32776004.8f91f","d23a7679.c30678"]]},{"id":"c0620f79.e9d71","type":"function","z":"52c072a8.81e16c","name":"writeable properties","func":"\ndelete msg.payload.$version;\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":830,"y":480,"wires":[["32776004.8f91f","6bdc432d.adb32c"]]},{"id":"f0d3aa72.4fe9a8","type":"comment","z":"52c072a8.81e16c","name":"IoT Plug and Play Mobile example","info":"This flow show how to use the Azure IOT Device node to connet to Azure IoT Central, using the Plig and Play Mobile device template.","x":838,"y":76,"wires":[]},{"id":"12c63560.b421bb","type":"switch","z":"52c072a8.81e16c","name":"Switch on topic","property":"topic","propertyType":"msg","rules":[{"t":"cont","v":"command","vt":"str"},{"t":"cont","v":"property","vt":"str"}],"checkall":"false","repair":false,"outputs":2,"x":1060,"y":260,"wires":[["f10e080b.e5c688"],["c0620f79.e9d71"]]},{"id":"acec1054.0ab04","type":"function","z":"52c072a8.81e16c","name":"device info properties","func":"msg = {\n 'topic': 'property',\n 'payload': { \n 'device_info':{\n 'manufacturer': 'IoT Blackbelt',\n 'model': 'node-red-contrib-azure-iot-device',\n 'swVersion': '0.2.5',\n 'osName': 'Node-Red',\n 'processorArchitecture': 'unknown',\n 'processorManufacturer': 'unknown',\n 'totalStorage': '2048000',\n 'totalMemory': '256000'\n }\n }\n}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":480,"y":280,"wires":[["32776004.8f91f"]]},{"id":"8d411fb4.d91e3","type":"inject","z":"52c072a8.81e16c","name":"Trigger device info properties","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":200,"y":280,"wires":[["acec1054.0ab04"]]},{"id":"6bdc432d.adb32c","type":"debug","z":"52c072a8.81e16c","name":"Debug property","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1100,"y":520,"wires":[]},{"id":"d23a7679.c30678","type":"debug","z":"52c072a8.81e16c","name":"Debug command","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1110,"y":420,"wires":[]}] 210 | ``` -------------------------------------------------------------------------------- /azureiotdevice/azureiotdevice.html: -------------------------------------------------------------------------------- 1 | 322 | 323 | 448 | 449 | -------------------------------------------------------------------------------- /azureiotdevice/azureiotdevice.js: -------------------------------------------------------------------------------- 1 | 2 | // Copyright (c) Eric van Uum. All rights reserved. 3 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 4 | 5 | /** 6 | * The "azure-iot-device" node enables you to represent an Azure IoT Device in Node-Red. 7 | * The node provide connecting a device using connection string and DPS 8 | * You can use a full connection string, a SAS key and a X.509 attestation 9 | * 10 | * The device node enables D2C, C2D messages, Direct Methods, Desired and Reported properties. 11 | * You can connect to IoT Edge as a downstream device, IoT Hub and IoT Central. 12 | */ 13 | module.exports = function (RED) { 14 | 'use strict' 15 | const Client = require('azure-iot-device').Client; 16 | const Message = require('azure-iot-device').Message; 17 | 18 | // Only AMQP(WS) or MQTT(WS) used as protocol, no HTTP support 19 | const Protocols = { 20 | amqp: require('azure-iot-device-amqp').Amqp, 21 | amqpWs: require('azure-iot-device-amqp').AmqpWs, 22 | mqtt: require('azure-iot-device-mqtt').Mqtt, 23 | mqttWs: require('azure-iot-device-mqtt').MqttWs 24 | }; 25 | 26 | // Only AMQP(WS) or MQTT(WS) used as protocol, no HTTP support 27 | const ProvisioningProtocols = { 28 | amqp: require('azure-iot-provisioning-device-amqp').Amqp, 29 | amqpWs: require('azure-iot-provisioning-device-amqp').AmqpWs, 30 | mqtt: require('azure-iot-provisioning-device-mqtt').Mqtt, 31 | mqttWs: require('azure-iot-provisioning-device-mqtt').MqttWs 32 | }; 33 | 34 | const SecurityClient = { 35 | x509: require('azure-iot-security-x509').X509Security, 36 | sas: require('azure-iot-security-symmetric-key').SymmetricKeySecurityClient 37 | }; 38 | 39 | const ProvisioningDeviceClient = require('azure-iot-provisioning-device').ProvisioningDeviceClient; 40 | const GlobalProvisoningEndpoint = "global.azure-devices-provisioning.net"; 41 | 42 | const crypto = require('crypto'); 43 | const forge = require('node-forge'); 44 | var pki = forge.pki; 45 | 46 | const { config } = require('process'); 47 | 48 | const statusEnum = { 49 | connected: { fill: "green", shape:"dot", text: "Connected" }, 50 | connecting: { fill: "blue", shape:"dot", text: "Connecting" }, 51 | provisioning: { fill: "blue", shape:"dot", text: "Provisioning" }, 52 | disconnected: { fill: "red", shape:"dot", text: "Disconnected" }, 53 | error: { fill: "grey", shape:"dot", text: "Error" } 54 | }; 55 | 56 | // Setup node-red node to represent Azure IoT Device 57 | function AzureIoTDevice(config) { 58 | // Create the Node-RED node 59 | RED.nodes.createNode(this, config); 60 | 61 | const node = this; 62 | 63 | // Set properties 64 | node.deviceid = config.deviceid; 65 | node.pnpModelid = config.pnpModelid; 66 | node.connectiontype = config.connectiontype; 67 | node.authenticationmethod = config.authenticationmethod; 68 | node.enrollmenttype = config.enrollmenttype; 69 | node.iothub = config.iothub; 70 | node.isIotcentral = config.isIotcentral; 71 | node.scopeid = config.scopeid; 72 | node.saskey = config.saskey; 73 | node.protocol = config.protocol; 74 | node.retryInterval = config.retryInterval; 75 | node.methods = config.methods; 76 | node.DPSpayload = config.DPSpayload; 77 | node.gatewayHostname = config.gatewayHostname; 78 | node.cert = config.cert; 79 | node.key = config.key; 80 | node.passphrase = config.passphrase; 81 | node.ca = config.ca; 82 | // Array to hold the direct method responses 83 | node.methodResponses = []; 84 | //** @type {device.Client} */ this.client; 85 | node.client = null; 86 | //** @type {device.Twin} */ this.twin; 87 | node.twin = null; 88 | 89 | setStatus(node, statusEnum.disconnected); 90 | 91 | // Initiate 92 | initiateDevice(node); 93 | }; 94 | 95 | // Set status of node on node-red 96 | var setStatus = function (node, status) { 97 | node.status({ fill: status.fill, shape: status.shape, text: status.text }); 98 | }; 99 | 100 | // Send catchable error to node-red 101 | var error = function (node, payload, message) { 102 | var msg = {}; 103 | msg.topic = 'error'; 104 | msg.message = message; 105 | msg.payload = payload; 106 | node.error(msg); 107 | } 108 | 109 | // Check if valid PEM cert 110 | function verifyCertificatePem(node, pem) { 111 | try { 112 | // Get the certificate from pem, if successful it is a cert 113 | node.log(node.deviceid + ' -> Verifying PEM Certificate'); 114 | var cert = pki.certificateFromPem(pem); 115 | } catch (err) { 116 | return false; 117 | } 118 | return true; 119 | }; 120 | 121 | // Compute device SAS key 122 | function computeDerivedSymmetricKey(masterKey, regId) { 123 | return crypto.createHmac('SHA256', Buffer.from(masterKey, 'base64')) 124 | .update(regId, 'utf8') 125 | .digest('base64'); 126 | }; 127 | 128 | // Close all listeners 129 | function closeAll(node) { 130 | node.log(node.deviceid + ' -> Closing all clients.'); 131 | try { 132 | node.twin.removeAllListeners(); 133 | node.twin = null; 134 | } catch (err) { 135 | node.twin = null; 136 | } 137 | try { 138 | node.client.removeAllListeners(); 139 | node.client.close((err,result) => { 140 | if (err) { 141 | node.log(node.deviceid + ' -> Azure IoT Device Client close failed: ' + JSON.stringify(err)); 142 | } else { 143 | node.log(node.deviceid + ' -> Azure IoT Device Client closed.'); 144 | } 145 | }); 146 | node.client = null; 147 | } catch (err) { 148 | node.client = null; 149 | } 150 | }; 151 | 152 | // Initiate provisioning and retry if network not available. 153 | function initiateDevice(node) { 154 | 155 | // Ensure resources are reset 156 | node.on('close', function(done) { 157 | closeAll(node); 158 | done(); 159 | }); 160 | 161 | // Listen to node input to send telemetry or reported properties 162 | node.on('input', function (msg) { 163 | if (typeof (msg.payload) === "string") { 164 | //Converting string to JSON Object 165 | msg.payload = JSON.parse(msg.payload); 166 | } 167 | if (msg.topic === 'telemetry') { 168 | sendDeviceTelemetry(node, msg, msg.properties); 169 | } else if (msg.topic === 'property' && node.twin) { 170 | sendDeviceProperties(node, msg); 171 | } else if (msg.topic === 'response') { 172 | node.log(node.deviceid + ' -> Method response received with id: ' + msg.payload.requestId); 173 | sendMethodResponse(node, msg) 174 | } else { 175 | error(node, msg, node.deviceid + ' -> Incorrect input. Must be of type \"telemetry\" or \"property\" or \"response\".'); 176 | } 177 | }); 178 | 179 | // Provision device 180 | node.retries = 0; 181 | provisionDevice(node).then( result => { 182 | if (result) { 183 | // Connect device to Azure IoT 184 | node.retries = 0; 185 | connectDevice(node, result).then( result => { 186 | // Get the twin, throw error if it fails 187 | if (result === null) { 188 | retrieveTwin(node).then( result => { 189 | node.log(node.deviceid + ' -> Device twin retrieved.'); 190 | }).catch( function (err) { 191 | error(node, err, node.deviceid + ' -> Retrieving device twin failed'); 192 | throw new Error(err); 193 | }); 194 | } else { 195 | throw new Error(result); 196 | } 197 | }).catch( function(err) { 198 | error(node, err, node.deviceid + ' -> Device connection failed'); 199 | }); 200 | } else { 201 | throw new Error(result); 202 | } 203 | }).catch( function(err) { 204 | error(node, err, node.deviceid + ' -> Device provisioning failed.'); 205 | }); 206 | }; 207 | 208 | // Provision the client 209 | function provisionDevice(node) { 210 | // Set status 211 | setStatus(node, statusEnum.provisioning); 212 | 213 | // Return a promise to enable retry 214 | return new Promise((resolve,reject) => { 215 | try { 216 | // Log the start 217 | node.log(node.deviceid + ' -> Initiate IoT Device settings.'); 218 | 219 | // Set the security properties 220 | var options = {}; 221 | if (node.authenticationmethod === "x509") { 222 | node.log(node.deviceid + ' -> Validating device certificates.'); 223 | // Set cert options 224 | // verify PEM work around for SDK issue 225 | if (verifyCertificatePem(node, node.cert)) 226 | { 227 | options = { 228 | cert : node.cert, 229 | key : node.key, 230 | passphrase : node.passphrase 231 | }; 232 | } else { 233 | reject("Invalid certificates."); 234 | } 235 | }; 236 | 237 | // Check if connection type is dps, if not skip the provisioning step 238 | if (node.connectiontype === "dps") { 239 | 240 | // Set provisioning protocol to selected (default to AMQP-WS) 241 | var provisioningProtocol = (node.protocol === "amqp") ? ProvisioningProtocols.amqp : 242 | (node.protocol === "amqpWs") ? ProvisioningProtocols.amqpWs : 243 | (node.protocol === "mqtt") ? ProvisioningProtocols.mqtt : 244 | (node.protocol === "mqttWs") ? ProvisioningProtocols.mqttWs : 245 | ProvisioningProtocols.amqpWs; 246 | 247 | // Set security client based on SAS or X.509 248 | var saskey = (node.enrollmenttype === "group") ? computeDerivedSymmetricKey(node.saskey, node.deviceid) : node.saskey; 249 | var provisioningSecurityClient = 250 | (node.authenticationmethod === "sas") ? new SecurityClient.sas(node.deviceid, saskey) : 251 | new SecurityClient.x509(node.deviceid, options); 252 | 253 | // Create provisioning client 254 | var provisioningClient = ProvisioningDeviceClient.create(GlobalProvisoningEndpoint, node.scopeid, new provisioningProtocol(), provisioningSecurityClient); 255 | 256 | // set the provisioning payload (for custom allocation) 257 | var payload = {}; 258 | if (node.DPSpayload) { 259 | // Turn payload into JSON 260 | try { 261 | payload = JSON.parse(node.DPSpayload); 262 | node.log(node.deviceid + ' -> DPS Payload added.'); 263 | } catch (err) { 264 | // do nothing 265 | } 266 | } 267 | 268 | // Register the device. 269 | node.log(node.deviceid + ' -> Provision IoT Device using DPS.'); 270 | if (node.connectiontype === "constr") { 271 | resolve(options); 272 | } else { 273 | provisioningClient.setProvisioningPayload(JSON.stringify(payload)); 274 | provisioningClient.register().then( result => { 275 | // Process provisioning details 276 | node.log(node.deviceid + ' -> DPS registration succeeded.'); 277 | node.log(node.deviceid + ' -> Assigned hub: ' + result.assignedHub); 278 | var msg = {}; 279 | msg.topic = 'provisioning'; 280 | msg.deviceId = result.deviceId; 281 | msg.payload = JSON.parse(JSON.stringify(result)); 282 | node.send(msg); 283 | node.iothub = result.assignedHub; 284 | node.deviceid = result.deviceId; 285 | setStatus(node, statusEnum.disconnected); 286 | resolve(options); 287 | }).catch( function(err) { 288 | // Handle error 289 | error(node, err, node.deviceid + ' -> DPS registration failed.'); 290 | setStatus(node, statusEnum.error); 291 | reject(err); 292 | }); 293 | } 294 | } else { 295 | resolve(options); 296 | } 297 | } catch (err) { 298 | reject("Failed to provision device: " + err); 299 | } 300 | }); 301 | } 302 | 303 | // Initiate an IoT device node in node-red 304 | function connectDevice(node, options){ 305 | // Set status 306 | setStatus(node, statusEnum.connecting); 307 | 308 | // Set provisioning protocol to selected (default to AMQP-WS) 309 | var deviceProtocol = (node.protocol === "amqp") ? Protocols.amqp : 310 | (node.protocol === "amqpWs") ? Protocols.amqpWs : 311 | (node.protocol === "mqtt") ? Protocols.mqtt : 312 | (node.protocol === "mqttWs") ? Protocols.mqttWs : 313 | Protocols.amqpWs; 314 | // Set the client connection string and options 315 | var connectionString = 'HostName=' + node.iothub + ';DeviceId=' + node.deviceid; 316 | // Finalize the connection string 317 | var saskey = (node.connectiontype === "dps" && node.enrollmenttype === "group" && node.authenticationmethod === 'sas') ? computeDerivedSymmetricKey(node.saskey, node.deviceid) : node.saskey; 318 | connectionString = connectionString + ((node.authenticationmethod === 'sas') ? (';SharedAccessKey=' + saskey) : ';x509=true'); 319 | 320 | // Update options 321 | if (node.gatewayHostname !== "") { 322 | node.log(node.deviceid + ' -> Connect through gateway: ' + node.gatewayHostname); 323 | try { 324 | options.ca = node.ca; 325 | connectionString = connectionString + ';GatewayHostName=' + node.gatewayHostname; 326 | } catch (err){ 327 | error(node, err, node.deviceid + ' -> Certificate file error.'); 328 | setStatus(node, statusEnum.error); 329 | }; 330 | } 331 | 332 | // Define the client 333 | node.client = Client.fromConnectionString(connectionString, deviceProtocol); 334 | 335 | // Add pnp modelid to options 336 | if (node.pnpModelid) { 337 | options.modelId = node.pnpModelid; 338 | node.log(node.deviceid + ' -> Set PnP Model ID: ' + node.pnpModelid); 339 | } 340 | 341 | // Return the promise 342 | return new Promise((resolve,reject) => { 343 | // Set the options first and then open the connection 344 | node.client.setOptions(options).then( result => { 345 | node.client.open().then( result => { 346 | // Setup the client 347 | // React or errors 348 | node.client.on('error', function (err) { 349 | error(node, err, node.deviceid + ' -> Device Client error.'); 350 | setStatus(node, statusEnum.error); 351 | }); 352 | 353 | // React on disconnect and try to reconnect 354 | node.client.on('disconnect', function (err) { 355 | error(node, err, node.deviceid + ' -> Device Client disconnected.'); 356 | setStatus(node, statusEnum.disconnected); 357 | closeAll(node); 358 | initiateDevice(node); 359 | }); 360 | 361 | // Listen to commands for defined direct methods 362 | for (let method in node.methods) { 363 | node.log(node.deviceid + ' -> Adding synchronous command: ' + node.methods[method].name); 364 | var mthd = node.methods[method].name; 365 | // Define the method on the client 366 | node.client.onDeviceMethod(mthd, function(request, response) { 367 | node.log(node.deviceid + ' -> Command received: ' + request.methodName); 368 | node.log(node.deviceid + ' -> Command payload: ' + JSON.stringify(request.payload)); 369 | node.send({payload: request, topic: "command", deviceId: node.deviceid}); 370 | 371 | // Now wait for the response 372 | getResponse(node, request.requestId).then( message => { 373 | var rspns = message.payload; 374 | node.log(node.deviceid + ' -> Method response status: ' + rspns.status); 375 | node.log(node.deviceid + ' -> Method response payload: ' + JSON.stringify(rspns.payload)); 376 | response.send(rspns.status, rspns.payload, function(err) { 377 | if (err) { 378 | node.log(node.deviceid + ' -> Failed sending method response: ' + err); 379 | } else { 380 | node.log(node.deviceid + ' -> Successfully sent method response: ' + request.methodName); 381 | } 382 | }); 383 | }) 384 | .catch( function(err){ 385 | error(node, err, node.deviceid + ' -> Failed sending method response: \"' + request.methodName + '\".'); 386 | }); 387 | }); 388 | }; 389 | 390 | // Start listening to C2D messages 391 | node.log(node.deviceid + ' -> Listening to C2D messages'); 392 | // Define the message listener 393 | node.client.on('message', function (msg) { 394 | node.log(node.deviceid + ' -> C2D message received, data: ' + msg.data); 395 | var message = { 396 | messageId: msg.messageId, 397 | data: msg.data.toString('utf8'), 398 | properties: msg.properties 399 | }; 400 | node.send({payload: message, topic: "message", deviceId: node.deviceid}); 401 | node.client.complete(msg, function (err) { 402 | if (err) { 403 | error(node, err, node.deviceid + ' -> C2D Message complete error.'); 404 | } else { 405 | node.log(node.deviceid + ' -> C2D Message completed.'); 406 | } 407 | }); 408 | }); 409 | 410 | node.log(node.deviceid + ' -> Device client connected.'); 411 | setStatus(node, statusEnum.connected); 412 | resolve(null); 413 | }).catch( function(err) { 414 | error(node, err, node.deviceid + ' -> Device client open failed.'); 415 | setStatus(node, statusEnum.error); 416 | reject(err); 417 | }); 418 | }).catch( function(err) { 419 | error(node, err, node.deviceid + ' -> Device options setting failed.'); 420 | setStatus(node, statusEnum.error); 421 | reject(err); 422 | }); 423 | 424 | }); 425 | }; 426 | 427 | // Get the device twin 428 | function retrieveTwin(node){ 429 | // Set the options first and then open the connection 430 | node.log(node.deviceid + ' -> Retrieve device twin.'); 431 | return new Promise((resolve,reject) => { 432 | node.client.getTwin().then( result => { 433 | node.log(node.deviceid + ' -> Device twin created.'); 434 | node.twin = result; 435 | node.log(node.deviceid + ' -> Twin contents: ' + JSON.stringify(node.twin.properties)); 436 | // Send the twin properties to Node Red 437 | var msg = {}; 438 | msg.topic = 'property'; 439 | msg.deviceId = node.deviceid; 440 | msg.payload = JSON.parse(JSON.stringify(node.twin.properties)); 441 | node.send(msg); 442 | 443 | // Get the desired properties 444 | node.twin.on('properties.desired', function(payload) { 445 | node.log(node.deviceid + ' -> Desired properties received: ' + JSON.stringify(payload)); 446 | var msg = {}; 447 | msg.topic = 'property'; 448 | msg.deviceId = node.deviceid; 449 | msg.payload = payload; 450 | node.send(msg); 451 | }); 452 | }).catch(err => { 453 | error(node, err, node.deviceid + ' -> Device twin retrieve failed.'); 454 | reject(err); 455 | }); 456 | }) 457 | }; 458 | 459 | // Send messages to IoT platform (Transparant Edge, IoT Hub, IoT Central) 460 | function sendDeviceTelemetry(node, message, properties) { 461 | if (validateMessage(message.payload)){ 462 | if (message.timestamp && isNaN(Date.parse(message.timestamp))) { 463 | error(node, message, node.deviceid + ' -> Invalid telemetry format: if present, timestamp must be in ISO format (e.g., YYYY-MM-DDTHH:mm:ss.sssZ).'); 464 | } else { 465 | // Create message and set encoding and type 466 | var msg = new Message(JSON.stringify(message.payload)); 467 | // Check if properties set and add if so 468 | if (properties){ 469 | for (let property in properties) { 470 | msg.properties.add(properties[property].key, properties[property].value); 471 | } 472 | } 473 | msg.contentEncoding = 'utf-8'; 474 | msg.contentType = 'application/json'; 475 | // Send the message 476 | if (node.client) { 477 | node.client.sendEvent(msg, function(err, res) { 478 | if(err) { 479 | error(node, err, node.deviceid + ' -> An error ocurred when sending telemetry.'); 480 | setStatus(node, statusEnum.error); 481 | } else { 482 | node.log(node.deviceid + ' -> Telemetry sent: ' + JSON.stringify(message.payload)); 483 | setStatus(node, statusEnum.connected); 484 | } 485 | }); 486 | } else { 487 | error(node, message, node.deviceid + ' -> Unable to send telemetry, device not connected.'); 488 | setStatus(node, statusEnum.error); 489 | } 490 | } 491 | } else { 492 | error(node, message, node.deviceid + ' -> Invalid telemetry format.'); 493 | } 494 | }; 495 | 496 | 497 | // Send device reported properties. 498 | function sendDeviceProperties(node, message) { 499 | if (node.twin) { 500 | node.twin.properties.reported.update(message.payload, function (err) { 501 | if (err) { 502 | error(node, err, node.deviceid + ' -> Sending device properties failed.'); 503 | setStatus(node, statusEnum.error); 504 | } else { 505 | node.log(node.deviceid + ' -> Device properties sent: ' + JSON.stringify(message.payload)); 506 | setStatus(node, statusEnum.connected); 507 | } 508 | }); 509 | } 510 | else { 511 | error(node, message, node.deviceid + ' -> Unable to send device properties, device not connected.'); 512 | } 513 | }; 514 | 515 | // Send device direct method response. 516 | function sendMethodResponse(node, message) { 517 | // Push the reponse to the array 518 | var methodResponse = message.payload; 519 | node.log(node.deviceid + ' -> Creating response for command: ' + methodResponse.methodName); 520 | node.methodResponses.push( 521 | {requestId: methodResponse.requestId, response: message} 522 | ); 523 | }; 524 | 525 | // Get method response using promise, and retry, and slow backoff 526 | function getResponse(node, requestId){ 527 | var retries = 20; 528 | var timeOut = 1000; 529 | // Retrieve client using progressive promise to wait for method response 530 | var promise = Promise.reject(); 531 | for(var i=1; i <= retries; i++) { 532 | promise = promise.catch( function() { 533 | var methodResponse = node.methodResponses.find(function(m){return m.requestId === requestId}); 534 | if (methodResponse){ 535 | // get the response and clean the array 536 | node.methodResponses.splice(node.methodResponses.findIndex(function(m){return m.requestId === requestId}),1); 537 | return methodResponse.response; 538 | } 539 | else { 540 | throw new Error(node.deviceid + ' -> Method Response not received..'); 541 | } 542 | }) 543 | .catch(function rejectDelay(reason) { 544 | return new Promise(function(resolve, reject) { 545 | setTimeout(reject.bind(null, reason), timeOut * ((i % 10) + 1)); 546 | }); 547 | }); 548 | } 549 | return promise; 550 | }; 551 | 552 | // @returns true if message object is valid, i.e., a map of field names to numbers, strings and booleans. 553 | function validateMessage(message) { 554 | if (!message || typeof message !== 'object') { 555 | return false; 556 | } 557 | for (let field in message) { 558 | if (typeof message[field] !== 'number' && typeof message[field] !== 'string' && typeof message[field] !== 'boolean') { 559 | if (typeof message[field] === 'object') 560 | { 561 | validateMessage(message[field]); 562 | } 563 | else { 564 | return false; 565 | } 566 | } 567 | } 568 | return true; 569 | }; 570 | 571 | // Registration of the node into Node-RED 572 | RED.nodes.registerType("azureiotdevice", AzureIoTDevice, { 573 | defaults: { 574 | deviceid: {value: ""}, 575 | pnpModelid: {value: ""}, 576 | connectiontype: {value: ""}, 577 | authenticationmethod: {value: ""}, 578 | enrollmenttype: {value: ""}, 579 | iothub: {value: ""}, 580 | isIotcentral: {value: false}, 581 | scopeid: {value: ""}, 582 | saskey: {value: ""}, 583 | certname: {value: ""}, 584 | keyname: {value: ""}, 585 | passphrase: {value:""}, 586 | protocol: {value: ""}, 587 | retryInterval: {value: 10}, 588 | methods: {value: []}, 589 | DPSpayload: {value: ""}, 590 | isDownstream: {value: false}, 591 | gatewayHostname: {value: ""}, 592 | caname: {value:""}, 593 | cert: {type:"text"}, 594 | key: {type:"text"}, 595 | ca: {type:"text"} 596 | } 597 | }); 598 | 599 | } 600 | -------------------------------------------------------------------------------- /azureiotdevice/examples/azure-iot-device-example.json: -------------------------------------------------------------------------------- 1 | [{"id":"3c650c72.46f314","type":"azureiotdevice","z":"abde64b2.51e538","deviceid":"","connectiontype":"dps","authenticationmethod":"sas","iothub":"","isIotcentral":false,"scopeid":"","enrollmenttype":"group","saskey":"","certname":"","keyname":"","protocol":"mqtt","methods":[{"name":"turnOnLed"},{"name":"blink"},{"name":"turnOffLed"},{"name":"echo"},{"name":"countdown"}],"information":[{"name":"manufacturer","value":"Contoso Inc."}],"gatewayHostname":"","caname":"","x":610,"y":240,"wires":[["152e3d20.3c97d3","647e8204.9689bc"]]},{"id":"152e3d20.3c97d3","type":"debug","z":"abde64b2.51e538","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":890,"y":180,"wires":[]},{"id":"50259ef3.8d50d","type":"inject","z":"abde64b2.51e538","name":"","topic":"","payload":"","payloadType":"date","repeat":"5","crontab":"","once":true,"onceDelay":0.1,"x":190,"y":160,"wires":[["8764f585.5784f8"]]},{"id":"8764f585.5784f8","type":"function","z":"abde64b2.51e538","name":"telemetry","func":"msg = {\n 'topic': 'telemetry',\n 'payload': {'humidity': Math.round(10000*Math.random())/100,\n 'temperature': 20 + (Math.round(2500*Math.random())/100),\n 'pressure': 850 + (Math.round(35000*Math.random())/100)\n }\n}\nreturn msg;","outputs":1,"noerr":0,"x":380,"y":160,"wires":[["3c650c72.46f314"]]},{"id":"fddc2c55.3c7d1","type":"inject","z":"abde64b2.51e538","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":200,"y":240,"wires":[["97da55db.2d8878"]]},{"id":"97da55db.2d8878","type":"function","z":"abde64b2.51e538","name":"properties","func":"msg = {\n 'topic': 'property',\n 'payload': { 'fanSpeed': {'value':120},\n 'voltage': {'value':5},\n 'current': {'value':55},\n 'irSwitch': {'value':true}\n }\n}\nreturn msg;","outputs":1,"noerr":0,"x":380,"y":240,"wires":[["3c650c72.46f314"]]},{"id":"647e8204.9689bc","type":"function","z":"abde64b2.51e538","name":"command response","func":"if (msg.topic == \"command\") {\n var response = {\n \"topic\": \"response\",\n \"requestId\": msg.payload.requestId,\n \"methodName\": msg.payload.methodName,\n \"status\": 200,\n \"payload\": msg.payload.payload\n };\n return response;\n}\n","outputs":1,"noerr":0,"x":630,"y":360,"wires":[["3c650c72.46f314"]]}] -------------------------------------------------------------------------------- /azureiotdevice/icons/azureiotdevice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iotblackbelt/node-red-contrib-azure-iot-device/a2b860fd4d2133c9fb4e6f1d0ef9619cbc1e769b/azureiotdevice/icons/azureiotdevice.png -------------------------------------------------------------------------------- /images/IoTC-plugandplay-mobile-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iotblackbelt/node-red-contrib-azure-iot-device/a2b860fd4d2133c9fb4e6f1d0ef9619cbc1e769b/images/IoTC-plugandplay-mobile-flow.png -------------------------------------------------------------------------------- /images/IoTC-plugandplay-mobile-template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iotblackbelt/node-red-contrib-azure-iot-device/a2b860fd4d2133c9fb4e6f1d0ef9619cbc1e769b/images/IoTC-plugandplay-mobile-template.png -------------------------------------------------------------------------------- /images/custom-provisioning-tab-00.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iotblackbelt/node-red-contrib-azure-iot-device/a2b860fd4d2133c9fb4e6f1d0ef9619cbc1e769b/images/custom-provisioning-tab-00.png -------------------------------------------------------------------------------- /images/device-identity-tab-00.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iotblackbelt/node-red-contrib-azure-iot-device/a2b860fd4d2133c9fb4e6f1d0ef9619cbc1e769b/images/device-identity-tab-00.png -------------------------------------------------------------------------------- /images/direct-methods-tab-00.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iotblackbelt/node-red-contrib-azure-iot-device/a2b860fd4d2133c9fb4e6f1d0ef9619cbc1e769b/images/direct-methods-tab-00.png -------------------------------------------------------------------------------- /images/example-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iotblackbelt/node-red-contrib-azure-iot-device/a2b860fd4d2133c9fb4e6f1d0ef9619cbc1e769b/images/example-flow.png -------------------------------------------------------------------------------- /images/iot-edge-gateway-tab-00.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iotblackbelt/node-red-contrib-azure-iot-device/a2b860fd4d2133c9fb4e6f1d0ef9619cbc1e769b/images/iot-edge-gateway-tab-00.png -------------------------------------------------------------------------------- /images/node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iotblackbelt/node-red-contrib-azure-iot-device/a2b860fd4d2133c9fb4e6f1d0ef9619cbc1e769b/images/node.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-red-contrib-azure-iot-device", 3 | "description": "Node-RED node to connect to the Azure IoT platform as a device.", 4 | "version": "0.2.6", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/iotblackbelt/node-red-contrib-azure-iot-device.git" 8 | }, 9 | "bugs": { 10 | "url": "https://github.com/iotblackbelt/node-red-contrib-azure-iot-device/issues" 11 | }, 12 | "homepage": "https://github.com/iotblackbelt/noderededgemodule", 13 | "engines": { 14 | "node": ">=0.10.0" 15 | }, 16 | "dependencies": { 17 | "azure-iot-device": "^1.18.0", 18 | "azure-iot-device-amqp": "^1.14.0", 19 | "azure-iot-device-mqtt": "^1.16.0", 20 | "azure-iot-provisioning-device": "^1.9.0", 21 | "azure-iot-provisioning-device-mqtt": "^1.8.0", 22 | "azure-iot-provisioning-device-amqp": "^1.9.0", 23 | "azure-iot-security-x509": "^1.8.0", 24 | "azure-iot-security-symmetric-key": "^1.8.0", 25 | "rhea": "^3.0.1", 26 | "node-forge": "^1.3.1" 27 | }, 28 | "keywords": [ 29 | "node-red", 30 | "Azure", 31 | "Azure IoT", 32 | "Azure Iot Hub", 33 | "Azure IoT Central", 34 | "Azure IoT Device Provisioning" 35 | ], 36 | "node-red": { 37 | "nodes": { 38 | "AzureIoTDevice": "azureiotdevice/azureiotdevice.js" 39 | } 40 | }, 41 | "author": { 42 | "name": "Eric van Uum" 43 | }, 44 | "license": "MIT" 45 | } 46 | --------------------------------------------------------------------------------