├── .babelrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── .vscode └── settings.json ├── CLA.md ├── LICENSE ├── README.md ├── docs └── index.html ├── package.json ├── samples ├── BatteryLife │ ├── README.md │ └── src │ │ ├── BatteryLife.js │ │ └── Config_BatteryLife.yaml └── webpack │ └── statusconsole │ ├── .babelrc │ ├── README.md │ ├── package.json │ ├── src │ ├── index.html │ ├── index.js │ └── style.css │ └── webpack.config.js ├── src ├── BaseClient.js ├── BaseConfig.js ├── api │ ├── ApiClient.js │ ├── ApiErrors.js │ ├── DscClient.js │ ├── LecClient.js │ ├── MgmtClient.js │ ├── RegistryClient.js │ ├── RulesClient.js │ └── StateClient.js ├── application │ ├── ApplicationClient.js │ ├── ApplicationConfig.js │ └── index.js ├── device │ ├── DeviceClient.js │ ├── DeviceConfig.js │ └── index.js ├── gateway │ ├── GatewayClient.js │ ├── GatewayConfig.js │ └── index.js ├── index.js └── util.js └── test ├── ApplicationClient.spec.js ├── ApplicationConfig.spec.js ├── ApplicationConfigFile.spec.yaml ├── DeviceClient.spec.js ├── DeviceConfig.spec.js ├── DeviceConfigFile.spec.yaml ├── DscClient.spec.js ├── GatewayClient.spec.js ├── GatewayConfig.spec.js ├── StateClient.spec.js └── incorrectLogLevelTest.spec.yaml /.babelrc: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "presets": [ "@babel/preset-env" ] 4 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .settings 2 | node_modules 3 | coverage 4 | .idea 5 | .DS_Store 6 | dist/iotf-client-bundle.js 7 | dist/iotf-client-bundle.min.js 8 | package-lock.json 9 | IoTFoundation.pem 10 | src/util/IoTFoundation.pem 11 | dist 12 | .nyc_output 13 | instrumented-src 14 | npm-debug.log 15 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .git 2 | .idea 3 | .nyc_output 4 | .settings 5 | .travis.yml 6 | .vscode 7 | coverage 8 | docs 9 | node_modules 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | matrix: 4 | include: 5 | - node_js: "12" 6 | env: RUN_COVERAGE=true 7 | - node_js: "11" 8 | 9 | before_install: 10 | # Upgrade NPM, without this a number of modules will fail to install 11 | - npm i -g npm 12 | 13 | install: 14 | - npm install 15 | 16 | script: 17 | # - npm ls 18 | # Run test, optionally with coverage enabled. Return non-0 if tests fail 19 | - | 20 | if [ -n "$RUN_COVERAGE" ]; then 21 | if npm run test-cov; then 22 | cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js 23 | exit 0 24 | fi 25 | else 26 | if npm run test; then 27 | exit 0 28 | fi 29 | fi 30 | exit 1 31 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "wcaForGP.enable": true 3 | } -------------------------------------------------------------------------------- /CLA.md: -------------------------------------------------------------------------------- 1 | IBM Contributor License Agreement 2 | ================================= 3 | 4 | Version 1.0.0 January 14, 2014 5 | 6 | In order for You (as defined below) to make intellectual property Contributions (as defined below) now or in the future to IBM GitHub repositories, 7 | You must agree to this Contributor License Agreement ("CLA"). 8 | 9 | Please read this CLA carefully before accepting its terms. By accepting the CLA, You are agreeing to be bound by its terms. 10 | If You submit a Pull Request against an IBM repository on GitHub You must include in the Pull Request a statement of Your acceptance of this CLA. 11 | 12 | As used in this CLA: 13 | (i) "You" (or "Your") shall mean the entity that is making this Agreement with IBM; 14 | (ii)"Contribution" shall mean any original work of authorship, including any modifications or additions to an existing work, that is submitted by You to IBM for inclusion in, 15 | or documentation of, any of the IBM GitHub repositories; 16 | (iii) "Submit" (or "Submitted") means any form of communication sent to IBM (e.g. the content You post in a GitHub Issue or submit as part of a GitHub Pull Request). 17 | 18 | This agreement applies to all Contributions You Submit. 19 | 20 | This CLA, and the license(s) associated with the particular IBM GitHub repositories You are contributing to, provides a license to Your Contributions to IBM and downstream consumers, 21 | but You still own Your Contributions, and except for the licenses provided for in this CLA, You reserve all right, title and interest in Your Contributions. 22 | 23 | IBM requires that each Contribution You Submit now or in the future comply with the following four commitments. 24 | 25 | 1) You will only Submit Contributions where You have authored 100% of the content. 26 | 2) You will only Submit Contributions to which You have the necessary rights. This means that if You are employed You have received the necessary permissions from Your employer to make the 27 | Contributions. 28 | 3) Whatever content You Contribute will be provided under the license(s) associated with the particular IBM GitHub repository You are contributing to. 29 | 4) You understand and agree that IBM GitHub repositories and Your contributions are public, and that a record of the contribution (including all personal information You submit with it) 30 | is maintained indefinitely and may be redistributed consistent with the license(s) involved. 31 | You will promptly notify the Eclipse Foundation if You become aware of any facts or circumstances that would make these commitments inaccurate in any way. 32 | To do so, please create an Issue in the appropriate GitHub repository. 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Eclipse Public License, Version 1.0 (EPL-1.0) 2 | 3 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC 4 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM 5 | CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 6 | 7 | 1. DEFINITIONS 8 | 9 | "Contribution" means: 10 | 11 | a) in the case of the initial Contributor, the initial code and documentation 12 | distributed under this Agreement, and 13 | b) in the case of each subsequent Contributor: 14 | i) changes to the Program, and 15 | ii) additions to the Program; 16 | 17 | where such changes and/or additions to the Program originate from and are 18 | distributed by that particular Contributor. A Contribution 'originates' 19 | from a Contributor if it was added to the Program by such Contributor 20 | itself or anyone acting on such Contributor's behalf. Contributions do not 21 | include additions to the Program which: (i) are separate modules of 22 | software distributed in conjunction with the Program under their own 23 | license agreement, and (ii) are not derivative works of the Program. 24 | 25 | "Contributor" means any person or entity that distributes the Program. 26 | 27 | "Licensed Patents" mean patent claims licensable by a Contributor which are 28 | necessarily infringed by the use or sale of its Contribution alone or when 29 | combined with the Program. 30 | 31 | "Program" means the Contributions distributed in accordance with this 32 | Agreement. 33 | 34 | "Recipient" means anyone who receives the Program under this Agreement, 35 | including all Contributors. 36 | 37 | 2. GRANT OF RIGHTS 38 | a) Subject to the terms of this Agreement, each Contributor hereby grants 39 | Recipient a non-exclusive, worldwide, royalty-free copyright license to 40 | reproduce, prepare derivative works of, publicly display, publicly 41 | perform, distribute and sublicense the Contribution of such Contributor, 42 | if any, and such derivative works, in source code and object code form. 43 | b) Subject to the terms of this Agreement, each Contributor hereby grants 44 | Recipient a non-exclusive, worldwide, royalty-free patent license under 45 | Licensed Patents to make, use, sell, offer to sell, import and otherwise 46 | transfer the Contribution of such Contributor, if any, in source code and 47 | object code form. This patent license shall apply to the combination of 48 | the Contribution and the Program if, at the time the Contribution is 49 | added by the Contributor, such addition of the Contribution causes such 50 | combination to be covered by the Licensed Patents. The patent license 51 | shall not apply to any other combinations which include the Contribution. 52 | No hardware per se is licensed hereunder. 53 | c) Recipient understands that although each Contributor grants the licenses 54 | to its Contributions set forth herein, no assurances are provided by any 55 | Contributor that the Program does not infringe the patent or other 56 | intellectual property rights of any other entity. Each Contributor 57 | disclaims any liability to Recipient for claims brought by any other 58 | entity based on infringement of intellectual property rights or 59 | otherwise. As a condition to exercising the rights and licenses granted 60 | hereunder, each Recipient hereby assumes sole responsibility to secure 61 | any other intellectual property rights needed, if any. For example, if a 62 | third party patent license is required to allow Recipient to distribute 63 | the Program, it is Recipient's responsibility to acquire that license 64 | before distributing the Program. 65 | d) Each Contributor represents that to its knowledge it has sufficient 66 | copyright rights in its Contribution, if any, to grant the copyright 67 | license set forth in this Agreement. 68 | 69 | 3. REQUIREMENTS 70 | 71 | A Contributor may choose to distribute the Program in object code form under 72 | its own license agreement, provided that: 73 | 74 | a) it complies with the terms and conditions of this Agreement; and 75 | b) its license agreement: 76 | i) effectively disclaims on behalf of all Contributors all warranties 77 | and conditions, express and implied, including warranties or 78 | conditions of title and non-infringement, and implied warranties or 79 | conditions of merchantability and fitness for a particular purpose; 80 | ii) effectively excludes on behalf of all Contributors all liability for 81 | damages, including direct, indirect, special, incidental and 82 | consequential damages, such as lost profits; 83 | iii) states that any provisions which differ from this Agreement are 84 | offered by that Contributor alone and not by any other party; and 85 | iv) states that source code for the Program is available from such 86 | Contributor, and informs licensees how to obtain it in a reasonable 87 | manner on or through a medium customarily used for software exchange. 88 | 89 | When the Program is made available in source code form: 90 | 91 | a) it must be made available under this Agreement; and 92 | b) a copy of this Agreement must be included with each copy of the Program. 93 | Contributors may not remove or alter any copyright notices contained 94 | within the Program. 95 | 96 | Each Contributor must identify itself as the originator of its Contribution, 97 | if 98 | any, in a manner that reasonably allows subsequent Recipients to identify the 99 | originator of the Contribution. 100 | 101 | 4. COMMERCIAL DISTRIBUTION 102 | 103 | Commercial distributors of software may accept certain responsibilities with 104 | respect to end users, business partners and the like. While this license is 105 | intended to facilitate the commercial use of the Program, the Contributor who 106 | includes the Program in a commercial product offering should do so in a manner 107 | which does not create potential liability for other Contributors. Therefore, 108 | if a Contributor includes the Program in a commercial product offering, such 109 | Contributor ("Commercial Contributor") hereby agrees to defend and indemnify 110 | every other Contributor ("Indemnified Contributor") against any losses, 111 | damages and costs (collectively "Losses") arising from claims, lawsuits and 112 | other legal actions brought by a third party against the Indemnified 113 | Contributor to the extent caused by the acts or omissions of such Commercial 114 | Contributor in connection with its distribution of the Program in a commercial 115 | product offering. The obligations in this section do not apply to any claims 116 | or Losses relating to any actual or alleged intellectual property 117 | infringement. In order to qualify, an Indemnified Contributor must: 118 | a) promptly notify the Commercial Contributor in writing of such claim, and 119 | b) allow the Commercial Contributor to control, and cooperate with the 120 | Commercial Contributor in, the defense and any related settlement 121 | negotiations. The Indemnified Contributor may participate in any such claim at 122 | its own expense. 123 | 124 | For example, a Contributor might include the Program in a commercial product 125 | offering, Product X. That Contributor is then a Commercial Contributor. If 126 | that Commercial Contributor then makes performance claims, or offers 127 | warranties related to Product X, those performance claims and warranties are 128 | such Commercial Contributor's responsibility alone. Under this section, the 129 | Commercial Contributor would have to defend claims against the other 130 | Contributors related to those performance claims and warranties, and if a 131 | court requires any other Contributor to pay any damages as a result, the 132 | Commercial Contributor must pay those damages. 133 | 134 | 5. NO WARRANTY 135 | 136 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN 137 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR 138 | IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, 139 | NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each 140 | Recipient is solely responsible for determining the appropriateness of using 141 | and distributing the Program and assumes all risks associated with its 142 | exercise of rights under this Agreement , including but not limited to the 143 | risks and costs of program errors, compliance with applicable laws, damage to 144 | or loss of data, programs or equipment, and unavailability or interruption of 145 | operations. 146 | 147 | 6. DISCLAIMER OF LIABILITY 148 | 149 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY 150 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, 151 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION 152 | LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 153 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 154 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 155 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY 156 | OF SUCH DAMAGES. 157 | 158 | 7. GENERAL 159 | 160 | If any provision of this Agreement is invalid or unenforceable under 161 | applicable law, it shall not affect the validity or enforceability of the 162 | remainder of the terms of this Agreement, and without further action by the 163 | parties hereto, such provision shall be reformed to the minimum extent 164 | necessary to make such provision valid and enforceable. 165 | 166 | If Recipient institutes patent litigation against any entity (including a 167 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself 168 | (excluding combinations of the Program with other software or hardware) 169 | infringes such Recipient's patent(s), then such Recipient's rights granted 170 | under Section 2(b) shall terminate as of the date such litigation is filed. 171 | 172 | All Recipient's rights under this Agreement shall terminate if it fails to 173 | comply with any of the material terms or conditions of this Agreement and does 174 | not cure such failure in a reasonable period of time after becoming aware of 175 | such noncompliance. If all Recipient's rights under this Agreement terminate, 176 | Recipient agrees to cease use and distribution of the Program as soon as 177 | reasonably practicable. However, Recipient's obligations under this Agreement 178 | and any licenses granted by Recipient relating to the Program shall continue 179 | and survive. 180 | 181 | Everyone is permitted to copy and distribute copies of this Agreement, but in 182 | order to avoid inconsistency the Agreement is copyrighted and may only be 183 | modified in the following manner. The Agreement Steward reserves the right to 184 | publish new versions (including revisions) of this Agreement from time to 185 | time. No one other than the Agreement Steward has the right to modify this 186 | Agreement. The Eclipse Foundation is the initial Agreement Steward. The 187 | Eclipse Foundation may assign the responsibility to serve as the Agreement 188 | Steward to a suitable separate entity. Each new version of the Agreement will 189 | be given a distinguishing version number. The Program (including 190 | Contributions) may always be distributed subject to the version of the 191 | Agreement under which it was received. In addition, after a new version of the 192 | Agreement is published, Contributor may elect to distribute the Program 193 | (including its Contributions) under the new version. Except as expressly 194 | stated in Sections 2(a) and 2(b) above, Recipient receives no rights or 195 | licenses to the intellectual property of any Contributor under this Agreement, 196 | whether expressly, by implication, estoppel or otherwise. All rights in the 197 | Program not expressly granted under this Agreement are reserved. 198 | 199 | This Agreement is governed by the laws of the State of New York and the 200 | intellectual property laws of the United States of America. No party to this 201 | Agreement will bring a legal action under this Agreement more than one year 202 | after the cause of action arose. Each party waives its rights to a jury trial in 203 | any resulting litigation. 204 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IBM Watson IoT Platform Javascript SDK 2 | 3 | [![Build Status](https://travis-ci.org/ibm-watson-iot/iot-nodejs.svg?branch=master)](https://travis-ci.org/ibm-watson-iot/iot-nodejs) 4 | [![Coverage Status](https://coveralls.io/repos/github/ibm-watson-iot/iot-nodejs/badge.svg?branch=master)](https://coveralls.io/github/ibm-watson-iot/iot-nodejs?branch=master) 5 | [![GitHub issues](https://img.shields.io/github/issues/ibm-watson-iot/iot-nodejs.svg)](https://github.com/ibm-watson-iot/iot-nodejs/issues) 6 | [![GitHub](https://img.shields.io/github/license/ibm-watson-iot/iot-nodejs.svg)](https://github.com/ibm-watson-iot/iot-nodejs/blob/master/LICENSE) 7 | 8 | 9 | 10 | ## Installation 11 | 12 | ``` 13 | npm install @wiotp/sdk --save 14 | ``` 15 | 16 | 17 | ## Usage 18 | 19 | ### Application 20 | 21 | ```javascript 22 | import {ApplicationClient, ApplicationConfig} from '@wiotp/sdk'; 23 | 24 | let appConfig = ApplicationConfig.parseEnvVars(); 25 | let appClient = new ApplicationClient(appConfig); 26 | appClient.connect(); 27 | // Do stuff 28 | appClient.disconnect(); 29 | ``` 30 | 31 | ### Device 32 | 33 | ```javascript 34 | import {DeviceClient, DeviceConfig} from '@wiotp/sdk'; 35 | 36 | let deviceConfig = DeviceConfig.parseEnvVars(); 37 | let deviceClient = new DeviceClient(deviceConfig); 38 | deviceClient.connect(); 39 | // Do stuff 40 | deviceClient.disconnect(); 41 | ``` 42 | 43 | ### Gateway 44 | 45 | ```javascript 46 | import {GatewayClient, GatewayConfig} from '@wiotp/sdk'; 47 | 48 | let gwConfig = GatewayConfig.parseEnvVars(); 49 | let gwClient = new GatewayClient(gwConfig); 50 | gwClient.connect(); 51 | // Do stuff 52 | gwClient.disconnect(); 53 | ``` 54 | 55 | ## Development 56 | 57 | ### Build 58 | ``` 59 | npm i 60 | npm run build 61 | ``` 62 | 63 | ## Publish 64 | 65 | ``` 66 | npm login 67 | npm publish . 68 | ``` 69 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | placeholder 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wiotp/sdk", 3 | "version": "0.8.2", 4 | "description": "SDK for developing device, gateway, and application clients for IBM Watson IoT Platform", 5 | "main": "dist/index.js", 6 | "files": [ 7 | "dist", 8 | "src", 9 | "package.json", 10 | "README.md", 11 | "LICENCE" 12 | ], 13 | "publishConfig": { 14 | "access": "public" 15 | }, 16 | "dependencies": { 17 | "axios": "^1.7.9", 18 | "bluebird": "^3.7.2", 19 | "btoa": "^1.2.1", 20 | "concat-stream": "^2.0.0", 21 | "esm": "^3.2.25", 22 | "events": "^3.3.0", 23 | "form-data": "^4.0.1", 24 | "format": "^0.2.2", 25 | "loglevel": "^1.9.2", 26 | "mqtt": "^4.3.8", 27 | "systeminformation": "^5.25.11", 28 | "tinycache": "^1.1.2", 29 | "uuid": "^9.0.1", 30 | "yaml": "2.6.1", 31 | "yargs": "^17.7.2" 32 | }, 33 | "devDependencies": { 34 | "@babel/cli": "^7.26.4", 35 | "@babel/core": "^7.26.0", 36 | "@babel/preset-env": "^7.26.0", 37 | "@babel/register": "^7.25.9", 38 | "@ibm-cloud/cloudant": "^0.8.3", 39 | "@istanbuljs/nyc-config-babel": "^3.0.0", 40 | "babel-plugin-istanbul": "^6.1.1", 41 | "babelify": "^10.0.0", 42 | "browserify": "^17.0.1", 43 | "chai": "^4.5.0", 44 | "chai-as-promised": "^7.1.2", 45 | "coveralls": "3.1.1", 46 | "mocha": "^10.8.2", 47 | "mocha-steps": "^1.3.0", 48 | "nyc": "15.1.0", 49 | "rimraf": "^5.0.10", 50 | "terser": "^5.37.0" 51 | }, 52 | "scripts": { 53 | "clean": "rimraf dist/*", 54 | "build": "npm run clean && npm run build:commonjs && npm run build:bundle && npm run build:min", 55 | "build:commonjs": "babel src --out-dir dist", 56 | "build:bundle": "browserify -t [ babelify --presets [ @babel/preset-env ] --global ] src --outfile dist/bundled/wiotp-bundle.js", 57 | "build:min": "terser dist/bundled/wiotp-bundle.js -o dist/bundled/wiotp-bundle.min.js", 58 | "test": "mocha --require @babel/register --require mocha-steps", 59 | "test-cov": "nyc --reporter=lcov --reporter=text-summary mocha --require mocha-steps", 60 | "test:watch": "mocha --require @babel/register --require mocha-steps --watch" 61 | }, 62 | "author": { 63 | "name": "David Parker", 64 | "email": "parkerda@uk.ibm.com" 65 | }, 66 | "contributors": [ 67 | { 68 | "name": "Tom Klapiscak", 69 | "email": "klapitom@uk.ibm.com" 70 | }, 71 | { 72 | "name": "Bryan Boyd", 73 | "email": "bboyd@us.ibm.com" 74 | }, 75 | { 76 | "name": "Amritha Raj Herle", 77 | "email": "herleraja@gmail.com" 78 | } 79 | ], 80 | "repository": { 81 | "type": "git", 82 | "url": "git+https://github.com/ibm-watson-iot/iot-nodejs.git" 83 | }, 84 | "license": "EPL-1.0", 85 | "nyc": { 86 | "require": [ 87 | "@babel/register" 88 | ], 89 | "all": true, 90 | "include": [ 91 | "src/**/*.js" 92 | ], 93 | "reporter": [ 94 | "lcov", 95 | "text" 96 | ], 97 | "sourceMap": false, 98 | "instrument": true 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /samples/BatteryLife/README.md: -------------------------------------------------------------------------------- 1 | # IBM Watson IoT Platform BatteryLife Device Client 2 | 3 | Device code for sending system utilization data to IBM Watson IoT Platform, powered by [sebhildebrandt/systeminformation](https://github.com/sebhildebrandt/systeminformation). 4 | 5 | > Systeminformation is a library for ightweight collection of 40+ functions to retrieve detailed hardware, system and OS information. 6 | 7 | The following data points are collected by the BatteryLife sample: 8 | * CPU speed (GHz) 9 | * Manufacturer (string) 10 | * Total Memory (bytes) 11 | * Free Memory (bytes) 12 | * Active Memory (bytes) 13 | * Battery Remaining (%) 14 | * Battery Charging Status (boolean) 15 | 16 | A tutorial guiding you through the process of setting up this sample on a Raspberry Pi is published on [IBM Developer developerWorks Recipes](https://developer.ibm.com/recipes/tutorials/raspberry-pi-4/) 17 | 18 | ## Event Format 19 | 20 | - `cpuSpeed` obtained from `Systeminformation.CpuData.speed` 21 | - `manufacturer` obtained from `Systeminformation.SystemData.manufacturer` 22 | - `memory.total` obtained from `Systeminformation.MemData.total` 23 | - `memory.free` calculated using `Systeminformation.MemData.free` 24 | - `memory.active` calculated using `Systeminformation.MemData.active` 25 | - `battery.percent` calculated using `Systeminformation.BatteryData.percent` 26 | - `battery.charging` calculated using `Systeminformation.BatteryData.ischarging` 27 | 28 | ## Before you Begin 29 | 30 | Register a device with IBM Watson IoT Platform. 31 | 32 | For information on how to register devices, see the [Connecting Devices](https://www.ibm.com/support/knowledgecenter/SSQP8H/iot/platform/iotplatform_task.html) topic in the IBM Watson IoT Platform documentation. 33 | 34 | At the end of the registration process, make a note of the following parameters: 35 | - Organization ID 36 | - Type ID 37 | - Device ID 38 | - Authentication Token -------------------------------------------------------------------------------- /samples/BatteryLife/src/BatteryLife.js: -------------------------------------------------------------------------------- 1 | // ***************************************************************************** 2 | // Copyright (c) 2014, 2019 IBM Corporation and other Contributors. 3 | // 4 | // All rights reserved. This program and the accompanying materials 5 | // are made available under the terms of the Eclipse Public License v1.0 6 | // which accompanies this distribution, and is available at 7 | // http://www.eclipse.org/legal/epl-v10.html 8 | // ***************************************************************************** 9 | 10 | import {DeviceClient, DeviceConfig} from '@wiotp/sdk'; 11 | // or you can use import {DeviceClient, DeviceConfig} from '/Users/*InsertYourPathHere/GitHub/iot-nodejs/src/index.js'; 12 | import { v4 as uuidv4 } from 'uuid'; 13 | const si = require('systeminformation') 14 | const argv = require('yargs') 15 | .option('quickstart', { 16 | alias: 'q', 17 | description: 'Connects the sample device to quickstart', 18 | type: 'boolean', 19 | }) 20 | .option('configFile', { 21 | alias: 'cfg', 22 | description: 'Connects the sample device using the Config_Sample.yaml file', 23 | type: 'config', 24 | }) 25 | .help() 26 | .alias('help', 'h') 27 | .epilogue("If neither the quickstart or configFile parameter is provided the device will attempt to parse the configuration from environment variables.") 28 | .argv; 29 | 30 | let deviceConfig = null; 31 | let deviceClient = null; 32 | let data = { 33 | "cpuSpeed": null, 34 | "manufacturer": null, 35 | "memory": { 36 | "total":null, 37 | "free": null, 38 | "active": null, 39 | }, 40 | "battery": { 41 | "percent":null, 42 | "charging": null, 43 | } 44 | }; 45 | 46 | 47 | if (argv.quickstart) { 48 | let identity = {orgId:"quickstart", typeId:"nodejsSample", deviceId:uuidv4()}; 49 | let options = { 50 | logLevel: "info", 51 | }; 52 | let auth = null; //As quickstart does not support authentication 53 | deviceConfig = new DeviceConfig(identity, auth, options) 54 | startClient() 55 | console.log("\x1b[35m"); //Text formatting 56 | console.log("Welcome to IBM Watson IoT Platform Quickstart, view a vizualization of live data from this device at the URL below:"); 57 | console.log("https://quickstart.internetofthings.ibmcloud.com/#/device/"+(identity["deviceId"])+"/sensor/"); 58 | console.log("\x1b[0m"); //Text formatting 59 | } 60 | else if(argv.configFile) 61 | { 62 | deviceConfig = DeviceConfig.parseConfigFile("Config_Sample.yaml"); 63 | startClient(); 64 | } 65 | else 66 | { 67 | // Intialize using: 68 | // export WIOTP_IDENTITY_ORGID=myOrg 69 | // export WIOTP_IDENTITY_TYPEID=myType 70 | // export WIOTP_IDENTITY_DEVICEID=myDevice 71 | // export WIOTP_AUTH_TOKEN=myToken 72 | deviceConfig = DeviceConfig.parseEnvVars(); 73 | startClient(); 74 | } 75 | 76 | function startClient(){ 77 | deviceClient = new DeviceClient(deviceConfig); 78 | deviceClient.connect(); 79 | console.log("Press {ctrl + c} to disconnect at any time.") 80 | var interval = 2 81 | setInterval(sendInformation, interval*1000) 82 | } 83 | 84 | function sendInformation() { 85 | si.cpu().then(cpuData => { 86 | data['cpuSpeed'] = cpuData.speed 87 | }) 88 | si.system().then(system =>{ 89 | data['manufacturer'] = system.manufacturer 90 | }) 91 | si.mem().then(memData => { 92 | var memoryRef = data['memory'] 93 | memoryRef['total'] = memData.total 94 | memoryRef['free'] = memData.free 95 | memoryRef['active'] = memData.active 96 | }) 97 | si.battery().then(batteryData => { 98 | var batteryRef = data['battery'] 99 | batteryRef['percent'] = batteryData.percent 100 | batteryRef['charging'] = batteryData.ischarging 101 | }) 102 | console.log(data) 103 | //console.log(data) ------ Use to visualise date in console 104 | deviceClient.publishEvent("BatteryLife","json",data); 105 | } -------------------------------------------------------------------------------- /samples/BatteryLife/src/Config_BatteryLife.yaml: -------------------------------------------------------------------------------- 1 | 2 | identity: 3 | orgId: myOrg 4 | typeId: myType 5 | deviceId: myDevice 6 | auth: 7 | token: myToken 8 | options: 9 | domain: internetofthings.ibmcloud.com 10 | logLevel: notAValidLogLevel 11 | mqtt: 12 | port: 8883 13 | transport: tcp 14 | cleanStart: true 15 | sessionExpiry: 3600 16 | keepAlive: 60 17 | caFile: myPath -------------------------------------------------------------------------------- /samples/webpack/statusconsole/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env" 4 | ] 5 | } -------------------------------------------------------------------------------- /samples/webpack/statusconsole/README.md: -------------------------------------------------------------------------------- 1 | # IBM Watson IoT Platform Status Console Demo Application 2 | 3 | 4 | ## Building the application 5 | 6 | ``` 7 | npm i 8 | npm run build-prod 9 | ``` 10 | 11 | ## Running the application 12 | 13 | Open `src/index.html` in your browser of choice 14 | 15 | 16 | ## Publishing the application 17 | 18 | ``` 19 | npm login 20 | npm publish . 21 | ``` 22 | -------------------------------------------------------------------------------- /samples/webpack/statusconsole/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wiotp/statusconsole", 3 | "version": "1.0.0", 4 | "description": "IBM Watson IoT Platform Status Console Demo Application", 5 | "private": true, 6 | "main": "index.js", 7 | "scripts": { 8 | "build-dev": "webpack -d --mode development", 9 | "build-prod": "webpack -p --mode production", 10 | "test": "mocha --require @babel/register" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "dependencies": { 16 | "@wiotp/sdk": "0.6.2" 17 | }, 18 | "devDependencies": { 19 | "@babel/cli": "^7.7.7", 20 | "@babel/register": "^7.7.7", 21 | "@babel/core": "^7.7.7", 22 | "@babel/preset-env": "^7.7.7", 23 | "babel-loader": "^8.0.6", 24 | "chai": "^4.2.0", 25 | "mocha": "^6.2.2", 26 | "webpack": "^4.41.4", 27 | "webpack-cli": "^3.3.10" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /samples/webpack/statusconsole/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IBM Watson IoT Platform Starter Application 6 | 7 | 8 | 9 |
DISCONNECTED
10 |
11 |
12 | Configuration: 13 | Application ID: 14 | API Key: 15 | Token: 16 | Port: 17 | Log Level: 18 | Initialize Application 19 |
20 |
21 | Connectivity: 22 | Connect Application | 23 | Disconnect Application 24 |
25 |
26 | Actions: 27 | Subscribe to all Events | 28 |
29 | 30 | -------------------------------------------------------------------------------- /samples/webpack/statusconsole/src/index.js: -------------------------------------------------------------------------------- 1 | import { ApplicationClient, ApplicationConfig } from '@wiotp/sdk' 2 | 3 | 4 | let appClient = null; 5 | 6 | window.initialize = function() { 7 | if (appClient != null) { 8 | console.log("Client is already initialized"); 9 | } 10 | else { 11 | let appId = document.getElementById("appId").value; 12 | let apikey = document.getElementById("apikey").value; 13 | let token = document.getElementById("token").value; 14 | let portNumber = document.getElementById("port").value; 15 | let logLevel = document.getElementById("logLevel").value; 16 | 17 | let identity = null; 18 | 19 | if (appId != null || appId != "") { 20 | identity = {appId: appId} 21 | } 22 | 23 | let auth = { 24 | key: apikey, 25 | token: token 26 | }; 27 | let options = { 28 | logLevel:logLevel, 29 | mqtt: { 30 | transport:"websockets", 31 | port: parseInt(portNumber) 32 | } 33 | }; 34 | let appConfig = new ApplicationConfig(identity, auth, options); 35 | appClient = new ApplicationClient(appConfig); 36 | 37 | // Event callbacks 38 | appClient.on("deviceEvent", function (typeId, deviceId, eventId, format, payload) { 39 | console.log("Device Event from :: " + typeId + " : " + deviceId + " of event " + eventId + " with format " + format + " - payload = " + payload); 40 | }); 41 | 42 | // Connectivity callbacks 43 | appClient.on("connect", function () { 44 | document.getElementById("status").innerHTML = "CONNECTED"; 45 | }); 46 | appClient.on("reconnect", function () { 47 | document.getElementById("status").innerHTML = "RECONNECTING"; 48 | }); 49 | appClient.on("close", function () { 50 | document.getElementById("status").innerHTML = "DISCONNECTED"; 51 | }); 52 | appClient.on("offline", function () { 53 | document.getElementById("status").innerHTML = "OFFLINE"; 54 | }); 55 | 56 | // Error callback 57 | appClient.on("error", function (err) { 58 | document.getElementById("lastError").innerHTML = err; 59 | }); 60 | } 61 | } 62 | 63 | window.connect = function() { 64 | if (appClient == null) { 65 | document.getElementById("lastError").innerHTML = "Need to initialize client before you can connect!"; 66 | return; 67 | } 68 | appClient.connect(); 69 | } 70 | 71 | window.disconnect = function() { 72 | if (appClient == null) { 73 | document.getElementById("lastError").innerHTML = "Need to initialize client before you can disconnect!"; 74 | return; 75 | } 76 | appClient.disconnect(); 77 | } 78 | 79 | window.subscribeToEvents = function() { 80 | if (appClient == null) { 81 | document.getElementById("lastError").innerHTML = "Need to initialize client before you can subscribe to events!"; 82 | return; 83 | } 84 | appClient.subscribeToEvents("+", "+", "+", "+", 0); 85 | } 86 | -------------------------------------------------------------------------------- /samples/webpack/statusconsole/src/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: tahoma; 3 | font-size: 12pt; 4 | } 5 | 6 | div#status { 7 | font-weight: bold; 8 | border:1px solid #000; 9 | padding: 5px; 10 | margin: 5px; 11 | } 12 | 13 | div#lastError { 14 | font-family: consolas; 15 | background-color: red; 16 | color: #fff; 17 | border:1px solid #000; 18 | padding: 5px; 19 | margin: 5px; 20 | } 21 | 22 | -------------------------------------------------------------------------------- /samples/webpack/statusconsole/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: './src/index.js', 5 | output: { 6 | filename: 'main.js', 7 | path: path.resolve(__dirname, 'dist') 8 | }, 9 | module: { 10 | rules: [ 11 | { 12 | test: /\.js$/, 13 | use: 'babel-loader', 14 | exclude: /node_modules/ 15 | } 16 | ] 17 | }, 18 | externals: { 19 | fs: "commonjs fs" 20 | //resolves webpack not calling FS as a module but as an environmental object 21 | }, 22 | }; -------------------------------------------------------------------------------- /src/BaseClient.js: -------------------------------------------------------------------------------- 1 | /** 2 | ***************************************************************************** 3 | Copyright (c) 2014, 2019 IBM Corporation and other Contributors. 4 | All rights reserved. This program and the accompanying materials 5 | are made available under the terms of the Eclipse Public License v1.0 6 | which accompanies this distribution, and is available at 7 | http://www.eclipse.org/legal/epl-v10.html 8 | ***************************************************************************** 9 | * 10 | */ 11 | import events from 'events'; 12 | import mqtt from 'mqtt'; 13 | import log from 'loglevel'; 14 | 15 | import TinyCache from 'tinycache'; 16 | 17 | const { v4: uuidv4 } = require('uuid'); 18 | 19 | export default class BaseClient extends events.EventEmitter { 20 | constructor(config){ 21 | super(); 22 | this.log = log; 23 | this.log.setDefaultLevel(config.options.logLevel); 24 | 25 | this.config = config; 26 | 27 | this.reconnectLog = 0; 28 | this.mqtt = null; 29 | 30 | this.lostConnectionLog = new TinyCache(); 31 | 32 | } 33 | 34 | 35 | isConnected() { 36 | if (this.mqtt == null) { 37 | return false; 38 | } 39 | return this.mqtt.connected; 40 | } 41 | 42 | 43 | connect(){ 44 | if(this.mqtt != null) { 45 | this.log.info("[BaseClient:connect] Reconnecting to " + this.config.getMqttHost() + " as " + this.config.getClientId()); 46 | this.mqtt.reconnect(); 47 | return; 48 | } 49 | 50 | this.log.info("[BaseClient:connect] Connecting to " + this.config.getMqttHost() + " as " + this.config.getClientId()); 51 | 52 | this.mqtt = mqtt.connect(this.config.getMqttHost(), this.config.getMqttConfig()); 53 | 54 | /* Events coming from mqtt 55 | * Event 'connect' - Emitted on successful (re)connection (i.e. connack rc=0). 56 | * Event 'reconnect' - Emitted when a reconnect starts. 57 | * Event 'close' - Emitted after a disconnection. 58 | * Event 'offline' - Emitted when the client goes offline. 59 | * Event 'error' - Emitted when the client cannot connect (i.e. connack rc != 0) or when a parsing error occurs. 60 | * Event 'end' - Emitted when mqtt.Client#end() is called. If a callback was passed to mqtt.Client#end(), this event is emitted once the callback returns. 61 | * Event 'message' - Emitted when the client receives a publish packet 62 | * Event 'packetsend' - Emitted when the client sends any packet. This includes .published() packets as well as packets used by MQTT for managing subscriptions and connections 63 | * Event 'packetreceive' - Emitted when the client receives any packet. This includes packets from subscribed topics as well as packets used by MQTT for managing subscriptions and connections 64 | */ 65 | 66 | this.mqtt.on('connect', () => { 67 | this.log.info("[BaseClient:onConnect] MQTT client is connected."); 68 | this.emit('connect'); 69 | 70 | // less than 3 connect attempts you get put to a connect delay of 1 second 71 | // after 3 connect attempts you get put to a connect delay of 2 seconds (3 seconds elapsed - 3 attempts @ 1 second intervals) 72 | // after 6 connect attempts you get put to a connect delay of 5 seconds (3 + 6 seconds elapsed - 3 attempts @ 2 second intervals) 73 | // after 9 connect attempts you get put to a connect delay of 20 seconds (3 + 6 + 15 seconds elapsed - 3 attempts @ 5 second intervals) 74 | let connectionLostCount = this.lostConnectionLog.size; 75 | 76 | // Default is 1 second reconnect period 77 | let reconnectPeriod = 1000; 78 | if (connectionLostCount >= 9) { 79 | reconnectPeriod = 20000; 80 | 81 | // Log this and raise the error EVERY time we reconnect under these conditions. 82 | this.log.warn("[BaseClient:onOffline] This client is likely suffering from clientId stealing (where two connections try to use the same client Id)."); 83 | this.emit("error", "Exceeded 9 connection losses in a 5 minute period. Check for clientId conflict with another connection.") 84 | } 85 | else if (connectionLostCount >= 6) { 86 | reconnectPeriod = 5000; 87 | } 88 | else if (connectionLostCount >= 3) { 89 | reconnectPeriod = 2000; 90 | } 91 | 92 | if (reconnectPeriod != this.mqtt.options.reconnectPeriod) { 93 | this.log.info("[BaseClient:onOffline] Client has lost connection " + connectionLostCount + " times during the last 5 minutes, reconnect delay adjusted to " + reconnectPeriod + " ms"); 94 | this.mqtt.options.reconnectPeriod = reconnectPeriod; 95 | } 96 | 97 | }); 98 | 99 | this.mqtt.on('reconnect', () => { 100 | this.log.info("[BaseClient:onReconnect] MQTT client is reconnecting."); 101 | // this.log.debug("[BaseClient:onReconnect] Resubscribe topics:"); 102 | // this.log.debug(this.mqtt._resubscribeTopics); 103 | this.emit('reconnect'); 104 | }); 105 | 106 | this.mqtt.on('close', () => { 107 | this.log.info("[BaseClient:onClose] MQTT client connection was closed."); 108 | this.emit('close'); 109 | }); 110 | 111 | this.mqtt.on('offline', () => { 112 | let newId = uuidv4(); 113 | this.log.info("[BaseClient:onOffline] MQTT client connection is offline. [" + newId + "]"); 114 | this.emit('offline'); 115 | // Record the disconnect event for 5 minutes 116 | this.lostConnectionLog.put( newId, '1', 300000 ); 117 | 118 | let connectionLostCount = this.lostConnectionLog.size; 119 | this.log.info("[BaseClient:onOffline] Connection losses in the last 5 minutes: " + connectionLostCount); 120 | 121 | }); 122 | 123 | this.mqtt.on('error', (error) => { 124 | this.log.error("[BaseClient:onError] " + error); 125 | 126 | let errorMsg = '' + error; 127 | if (errorMsg.indexOf('Not authorized') > -1) { 128 | this.log.error("[BaseClient:onError] One or more configuration parameters are wrong. Modify the configuration before trying to reconnect."); 129 | this.mqtt.end(false, () => { 130 | this.log.info("[BaseClient:onError] Closed the MQTT connection due to client misconfiguration"); 131 | }); 132 | } 133 | this.emit('error', error); 134 | }); 135 | } 136 | 137 | 138 | disconnect(){ 139 | if(this.mqtt == null) { 140 | this.log.info("[BaseClient:disconnect] Client was never connected"); 141 | return; 142 | } 143 | 144 | this.mqtt.end(false, () => { 145 | this.log.info("[BaseClient:disconnect] Closed the MQTT connection due to disconnect() call"); 146 | }); 147 | } 148 | 149 | 150 | _subscribe(topic, QoS, callback) { 151 | if (this.mqtt == null) { 152 | this.emit('error', "[BaseClient:_subscribe] MQTT Client is not initialized - call connect() first"); 153 | return; 154 | } 155 | if (!this.mqtt.connected) { 156 | this.emit('error', "[BaseClient:_subscribe] MQTT Client is not connected - call connect() first"); 157 | return; 158 | } 159 | 160 | QoS = QoS || 0; 161 | callback = callback || function (err, granted) { 162 | if (err == null) { 163 | for (var index in granted) { 164 | let grant = granted[index]; 165 | this.log.debug("[BaseClient:_subscribe] Subscribed to " + grant.topic + " at QoS " + grant.qos); 166 | } 167 | } else { 168 | this.log.error("[BaseClient:_subscribe] " + err); 169 | this.emit("error", err); 170 | } 171 | }.bind(this); 172 | 173 | this.log.debug("[BaseClient:_subscribe] Subscribing to topic " + topic + " with QoS " + QoS); 174 | this.mqtt.subscribe(topic, { qos: parseInt(QoS) }, callback); 175 | } 176 | 177 | 178 | _unsubscribe(topic, callback) { 179 | if (this.mqtt == null) { 180 | this.emit('error', "[BaseClient:_unsubscribe] MQTT Client is not initialized - call connect() first"); 181 | return; 182 | } 183 | if (!this.mqtt.connected) { 184 | this.emit('error', "[BaseClient:_unsubscribe] MQTT Client is not connected - call connect() first"); 185 | return; 186 | } 187 | 188 | callback = callback || function (err) { 189 | if (err == null) { 190 | this.log.debug("[BaseClient:_unsubscribe] Unsubscribed from: " + topic); 191 | } else { 192 | this.log.error("[BaseClient:_unsubscribe] " + err); 193 | this.emit("error", err); 194 | } 195 | }.bind(this); 196 | 197 | this.log.debug("[BaseClient:_unsubscribe] Unsubscribe: " + topic); 198 | this.mqtt.unsubscribe(topic, callback); 199 | } 200 | 201 | 202 | _publish(topic, msg, QoS, callback) { 203 | QoS = QoS || 0; 204 | 205 | if (this.mqtt == null) { 206 | this.emit('error', "[BaseClient:_unsubscribe] MQTT Client is not initialized - call connect() first"); 207 | return; 208 | } 209 | if (!this.mqtt.connected) { 210 | this.emit('error', "[BaseClient:_unsubscribe] MQTT Client is not connected - call connect() first"); 211 | return; 212 | } 213 | 214 | if ((typeof msg === 'object' || typeof msg === 'boolean' || typeof msg === 'number') && !Buffer.isBuffer(msg)) { 215 | // mqtt library does not support sending JSON/Boolean/Number data. So stringifying it. 216 | // All JSON object, array will be encoded. 217 | msg = JSON.stringify(msg); 218 | } 219 | 220 | this.log.debug("[BaseClient:_publish] Publish: " + topic + ", " + msg + ", QoS : " + QoS); 221 | this.mqtt.publish(topic, msg, { qos: parseInt(QoS) }, callback); 222 | } 223 | 224 | } 225 | -------------------------------------------------------------------------------- /src/BaseConfig.js: -------------------------------------------------------------------------------- 1 | /** 2 | ***************************************************************************** 3 | Copyright (c) 2019 IBM Corporation and other Contributors. 4 | All rights reserved. This program and the accompanying materials 5 | are made available under the terms of the Eclipse Public License v1.0 6 | which accompanies this distribution, and is available at 7 | http://www.eclipse.org/legal/epl-v10.html 8 | ***************************************************************************** 9 | * 10 | */ 11 | 12 | export default class BaseConfig{ 13 | constructor(identity, auth, options) { 14 | this.identity = identity; 15 | this.auth = auth; 16 | this.options = options; 17 | 18 | // Validation for options common to all confiugration 19 | 20 | if (this.options != null && "mqtt" in this.options) { 21 | // validate port 22 | if ("port" in this.options.mqtt && this.options.mqtt.port != null) { 23 | if (isNaN(this.options.mqtt.port)) { 24 | throw new Error("Optional setting options.mqtt.port must be a number if provided"); 25 | } 26 | } 27 | // Validate cleanStart 28 | if ("cleanStart" in this.options.mqtt && typeof(this.options.mqtt.cleanStart) != "boolean") { 29 | throw new Error("Optional setting options.mqtt.cleanStart must be a boolean if provided"); 30 | } 31 | } 32 | 33 | // Set defaults for optional configuration 34 | if (this.options == null) { 35 | this.options = {}; 36 | } 37 | if (!("domain" in this.options) || this.options.domain == null) { 38 | this.options.domain = "internetofthings.ibmcloud.com"; 39 | } 40 | 41 | if (!("logLevel" in this.options) || this.options.logLevel == null) { 42 | this.options.logLevel = "info"; 43 | } 44 | 45 | if (!("mqtt" in this.options)) { 46 | this.options.mqtt = {}; 47 | } 48 | 49 | if (!("port" in this.options.mqtt) || this.options.mqtt.port == null) { 50 | this.options.mqtt.port = 8883; 51 | } 52 | if (!("transport" in this.options.mqtt) || this.options.mqtt.transport == null) { 53 | this.options.mqtt.transport = "tcp"; 54 | } 55 | 56 | if (!("cleanStart" in this.options.mqtt)) { 57 | this.options.mqtt.cleanStart = true; 58 | } 59 | if (!("sessionExpiry" in this.options.mqtt)) { 60 | this.options.mqtt.sessionExpiry = 3600; 61 | } 62 | if (!("keepAlive" in this.options.mqtt)) { 63 | this.options.mqtt.keepAlive = 60; 64 | } 65 | 66 | if (!("caFile" in this.options.mqtt)) { 67 | this.options.mqtt.caFile = null; 68 | } 69 | } 70 | 71 | getOrgId() { 72 | throw new Error("Sub class must implement getOrgId()"); 73 | } 74 | 75 | isQuickstart() { 76 | return this.getOrgId() == "quickstart"; 77 | } 78 | 79 | getClientId() { 80 | throw new Error("Sub class must implement getClientId()"); 81 | } 82 | 83 | getMqttUsername() { 84 | throw new Error("Sub class must implement getMqttUsername()"); 85 | } 86 | 87 | getMqttPassword() { 88 | throw new Error("Sub class must implement getMqttPassowrd()"); 89 | } 90 | 91 | getMqttConfig() { 92 | // See: https://www.npmjs.com/package/mqtt#mqttclientstreambuilder-options 93 | let mqttConfig = { 94 | // Identity 95 | clientId: this.getClientId(), 96 | 97 | // Basic Connectivity 98 | keepalive: this.options.mqtt.keepAlive, // in seconds 99 | connectTimeout: 90*1000, // milliseconds, time to wait before a CONNACK is received 100 | reconnectPeriod: 1000, // milliseconds, interval between two reconnections 101 | queueQoSZero: true, // if connection is broken, queue outgoing QoS zero messages 102 | resubscribe: true, // if connection is broken and reconnects, subscribed topics are automatically subscribed again 103 | 104 | clean: this.options.mqtt.cleanStart, // set to false to receive QoS 1 and 2 messages while offline 105 | 106 | // Authentication 107 | username: this.getMqttUsername(), 108 | password: this.getMqttPassword(), 109 | 110 | // Security 111 | // If you are using a self-signed certificate, pass the rejectUnauthorized: false option. Beware 112 | // that you are exposing yourself to man in the middle attacks, so it is a configuration that 113 | // is not recommended for production environments. 114 | rejectUnauthorized: true, 115 | 116 | // MQTTv5 support doesn't work with Watson IoT Platform, so stick to default for now 117 | // protocolId: "MQTT", 118 | // protocolVersion: 5 119 | } 120 | return mqttConfig; 121 | } 122 | 123 | getMqttHost() { 124 | let server = this.getOrgId() + ".messaging." + this.options.domain + ":" + this.options.mqtt.port; 125 | 126 | // For unencrpyted ports 127 | if (this.options.mqtt.port == 80 || this.options.mqtt.port == 1883) { 128 | if (this.options.mqtt.transport == "tcp") { 129 | return "tcp://" + server; 130 | } 131 | if (this.options.mqtt.transport == "websockets") { 132 | return "ws://" + server; 133 | } 134 | } 135 | 136 | // For encrypted ports 137 | if (this.options.mqtt.port == 443 || this.options.mqtt.port == 8883) { 138 | if (this.options.mqtt.transport == "tcp") { 139 | return "ssl://" + server; 140 | } 141 | if (this.options.mqtt.transport == "websockets") { 142 | return "wss://" + server; 143 | } 144 | } 145 | 146 | // Default to something, but really shouldn't hit this scenario unless misconfigured 147 | return "ssl://" + server; 148 | } 149 | 150 | static parseEnvVars() { 151 | throw new Error("Sub class must implement parseEnvVars()"); 152 | } 153 | 154 | static parseConfigFile() { 155 | throw new Error("Sub class must implement parseConfigFile()"); 156 | } 157 | 158 | }; 159 | -------------------------------------------------------------------------------- /src/api/ApiClient.js: -------------------------------------------------------------------------------- 1 | /** 2 | ***************************************************************************** 3 | Copyright (c) 2014, 2019 IBM Corporation and other Contributors. 4 | All rights reserved. This program and the accompanying materials 5 | are made available under the terms of the Eclipse Public License v1.0 6 | which accompanies this distribution, and is available at 7 | http://www.eclipse.org/legal/epl-v10.html 8 | ***************************************************************************** 9 | * 10 | */ 11 | import xhr from 'axios'; 12 | import Promise from 'bluebird'; 13 | import btoa from 'btoa'; 14 | import FormData from 'form-data'; 15 | import log from 'loglevel'; 16 | 17 | import { isBrowser } from '../util'; 18 | 19 | export default class ApiClient { 20 | constructor(config) { 21 | this.log = log; 22 | this.log.setDefaultLevel(config.options.logLevel); 23 | 24 | this.config = config; 25 | this.useLtpa = this.config.auth && this.config.auth.useLtpa; 26 | 27 | this.log.debug("[ApiClient:constructor] ApiClient initialized for " + this.config.getApiBaseUri()); 28 | } 29 | 30 | // e.g. [{name: true}, {description: false}] => -name,description 31 | parseSortSpec(sortSpec) { 32 | return sortSpec 33 | ? sortSpec.map(s=>{ 34 | const e = Object.entries(s)[0]; 35 | return `${e[1] ? '-' : ''}${e[0]}` 36 | }).join(',') 37 | : undefined; 38 | }; 39 | 40 | callApi(method, expectedHttpCode, expectJsonContent, paths, body, params) { 41 | return new Promise((resolve, reject) => { 42 | let uri = this.config.getApiBaseUri(); 43 | 44 | if (Array.isArray(paths)) { 45 | for (var i = 0, l = paths.length; i < l; i++) { 46 | uri += '/' + paths[i]; 47 | } 48 | } 49 | 50 | let xhrConfig = { 51 | url: uri, 52 | method: method, 53 | headers: { 54 | 'Content-Type': 'application/json' 55 | }, 56 | validateStatus: (status) => { 57 | if(Array.isArray(expectedHttpCode)) { 58 | return expectedHttpCode.includes(status); 59 | } else { 60 | return status === expectedHttpCode; 61 | } 62 | } 63 | }; 64 | 65 | if (this.useLtpa) { 66 | xhrConfig.withCredentials = true; 67 | } 68 | else { 69 | xhrConfig.headers['Authorization'] = 'Basic ' + btoa(this.config.auth.key + ':' + this.config.auth.token); 70 | } 71 | 72 | if (body) { 73 | xhrConfig.data = body; 74 | } 75 | 76 | if (params) { 77 | xhrConfig.params = params; 78 | } 79 | 80 | if (this.config.getAdditionalHeaders()) { 81 | xhrConfig.headers = {...xhrConfig.headers, ...this.config.getAdditionalHeaders()}; 82 | } 83 | 84 | function transformResponse(response) { 85 | if (expectJsonContent && !(typeof response.data === 'object')) { 86 | try { 87 | resolve(JSON.parse(response.data)); 88 | } catch (e) { 89 | reject(e); 90 | } 91 | } else { 92 | resolve(response.data); 93 | } 94 | } 95 | this.log.debug("[ApiClient:transformResponse] " + xhrConfig); 96 | xhr(xhrConfig).then(transformResponse, reject); 97 | }); 98 | } 99 | 100 | getOrganizationDetails() { 101 | this.log.debug("[ApiClient] getOrganizationDetails()"); 102 | return this.callApi('GET', 200, true, null, null); 103 | } 104 | 105 | 106 | getServiceStatus() { 107 | this.log.debug("[ApiClient] getServiceStatus()"); 108 | return this.callApi('GET', 200, true, ['service-status'], null); 109 | } 110 | 111 | //Usage Management 112 | getActiveDevices(start, end, detail) { 113 | this.log.debug("[ApiClient] getActiveDevices(" + start + ", " + end + ")"); 114 | detail = detail | false; 115 | let params = { 116 | start: start, 117 | end: end, 118 | detail: detail 119 | }; 120 | return this.callApi('GET', 200, true, ['usage', 'active-devices'], null, params); 121 | } 122 | 123 | getHistoricalDataUsage(start, end, detail) { 124 | this.log.debug("[ApiClient] getHistoricalDataUsage(" + start + ", " + end + ")"); 125 | detail = detail | false; 126 | let params = { 127 | start: start, 128 | end: end, 129 | detail: detail 130 | }; 131 | return this.callApi('GET', 200, true, ['usage', 'historical-data'], null, params); 132 | } 133 | 134 | getDataUsage(start, end, detail) { 135 | this.log.debug("[ApiClient] getDataUsage(" + start + ", " + end + ")"); 136 | detail = detail | false; 137 | let params = { 138 | start: start, 139 | end: end, 140 | detail: detail 141 | }; 142 | return this.callApi('GET', 200, true, ['usage', 'data-traffic'], null, params); 143 | } 144 | 145 | 146 | //client connectivity status 147 | getConnectionStates(){ 148 | this.log.debug("[ApiClient] getConnectionStates() - client connectivity"); 149 | return this.callApi('GET', 200, true, ["clientconnectionstates"], null); 150 | } 151 | 152 | getConnectionState(id){ 153 | this.log.debug("[ApiClient] getConnectionState() - client connectivity"); 154 | return this.callApi('GET', 200, true, ["clientconnectionstates/" + id], null); 155 | } 156 | 157 | getConnectedClientsConnectionStates(){ 158 | this.log.debug("[ApiClient] getConnectedClientsConnectionStates() - client connectivity"); 159 | return this.callApi('GET', 200, true, ["clientconnectionstates?connectionStatus=connected"], null); 160 | } 161 | 162 | getRecentConnectionStates(date){ 163 | this.log.debug("[ApiClient] getRecentConnectionStates() - client connectivity"); 164 | return this.callApi('GET', 200, true, ["clientconnectionstates?connectedAfter=" + date], null); 165 | } 166 | 167 | getCustomConnectionState(query){ 168 | this.log.debug("[ApiClient] getCustomConnectionStates() - client connectivity"); 169 | return this.callApi('GET', 200, true, ["clientconnectionstates" + query], null); 170 | } 171 | 172 | //bulk apis 173 | getAllDevices(params) { 174 | this.log.debug("[ApiClient] getAllDevices() - BULK"); 175 | return this.callApi('GET', 200, true, ["bulk", "devices"], null, params); 176 | } 177 | 178 | /** 179 | * Gateway Access Control (Beta) 180 | * The methods in this section follow the documentation listed under the link: 181 | * https://console.ng.bluemix.net/docs/services/IoT/gateways/gateway-access-control.html#gateway-access-control-beta- 182 | * Involves the following sections from the above mentioned link: 183 | * Assigning a role to a gateway 184 | * Adding devices to and removing devices from a resource group 185 | * Finding a resource group 186 | * Querying a resource group 187 | * Creating and deleting a resource group 188 | * Updating group properties 189 | * Retrieving and updating device properties 190 | * 191 | */ 192 | 193 | //getGatewayGroup(gatewayId) 194 | //updateDeviceRoles(deviceId, roles[]) 195 | //getAllDevicesInGropu(groupId) 196 | //addDevicesToGroup(groupId, deviceList[]) 197 | //removeDevicesFromGroup(groupId, deviceList[]) 198 | 199 | getGroupIdsForDevice(deviceId){ 200 | this.log.debug("[ApiClient] getGroupIdsForDevice("+deviceId+")"); 201 | return this.callApi('GET', 200, true, ['authorization', 'devices' , deviceId], null); 202 | } 203 | 204 | updateDeviceRoles(deviceId, roles){ 205 | this.log.debug("[ApiClient] updateDeviceRoles("+deviceId+","+roles+")"); 206 | return this.callApi('PUT', 200, false, ['authorization', 'devices', deviceId, 'roles'], roles); 207 | } 208 | 209 | getAllDevicesInGroup(groupId){ 210 | this.log.debug("[ApiClient] getAllDevicesInGropu("+groupId+")"); 211 | return this.callApi('GET', 200, true, ['bulk', 'devices' , groupId], null); 212 | } 213 | 214 | addDevicesToGroup(groupId, deviceList){ 215 | this.log.debug("[ApiClient] addDevicesToGroup("+groupId+","+deviceList+")"); 216 | return this.callApi('PUT', 200, false, ['bulk', 'devices' , groupId, "add"], deviceList); 217 | } 218 | 219 | removeDevicesFromGroup(groupId, deviceList){ 220 | this.log.debug("[ApiClient] removeDevicesFromGroup("+groupId+","+deviceList+")"); 221 | return this.callApi('PUT', 200, false, ['bulk', 'devices' , groupId, "remove"], deviceList); 222 | } 223 | 224 | // https://console.ng.bluemix.net/docs/services/IoT/gateways/gateway-access-control.html 225 | 226 | // Finding a Resource Group 227 | // getGatewayGroups() 228 | // Querying a resource group 229 | // getUniqueDevicesInGroup(groupId) 230 | // getUniqueGatewayGroup(groupId) 231 | // Creating and deleting a resource group 232 | // createGatewayGroup(groupName) 233 | // deleteGatewayGroup(groupId) 234 | // Retrieving and updating device properties 235 | // getGatewayGroupProperties() 236 | // getDeviceRoles(deviceId) 237 | // updateGatewayProperties(gatewayId,control_props) 238 | // updateDeviceControlProperties(deviceId, withroles) 239 | 240 | // Finding a Resource Group 241 | getAllGroups(){ 242 | this.log.debug("[ApiClient] getAllGroups()"); 243 | return this.callApi('GET', 200, true, ['groups'], null); 244 | } 245 | 246 | // Querying a resource group 247 | 248 | // Get unique identifiers of the members of the resource group 249 | getAllDeviceIdsInGroup(groupId){ 250 | this.log.debug("[ApiClient] getAllDeviceIdsInGroup("+groupId+")"); 251 | return this.callApi('GET', 200, true, ['bulk', 'devices' , groupId, "ids"], null); 252 | } 253 | 254 | // properties of the resource group 255 | getGroup(groupId){ 256 | this.log.debug("[ApiClient] getGroup("+groupId+")"); 257 | return this.callApi('GET', 200, true, ['groups', groupId], null); 258 | } 259 | 260 | // Creating and deleting a resource group 261 | 262 | // Create a Resource Group 263 | createGroup(groupInfo){ 264 | this.log.debug("[ApiClient] createGroup()"); 265 | return this.callApi('POST', 201, true, ['groups'], groupInfo); 266 | } 267 | 268 | // Delete a Resource Group 269 | deleteGroup(groupId){ 270 | this.log.debug("[ApiClient] deleteGroup("+groupId+")"); 271 | return this.callApi('DELETE', 200, false, ['groups', groupId], null); 272 | } 273 | 274 | // Retrieving and updating device properties 275 | 276 | // Get the ID of the devices group of a gateway 277 | getAllDeviceAccessControlProperties(){ 278 | this.log.debug("[ApiClient] getAllDeviceAccessControlProperties()"); 279 | return this.callApi('GET', 200, true, ['authorization', 'devices' ], null); 280 | } 281 | 282 | // Get standard role of a gateway 283 | getDeviceAccessControlProperties(deviceId){ 284 | this.log.debug("[ApiClient] getDeviceAccessControlProperties("+deviceId+")"); 285 | return this.callApi('GET', 200, true, ['authorization', 'devices', deviceId, 'roles'], null); 286 | } 287 | 288 | // Update device properties without affecting the access control properties 289 | updateDeviceAccessControlProperties(deviceId,deviceProps){ 290 | this.log.debug("[ApiClient] updateDeviceAccessControlProperties("+deviceId+")"); 291 | return this.callApi('PUT', 200, true, ['authorization', 'devices' , deviceId], deviceProps); 292 | } 293 | 294 | // Assign a standard role to a gateway 295 | updateDeviceAccessControlPropertiesWithRoles(deviceId, devicePropsWithRoles){ 296 | this.log.debug("[ApiClient] updateDeviceAccessControlPropertiesWithRoles("+deviceId+","+devicePropsWithRoles+")"); 297 | return this.callApi('PUT', 200, true, ['authorization', 'devices', deviceId, 'withroles'], devicePropsWithRoles); 298 | } 299 | 300 | // Duplicating updateDeviceRoles(deviceId, roles) for Gateway Roles 301 | updateGatewayRoles(gatewayId, roles){ 302 | this.log.debug("[ApiClient] updateGatewayRoles("+gatewayId+","+roles+")"); 303 | return this.callApi('PUT', 200, false, ['authorization', 'devices', gatewayId, 'roles'], roles); 304 | } 305 | 306 | // Extending getAllGroups() to fetch individual Groups 307 | getGroups(groupId){ 308 | this.log.debug("[ApiClient] getGroups("+groupId+")"); 309 | return this.callApi('GET', 200, true, ['groups', groupId], null); 310 | } 311 | 312 | 313 | callFormDataApi(method, expectedHttpCode, expectJsonContent, paths, body, params) { 314 | return new Promise((resolve, reject) => { 315 | let uri = this.config.getApiBaseUri(); 316 | 317 | if (Array.isArray(paths)) { 318 | for (var i = 0, l = paths.length; i < l; i++) { 319 | uri += '/' + paths[i]; 320 | } 321 | } 322 | 323 | let xhrConfig = { 324 | url: uri, 325 | method: method, 326 | headers: { 327 | 'Content-Type': 'multipart/form-data' 328 | } 329 | }; 330 | 331 | if (this.useLtpa) { 332 | xhrConfig.withCredentials = true; 333 | } 334 | else { 335 | xhrConfig.headers['Authorization'] = 'Basic ' + btoa(this.apiKey + ':' + this.apiToken); 336 | } 337 | 338 | if (body) { 339 | xhrConfig.data = body; 340 | if(isBrowser()) { 341 | xhrConfig.transformRequest = [function (data) { 342 | var formData = new FormData() 343 | 344 | if(xhrConfig.method == "POST") { 345 | if(data.schemaFile) { 346 | var blob = new Blob([data.schemaFile], { type: "application/json" }) 347 | var schemaFileName = `${data.name || 'schema'}.json`; 348 | formData.append('schemaFile', blob, schemaFileName); 349 | } 350 | 351 | if(data.name) { 352 | formData.append('name', data.name) 353 | } 354 | 355 | if (data.schemaType) { 356 | formData.append('schemaType', 'json-schema') 357 | } 358 | if (data.description) { 359 | formData.append('description', data.description) 360 | } 361 | } else if(xhrConfig.method == "PUT") { 362 | if(data.schemaFile) { 363 | var blob = new Blob([data.schemaFile], { type: "application/json", name: data.name }) 364 | var schemaFileName = `${data.name || 'schema'}.json`; 365 | formData.append('schemaFile', blob, schemaFileName); 366 | } 367 | } 368 | 369 | return formData; 370 | }] 371 | } 372 | } 373 | 374 | if (params) { 375 | xhrConfig.params = params; 376 | } 377 | 378 | if (this.config.getAdditionalHeaders()) { 379 | xhrConfig.headers = {...xhrConfig.headers, ...this.config.getAdditionalHeaders()}; 380 | } 381 | 382 | function transformResponse(response) { 383 | if (response.status === expectedHttpCode) { 384 | if (expectJsonContent && !(typeof response.data === 'object')) { 385 | try { 386 | resolve(JSON.parse(response.data)); 387 | } catch (e) { 388 | reject(e); 389 | } 390 | } else { 391 | resolve(response.data); 392 | } 393 | } else { 394 | reject(new Error(method + " " + uri + ": Expected HTTP " + expectedHttpCode + " from server but got HTTP " + response.status + ". Error Body: " + JSON.stringify(response.data))); 395 | } 396 | } 397 | this.log.debug("[ApiClient:transformResponse] " + xhrConfig); 398 | 399 | if(isBrowser()) { 400 | xhr(xhrConfig).then(transformResponse, reject); 401 | } else { 402 | var formData = null 403 | var config = { 404 | url: uri, 405 | method: method, 406 | headers: {'Content-Type': 'multipart/form-data'}, 407 | auth : { 408 | user : this.apiKey, 409 | pass : this.apiToken 410 | }, 411 | formData: {}, 412 | rejectUnauthorized: false 413 | } 414 | 415 | if(xhrConfig.method == "POST") { 416 | formData = { 417 | 'schemaFile': { 418 | 'value': body.schemaFile, 419 | 'options': { 420 | 'contentType': 'application/json', 421 | 'filename': body.name 422 | } 423 | }, 424 | 'schemaType': 'json-schema', 425 | 'name': body.name 426 | } 427 | config.formData = formData 428 | } else if(xhrConfig.method == "PUT") { 429 | formData = { 430 | 'schemaFile': { 431 | 'value': body.schemaFile, 432 | 'options': { 433 | 'contentType': 'application/json', 434 | 'filename': body.name 435 | } 436 | } 437 | } 438 | config.formData = formData 439 | } 440 | request(config, function optionalCallback(err, response, body) { 441 | if (response.statusCode === expectedHttpCode) { 442 | if (expectJsonContent && !(typeof response.data === 'object')) { 443 | try { 444 | resolve(JSON.parse(body)); 445 | } catch (e) { 446 | reject(e); 447 | } 448 | } else { 449 | resolve(body); 450 | } 451 | } else { 452 | reject(new Error(method + " " + uri + ": Expected HTTP " + expectedHttpCode + " from server but got HTTP " + response.statusCode + ". Error Body: " + err)); 453 | } 454 | }); 455 | } 456 | }); 457 | } 458 | 459 | invalidOperation(message) { 460 | return new Promise((resolve, reject) => { 461 | resolve(message) 462 | }) 463 | } 464 | } 465 | -------------------------------------------------------------------------------- /src/api/ApiErrors.js: -------------------------------------------------------------------------------- 1 | 2 | export class WiotpError extends Error { 3 | constructor(message, cause) { 4 | super(message); 5 | this.cause = cause; 6 | this.name = this.constructor.name; 7 | } 8 | } 9 | 10 | export class InvalidServiceCredentials extends WiotpError {} 11 | 12 | export class DestinationAlreadyExists extends WiotpError {} 13 | 14 | export class ServiceNotFound extends WiotpError {} 15 | 16 | 17 | export const handleError = (err, errorMappings) => { 18 | if(err && err.response && err.response.data && err.response.data.exception && err.response.data.exception.id) { 19 | if(errorMappings && errorMappings[err.response.data.exception.id]) { 20 | throw new errorMappings[err.response.data.exception.id](err.response.data.message, err); 21 | } else { 22 | throw new WiotpError(err.response.data.message, err); 23 | } 24 | } else { 25 | throw err; 26 | } 27 | } 28 | 29 | export default { 30 | WiotpError, 31 | InvalidServiceCredentials, 32 | DestinationAlreadyExists, 33 | ServiceNotFound, 34 | } -------------------------------------------------------------------------------- /src/api/DscClient.js: -------------------------------------------------------------------------------- 1 | /** 2 | ***************************************************************************** 3 | Copyright (c) 2014, 2019 IBM Corporation and other Contributors. 4 | All rights reserved. This program and the accompanying materials 5 | are made available under the terms of the Eclipse Public License v1.0 6 | which accompanies this distribution, and is available at 7 | http://www.eclipse.org/legal/epl-v10.html 8 | ***************************************************************************** 9 | * 10 | */ 11 | import log from 'loglevel'; 12 | 13 | import * as errors from './ApiErrors'; 14 | 15 | 16 | export default class DscClient { 17 | constructor(apiClient) { 18 | this.log = log; 19 | 20 | // callApi(method, expectedHttpCode, expectJsonContent, paths, body, params) { 21 | this.apiClient = apiClient; 22 | } 23 | 24 | 25 | 26 | /************************************** 27 | ** Services 28 | **************************************/ 29 | 30 | // {name, description, type, credentials} 31 | createService(service) { 32 | return this.apiClient.callApi('POST', 201, true, ['s2s', 'services'], service) 33 | .catch(err => errors.handleError(err, {CUDSS0026E: errors.InvalidServiceCredentials})); 34 | } 35 | 36 | 37 | createCloudantService({ 38 | name, description, 39 | username, password, host=`${username}.cloudant.com`, port=443, url=`https://${username}:${password}@${host}`, 40 | apikey, iam_apikey_name, iam_apikey_description, iam_role_crn, iam_serviceid_crn 41 | }) { 42 | return this.createService({name, description, type: 'cloudant', credentials: { 43 | username, password, host, port, url, 44 | apikey, iam_apikey_name, iam_apikey_description, iam_role_crn, iam_serviceid_crn 45 | }}) 46 | } 47 | 48 | createEventstreamsService({name, description, 49 | api_key, kafka_admin_url, kafka_brokers_sasl, user, password, 50 | apikey, iam_apikey_name, iam_apikey_description, iam_role_crn, iam_serviceid_crn 51 | }) { 52 | return this.createService({name, description, type: 'eventstreams', credentials: { 53 | api_key, kafka_admin_url, kafka_brokers_sasl, user, password, 54 | apikey, iam_apikey_name, iam_apikey_description, iam_role_crn, iam_serviceid_crn 55 | }}) 56 | } 57 | 58 | getService(serviceId) { 59 | return this.apiClient.callApi('GET', 200, true, ['s2s', 'services', serviceId]) 60 | .catch(err => errors.handleError(err, {CUDSS0019E: errors.ServiceNotFound})); 61 | } 62 | 63 | getServices(serviceType) { 64 | return this.apiClient.callApi('GET', 200, true, ['s2s', 'services'], null, { bindingMode:'manual', serviceType }) 65 | .catch(err => errors.handleError(err, {})); 66 | } 67 | 68 | 69 | deleteService(serviceId) { 70 | return this.apiClient.callApi('DELETE', 204, false, ['s2s', 'services', serviceId]) 71 | .catch(err => errors.handleError(err, {})); 72 | } 73 | 74 | 75 | /************************************** 76 | ** Historian Connectors 77 | **************************************/ 78 | 79 | // {name, description, serviceId, timezone, enabled} 80 | createConnector({name, type, description=undefined, serviceId, timezone='UTC', enabled=true}) { 81 | return this.apiClient.callApi('POST', 201, true, ['historianconnectors'], {name, description, type, serviceId, timezone, enabled}) 82 | .catch(err => errors.handleError(err, {})); 83 | } 84 | 85 | 86 | updateConnector({id, name, description, serviceId, type, enabled, timezone}) { 87 | return this.apiClient.callApi('PUT', 200, true, ['historianconnectors', id], {id, name, description, serviceId, type, enabled, timezone}) 88 | .catch(err => errors.handleError(err, {})); 89 | } 90 | 91 | getConnectors({name, serviceType, enabled, serviceId}) { 92 | return this.apiClient.callApi('GET', 200, true, ['historianconnectors'], null, { 93 | name: name ? name : undefined, 94 | type: serviceType ? serviceType : undefined, 95 | enabled: enabled === undefined ? undefined : enabled, 96 | serviceId: serviceId ? serviceId : undefined, 97 | }) 98 | .catch(err => errors.handleError(err, {})); 99 | } 100 | 101 | deleteConnector(connectorId) { 102 | return this.apiClient.callApi('DELETE', 204, false, ['historianconnectors', connectorId]) 103 | .catch(err => errors.handleError(err, {})); 104 | } 105 | 106 | 107 | /************************************** 108 | ** Destinations 109 | **************************************/ 110 | // {name, type, configuration} 111 | createDestination(connectorId, destination) { 112 | return this.apiClient.callApi('POST', 201, true, ['historianconnectors', connectorId, 'destinations'], destination) 113 | .catch(err => errors.handleError(err, {CUDDSC0103E: errors.DestinationAlreadyExists})); 114 | } 115 | 116 | createCloudantDestination(connectorId, {name, bucketInterval}) { 117 | return this.createDestination(connectorId, {name, type: 'cloudant', configuration: { bucketInterval }}); 118 | } 119 | 120 | createEventstreamsDestination(connectorId, {name, partitions=1}) { 121 | return this.createDestination(connectorId, {name, type: 'eventstreams', configuration: { partitions }}); 122 | } 123 | 124 | getDestinations(connectorId, params={name:undefined}) { 125 | const {name} = params; 126 | return this.apiClient.callApi('GET', 200, true, ['historianconnectors', connectorId, 'destinations'], null, { 127 | name: name ? name : undefined, 128 | }) 129 | .catch(err => errors.handleError(err, {})); 130 | } 131 | 132 | deleteDestination(connectorId, destinationName) { 133 | return this.apiClient.callApi('DELETE', [200, 204], false, ['historianconnectors', connectorId, 'destinations', destinationName]) 134 | .catch(err => errors.handleError(err, {})); 135 | } 136 | 137 | 138 | /************************************** 139 | ** Forwarding Rules 140 | **************************************/ 141 | 142 | // {name, destinationName, type:event, selector: {deviceType, eventId}} 143 | // {name, destinationName, type:state, selector: {logicalInterfaceId}} 144 | createForwardingRule(connectorId, forwardingrule) { 145 | return this.apiClient.callApi('POST', 201, true, ['historianconnectors', connectorId, 'forwardingrules'], forwardingrule) 146 | .catch(err => errors.handleError(err, {})); 147 | } 148 | 149 | createEventForwardingRule(connectorId, {name, destinationName, deviceType='*', eventId='*'}) { 150 | return this.createForwardingRule(connectorId, {name, destinationName, type: 'event', selector: {deviceType, eventId}}) 151 | } 152 | 153 | getForwardingRules(connectorId) { 154 | // TODO: QS params 155 | return this.apiClient.callApi('GET', 200, true, ['historianconnectors', connectorId, 'forwardingrules']) 156 | .catch(err => errors.handleError(err, {})); 157 | } 158 | 159 | deleteForwardingRule(connectorId, forwardingRuleId) { 160 | return this.apiClient.callApi('DELETE', 204, false, ['historianconnectors', connectorId, 'forwardingrules', forwardingRuleId]) 161 | .catch(err => errors.handleError(err, {})); 162 | } 163 | 164 | 165 | } -------------------------------------------------------------------------------- /src/api/LecClient.js: -------------------------------------------------------------------------------- 1 | /** 2 | ***************************************************************************** 3 | Copyright (c) 2014, 2019 IBM Corporation and other Contributors. 4 | All rights reserved. This program and the accompanying materials 5 | are made available under the terms of the Eclipse Public License v1.0 6 | which accompanies this distribution, and is available at 7 | http://www.eclipse.org/legal/epl-v10.html 8 | ***************************************************************************** 9 | * 10 | */ 11 | import log from 'loglevel'; 12 | 13 | export default class LecClient { 14 | constructor(apiClient) { 15 | this.log = log; 16 | 17 | this.apiClient = apiClient; 18 | 19 | // Create an alias to the apiClient's callApi 20 | this.callApi = this.apiClient.callApi.bind(this.apiClient); 21 | } 22 | 23 | getLastEvents(type, id) { 24 | this.log.debug("[ApiClient] getLastEvents() - event cache"); 25 | return this.callApi('GET', 200, true, ["device", "types", type, "devices", id, "events"], null); 26 | } 27 | 28 | getLastEventsByEventType(type, id, eventType) { 29 | this.log.debug("[ApiClient] getLastEventsByEventType() - event cache"); 30 | return this.callApi('GET', 200, true, ["device", "types", type, "devices", id, "events", eventType], null); 31 | } 32 | 33 | }; -------------------------------------------------------------------------------- /src/api/MgmtClient.js: -------------------------------------------------------------------------------- 1 | /** 2 | ***************************************************************************** 3 | Copyright (c) 2014, 2019 IBM Corporation and other Contributors. 4 | All rights reserved. This program and the accompanying materials 5 | are made available under the terms of the Eclipse Public License v1.0 6 | which accompanies this distribution, and is available at 7 | http://www.eclipse.org/legal/epl-v10.html 8 | ***************************************************************************** 9 | * 10 | */ 11 | import log from 'loglevel'; 12 | 13 | export default class MgmtClient { 14 | constructor(apiClient) { 15 | this.log = log; 16 | 17 | this.apiClient = apiClient; 18 | 19 | // Create an alias to the apiClient's callApi 20 | this.callApi = this.apiClient.callApi.bind(this.apiClient); 21 | } 22 | 23 | getAllDeviceManagementRequests() { 24 | this.log.debug("[ApiClient] getAllDeviceManagementRequests()"); 25 | return this.callApi('GET', 200, true, ['mgmt', 'requests'], null); 26 | } 27 | 28 | initiateDeviceManagementRequest(action, parameters, devices) { 29 | this.log.debug("[ApiClient] initiateDeviceManagementRequest(" + action + ", " + parameters + ", " + devices + ")"); 30 | let body = { 31 | action: action, 32 | parameters: parameters, 33 | devices: devices 34 | }; 35 | return this.callApi('POST', 202, true, ['mgmt', 'requests'], JSON.stringify(body)); 36 | } 37 | 38 | getDeviceManagementRequest(requestId) { 39 | this.log.debug("[ApiClient] getDeviceManagementRequest(" + requestId + ")"); 40 | return this.callApi('GET', 200, true, ['mgmt', 'requests', requestId], null); 41 | } 42 | 43 | deleteDeviceManagementRequest(requestId) { 44 | this.log.debug("[ApiClient] deleteDeviceManagementRequest(" + requestId + ")"); 45 | return this.callApi('DELETE', 204, false, ['mgmt', 'requests', requestId], null); 46 | } 47 | 48 | getDeviceManagementRequestStatus(requestId) { 49 | this.log.debug("[ApiClient] getDeviceManagementRequestStatus(" + requestId + ")"); 50 | return this.callApi('GET', 200, true, ['mgmt', 'requests', requestId, 'deviceStatus'], null); 51 | } 52 | 53 | getDeviceManagementRequestStatusByDevice(requestId, typeId, deviceId) { 54 | this.log.debug("[ApiClient] getDeviceManagementRequestStatusByDevice(" + requestId + ", " + typeId + ", " + deviceId + ")"); 55 | return this.callApi('GET', 200, true, ['mgmt', 'requests', requestId, 'deviceStatus', typeId, deviceId], null); 56 | } 57 | 58 | }; -------------------------------------------------------------------------------- /src/api/RegistryClient.js: -------------------------------------------------------------------------------- 1 | /** 2 | ***************************************************************************** 3 | Copyright (c) 2014, 2019 IBM Corporation and other Contributors. 4 | All rights reserved. This program and the accompanying materials 5 | are made available under the terms of the Eclipse Public License v1.0 6 | which accompanies this distribution, and is available at 7 | http://www.eclipse.org/legal/epl-v10.html 8 | ***************************************************************************** 9 | * 10 | */ 11 | 12 | export default class RegistryClient { 13 | constructor(apiClient) { 14 | this.apiClient = apiClient; 15 | } 16 | 17 | listAllDevicesOfType(type) { 18 | this.apiClient.log.debug("[ApiClient] listAllDevicesOfType(" + type + ")"); 19 | return this.apiClient.callApi('GET', 200, true, ['device', 'types', type, 'devices'], null); 20 | } 21 | 22 | deleteDeviceType(type) { 23 | this.apiClient.log.debug("[ApiClient] deleteDeviceType(" + type + ")"); 24 | return this.apiClient.callApi('DELETE', 204, false, ['device', 'types', type], null); 25 | } 26 | 27 | getDeviceType(type) { 28 | this.apiClient.log.debug("[ApiClient] getDeviceType(" + type + ")"); 29 | return this.apiClient.callApi('GET', 200, true, ['device', 'types', type], null); 30 | } 31 | 32 | getAllDeviceTypes() { 33 | this.apiClient.log.debug("[ApiClient] getAllDeviceTypes()"); 34 | return this.apiClient.callApi('GET', 200, true, ['device', 'types'], null); 35 | } 36 | 37 | updateDeviceType(type, description, deviceInfo, metadata) { 38 | this.apiClient.log.debug("[ApiClient] updateDeviceType(" + type + ", " + description + ", " + deviceInfo + ", " + metadata + ")"); 39 | let body = { 40 | deviceInfo: deviceInfo, 41 | description: description, 42 | metadata: metadata 43 | }; 44 | 45 | return this.apiClient.callApi('PUT', 200, true, ['device', 'types', type], JSON.stringify(body)); 46 | } 47 | 48 | registerDeviceType(typeId, description, deviceInfo, metadata, classId) { 49 | this.apiClient.log.debug("[ApiClient] registerDeviceType(" + typeId + ", " + description + ", " + deviceInfo + ", " + metadata + ", " + classId + ")"); 50 | // TODO: field validation 51 | classId = classId || "Device"; 52 | let body = { 53 | id: typeId, 54 | classId: classId, 55 | deviceInfo: deviceInfo, 56 | description: description, 57 | metadata: metadata 58 | }; 59 | 60 | return this.apiClient.callApi('POST', 201, true, ['device', 'types'], JSON.stringify(body)); 61 | } 62 | 63 | registerDevice(type, deviceId, authToken, deviceInfo, location, metadata) { 64 | this.apiClient.log.debug("[ApiClient] registerDevice(" + type + ", " + deviceId + ", " + deviceInfo + ", " + location + ", " + metadata + ")"); 65 | // TODO: field validation 66 | let body = { 67 | deviceId: deviceId, 68 | authToken: authToken, 69 | deviceInfo: deviceInfo, 70 | location: location, 71 | metadata: metadata 72 | }; 73 | 74 | return this.apiClient.callApi('POST', 201, true, ['device', 'types', type, 'devices'], JSON.stringify(body)); 75 | } 76 | 77 | unregisterDevice(type, deviceId) { 78 | this.apiClient.log.debug("[ApiClient] unregisterDevice(" + type + ", " + deviceId + ")"); 79 | return this.apiClient.callApi('DELETE', 204, false, ['device', 'types', type, 'devices', deviceId], null); 80 | } 81 | 82 | updateDevice(type, deviceId, deviceInfo, status, metadata, extensions) { 83 | this.apiClient.log.debug("[ApiClient] updateDevice(" + type + ", " + deviceId + ", " + deviceInfo + ", " + status + ", " + metadata + ")"); 84 | let body = { 85 | deviceInfo: deviceInfo, 86 | status: status, 87 | metadata: metadata, 88 | extensions: extensions 89 | }; 90 | 91 | return this.apiClient.callApi('PUT', 200, true, ['device', 'types', type, 'devices', deviceId], JSON.stringify(body)); 92 | } 93 | 94 | getDevice(type, deviceId) { 95 | this.apiClient.log.debug("[ApiClient] getDevice(" + type + ", " + deviceId + ")"); 96 | return this.apiClient.callApi('GET', 200, true, ['device', 'types', type, 'devices', deviceId], null); 97 | } 98 | 99 | 100 | /** 101 | * Register multiple new devices, each request can contain a maximum of 512KB. 102 | * The response body will contain the generated authentication tokens for all devices. 103 | * The caller of the method must make sure to record these tokens when processing 104 | * the response. The IBM Watson IoT Platform will not be able to retrieve lost authentication tokens 105 | * 106 | * @param arryOfDevicesToBeAdded Array of JSON devices to be added. Refer to 107 | * link 108 | * for more information about the schema to be used 109 | */ 110 | registerMultipleDevices(arryOfDevicesToBeAdded) { 111 | this.apiClient.log.debug("[ApiClient] arryOfDevicesToBeAdded() - BULK"); 112 | return this.apiClient.callApi('POST', 201, true, ["bulk", "devices", "add"], JSON.stringify(arryOfDevicesToBeAdded)); 113 | } 114 | 115 | /** 116 | * Delete multiple devices, each request can contain a maximum of 512Kb 117 | * 118 | * @param arryOfDevicesToBeDeleted Array of JSON devices to be deleted. Refer to 119 | * link 120 | * for more information about the schema to be used. 121 | */ 122 | deleteMultipleDevices(arryOfDevicesToBeDeleted) { 123 | 124 | this.apiClient.log.debug("[ApiClient] deleteMultipleDevices() - BULK"); 125 | return this.apiClient.callApi('POST', 201, true, ["bulk", "devices", "remove"], JSON.stringify(arryOfDevicesToBeDeleted)); 126 | } 127 | 128 | getDeviceLocation(type, deviceId) { 129 | this.apiClient.log.debug("[ApiClient] getDeviceLocation(" + type + ", " + deviceId + ")"); 130 | return this.apiClient.callApi('GET', 200, true, ['device', 'types', type, 'devices', deviceId, 'location'], null); 131 | } 132 | 133 | updateDeviceLocation(type, deviceId, location) { 134 | this.apiClient.log.debug("[ApiClient] updateDeviceLocation(" + type + ", " + deviceId + ", " + location + ")"); 135 | 136 | return this.apiClient.callApi('PUT', 200, true, ['device', 'types', type, 'devices', deviceId, 'location'], JSON.stringify(location)); 137 | } 138 | 139 | 140 | getDeviceManagementInformation(type, deviceId) { 141 | this.apiClient.log.debug("[ApiClient] getDeviceManagementInformation(" + type + ", " + deviceId + ")"); 142 | return this.apiClient.callApi('GET', 200, true, ['device', 'types', type, 'devices', deviceId, 'mgmt'], null); 143 | } 144 | 145 | getAllDiagnosticLogs(type, deviceId) { 146 | this.apiClient.log.debug("[ApiClient] getAllDiagnosticLogs(" + type + ", " + deviceId + ")"); 147 | return this.apiClient.callApi('GET', 200, true, ['device', 'types', type, 'devices', deviceId, 'diag', 'logs'], null); 148 | } 149 | 150 | clearAllDiagnosticLogs(type, deviceId) { 151 | this.apiClient.log.debug("[ApiClient] clearAllDiagnosticLogs(" + type + ", " + deviceId + ")"); 152 | return this.apiClient.callApi('DELETE', 204, false, ['device', 'types', type, 'devices', deviceId, 'diag', 'logs'], null); 153 | } 154 | 155 | addDeviceDiagLogs(type, deviceId, log) { 156 | this.apiClient.log.debug("[ApiClient] addDeviceDiagLogs(" + type + ", " + deviceId + ", " + log + ")"); 157 | return this.apiClient.callApi('POST', 201, false, ['device', 'types', type, 'devices', deviceId, 'diag', 'logs'], JSON.stringify(log)); 158 | } 159 | 160 | getDiagnosticLog(type, deviceId, logId) { 161 | this.apiClient.log.debug("[ApiClient] getAllDiagnosticLogs(" + type + ", " + deviceId + ", " + logId + ")"); 162 | return this.apiClient.callApi('GET', 200, true, ['device', 'types', type, 'devices', deviceId, 'diag', 'logs', logId], null); 163 | } 164 | 165 | deleteDiagnosticLog(type, deviceId, logId) { 166 | this.apiClient.log.debug("[ApiClient] deleteDiagnosticLog(" + type + ", " + deviceId + ", " + logId + ")"); 167 | return this.apiClient.callApi('DELETE', 204, false, ['device', 'types', type, 'devices', deviceId, 'diag', 'logs', logId], null); 168 | } 169 | 170 | getDeviceErrorCodes(type, deviceId) { 171 | this.apiClient.log.debug("[ApiClient] getDeviceErrorCodes(" + type + ", " + deviceId + ")"); 172 | return this.apiClient.callApi('GET', 200, true, ['device', 'types', type, 'devices', deviceId, 'diag', 'errorCodes'], null); 173 | } 174 | 175 | clearDeviceErrorCodes(type, deviceId) { 176 | this.apiClient.log.debug("[ApiClient] clearDeviceErrorCodes(" + type + ", " + deviceId + ")"); 177 | return this.apiClient.callApi('DELETE', 204, false, ['device', 'types', type, 'devices', deviceId, 'diag', 'errorCodes'], null); 178 | } 179 | 180 | addErrorCode(type, deviceId, log) { 181 | this.apiClient.log.debug("[ApiClient] addErrorCode(" + type + ", " + deviceId + ", " + log + ")"); 182 | return this.apiClient.callApi('POST', 201, false, ['device', 'types', type, 'devices', deviceId, 'diag', 'errorCodes'], JSON.stringify(log)); 183 | } 184 | 185 | getDeviceConnectionLogs(typeId, deviceId) { 186 | this.apiClient.log.debug("[ApiClient] getDeviceConnectionLogs(" + typeId + ", " + deviceId + ")"); 187 | let params = { 188 | typeId: typeId, 189 | deviceId: deviceId 190 | }; 191 | return this.apiClient.callApi('GET', 200, true, ['logs', 'connection'], null, params); 192 | } 193 | 194 | 195 | }; -------------------------------------------------------------------------------- /src/api/RulesClient.js: -------------------------------------------------------------------------------- 1 | /** 2 | ***************************************************************************** 3 | Copyright (c) 2014, 2019 IBM Corporation and other Contributors. 4 | All rights reserved. This program and the accompanying materials 5 | are made available under the terms of the Eclipse Public License v1.0 6 | which accompanies this distribution, and is available at 7 | http://www.eclipse.org/legal/epl-v10.html 8 | ***************************************************************************** 9 | * 10 | */ 11 | import log from 'loglevel'; 12 | 13 | export default class RulesClient { 14 | constructor(apiClient) { 15 | this.log = log; 16 | 17 | this.apiClient = apiClient; 18 | 19 | // Create an alias to the apiClient's callApi 20 | this.callApi = this.apiClient.callApi.bind(this.apiClient); 21 | } 22 | 23 | 24 | getRulesForLogicalInterface(logicalInterfaceId) { 25 | if (this.draftMode) { 26 | return this.getRulesForLogicalInterface(logicalInterfaceId); 27 | } else { 28 | return this.getActiveRulesForLogicalInterface(logicalInterfaceId); 29 | } 30 | } 31 | 32 | 33 | getDraftRulesForLogicalInterface(logicalInterfaceId) { 34 | return this.callApi('GET', 200, true, ['draft', 'logicalinterfaces', logicalInterfaceId, 'rules']); 35 | } 36 | 37 | 38 | getActiveRulesForLogicalInterface(logicalInterfaceId) { 39 | return this.callApi('GET', 200, true, ['logicalinterfaces', logicalInterfaceId, 'rules']); 40 | } 41 | 42 | 43 | createRule(logicalInterfaceId, name, condition, description=undefined, notificationStrategy=RulesClient.RuleNotificationStrategy.EVERY_TIME()) { 44 | var body = { 45 | name, 46 | condition, 47 | notificationStrategy 48 | } 49 | if (description) body['description'] = description; 50 | var base = this.draftMode ? ['draft', 'logicalinterfaces', logicalInterfaceId, 'rules'] : ['logicalinterfaces', logicalInterfaceId, 'rules']; 51 | return this.callApi('POST', 201, true, base, JSON.stringify(body)); 52 | } 53 | 54 | 55 | updateRule(rule) { 56 | var base = this.draftMode ? ['draft', 'logicalinterfaces', rule.logicalInterfaceId, 'rules', rule.id] : ['logicalinterfaces', rule.logicalInterfaceId, 'rules', rule.id]; 57 | return this.callApi('PUT', 200, true, base, JSON.stringify(rule)); 58 | } 59 | 60 | 61 | deleteRule(logicalInterfaceId, ruleId) { 62 | var base = this.draftMode ? ['draft', 'logicalinterfaces', logicalInterfaceId, 'rules', ruleId] : ['logicalinterfaces', logicalInterfaceId, 'rules', ruleId]; 63 | return this.callApi('DELETE', 204, false, base); 64 | } 65 | 66 | } 67 | 68 | RulesClient.RuleNotificationStrategy = { 69 | EVERY_TIME: () => ({ 70 | when: 'every-time' 71 | }), 72 | BECOMES_TRUE: () => ({ 73 | when: 'becomes-true', 74 | }), 75 | X_IN_Y: (count) => ({ 76 | when: 'x-in-y', 77 | count, 78 | }), 79 | PERSISTS: (count, timePeriod) => ({ 80 | when: 'persists', 81 | count, 82 | timePeriod 83 | }), 84 | 85 | }; -------------------------------------------------------------------------------- /src/api/StateClient.js: -------------------------------------------------------------------------------- 1 | /** 2 | ***************************************************************************** 3 | Copyright (c) 2014, 2019 IBM Corporation and other Contributors. 4 | All rights reserved. This program and the accompanying materials 5 | are made available under the terms of the Eclipse Public License v1.0 6 | which accompanies this distribution, and is available at 7 | http://www.eclipse.org/legal/epl-v10.html 8 | ***************************************************************************** 9 | * 10 | */ 11 | import log from 'loglevel'; 12 | 13 | export default class StateClient { 14 | constructor(apiClient) { 15 | this.log = log; 16 | 17 | this.apiClient = apiClient; 18 | this.draftMode = true; 19 | 20 | // Create an alias to the apiClient's methods that we use 21 | this.callApi = this.apiClient.callApi.bind(this.apiClient); 22 | this.parseSortSpec = this.apiClient.parseSortSpec.bind(this.apiClient); 23 | this.callFormDataApi = this.apiClient.callFormDataApi.bind(this.apiClient); 24 | this.invalidOperation = this.apiClient.invalidOperation.bind(this.apiClient); 25 | } 26 | 27 | workWithActive() { 28 | this.draftMode = false; 29 | } 30 | 31 | workWithDraft() { 32 | this.draftMode = true; 33 | } 34 | 35 | // IM Device state API 36 | createSchema(schemaContents, name, description) { 37 | var body = { 38 | 'schemaFile': schemaContents, 39 | 'schemaType': 'json-schema', 40 | 'name': name, 41 | } 42 | 43 | if (description) { 44 | body.description = description 45 | } 46 | 47 | var base = this.draftMode ? ["draft", "schemas"] : ["schemas"] 48 | return this.callFormDataApi('POST', 201, true, base, body, null); 49 | } 50 | 51 | getSchema(schemaId) { 52 | var base = this.draftMode ? ["draft", "schemas", schemaId] : ["schemas", schemaId] 53 | return this.callApi('GET', 200, true, base); 54 | } 55 | 56 | getActiveSchema(schemaId) { 57 | return this.callApi('GET', 200, true, ["schemas", schemaId]); 58 | } 59 | 60 | getSchemas() { 61 | var base = this.draftMode ? ["draft", "schemas"] : ["schemas"] 62 | return this.callApi('GET', 200, true, base); 63 | } 64 | 65 | getActiveSchemas() { 66 | return this.callApi('GET', 200, true, ["schemas"]); 67 | } 68 | 69 | updateSchema(schemaId, name, description) { 70 | var body = { 71 | "id": schemaId, 72 | "name": name, 73 | "description": description 74 | } 75 | 76 | var base = this.draftMode ? ["draft", "schemas", schemaId] : ["schemas", schemaId] 77 | return this.callApi('PUT', 200, true, base, body); 78 | } 79 | 80 | updateSchemaContent(schemaId, schemaContents, filename) { 81 | var body = { 82 | 'schemaFile': schemaContents, 83 | 'name': filename 84 | } 85 | 86 | var base = this.draftMode ? ["draft", "schemas", schemaId, "content"] : ["schemas", schemaId, "content"] 87 | return this.callFormDataApi('PUT', 204, false, base, body, null); 88 | } 89 | 90 | getSchemaContent(schemaId) { 91 | var base = this.draftMode ? ["draft", "schemas", schemaId, "content"] : ["schemas", schemaId, "content"] 92 | return this.callApi('GET', 200, true, base); 93 | } 94 | 95 | getActiveSchemaContent(schemaId) { 96 | return this.callApi('GET', 200, true, ["schemas", schemaId, "content"]); 97 | } 98 | 99 | deleteSchema(schemaId) { 100 | var base = this.draftMode ? ["draft", "schemas", schemaId] : ["schemas", schemaId] 101 | return this.callApi('DELETE', 204, false, base, null); 102 | } 103 | 104 | 105 | createEventType(name, description, schemaId) { 106 | var body = { 107 | 'name': name, 108 | 'description': description, 109 | 'schemaId': schemaId, 110 | } 111 | var base = this.draftMode ? ["draft", "event", "types"] : ["event", "types"] 112 | return this.callApi('POST', 201, true, base, JSON.stringify(body)); 113 | } 114 | 115 | getEventType(eventTypeId) { 116 | var base = this.draftMode ? ["draft", "event", "types", eventTypeId] : ["event", "types", eventTypeId] 117 | return this.callApi('GET', 200, true, base); 118 | } 119 | 120 | getActiveEventType(eventTypeId) { 121 | return this.callApi('GET', 200, true, ["event", "types", eventTypeId]); 122 | } 123 | 124 | deleteEventType(eventTypeId) { 125 | var base = this.draftMode ? ["draft", "event", "types", eventTypeId] : ["event", "types", eventTypeId] 126 | return this.callApi('DELETE', 204, false, base); 127 | } 128 | 129 | updateEventType(eventTypeId, name, description, schemaId) { 130 | var body = { 131 | "id": eventTypeId, 132 | "name": name, 133 | "description": description, 134 | "schemaId": schemaId 135 | } 136 | 137 | var base = this.draftMode ? ["draft", "event", "types", eventTypeId] : ["event", "types", eventTypeId] 138 | return this.callApi('PUT', 200, true, base, body); 139 | } 140 | 141 | getEventTypes() { 142 | var base = this.draftMode ? ["draft", "event", "types"] : ["event", "types"] 143 | return this.callApi('GET', 200, true, base); 144 | } 145 | 146 | getActiveEventTypes() { 147 | return this.callApi('GET', 200, true, ["event", "types"]); 148 | } 149 | 150 | createPhysicalInterface(name, description) { 151 | var body = { 152 | 'name': name, 153 | 'description': description 154 | } 155 | 156 | var base = this.draftMode ? ["draft", "physicalinterfaces"] : ["physicalinterfaces"] 157 | return this.callApi('POST', 201, true, base, body); 158 | } 159 | 160 | getPhysicalInterface(physicalInterfaceId) { 161 | var base = this.draftMode ? ["draft", "physicalinterfaces", physicalInterfaceId] : ["physicalinterfaces", physicalInterfaceId] 162 | return this.callApi('GET', 200, true, base); 163 | } 164 | 165 | getActivePhysicalInterface(physicalInterfaceId) { 166 | return this.callApi('GET', 200, true, ["physicalinterfaces", physicalInterfaceId]); 167 | } 168 | 169 | deletePhysicalInterface(physicalInterfaceId) { 170 | var base = this.draftMode ? ["draft", "physicalinterfaces", physicalInterfaceId] : ["physicalinterfaces", physicalInterfaceId] 171 | return this.callApi('DELETE', 204, false, base); 172 | } 173 | 174 | updatePhysicalInterface(physicalInterfaceId, name, description) { 175 | var body = { 176 | 'id': physicalInterfaceId, 177 | 'name': name, 178 | 'description': description 179 | } 180 | 181 | var base = this.draftMode ? ["draft", "physicalinterfaces", physicalInterfaceId] : ["physicalinterfaces", physicalInterfaceId] 182 | return this.callApi('PUT', 200, true, base, body); 183 | } 184 | 185 | getPhysicalInterfaces() { 186 | var base = this.draftMode ? ["draft", "physicalinterfaces"] : ["physicalinterfaces"] 187 | return this.callApi('GET', 200, true, base); 188 | } 189 | 190 | getActivePhysicalInterfaces() { 191 | return this.callApi('GET', 200, true, ["physicalinterfaces"]); 192 | } 193 | 194 | createPhysicalInterfaceEventMapping(physicalInterfaceId, eventId, eventTypeId) { 195 | var body = { 196 | "eventId": eventId, 197 | "eventTypeId": eventTypeId 198 | } 199 | 200 | var base = this.draftMode ? ["draft", "physicalinterfaces", physicalInterfaceId, "events"] : ["physicalinterfaces", physicalInterfaceId, "events"] 201 | return this.callApi('POST', 201, true, base, body); 202 | } 203 | 204 | getPhysicalInterfaceEventMappings(physicalInterfaceId) { 205 | var base = this.draftMode ? ["draft", "physicalinterfaces", physicalInterfaceId, "events"] : ["physicalinterfaces", physicalInterfaceId, "events"] 206 | return this.callApi('GET', 200, true, base); 207 | } 208 | 209 | getActivePhysicalInterfaceEventMappings(physicalInterfaceId) { 210 | return this.callApi('GET', 200, true, ["physicalinterfaces", physicalInterfaceId, "events"]); 211 | } 212 | 213 | deletePhysicalInterfaceEventMapping(physicalInterfaceId, eventId) { 214 | var base = this.draftMode ? ["draft", "physicalinterfaces", physicalInterfaceId, "events", eventId] : ["physicalinterfaces", physicalInterfaceId, "events", eventId] 215 | return this.callApi('DELETE', 204, false, base); 216 | } 217 | 218 | createLogicalInterface(name, description, schemaId, alias) { 219 | var body = { 220 | 'name': name, 221 | 'description': description, 222 | 'schemaId': schemaId, 223 | } 224 | if (alias !== undefined) { 225 | body.alias = alias; 226 | } 227 | 228 | var base = this.draftMode ? ["draft", "logicalinterfaces"] : ["applicationinterfaces"] 229 | return this.callApi('POST', 201, true, base, body); 230 | } 231 | 232 | getLogicalInterface(logicalInterfaceId) { 233 | var base = this.draftMode ? ["draft", "logicalinterfaces", logicalInterfaceId] : ["applicationinterfaces", logicalInterfaceId] 234 | return this.callApi('GET', 200, true, base); 235 | } 236 | 237 | getActiveLogicalInterface(logicalInterfaceId) { 238 | return this.callApi('GET', 200, true, ["logicalinterfaces", logicalInterfaceId]); 239 | } 240 | 241 | deleteLogicalInterface(logicalInterfaceId) { 242 | var base = this.draftMode ? ["draft", "logicalinterfaces", logicalInterfaceId] : ["applicationinterfaces", logicalInterfaceId] 243 | return this.callApi('DELETE', 204, false, base); 244 | } 245 | 246 | updateLogicalInterface(logicalInterfaceId, name, description, schemaId, alias) { 247 | var body = { 248 | "id": logicalInterfaceId, 249 | "name": name, 250 | "description": description, 251 | "schemaId": schemaId 252 | } 253 | if (alias !== undefined) { 254 | body.alias = alias; 255 | } 256 | 257 | var base = this.draftMode ? ["draft", "logicalinterfaces", logicalInterfaceId] : ["applicationinterfaces", logicalInterfaceId] 258 | return this.callApi('PUT', 200, true, base, body); 259 | } 260 | 261 | 262 | getLogicalInterfaces(bookmark, limit, sort, name) { 263 | var base = this.draftMode ? ["draft", "logicalinterfaces"] : ["logicalinterfaces"] 264 | 265 | const _sort = this.parseSortSpec(sort); 266 | 267 | return this.callApi('GET', 200, true, base, undefined, {_bookmark: bookmark,_limit: limit, _sort, name : name ? name : undefined}); 268 | } 269 | 270 | getActiveLogicalInterfaces() { 271 | return this.callApi('GET', 200, true, ["logicalinterfaces"]); 272 | } 273 | 274 | // Application interface patch operation on draft version 275 | // Acceptable operation id - validate-configuration, activate-configuration, list-differences 276 | patchOperationLogicalInterface(logicalInterfaceId, operationId) { 277 | var body = { 278 | "operation": operationId 279 | } 280 | 281 | if(this.draftMode) { 282 | switch(operationId) { 283 | case 'validate-configuration': 284 | return this.callApi('PATCH', 200, true, ["draft", "logicalinterfaces", logicalInterfaceId], body); 285 | case 'activate-configuration': 286 | return this.callApi('PATCH', 202, true, ["draft", "logicalinterfaces", logicalInterfaceId], body); 287 | case 'deactivate-configuration': 288 | return this.callApi('PATCH', 202, true, ["draft", "logicalinterfaces", logicalInterfaceId], body); 289 | case 'list-differences': 290 | return this.callApi('PATCH', 200, true, ["draft", "logicalinterfaces", logicalInterfaceId], body); 291 | default: 292 | return this.callApi('PATCH', 200, true, ["draft", "logicalinterfaces", logicalInterfaceId], body); 293 | } 294 | } else { 295 | return this.invalidOperation("PATCH operation not allowed on active logical interface"); 296 | } 297 | } 298 | 299 | // Application interface patch operation on active version 300 | // Acceptable operation id - deactivate-configuration 301 | patchOperationActiveLogicalInterface(logicalInterfaceId, operationId) { 302 | var body = { 303 | "operation": operationId 304 | } 305 | 306 | if(this.draftMode) { 307 | return this.callApi('PATCH', 202, true, ["logicalinterfaces", logicalInterfaceId], body) 308 | } 309 | else { 310 | return this.invalidOperation("PATCH operation 'deactivate-configuration' not allowed on logical interface"); 311 | } 312 | } 313 | 314 | // Application interface patch operation on draft version 315 | // Acceptable operation id - validate-configuration, activate-configuration, list-differences 316 | patchOperationDeviceType(deviceTypeId, operationId) { 317 | var body = { 318 | "operation": operationId 319 | } 320 | 321 | if(this.draftMode) { 322 | switch(operationId) { 323 | case 'validate-configuration': 324 | return this.callApi('PATCH', 200, true, ["draft", "device", "types", deviceTypeId], body); 325 | case 'activate-configuration': 326 | return this.callApi('PATCH', 202, true, ["draft", "device", "types", deviceTypeId], body); 327 | case 'deactivate-configuration': 328 | return this.callApi('PATCH', 202, true, ["draft", "device", "types", deviceTypeId], body); 329 | case 'list-differences': 330 | return this.callApi('PATCH', 200, true, ["draft", "device", "types", deviceTypeId], body); 331 | default: 332 | return this.callApi('PATCH', 200, true, ["draft", "device", "types", deviceTypeId], body); 333 | } 334 | } else { 335 | return this.invalidOperation("this method cannot be called when draftMode=false"); 336 | } 337 | } 338 | 339 | // Create device type with physical Interface Id 340 | createDeviceType(typeId, description, deviceInfo, metadata, classId, physicalInterfaceId) { 341 | this.log.debug("[ApiClient] registerDeviceType(" + typeId + ", " + description + ", " + deviceInfo + ", " + metadata + ", " + classId + ", " + physicalInterfaceId + ")"); 342 | classId = classId || "Device"; 343 | let body = { 344 | id: typeId, 345 | classId: classId, 346 | deviceInfo: deviceInfo, 347 | description: description, 348 | metadata: metadata, 349 | physicalInterfaceId: physicalInterfaceId 350 | }; 351 | 352 | return this.callApi('POST', 201, true, ['device', 'types'], JSON.stringify(body)); 353 | } 354 | 355 | 356 | getDeviceTypesByLogicalInterfaceId(logicalInterfaceId, bookmark=0, limit=10) { 357 | if(this.draftMode) { 358 | return this.callApi('GET', 200, true, ['draft', 'device', 'types'], null, {logicalInterfaceId, '_bookmark': bookmark, '_limit': limit}); 359 | } else { 360 | return this.callApi('GET', 200, true, ['device', 'types'], null, {'_bookmark': bookmark, '_limit': limit}); 361 | } 362 | } 363 | 364 | createDeviceTypePhysicalInterfaceAssociation(typeId, physicalInterfaceId) { 365 | let body = { 366 | id: physicalInterfaceId 367 | }; 368 | 369 | if(this.draftMode) { 370 | return this.callApi('POST', 201, true, ['draft', 'device', 'types', typeId, 'physicalinterface'], JSON.stringify(body)); 371 | } else { 372 | return this.callApi('PUT', 200, true, ['device', 'types', typeId], JSON.stringify({physicalInterfaceId : physicalInterfaceId})); 373 | } 374 | 375 | } 376 | 377 | getDeviceTypePhysicalInterfaces(typeId) { 378 | if(this.draftMode) { 379 | return this.callApi('GET', 200, true, ['draft', 'device', 'types', typeId, 'physicalinterface']); 380 | } else { 381 | return this.invalidOperation("GET Device type's physical interface is not allowed"); 382 | } 383 | } 384 | 385 | getActiveDeviceTypePhysicalInterfaces(typeId) { 386 | return this.callApi('GET', 200, true, ['device', 'types', typeId, 'physicalinterface']); 387 | } 388 | 389 | 390 | deleteDeviceTypePhysicalInterfaceAssociation(typeId) { 391 | if(this.draftMode) { 392 | return this.callApi('DELETE', 204, false, ['draft', 'device', 'types', typeId, 'physicalinterface']); 393 | } else { 394 | return this.invalidOperation("DELETE Device type's physical interface is not allowed"); 395 | } 396 | } 397 | 398 | createDeviceTypeLogicalInterfaceAssociation(typeId, logicalInterfaceId) { 399 | var body = { 400 | 'id': logicalInterfaceId 401 | } 402 | 403 | var base = this.draftMode ? ['draft', 'device', 'types', typeId, 'logicalinterfaces'] : ['device', 'types', typeId, 'applicationinterfaces'] 404 | return this.callApi('POST', 201, true, base, body); 405 | } 406 | 407 | getDeviceTypeLogicalInterfaces(typeId) { 408 | var base = this.draftMode ? ['draft', 'device', 'types', typeId, 'logicalinterfaces'] : ['device', 'types', typeId, 'applicationinterfaces'] 409 | return this.callApi('GET', 200, true, base); 410 | } 411 | 412 | getActiveDeviceTypeLogicalInterfaces(typeId) { 413 | return this.callApi('GET', 200, true, ['device', 'types', typeId, 'logicalinterfaces']); 414 | } 415 | 416 | createDeviceTypeLogicalInterfacePropertyMappings(typeId, logicalInterfaceId, mappings, notificationStrategy) { 417 | var body = null, base = null 418 | if(this.draftMode) { 419 | body = { 420 | "logicalInterfaceId": logicalInterfaceId, 421 | "propertyMappings": mappings, 422 | "notificationStrategy": "never" 423 | } 424 | 425 | if(notificationStrategy) { 426 | body.notificationStrategy = notificationStrategy 427 | } 428 | 429 | base = ['draft', 'device', 'types', typeId, 'mappings'] 430 | } else { 431 | body = { 432 | "applicationInterfaceId": logicalInterfaceId, 433 | "propertyMappings": mappings 434 | } 435 | base = ['device', 'types', typeId, 'mappings'] 436 | } 437 | 438 | return this.callApi('POST', 201, true, base, body); 439 | } 440 | 441 | getDeviceTypePropertyMappings(typeId) { 442 | var base = this.draftMode ? ['draft', 'device', 'types', typeId, 'mappings'] : ['device', 'types', typeId, 'mappings'] 443 | return this.callApi('GET', 200, true, base); 444 | } 445 | 446 | getActiveDeviceTypePropertyMappings(typeId) { 447 | return this.callApi('GET', 200, true, ['device', 'types', typeId, 'mappings']); 448 | } 449 | 450 | getDeviceTypeLogicalInterfacePropertyMappings(typeId, logicalInterfaceId) { 451 | var base = this.draftMode ? ['draft', 'device', 'types', typeId, 'mappings', logicalInterfaceId] : ['device', 'types', typeId, 'mappings', logicalInterfaceId] 452 | return this.callApi('GET', 200, true, base); 453 | } 454 | 455 | getActiveDeviceTypeLogicalInterfacePropertyMappings(typeId, logicalInterfaceId) { 456 | return this.callApi('GET', 200, true, ['device', 'types', typeId, 'mappings', logicalInterfaceId]); 457 | } 458 | 459 | updateDeviceTypeLogicalInterfacePropertyMappings(typeId, logicalInterfaceId, mappings, notificationStrategy) { 460 | var body = null, base = null 461 | if(this.draftMode) { 462 | body = { 463 | "logicalInterfaceId": logicalInterfaceId, 464 | "propertyMappings": mappings, 465 | "notificationStrategy": "never" 466 | } 467 | 468 | if(notificationStrategy) { 469 | body.notificationStrategy = notificationStrategy 470 | } 471 | 472 | base = ['draft', 'device', 'types', typeId, 'mappings', logicalInterfaceId] 473 | } else { 474 | body = { 475 | "applicationInterfaceId": logicalInterfaceId, 476 | "propertyMappings": mappings 477 | } 478 | base = ['device', 'types', typeId, 'mappings', logicalInterfaceId] 479 | } 480 | return this.callApi('PUT', 200, false, base, body); 481 | } 482 | 483 | deleteDeviceTypeLogicalInterfacePropertyMappings(typeId, logicalInterfaceId) { 484 | var base = this.draftMode ? ['draft', 'device', 'types', typeId, 'mappings', logicalInterfaceId] : ['device', 'types', typeId, 'mappings', logicalInterfaceId] 485 | return this.callApi('DELETE', 204, false, base); 486 | } 487 | 488 | deleteDeviceTypeLogicalInterfaceAssociation(typeId, logicalInterfaceId) { 489 | var base = this.draftMode ? ['draft', 'device', 'types', typeId, 'logicalinterfaces', logicalInterfaceId] : ['device', 'types', typeId, 'applicationinterfaces', logicalInterfaceId] 490 | return this.callApi('DELETE', 204, false, base); 491 | } 492 | 493 | // Device Type patch operation on draft version 494 | // Acceptable operation id - validate-configuration, activate-configuration, list-differences 495 | patchOperationDeviceType(typeId, operationId) { 496 | if(!operationId) { 497 | return invalidOperation("PATCH operation is not allowed. Operation id is expected") 498 | } 499 | 500 | var body = { 501 | "operation": operationId 502 | } 503 | 504 | var base = this.draftMode ? ['draft', 'device', 'types', typeId]: ['device', 'types', typeId] 505 | 506 | if(this.draftMode) { 507 | switch(operationId) { 508 | case 'validate-configuration': 509 | return this.callApi('PATCH', 200, true, base, body); 510 | break 511 | case 'activate-configuration': 512 | return this.callApi('PATCH', 202, true, base, body); 513 | break 514 | case 'deactivate-configuration': 515 | return this.callApi('PATCH', 202, true, base, body); 516 | break 517 | case 'list-differences': 518 | return this.callApi('PATCH', 200, true, base, body); 519 | break 520 | default: 521 | return this.invalidOperation("PATCH operation is not allowed. Invalid operation id") 522 | } 523 | } else { 524 | switch(operationId) { 525 | case 'validate-configuration': 526 | return this.callApi('PATCH', 200, true, base, body); 527 | break 528 | case 'deploy-configuration': 529 | return this.callApi('PATCH', 202, true, base, body); 530 | break 531 | case 'remove-deployed-configuration': 532 | return this.callApi('PATCH', 202, true, base, body); 533 | break 534 | case 'list-differences': 535 | return this.invalidOperation("PATCH operation 'list-differences' is not allowed") 536 | break 537 | default: 538 | return this.invalidOperation("PATCH operation is not allowed. Invalid operation id") 539 | } 540 | } 541 | } 542 | 543 | 544 | // Device Type patch operation on active version 545 | // Acceptable operation id - deactivate-configuration 546 | patchOperationActiveDeviceType(typeId, operationId) { 547 | var body = { 548 | "operation": operationId 549 | } 550 | 551 | if(this.draftMode) { 552 | return this.callApi('PATCH', 202, true, ['device', 'types', typeId], body); 553 | } 554 | else { 555 | return this.invalidOperation("PATCH operation 'deactivate-configuration' is not allowed"); 556 | } 557 | } 558 | 559 | getDeviceTypeDeployedConfiguration(typeId) { 560 | if(this.draftMode) { 561 | return this.invalidOperation("GET deployed configuration is not allowed"); 562 | } else { 563 | return this.callApi('GET', 200, true, ['device', 'types', typeId, 'deployedconfiguration']); 564 | } 565 | } 566 | 567 | getDeviceState(typeId, deviceId, logicalInterfaceId) { 568 | return this.callApi('GET', 200, true, ['device', 'types', typeId, 'devices', deviceId, 'state', logicalInterfaceId]); 569 | } 570 | 571 | createSchemaAndEventType(schemaContents, schemaFileName, eventTypeName, eventDescription) { 572 | var body = { 573 | 'schemaFile': schemaContents, 574 | 'schemaType': 'json-schema', 575 | 'name': schemaFileName 576 | } 577 | 578 | var createSchema = new Promise((resolve, reject) => { 579 | var base = this.draftMode ? ["draft", "schemas"] : ["schemas"] 580 | this.callFormDataApi('POST', 201, true, base, body, null).then(result => { 581 | resolve(result) 582 | }, error => { 583 | reject(error) 584 | }) 585 | }) 586 | 587 | return createSchema.then(value => { 588 | var schemaId = value["id"] 589 | return this.createEventType(eventTypeName, eventDescription, schemaId) 590 | }) 591 | } 592 | 593 | createSchemaAndLogicalInterface(schemaContents, schemaFileName, appInterfaceName, appInterfaceDescription, appInterfaceAlias) { 594 | var body = { 595 | 'schemaFile': schemaContents, 596 | 'schemaType': 'json-schema', 597 | 'name': schemaFileName 598 | } 599 | 600 | var createSchema = new Promise((resolve, reject) => { 601 | var base = this.draftMode ? ["draft", "schemas"] : ["schemas"] 602 | this.callFormDataApi('POST', 201, true, base, body, null).then(result => { 603 | resolve(result) 604 | }, error => { 605 | reject(error) 606 | }) 607 | }) 608 | 609 | return createSchema.then(value => { 610 | var schemaId = value.id 611 | return this.createLogicalInterface(appInterfaceName, appInterfaceDescription, schemaId, appInterfaceAlias) 612 | }) 613 | } 614 | 615 | createPhysicalInterfaceWithEventMapping(physicalInterfaceName, description, eventId, eventTypeId) { 616 | var createPhysicalInterface = new Promise((resolve, reject) => { 617 | this.createPhysicalInterface(physicalInterfaceName, description).then(result => { 618 | resolve(result) 619 | }, error => { 620 | reject(error) 621 | }) 622 | }) 623 | 624 | return createPhysicalInterface.then(value => { 625 | var physicalInterface = value 626 | 627 | var PhysicalInterfaceEventMapping = new Promise((resolve, reject) => { 628 | this.createPhysicalInterfaceEventMapping(physicalInterface.id, eventId, eventTypeId).then(result => { 629 | resolve([physicalInterface, result]) 630 | }, error => { 631 | reject(error) 632 | }) 633 | }) 634 | 635 | return PhysicalInterfaceEventMapping.then(result => { 636 | return result 637 | }) 638 | }) 639 | } 640 | 641 | createDeviceTypeLogicalInterfaceEventMapping(deviceTypeName, description, logicalInterfaceId, eventMapping, notificationStrategy) { 642 | var createDeviceType = new Promise((resolve, reject) => { 643 | this.createDeviceType(deviceTypeName, description).then(result => { 644 | resolve(result) 645 | }, error => { 646 | reject(error) 647 | }) 648 | }) 649 | 650 | return createDeviceType.then(result => { 651 | var deviceObject = result 652 | var deviceTypeLogicalInterface = null 653 | var deviceTypeLogicalInterface = new Promise((resolve, reject) => { 654 | this.createDeviceTypeLogicalInterfaceAssociation(deviceObject.id, logicalInterfaceId).then(result => { 655 | resolve(result) 656 | }, error => { 657 | reject(error) 658 | }) 659 | }) 660 | 661 | return deviceTypeLogicalInterface.then(result => { 662 | deviceTypeLogicalInterface = result 663 | var deviceTypeLogicalInterfacePropertyMappings = new Promise((resolve, reject) => { 664 | var notificationstrategy = "never" 665 | if(notificationStrategy) { 666 | notificationstrategy = notificationStrategy 667 | } 668 | 669 | this.createDeviceTypeLogicalInterfacePropertyMappings(deviceObject.id, logicalInterfaceId, eventMapping, notificationstrategy).then(result => { 670 | var arr = [deviceObject, deviceTypeLogicalInterface, result] 671 | resolve(arr) 672 | }, error => { 673 | reject(error) 674 | }) 675 | }) 676 | 677 | return deviceTypeLogicalInterfacePropertyMappings.then(result => { 678 | return result 679 | }) 680 | }) 681 | }) 682 | } 683 | 684 | }; -------------------------------------------------------------------------------- /src/application/ApplicationClient.js: -------------------------------------------------------------------------------- 1 | /** 2 | ***************************************************************************** 3 | Copyright (c) 2014, 2019 IBM Corporation and other Contributors. 4 | All rights reserved. This program and the accompanying materials 5 | are made available under the terms of the Eclipse Public License v1.0 6 | which accompanies this distribution, and is available at 7 | http://www.eclipse.org/legal/epl-v10.html 8 | ***************************************************************************** 9 | * 10 | */ 11 | import { isDefined } from '../util'; 12 | import { default as BaseClient } from '../BaseClient'; 13 | import { default as ApiClient } from '../api/ApiClient'; 14 | import { default as RegistryClient } from '../api/RegistryClient'; 15 | import { default as MgmtClient } from '../api/MgmtClient'; 16 | import { default as LecClient } from '../api/LecClient'; 17 | import { default as DscClient } from '../api/DscClient'; 18 | import { default as RulesClient } from '../api/RulesClient'; 19 | import { default as StateClient } from '../api/StateClient'; 20 | 21 | import { default as ApplicationConfig } from './ApplicationConfig'; 22 | 23 | const DEVICE_EVT_RE = /^iot-2\/type\/(.+)\/id\/(.+)\/evt\/(.+)\/fmt\/(.+)$/; 24 | const DEVICE_CMD_RE = /^iot-2\/type\/(.+)\/id\/(.+)\/cmd\/(.+)\/fmt\/(.+)$/; 25 | const DEVICE_STATE_RE = /^iot-2\/type\/(.+)\/id\/(.+)\/intf\/(.+)\/evt\/state$/; 26 | const DEVICE_STATE_ERROR_RE = /^iot-2\/type\/(.+)\/id\/(.+)\/err\/data$/; 27 | const RULE_TRIGGER_RE = /^iot-2\/intf\/(.+)\/rule\/(.+)\/evt\/trigger$/; 28 | const RULE_ERROR_RE = /^iot-2\/intf\/(.+)\/rule\/(.+)\/err\/data$/; 29 | const DEVICE_MON_RE = /^iot-2\/type\/(.+)\/id\/(.+)\/mon$/; 30 | const APP_MON_RE = /^iot-2\/app\/(.+)\/mon$/; 31 | 32 | export default class ApplicationClient extends BaseClient { 33 | constructor(config) { 34 | if (!config instanceof ApplicationConfig) { 35 | throw new Error("Config must be an instance of ApplicationConfig"); 36 | } 37 | super(config); 38 | this.useLtpa = config.auth && config.auth.useLtpa; 39 | 40 | if ((config.auth && config.auth.useLtpa) || config.getOrgId() != "quickstart") { 41 | this._apiClient = new ApiClient(this.config); 42 | 43 | this.dsc = new DscClient(this._apiClient); 44 | this.lec = new LecClient(this._apiClient); 45 | this.mgmt = new MgmtClient(this._apiClient); 46 | this.registry = new RegistryClient(this._apiClient); 47 | this.rules = new RulesClient(this._apiClient); 48 | this.state = new StateClient(this._apiClient); 49 | } 50 | 51 | this.log.debug("[ApplicationClient:constructor] ApplicationClient initialized for organization : " + config.getOrgId()); 52 | } 53 | 54 | 55 | connect() { 56 | super.connect(); 57 | 58 | this.mqtt.on('message', (topic, payload) => { 59 | this.log.trace("[ApplicationClient:onMessage] mqtt: ", topic, payload.toString()); 60 | 61 | // For each type of registered callback, check the incoming topic against a Regexp. 62 | // If matches, forward the payload and various fields from the topic (extracted using groups in the regexp) 63 | 64 | var match = DEVICE_EVT_RE.exec(topic); 65 | if (match) { 66 | this.emit('deviceEvent', match[1], match[2], match[3], match[4], payload, topic); 67 | return; 68 | } 69 | 70 | 71 | var match = DEVICE_CMD_RE.exec(topic); 72 | if (match) { 73 | this.emit('deviceCommand', match[1], match[2], match[3], match[4], payload, topic); 74 | return; 75 | } 76 | 77 | var match = DEVICE_STATE_RE.exec(topic); 78 | if(match){ 79 | this.emit('deviceState', match[1], match[2], match[3], payload, topic); 80 | return; 81 | } 82 | 83 | var match = DEVICE_STATE_ERROR_RE.exec(topic); 84 | if(match){ 85 | this.emit('deviceStateError', match[1], match[2], payload, topic); 86 | return; 87 | } 88 | 89 | var match = RULE_TRIGGER_RE.exec(topic); 90 | if(match){ 91 | this.emit('ruleTrigger', match[1], match[2], payload, topic); 92 | return; 93 | } 94 | 95 | var match = RULE_ERROR_RE.exec(topic); 96 | if(match){ 97 | this.emit('ruleError', match[1], match[2], payload, topic); 98 | return; 99 | } 100 | 101 | var match = DEVICE_MON_RE.exec(topic); 102 | if (match) { 103 | this.emit('deviceStatus', match[1], match[2], payload, topic); 104 | return; 105 | } 106 | 107 | var match = APP_MON_RE.exec(topic); 108 | if (match) { 109 | this.emit('appStatus', match[1], payload, topic); 110 | return; 111 | } 112 | 113 | // catch all which logs the receipt of an unexpected message 114 | this.log.warn("[ApplicationClient:onMessage] Message received on unexpected topic" + ", " + topic + ", " + payload); 115 | }); 116 | } 117 | 118 | 119 | // ========================================================================== 120 | // Device Events 121 | // ========================================================================== 122 | 123 | publishEvent(typeId, deviceId, eventId, format, data, qos, callback) { 124 | qos = qos || 0; 125 | if (!isDefined(typeId) || !isDefined(deviceId) || !isDefined(eventId) || !isDefined(format)) { 126 | this.log.error("[ApplicationClient:publishDeviceEvent] Required params for publishDeviceEvent not present"); 127 | this.emit('error', "[ApplicationClient:publishDeviceEvent] Required params for publishDeviceEvent not present"); 128 | return; 129 | } 130 | var topic = "iot-2/type/" + typeId + "/id/" + deviceId + "/evt/" + eventId + "/fmt/" + format; 131 | this._publish(topic, data, qos, callback); 132 | return this; 133 | } 134 | 135 | subscribeToEvents(typeId, deviceId, eventId, format, qos, callback) { 136 | typeId = typeId || '+'; 137 | deviceId = deviceId || '+'; 138 | eventId = eventId || '+'; 139 | format = format || '+'; 140 | qos = qos || 0; 141 | 142 | var topic = "iot-2/type/" + typeId + "/id/" + deviceId + "/evt/" + eventId + "/fmt/" + format; 143 | this._subscribe(topic, qos, callback); 144 | return this; 145 | } 146 | 147 | unsubscribeFromEvents(typeId, deviceId, eventId, format, callback) { 148 | typeId = typeId || '+'; 149 | deviceId = deviceId || '+'; 150 | eventId = eventId || '+'; 151 | format = format || '+'; 152 | 153 | var topic = "iot-2/type/" + typeId + "/id/" + deviceId + "/evt/" + eventId + "/fmt/" + format; 154 | this._unsubscribe(topic, callback); 155 | return this; 156 | } 157 | 158 | 159 | // ========================================================================== 160 | // Device Commands 161 | // ========================================================================== 162 | 163 | publishCommand(typeId, deviceId, commandId, format, data, qos, callback) { 164 | qos = qos || 0; 165 | if (!isDefined(typeId) || !isDefined(deviceId) || !isDefined(commandId) || !isDefined(format)) { 166 | this.log.error("[ApplicationClient:publishDeviceCommand] Required params for publishDeviceCommand not present"); 167 | this.emit('error', "[ApplicationClient:publishDeviceCommand] Required params for publishDeviceCommand not present"); 168 | return; 169 | } 170 | var topic = "iot-2/type/" + typeId + "/id/" + deviceId + "/cmd/" + commandId + "/fmt/" + format; 171 | this._publish(topic, data, qos, callback); 172 | return this; 173 | } 174 | 175 | subscribeToCommands(typeId, deviceId, commandId, format, qos, callback){ 176 | typeId = typeId || '+'; 177 | deviceId = deviceId || '+'; 178 | commandId = commandId || '+'; 179 | format = format || '+'; 180 | qos = qos || 0; 181 | 182 | var topic = "iot-2/type/" + typeId + "/id/" + deviceId + "/cmd/" + commandId + "/fmt/" + format; 183 | this.log.debug("[ApplicationClient:subscribeToDeviceCommands] Calling subscribe with QoS " + qos); 184 | this._subscribe(topic, qos, callback); 185 | return this; 186 | } 187 | 188 | unsubscribeFromCommands(typeId, deviceId, commandId, format, callback) { 189 | typeId = typeId || '+'; 190 | deviceId = deviceId || '+'; 191 | commandId = commandId || '+'; 192 | format = format || '+'; 193 | 194 | var topic = "iot-2/type/" + typeId + "/id/" + deviceId + "/cmd/" + commandId + "/fmt/" + format; 195 | this._unsubscribe(topic, callback); 196 | return this; 197 | } 198 | 199 | 200 | // ========================================================================== 201 | // Device State Events 202 | // ========================================================================== 203 | 204 | subscribeToDeviceState(type, id, interfaceId, qos){ 205 | type = type || '+'; 206 | id = id || '+'; 207 | interfaceId = interfaceId || '+'; 208 | qos = qos || 0; 209 | 210 | var topic = "iot-2/type/" + type + "/id/" + id + "/intf/"+ interfaceId + "/evt/state"; 211 | this.log.debug("[ApplicationClient:subscribeToDeviceState] Calling subscribe with QoS "+qos); 212 | this._subscribe(topic, qos); 213 | return this; 214 | } 215 | 216 | unsubscribeToDeviceState(type, id, interfaceId){ 217 | type = type || '+'; 218 | id = id || '+'; 219 | interfaceId = interfaceId || '+'; 220 | 221 | var topic = "iot-2/type/" + type + "/id/" + id + "/intf/"+ interfaceId + "/evt/state"; 222 | this._unsubscribe(topic); 223 | return this; 224 | } 225 | 226 | 227 | // ========================================================================== 228 | // Device State Errors 229 | // ========================================================================== 230 | 231 | subscribeToDeviceErrors(type, id, qos){ 232 | type = type || '+'; 233 | id = id || '+'; 234 | qos = qos || 0; 235 | 236 | var topic = "iot-2/type/" + type + "/id/" + id + "/err/data"; 237 | this.log.debug("[ApplicationClient:subscribeToDeviceErrors] Calling subscribe with QoS "+qos); 238 | this._subscribe(topic, qos); 239 | return this; 240 | } 241 | 242 | unsubscribeToDeviceErrors(type, id){ 243 | type = type || '+'; 244 | id = id || '+'; 245 | 246 | var topic = "iot-2/type/" + type + "/id/" + id + "/err/data"; 247 | this._unsubscribe(topic); 248 | return this; 249 | } 250 | 251 | 252 | // ========================================================================== 253 | // Rule Triggers 254 | // ========================================================================== 255 | 256 | subscribeToRuleTriggers(interfaceId, ruleId, qos){ 257 | interfaceId = interfaceId || '+'; 258 | ruleId = ruleId || '+'; 259 | qos = qos || 0; 260 | 261 | var topic = "iot-2/intf/" + interfaceId + "/rule/" + ruleId + "/evt/trigger"; 262 | this.log.debug("[ApplicationClient:subscribeToRuleTriggers] Calling subscribe with QoS "+qos); 263 | this._subscribe(topic, qos); 264 | return this; 265 | } 266 | 267 | unsubscribeToRuleTriggers(interfaceId, ruleId){ 268 | interfaceId = interfaceId || '+'; 269 | ruleId = ruleId || '+'; 270 | 271 | var topic = "iot-2/intf/" + interfaceId + "/rule/" + ruleId + "/evt/trigger"; 272 | this._unsubscribe(topic); 273 | return this; 274 | } 275 | 276 | 277 | // ========================================================================== 278 | // Rule Trigger Errors 279 | // ========================================================================== 280 | 281 | subscribeToRuleErrors(interfaceId, ruleId, qos){ 282 | interfaceId = interfaceId || '+'; 283 | ruleId = ruleId || '+'; 284 | qos = qos || 0; 285 | 286 | var topic = "iot-2/intf/" + interfaceId + "/rule/" + ruleId + "/err/data"; 287 | this.log.debug("[ApplicationClient:subscribeToRuleErrors] Calling subscribe with QoS "+qos); 288 | this._subscribe(topic, qos); 289 | return this; 290 | } 291 | 292 | unsubscribeToRuleErrors(interfaceId, ruleId){ 293 | interfaceId = interfaceId || '+'; 294 | ruleId = ruleId || '+'; 295 | 296 | var topic = "iot-2/intf/" + interfaceId + "/rule/" + ruleId + "/err/data"; 297 | this._unsubscribe(topic); 298 | return this; 299 | } 300 | 301 | 302 | // ========================================================================== 303 | // Device Status 304 | // ========================================================================== 305 | 306 | subscribeToDeviceStatus(type, id, qos) { 307 | type = type || '+'; 308 | id = id || '+'; 309 | qos = qos || 0; 310 | 311 | var topic = "iot-2/type/" + type + "/id/" + id + "/mon"; 312 | this.log.debug("[ApplicationClient:subscribeToDeviceStatus] Calling subscribe with QoS " + qos); 313 | this._subscribe(topic, qos); 314 | return this; 315 | } 316 | 317 | unsubscribeToDeviceStatus(type, id) { 318 | type = type || '+'; 319 | id = id || '+'; 320 | 321 | var topic = "iot-2/type/" + type + "/id/" + id + "/mon"; 322 | this._unsubscribe(topic); 323 | return this; 324 | } 325 | 326 | // ========================================================================== 327 | // Application Status 328 | // ========================================================================== 329 | 330 | subscribeToAppStatus(id, qos) { 331 | id = id || '+'; 332 | qos = qos || 0; 333 | 334 | var topic = "iot-2/app/" + id + "/mon"; 335 | this.log.debug("[ApplicationClient:subscribeToAppStatus] Calling subscribe with QoS " + qos); 336 | this._subscribe(topic, qos); 337 | return this; 338 | } 339 | 340 | unsubscribeToAppStatus(id) { 341 | id = id || '+'; 342 | 343 | var topic = "iot-2/app/" + id + "/mon"; 344 | this._unsubscribe(topic); 345 | 346 | return this; 347 | } 348 | 349 | }; -------------------------------------------------------------------------------- /src/application/ApplicationConfig.js: -------------------------------------------------------------------------------- 1 | /** 2 | ***************************************************************************** 3 | Copyright (c) 2019 IBM Corporation and other Contributors. 4 | All rights reserved. This program and the accompanying materials 5 | are made available under the terms of the Eclipse Public License v1.0 6 | which accompanies this distribution, and is available at 7 | http://www.eclipse.org/legal/epl-v10.html 8 | ***************************************************************************** 9 | * 10 | */ 11 | import { default as BaseConfig } from '../BaseConfig'; 12 | import log from 'loglevel'; 13 | const YAML = require('yaml'); 14 | const fs = require('fs'); 15 | 16 | const { v4: uuidv4 } = require('uuid'); 17 | 18 | export default class ApplicationConfig extends BaseConfig{ 19 | constructor(identity, auth, options) { 20 | super(identity, auth, options); 21 | 22 | // Authentication is not supported for quickstart 23 | // and not required when auth.useLtpa is set 24 | if (this.auth != null && !this.auth.useLtpa) { 25 | if (!("key" in this.auth) || this.auth.key == null) { 26 | throw new Error("Missing auth.key from configuration"); 27 | } 28 | if (!("token" in this.auth) || this.auth.token == null) { 29 | throw new Error("Missing auth.token from configuration"); 30 | } 31 | } 32 | 33 | // Set defaults for optional configuration 34 | if (this.identity == null) { 35 | this.identity = {}; 36 | } 37 | if (!("appId" in this.identity)) { 38 | this.identity.appId = uuidv4(); 39 | } 40 | 41 | if (!("sharedSubscription" in this.options.mqtt)) { 42 | this.options.mqtt.sharedSubscription = false; 43 | } 44 | } 45 | 46 | getOrgId() { 47 | if (this.auth == null) { 48 | return "quickstart"; 49 | } else if (this.auth.key) { 50 | return this.auth.key.split("-")[1]; 51 | } else { 52 | return null; 53 | } 54 | } 55 | 56 | // Returns options.apiRoot if set, 'api/v0002' otherwiss 57 | getApiRoot() { 58 | return this.options.apiRoot || 'api/v0002'; 59 | } 60 | 61 | getApiBaseUri() { 62 | return this.auth && this.auth.useLtpa 63 | ? `/${this.getApiRoot()}` 64 | : `https://${this.getOrgId()}.${this.options.domain}/${this.getApiRoot()}`; 65 | } 66 | 67 | getClientId() { 68 | let clientIdPrefix = "a"; 69 | if (this.options.mqtt.sharedSubscription == true) { 70 | clientIdPrefix = "A"; 71 | } 72 | return clientIdPrefix + ":" + this.getOrgId() + ":" + this.identity.appId; 73 | } 74 | 75 | getMqttUsername() { 76 | return this.auth.key; 77 | } 78 | 79 | getMqttPassword() { 80 | return this.auth.token; 81 | } 82 | 83 | getAdditionalHeaders() { 84 | return this.options && this.options.http 85 | ? this.options.http.additionalHeaders 86 | : []; 87 | } 88 | 89 | setAdditionalHeader(key, value) { 90 | if(!this.options.http) this.options.http = {}; 91 | if(!this.options.http.additionalHeaders) this.options.http.additionalHeaders = {} 92 | this.options.http.additionalHeaders[key] = value; 93 | } 94 | 95 | static parseEnvVars() { 96 | // Auth 97 | let authKey = process.env.WIOTP_AUTH_KEY || null; 98 | let authToken = process.env.WIOTP_AUTH_TOKEN || null; 99 | 100 | // Also support WIOTP_API_KEY / WIOTP_API_TOKEN usage 101 | if (authKey == null && authToken == null) { 102 | authKey = process.env.WIOTP_API_KEY || null; 103 | authToken = process.env.WIOTP_API_TOKEN || null; 104 | } 105 | 106 | // Identity 107 | let appId = process.env.WIOTP_IDENTITY_APPID || uuidv4(); 108 | 109 | // Options 110 | let domain = process.env.WIOTP_OPTIONS_DOMAIN || null; 111 | let apiRoot = process.env.WIOTP_OPTIONS_API_ROOT || null; 112 | let logLevel = process.env.WIOTP_OPTIONS_LOGLEVEL || "info"; 113 | let port = process.env.WIOTP_OPTIONS_MQTT_PORT || null; 114 | let transport = process.env.WIOTP_OPTIONS_MQTT_TRANSPORT || null; 115 | let caFile = process.env.WIOTP_OPTIONS_MQTT_CAFILE || null; 116 | let cleanStart = process.env.WIOTP_OPTIONS_MQTT_CLEANSTART || "true"; 117 | let sessionExpiry = process.env.WIOTP_OPTIONS_MQTT_SESSIONEXPIRY || 3600; 118 | let keepAlive = process.env.WIOTP_OPTIONS_MQTT_KEEPALIVE || 60; 119 | let sharedSubs = process.env.WIOTP_OPTIONS_MQTT_SHAREDSUBSCRIPTION || "false"; 120 | let verifyCert = process.env.WIOTP_OPTIONS_HTTP_VERIFY || "true"; 121 | 122 | // TODO: add environment variable parsing for options.http.additionalHeaders 123 | 124 | // String to int conversions 125 | if (port != null) { 126 | port = parseInt(port); 127 | } 128 | sessionExpiry = parseInt(sessionExpiry); 129 | keepAlive = parseInt(keepAlive) 130 | 131 | let identity = {appId: appId}; 132 | let options = { 133 | domain: domain, 134 | apiRoot: apiRoot, 135 | logLevel: logLevel, 136 | mqtt: { 137 | port: port, 138 | transport: transport, 139 | cleanStart: (["True", "true", "1"].includes(cleanStart)), 140 | sessionExpiry: sessionExpiry, 141 | keepAlive: keepAlive, 142 | sharedSubscription: (["True", "true", "1"].includes(sharedSubs)), 143 | caFile: caFile, 144 | }, 145 | http: { 146 | verify: (["True", "true", "1"].includes(verifyCert)) 147 | }, 148 | }; 149 | let auth = null; 150 | // Quickstart doesn't support auth, so ensure we only add this if it's defined 151 | if (authToken != null) { 152 | auth = {key: authKey, token: authToken}; 153 | } 154 | 155 | return new ApplicationConfig(identity, auth, options); 156 | } 157 | 158 | static parseConfigFile(configFilePath) { 159 | 160 | //Example Application Configuration File: 161 | 162 | /* 163 | identity: 164 | appId: myApp 165 | auth: 166 | key: a-23gh56-sdsdajhjnee 167 | token: Ab$76s)asj8_s5 168 | options: 169 | domain: internetofthings.ibmcloud.com 170 | apiRoot: 'api/platform' 171 | logLevel: error|warning|info|debug 172 | mqtt: 173 | instanceId: myInstance 174 | port: 8883 175 | transport: tcp 176 | cleanStart: false 177 | sessionExpiry: 3600 178 | keepAlive: 60 179 | caFile: /path/to/certificateAuthorityFile.pem 180 | http: 181 | verify: true 182 | additionalHeaders: 183 | X-Csrf-Token: sdfsdfsdf 184 | X-myheader: hello 185 | */ 186 | 187 | const configFile = fs.readFileSync(configFilePath, 'utf8'); 188 | var data = YAML.parse(configFile); 189 | 190 | if(!fs.existsSync(configFilePath)) { 191 | throw new Error("File not found"); 192 | }else 193 | {try { 194 | const configFile = fs.readFileSync(configFilePath, 'utf8'); 195 | var data = YAML.parse(configFile); 196 | } catch (err) { 197 | throw new Error("Error reading device configuration file: " + err.code); 198 | } 199 | } 200 | 201 | 202 | if (("options" in data) & ("logLevel" in data["options"])) 203 | { 204 | var validLevels = ["error", "warning", "info", "debug"]; 205 | if (!(validLevels.includes(data["options"]["logLevel"]))) 206 | { 207 | throw new Error("Optional setting options.logLevel (Currently: " + data["options"]["logLevel"] + ") must be one of error, warning, info, debug") 208 | } 209 | } 210 | else 211 | { 212 | data["options"]["logLevel"] = log.GetLogger(data["options"]["logLevel"].toUpperCase()); 213 | } 214 | 215 | return new ApplicationConfig(data['identity'],data['auth'],data['options']) 216 | } 217 | } -------------------------------------------------------------------------------- /src/application/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | ***************************************************************************** 3 | Copyright (c) 2019 IBM Corporation and other Contributors. 4 | All rights reserved. This program and the accompanying materials 5 | are made available under the terms of the Eclipse Public License v1.0 6 | which accompanies this distribution, and is available at 7 | http://www.eclipse.org/legal/epl-v10.html 8 | ***************************************************************************** 9 | * 10 | */ 11 | 12 | export { default as ApplicationClient } from './ApplicationClient'; 13 | export { default as ApplicationConfig } from './ApplicationConfig'; -------------------------------------------------------------------------------- /src/device/DeviceClient.js: -------------------------------------------------------------------------------- 1 | /** 2 | ***************************************************************************** 3 | Copyright (c) 2014, 2019 IBM Corporation and other Contributors. 4 | All rights reserved. This program and the accompanying materials 5 | are made available under the terms of the Eclipse Public License v1.0 6 | which accompanies this distribution, and is available at 7 | http://www.eclipse.org/legal/epl-v10.html 8 | ***************************************************************************** 9 | * 10 | */ 11 | import { isDefined } from '../util'; 12 | import { default as BaseClient } from '../BaseClient'; 13 | import { default as DeviceConfig } from './DeviceConfig'; 14 | 15 | const WILDCARD_TOPIC = 'iot-2/cmd/+/fmt/+'; 16 | const CMD_RE = /^iot-2\/cmd\/(.+)\/fmt\/(.+)$/; 17 | 18 | const util = require('util'); 19 | 20 | export default class DeviceClient extends BaseClient { 21 | 22 | constructor(config){ 23 | if (!config instanceof DeviceConfig) { 24 | throw new Error("Config must be an instance of DeviceConfig"); 25 | } 26 | super(config); 27 | 28 | this.log.debug("[DeviceClient:constructor] DeviceClient initialized for " + config.getClientId()); 29 | } 30 | 31 | _commandSubscriptionCallback(err, granted) { 32 | if (err == null) { 33 | for (var index in granted) { 34 | let grant = granted[index]; 35 | this.log.debug("[DeviceClient:connect] Subscribed to device commands on " + grant.topic + " at QoS " + grant.qos); 36 | } 37 | } else { 38 | this.log.error("[DeviceClient:connect] Unable to establish subscription for device commands: " + err); 39 | this.emit("error", new Error("Unable to establish subscription for device commands: " + err)); 40 | } 41 | } 42 | 43 | connect(){ 44 | super.connect(); 45 | 46 | this.mqtt.on('connect', () => { 47 | // On connect establish a subscription for commands sent to this device (but not if connecting to quickstart) 48 | if(!this.config.isQuickstart()){ 49 | // You need to bind a particular this context to the method before you can use it as a callback 50 | this.mqtt.subscribe(WILDCARD_TOPIC, { qos: 1 }, this._commandSubscriptionCallback.bind(this)); 51 | } 52 | }); 53 | 54 | this.mqtt.on('message', (topic, payload) => { 55 | this.log.debug("[DeviceClient:onMessage] Message received on topic : "+ topic + " with payload : "+ payload); 56 | 57 | let match = CMD_RE.exec(topic); 58 | 59 | if (match) { 60 | this.emit('command', match[1], match[2], payload, topic); 61 | } 62 | }); 63 | } 64 | 65 | publishEvent(eventId, format, data, qos, callback){ 66 | qos = qos || 0; 67 | 68 | if (!isDefined(eventId) || !isDefined(format)) { 69 | this.log.error("[DeviceClient:publishEvent] Required params for publishEvent not present"); 70 | this.emit('error', "[DeviceClient:publishEvent] Required params for publishEvent not present"); 71 | return; 72 | } 73 | 74 | let topic = util.format("iot-2/evt/%s/fmt/%s", eventId, format); 75 | this._publish(topic, data, qos, callback); 76 | return this; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/device/DeviceConfig.js: -------------------------------------------------------------------------------- 1 | /** 2 | ***************************************************************************** 3 | Copyright (c) 2019 IBM Corporation and other Contributors. 4 | All rights reserved. This program and the accompanying materials 5 | are made available under the terms of the Eclipse Public License v1.0 6 | which accompanies this distribution, and is available at 7 | http://www.eclipse.org/legal/epl-v10.html 8 | ***************************************************************************** 9 | * 10 | */ 11 | import { default as BaseConfig } from '../BaseConfig'; 12 | import log from 'loglevel'; 13 | const YAML = require('yaml'); 14 | const fs = require('fs'); 15 | 16 | 17 | export default class DeviceConfig extends BaseConfig{ 18 | constructor(identity, auth, options) { 19 | super(identity, auth, options); 20 | 21 | //Validate the arguments 22 | if (this.identity == null) { 23 | throw new Error("Missing identity from configuration"); 24 | } 25 | if (!("orgId" in this.identity) || this.identity.orgId == null) { 26 | throw new Error("Missing identity.orgId from configuration"); 27 | } 28 | if (!("typeId" in this.identity) || this.identity.typeId == null) { 29 | throw new Error("Missing identity.typeId from configuration"); 30 | } 31 | if (!("deviceId" in this.identity) || this.identity.deviceId == null) { 32 | throw new Error("Missing identity.deviceId from configuration"); 33 | } 34 | 35 | // Authentication is not supported for quickstart 36 | if (this.identity.orgId == "quickstart") { 37 | if (this.auth != null) { 38 | throw new Error("Quickstart service does not support device authentication"); 39 | } 40 | } else { 41 | if (this.auth == null) { 42 | throw new Error("Missing auth from configuration"); 43 | } 44 | if (!("token" in this.auth) || this.auth.token == null) { 45 | throw new Error("Missing auth.token from configuration"); 46 | } 47 | } 48 | 49 | } 50 | 51 | getOrgId() { 52 | return this.identity.orgId; 53 | } 54 | 55 | getClientId() { 56 | return "d:" + this.identity.orgId + ":" + this.identity.typeId + ":" + this.identity.deviceId; 57 | } 58 | 59 | getMqttUsername() { 60 | return "use-token-auth"; 61 | } 62 | getMqttPassword() { 63 | if (this.identity.orgId != "quickstart") 64 | { 65 | return this.auth.token; 66 | } 67 | else{ 68 | return null; 69 | } 70 | } 71 | 72 | static parseEnvVars() { 73 | 74 | //Identity 75 | let orgId = process.env.WIOTP_IDENTITY_ORGID || null; 76 | let typeId = process.env.WIOTP_IDENTITY_TYPEID || null; 77 | let deviceId = process.env.WIOTP_IDENTITY_DEVICEID || null; 78 | 79 | // Auth 80 | let authToken = process.env.WIOTP_AUTH_TOKEN || null; 81 | 82 | // Also support WIOTP_API_TOKEN usage 83 | if (authToken == null) { 84 | authToken = process.env.WIOTP_API_TOKEN || null; 85 | } 86 | 87 | // Options 88 | let domain = process.env.WIOTP_OPTIONS_DOMAIN || null; 89 | let logLevel = process.env.WIOTP_OPTIONS_LOGLEVEL || "info"; 90 | let port = process.env.WIOTP_OPTIONS_MQTT_PORT || null; 91 | let transport = process.env.WIOTP_OPTIONS_MQTT_TRANSPORT || null; 92 | let caFile = process.env.WIOTP_OPTIONS_MQTT_CAFILE || null; 93 | let cleanStart = process.env.WIOTP_OPTIONS_MQTT_CLEANSTART || "true"; 94 | let sessionExpiry = process.env.WIOTP_OPTIONS_MQTT_SESSIONEXPIRY || 3600; 95 | let keepAlive = process.env.WIOTP_OPTIONS_MQTT_KEEPALIVE || 60; 96 | let sharedSubs = process.env.WIOTP_OPTIONS_MQTT_SHAREDSUBSCRIPTION || "false"; 97 | 98 | // String to int conversions 99 | if (port != null) { 100 | port = parseInt(port); 101 | } 102 | sessionExpiry = parseInt(sessionExpiry); 103 | keepAlive = parseInt(keepAlive) 104 | 105 | let identity = {orgId:orgId, typeId: typeId, deviceId:deviceId}; 106 | let options = { 107 | domain: domain, 108 | logLevel: logLevel, 109 | mqtt: { 110 | port: port, 111 | transport: transport, 112 | cleanStart: (["True", "true", "1"].includes(cleanStart)), 113 | sessionExpiry: sessionExpiry, 114 | keepAlive: keepAlive, 115 | sharedSubscription: (["True", "true", "1"].includes(sharedSubs)), 116 | caFile: caFile, 117 | }, 118 | }; 119 | let auth = null; 120 | // Quickstart doesn't support auth, so ensure we only add this if it's defined 121 | if (authToken != null) { 122 | auth = {token: authToken}; 123 | } 124 | 125 | return new DeviceConfig(identity, auth, options); 126 | } 127 | 128 | static parseConfigFile(configFilePath) { 129 | 130 | //Example Device Configuration File: 131 | 132 | /* 133 | identity: 134 | orgId: org1id 135 | typeId: raspberry-pi-3 136 | deviceId: 00ef08ac05 137 | auth: 138 | token: Ab$76s)asj8_s5 139 | options: 140 | domain: internetofthings.ibmcloud.com 141 | logLevel: error|warning|info|debug 142 | mqtt: 143 | port: 8883 144 | transport: tcp 145 | cleanStart: true 146 | sessionExpiry: 3600 147 | keepAlive: 60 148 | caFile: /path/to/certificateAuthorityFile.pem 149 | */ 150 | 151 | const configFile = fs.readFileSync(configFilePath, 'utf8'); 152 | var data = YAML.parse(configFile); 153 | 154 | 155 | if(!fs.existsSync(configFilePath)) { 156 | throw new Error("File not found"); 157 | }else 158 | {try { 159 | const configFile = fs.readFileSync(configFilePath, 'utf8'); 160 | var data = YAML.parse(configFile); 161 | } catch (err) { 162 | throw new Error("Error reading device configuration file: " + err.code); 163 | } 164 | } 165 | 166 | 167 | if (("options" in data) & ("logLevel" in data["options"])) 168 | { 169 | var validLevels = ["error", "warning", "info", "debug"]; 170 | if (!(validLevels.includes(data["options"]["logLevel"]))) 171 | { 172 | throw new Error("Optional setting options.logLevel (Currently: " + data["options"]["logLevel"] + ") must be one of error, warning, info, debug") 173 | } 174 | } 175 | else 176 | { 177 | data["options"]["logLevel"] = log.GetLogger(data["options"]["logLevel"].toUpperCase()); 178 | } 179 | 180 | return new DeviceConfig(data['identity'],data['auth'],data['options']) 181 | } 182 | } -------------------------------------------------------------------------------- /src/device/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | ***************************************************************************** 3 | Copyright (c) 2019 IBM Corporation and other Contributors. 4 | All rights reserved. This program and the accompanying materials 5 | are made available under the terms of the Eclipse Public License v1.0 6 | which accompanies this distribution, and is available at 7 | http://www.eclipse.org/legal/epl-v10.html 8 | ***************************************************************************** 9 | * 10 | */ 11 | 12 | export { default as DeviceClient } from './DeviceClient'; 13 | export { default as DeviceConfig } from './DeviceConfig'; 14 | 15 | -------------------------------------------------------------------------------- /src/gateway/GatewayClient.js: -------------------------------------------------------------------------------- 1 | /** 2 | ***************************************************************************** 3 | Copyright (c) 2014, 2019 IBM Corporation and other Contributors. 4 | All rights reserved. This program and the accompanying materials 5 | are made available under the terms of the Eclipse Public License v1.0 6 | which accompanies this distribution, and is available at 7 | http://www.eclipse.org/legal/epl-v10.html 8 | ***************************************************************************** 9 | * 10 | */ 11 | import { default as BaseClient } from '../BaseClient'; 12 | import { default as GatewayConfig } from './GatewayConfig'; 13 | 14 | const CMD_RE = /^iot-2\/type\/(.+)\/id\/(.+)\/cmd\/(.+)\/fmt\/(.+)$/; 15 | 16 | const util = require('util'); 17 | 18 | export default class GatewayClient extends BaseClient { 19 | 20 | constructor(config){ 21 | if (!config instanceof GatewayConfig) { 22 | throw new Error("Config must be an instance of GatewayConfig"); 23 | } 24 | if (config.isQuickstart()) { 25 | throw new Error('[GatewayClient:constructor] Quickstart not supported in Gateways'); 26 | } 27 | super(config); 28 | 29 | this.log.debug("[GatewayClient:constructor] GatewayClient initialized for " + config.getClientId()); 30 | } 31 | 32 | 33 | connect(){ 34 | super.connect(); 35 | 36 | this.mqtt.on('connect', () => { 37 | // This gateway client implemention does not automatically subscribe to any commands!? 38 | }); 39 | 40 | this.mqtt.on('message', (topic, payload) => { 41 | this.log.debug("[GatewayClient:onMessage] Message received on topic : "+ topic + " with payload : "+ payload); 42 | 43 | let match = CMD_RE.exec(topic); 44 | if(match){ 45 | this.emit('command', match[1], match[2], match[3], match[4], payload, topic); 46 | } 47 | }); 48 | } 49 | 50 | 51 | publishEvent(eventId, format, payload, qos, callback){ 52 | return this._publishEvent(this.config.identity.typeId, this.config.identity.deviceId, eventId, format, payload, qos, callback); 53 | } 54 | 55 | 56 | publishDeviceEvent(typeid, deviceId, eventid, format, payload, qos, callback){ 57 | return this._publishEvent(typeId, deviceId, eventid, format, payload, qos, callback); 58 | } 59 | 60 | 61 | _publishEvent(typeId, deviceId, eventId, format, data, qos, callback){ 62 | let topic = util.format("iot-2/type/%s/id/%s/evt/%s/fmt/%s", typeId, deviceId, eventId, format); 63 | qos = qos || 0; 64 | this._publish(topic, data, qos, callback); 65 | return this; 66 | } 67 | 68 | 69 | subscribeToDeviceCommands(typeId, deviceId, commandId, format, qos, callback){ 70 | typeId = typeId || '+'; 71 | deviceId = deviceId || '+'; 72 | commandId = commandid || '+'; 73 | format = format || '+'; 74 | qos = qos || 0; 75 | 76 | let topic = "iot-2/type/" + typeId + "/id/" + deviceId + "/cmd/"+ commandId + "/fmt/" + format; 77 | this._subscribe(topic,qos, callback); 78 | return this; 79 | } 80 | 81 | 82 | unsubscribeFromDeviceCommands(typeId, deviceId, commandId, format, callback){ 83 | typeId = typeId || '+'; 84 | deviceId = deviceId || '+'; 85 | commandId = commandId || '+'; 86 | format = format || '+'; 87 | 88 | let topic = "iot-2/type/" + typeId + "/id/" + deviceId + "/cmd/"+ commandId + "/fmt/" + format; 89 | this._unsubscribe(topic, callback); 90 | return this; 91 | } 92 | 93 | 94 | subscribeToCommands(commandId, format, qos, callback){ 95 | commandId = commandId || '+'; 96 | format = format || '+'; 97 | qos = qos || 0; 98 | 99 | let topic = "iot-2/type/" + this.config.identity.typeId + "/id/" + this.config.identity.deviceId + "/cmd/"+ commandId + "/fmt/" + format; 100 | this._subscribe(topic, qos, callback); 101 | return this; 102 | } 103 | 104 | 105 | unsubscribeFromCommands(commandId, format, callback){ 106 | commandId = commandId || '+'; 107 | format = format || '+'; 108 | 109 | let topic = "iot-2/type/" + this.config.identity.typeId + "/id/" + this.config.identity.deviceId + "/cmd/"+ commandId + "/fmt/" + format; 110 | this._unsubscribe(topic, callback); 111 | return this; 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /src/gateway/GatewayConfig.js: -------------------------------------------------------------------------------- 1 | /** 2 | ***************************************************************************** 3 | Copyright (c) 2019 IBM Corporation and other Contributors. 4 | All rights reserved. This program and the accompanying materials 5 | are made available under the terms of the Eclipse Public License v1.0 6 | which accompanies this distribution, and is available at 7 | http://www.eclipse.org/legal/epl-v10.html 8 | ***************************************************************************** 9 | * 10 | */ 11 | import { default as DeviceConfig } from '../device/DeviceConfig'; 12 | 13 | export default class GatewayConfig extends DeviceConfig{ 14 | constructor(identity, auth, options) { 15 | super(identity, auth, options); 16 | } 17 | 18 | getClientId() { 19 | return "g:" + this.identity.orgId + ":" + this.identity.typeId + ":" + this.identity.deviceId; 20 | } 21 | 22 | }; -------------------------------------------------------------------------------- /src/gateway/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | ***************************************************************************** 3 | Copyright (c) 2019 IBM Corporation and other Contributors. 4 | All rights reserved. This program and the accompanying materials 5 | are made available under the terms of the Eclipse Public License v1.0 6 | which accompanies this distribution, and is available at 7 | http://www.eclipse.org/legal/epl-v10.html 8 | ***************************************************************************** 9 | * 10 | */ 11 | 12 | export { default as GatewayClient } from './GatewayClient'; 13 | export { default as GatewayConfig } from './GatewayConfig'; 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | ***************************************************************************** 3 | Copyright (c) 2019 IBM Corporation and other Contributors. 4 | All rights reserved. This program and the accompanying materials 5 | are made available under the terms of the Eclipse Public License v1.0 6 | which accompanies this distribution, and is available at 7 | http://www.eclipse.org/legal/epl-v10.html 8 | ***************************************************************************** 9 | * 10 | */ 11 | 12 | export { default as ApplicationClient } from './application/ApplicationClient'; 13 | export { default as DeviceClient } from './device/DeviceClient'; 14 | export { default as GatewayClient } from './gateway/GatewayClient'; 15 | 16 | export { default as ApplicationConfig } from './application/ApplicationConfig'; 17 | export { default as DeviceConfig } from './device/DeviceConfig'; 18 | 19 | export { default as ApiClient} from './api/ApiClient'; 20 | export { default as ApiErrors} from './api/ApiErrors'; 21 | export { default as DscClient} from './api/DscClient'; 22 | export { default as LecClient} from './api/LecClient'; 23 | export { default as MgmtClient} from './api/MgmtClient'; 24 | export { default as RegistryClient} from './api/RegistryClient'; 25 | export { default as RulesClient} from './api/RulesClient'; 26 | export { default as StateClient} from './api/StateClient'; -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | /** 2 | ***************************************************************************** 3 | Copyright (c) 2014, 2019 IBM Corporation and other Contributors. 4 | All rights reserved. This program and the accompanying materials 5 | are made available under the terms of the Eclipse Public License v1.0 6 | which accompanies this distribution, and is available at 7 | http://www.eclipse.org/legal/epl-v10.html 8 | ***************************************************************************** 9 | * 10 | */ 11 | 12 | export function isString(value){ 13 | return typeof value === 'string'; 14 | } 15 | 16 | export function isNumber(value){ 17 | return typeof value === 'number'; 18 | } 19 | 20 | export function isBoolean(value){ 21 | return typeof value === 'boolean'; 22 | } 23 | 24 | export function isDefined(value){ 25 | return value !== undefined && value !== null; 26 | } 27 | 28 | export const isBrowser = new Function("try {return this===window;}catch(e){ return false;}"); 29 | 30 | export const isNode = new Function("try {return this===global;}catch(e){return false;}"); 31 | 32 | export function generateUUID() { 33 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { 34 | var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); 35 | return v.toString(16); 36 | }); 37 | } 38 | 39 | -------------------------------------------------------------------------------- /test/ApplicationClient.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | ***************************************************************************** 3 | Copyright (c) 2019 IBM Corporation and other Contributors. 4 | All rights reserved. This program and the accompanying materials 5 | are made available under the terms of the Eclipse Public License v1.0 6 | which accompanies this distribution, and is available at 7 | http://www.eclipse.org/legal/epl-v10.html 8 | ***************************************************************************** 9 | * 10 | */ 11 | import { ApplicationConfig, ApplicationClient } from '../src/application'; 12 | import { assert } from 'chai'; 13 | 14 | // Turn off console output 15 | console.info = () => {}; 16 | 17 | 18 | describe('WIoTP Application Capabilities', function() { 19 | 20 | let appConfig = ApplicationConfig.parseEnvVars(); 21 | 22 | describe("Event Publication and Subscription", function() { 23 | let testTypeId = "foo"; 24 | let testDeviceId = "bar"; 25 | let testEventId = "testEvent"; 26 | let testEventFormat = "json"; 27 | let testEventData = "{'foo': 'bar'}"; 28 | let testQos = 1; 29 | 30 | let appClient = null; 31 | 32 | before("Initialize the application", function(){ 33 | appClient = new ApplicationClient(appConfig); 34 | }) 35 | 36 | afterEach("Remove error listener(s)", function() { 37 | appClient.removeAllListeners("error"); 38 | }); 39 | 40 | step("Connect within 5 seconds", function(done){ 41 | this.timeout(5000); 42 | appClient.on("connect", done); 43 | appClient.on("error", done); 44 | appClient.connect(); 45 | }); 46 | 47 | step("Subscribe to device events", function(done){ 48 | let onSubscribe = function(err, granted) { 49 | if (err != null) { 50 | done(err); 51 | } 52 | done(); 53 | } 54 | appClient.on("error", done); 55 | appClient.subscribeToEvents(testTypeId, testDeviceId, testEventId, testEventFormat, testQos, onSubscribe); 56 | }); 57 | 58 | step("Publish & recieve device event", function(done){ 59 | let onEventSent = function(err) { 60 | if (err != null) { 61 | done(err); 62 | } 63 | } 64 | 65 | let onEventReceived = function(typeId, deviceId, eventId, format, payload) { 66 | assert(typeId == testTypeId, "Type ID does not match"); 67 | assert(deviceId == testDeviceId, "Device ID does not match"); 68 | assert(eventId == testEventId, "Event ID does not match"); 69 | assert(format == testEventFormat, "Format does not match"); 70 | assert(payload == testEventData, "Payload does not match"); 71 | done(); 72 | } 73 | 74 | appClient.on("error", done); 75 | appClient.on("deviceEvent", onEventReceived); 76 | 77 | appClient.publishEvent(testTypeId, testDeviceId, testEventId, testEventFormat, testEventData, testQos, onEventSent); 78 | }); 79 | 80 | step("Disconnect the MQTT client", function(done) { 81 | appClient.on("error", done); 82 | appClient.on("close", function() { 83 | assert(appClient.isConnected() == false, "Application is still connected"); 84 | done(); 85 | }); 86 | appClient.disconnect(); 87 | }); 88 | 89 | after("Ensure the the MQTT client is disconnected", function() { 90 | if (appClient != null && appClient.isConnected()) { 91 | appClient.disconnect(); 92 | } 93 | }); 94 | }); 95 | 96 | describe("Command Publication and Subscription", function() { 97 | let testTypeId = "foo"; 98 | let testDeviceId = "bar"; 99 | let testCommandId = "testCommand"; 100 | let testCommandFormat = "json"; 101 | let testCommandData = "{'foo': 'bar'}"; 102 | let testQos = 1; 103 | 104 | let appClient = null; 105 | 106 | before("Initialize the application", function(){ 107 | appClient = new ApplicationClient(appConfig); 108 | }) 109 | 110 | afterEach("Remove error listener(s)", function() { 111 | appClient.removeAllListeners("error"); 112 | }); 113 | 114 | step("Connect within 5 seconds", function(done){ 115 | this.timeout(5000); 116 | appClient.on("connect", done); 117 | appClient.on("error", done); 118 | appClient.connect(); 119 | }); 120 | 121 | step("Subscribe to device command", function(done){ 122 | let onSubscribe = function(err, granted) { 123 | if (err != null) { 124 | done(err); 125 | } 126 | done(); 127 | } 128 | appClient.on("error", done); 129 | appClient.subscribeToCommands(testTypeId, testDeviceId, testCommandId, testCommandFormat, testQos, onSubscribe); 130 | }); 131 | 132 | step("Publish & recieve device command", function(done){ 133 | let onCommandSent = function(err) { 134 | if (err != null) { 135 | done(err); 136 | } 137 | } 138 | 139 | let onCommandReceived = function(typeId, deviceId, commandId, format, payload) { 140 | assert(typeId == testTypeId, "Type ID does not match"); 141 | assert(deviceId == testDeviceId, "Device ID does not match"); 142 | assert(commandId == testCommandId, "Command ID does not match"); 143 | assert(format == testCommandFormat, "Format does not match"); 144 | assert(payload == testCommandData, "Payload does not match"); 145 | done(); 146 | } 147 | 148 | appClient.on("error", done); 149 | appClient.on("deviceCommand", onCommandReceived); 150 | 151 | appClient.publishCommand(testTypeId, testDeviceId, testCommandId, testCommandFormat, testCommandData, testQos, onCommandSent); 152 | }); 153 | 154 | step("Disconnect the MQTT client", function(done) { 155 | appClient.on("error", done); 156 | appClient.on("close", function() { 157 | assert(appClient.isConnected() == false, "Application is still connected"); 158 | done(); 159 | }); 160 | appClient.disconnect(); 161 | }); 162 | 163 | after("Ensure the the MQTT client is disconnected", function() { 164 | if (appClient != null && appClient.isConnected()) { 165 | appClient.disconnect(); 166 | } 167 | }); 168 | }); 169 | 170 | // TODO: Add test coverage 171 | describe("Subscribe & receive device status updates", function() {}); 172 | describe("Subscribe & receive app status updates", function() {}); 173 | 174 | describe("Subscribe & receive device state events", function() {}); 175 | describe("Subscribe & receive device state errors", function() {}); 176 | 177 | describe("Subscribe & receive rule trigger events", function() {}); 178 | describe("Subscribe & receive rule trigger errors", function() {}); 179 | 180 | }); 181 | -------------------------------------------------------------------------------- /test/ApplicationConfig.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | ***************************************************************************** 3 | Copyright (c) 2014, 2019 IBM Corporation and other Contributors. 4 | All rights reserved. This program and the accompanying materials 5 | are made available under the terms of the Eclipse Public License v1.0 6 | which accompanies this distribution, and is available at 7 | http://www.eclipse.org/legal/epl-v10.html 8 | ***************************************************************************** 9 | * 10 | */ 11 | import { ApplicationConfig } from '../src/application'; 12 | import { expect } from 'chai'; 13 | 14 | // Turn off console output 15 | console.info = () => {}; 16 | 17 | describe('WIoTP Application Configuration', () => { 18 | 19 | it('Initialise with the minimum required configuration', () => { 20 | let identity = null; 21 | let auth = {key: "a-orgid-sssssss", token: "test"}; 22 | let options = null; 23 | 24 | let config = new ApplicationConfig(identity, auth, options); 25 | expect(config.getOrgId()).to.equal("orgid"); 26 | expect(config.identity.appId).to.not.be.null; // Should be a generated UUID 27 | expect(config.options.logLevel).to.equal("info"); 28 | expect(config.options.domain).to.equal("internetofthings.ibmcloud.com"); 29 | expect(config.options.mqtt.transport).to.equal("tcp"); 30 | expect(config.options.mqtt.port).to.equal(8883); 31 | }); 32 | 33 | it('Load mimimal configuration from environment variables', () => { 34 | let config = ApplicationConfig.parseEnvVars(); 35 | expect(config.getOrgId()).to.equal(process.env.WIOTP_API_KEY.split("-")[1]); 36 | expect(config.identity.appId).to.not.be.null; // Should be a generated UUID 37 | expect(config.options.logLevel).to.equal("info"); 38 | expect(config.options.domain).to.equal("internetofthings.ibmcloud.com"); 39 | expect(config.options.mqtt.transport).to.equal("tcp"); 40 | expect(config.options.mqtt.port).to.equal(8883); 41 | }); 42 | 43 | it('Load configuration from yaml config file', () => { 44 | let config = ApplicationConfig.parseConfigFile("./test/ApplicationConfigFile.spec.yaml"); 45 | expect(config.identity.appId).to.equal("myApp") 46 | expect(config.auth.key).to.equal("myKey") 47 | expect(config.auth.token).to.equal("myToken") 48 | expect(config.options.domain).to.equal("internetofthings.ibmcloud.com"); 49 | expect(config.options.logLevel).to.equal("info"); 50 | expect(config.options.apiRoot).equal('api/v0002'); 51 | expect(config.options.mqtt.port).to.equal(8883); 52 | expect(config.options.mqtt.transport).to.equal("tcp"); 53 | expect(config.options.mqtt.cleanStart).to.equal(false); 54 | expect(config.options.mqtt.sessionExpiry).to.equal(3600); 55 | expect(config.options.mqtt.keepAlive).to.equal(60); 56 | expect(config.options.mqtt.caFile).to.equal("myPath"); 57 | expect(config.options.http.verify).to.equal(true); 58 | expect(config.options.http.additionalHeaders).to.deep.equal({hello: 'world', name: 'tom'}); 59 | }); 60 | 61 | it('Missing auth.key throws error', () => { 62 | let identity = null; 63 | let auth = {key: null}; 64 | let options = null; 65 | var applicationConfigTest = function(){new ApplicationConfig(identity, auth, options)}; 66 | expect(applicationConfigTest).to.throw('Missing auth.key from configuration'); 67 | }); 68 | 69 | it('Missing auth.token throws error', () => { 70 | let identity = null; 71 | let auth = {key: "MyKey", token: null}; 72 | let options = null; 73 | var applicationConfigTest = function(){new ApplicationConfig(identity, auth, options)}; 74 | expect(applicationConfigTest).to.throw('Missing auth.token from configuration'); 75 | }); 76 | 77 | it('Initialise without auth to induce quickstart', () => { 78 | let identity = null; 79 | let auth = null; 80 | let options = null; 81 | 82 | let config = new ApplicationConfig(identity, auth, options); 83 | expect(config.getOrgId()).to.equal("quickstart"); 84 | }); 85 | 86 | it('Load port as a string with environment variables', () => { 87 | process.env['WIOTP_OPTIONS_MQTT_PORT'] = '8883'; 88 | let config = ApplicationConfig.parseEnvVars(); 89 | expect(config.options.mqtt.port).to.equal(8883); 90 | }); 91 | 92 | it('Incorrect logLevel in config file throws error', () => { 93 | var applicationConfigTest = function(){new ApplicationConfig.parseConfigFile("./test/incorrectLogLevelTest.spec.yaml")}; 94 | expect(applicationConfigTest).to.throw('Optional setting options.logLevel (Currently: notALogLevel) must be one of error, warning, info, debug'); 95 | }); 96 | 97 | }); 98 | -------------------------------------------------------------------------------- /test/ApplicationConfigFile.spec.yaml: -------------------------------------------------------------------------------- 1 | 2 | identity: 3 | appId: myApp 4 | auth: 5 | key: myKey 6 | token: myToken 7 | options: 8 | apiRoot: api/v0002 9 | domain: internetofthings.ibmcloud.com 10 | logLevel: info 11 | mqtt: 12 | instanceId: myInstance 13 | port: 8883 14 | transport: tcp 15 | cleanStart: false 16 | sessionExpiry: 3600 17 | keepAlive: 60 18 | caFile: myPath 19 | http: 20 | verify: true 21 | additionalHeaders: 22 | hello: world 23 | name: tom -------------------------------------------------------------------------------- /test/DeviceClient.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | ***************************************************************************** 3 | Copyright (c) 2019 IBM Corporation and other Contributors. 4 | All rights reserved. This program and the accompanying materials 5 | are made available under the terms of the Eclipse Public License v1.0 6 | which accompanies this distribution, and is available at 7 | http://www.eclipse.org/legal/epl-v10.html 8 | ***************************************************************************** 9 | * 10 | */ 11 | import { ApplicationConfig, ApplicationClient } from '../src/application'; 12 | import { DeviceConfig, DeviceClient } from '../src/device'; 13 | import { assert } from 'chai'; 14 | import { step } from 'mocha-steps'; 15 | 16 | const { v4: uuidv4 } = require('uuid'); 17 | 18 | // Turn off console output 19 | console.info = () => {}; 20 | 21 | describe('WIoTP Device Capabilities', function() { 22 | 23 | let appConfig = ApplicationConfig.parseEnvVars(); 24 | 25 | describe("Event Publication", function() { 26 | let testTypeId = "iotnodejs-test"; 27 | let testDeviceId = uuidv4(); 28 | let testEventId = "testEvent"; 29 | let testEventFormat = "json"; 30 | let testEventData = "{'foo': 'bar'}"; 31 | let testQos = 1; 32 | 33 | let appClient = null; 34 | let deviceConfig = null; 35 | let deviceClient = null; 36 | 37 | before("Register the test device", function(done){ 38 | this.timeout(10000); 39 | appClient = new ApplicationClient(appConfig); 40 | 41 | // Register the device type 42 | appClient.registry.registerDeviceType(testTypeId) 43 | .catch(function(err){ 44 | if (err.response.status != 409) { 45 | // Anything other than conflict is bad! 46 | throw err; 47 | } 48 | console.info("device type already exists, carry on!") 49 | }) 50 | .then(function(result) { 51 | // Register the device 52 | return appClient.registry.registerDevice(testTypeId, testDeviceId); 53 | }) 54 | .then(function(deviceDetails) { 55 | console.info(deviceDetails); 56 | let identity = {orgId: appConfig.getOrgId(), typeId: deviceDetails.typeId, deviceId: deviceDetails.deviceId}; 57 | let auth = {token: deviceDetails.authToken} 58 | let options = {logLevel: "info"}; 59 | deviceConfig = new DeviceConfig(identity, auth, options); 60 | deviceClient = new DeviceClient(deviceConfig); 61 | done(); 62 | }) 63 | .catch(function(err){ 64 | done(err); 65 | }); 66 | }) 67 | 68 | afterEach("Remove error listener(s)", function() { 69 | appClient.removeAllListeners("error"); 70 | deviceClient.removeAllListeners("error"); 71 | }); 72 | 73 | step("Connect application within 5 seconds", function(done){ 74 | this.timeout(5000); 75 | appClient.on("connect", done); 76 | appClient.on("error", done); 77 | appClient.connect(); 78 | }); 79 | 80 | step("Connect device within 5 seconds", function(done){ 81 | this.timeout(5000); 82 | deviceClient.on("connect", done); 83 | deviceClient.on("error", done); 84 | deviceClient.connect(); 85 | }); 86 | 87 | step("Subscribe to device events", function(done){ 88 | let onSubscribe = function(err, granted) { 89 | if (err != null) { 90 | done(err); 91 | } 92 | done(); 93 | } 94 | appClient.on("error", done); 95 | appClient.subscribeToEvents(testTypeId, testDeviceId, testEventId, testEventFormat, testQos, onSubscribe); 96 | }); 97 | 98 | step("Publish & recieve device event within 10 seconds", function(done){ 99 | this.timeout(10000); 100 | 101 | let onEventSent = function(err) { 102 | if (err != null) { 103 | done(err); 104 | } 105 | } 106 | 107 | let onEventReceived = function(typeId, deviceId, eventId, format, payload) { 108 | assert(typeId == testTypeId, "Type ID does not match"); 109 | assert(deviceId == testDeviceId, "Device ID does not match"); 110 | assert(eventId == testEventId, "Event ID does not match"); 111 | assert(format == testEventFormat, "Format does not match"); 112 | assert(payload == testEventData, "Payload does not match"); 113 | done(); 114 | } 115 | 116 | deviceClient.on("error", done); 117 | appClient.on("error", done); 118 | appClient.on("deviceEvent", onEventReceived); 119 | 120 | deviceClient.publishEvent(testEventId, testEventFormat, testEventData, testQos, onEventSent); 121 | }); 122 | 123 | step("Disconnect the device MQTT client", function(done) { 124 | deviceClient.on("error", done); 125 | deviceClient.on("close", function() { 126 | assert(deviceClient.isConnected() == false, "Device is still connected"); 127 | done(); 128 | }); 129 | deviceClient.disconnect(); 130 | }); 131 | 132 | step("Disconnect the application MQTT client", function(done) { 133 | appClient.on("error", done); 134 | appClient.on("close", function() { 135 | assert(appClient.isConnected() == false, "Application is still connected"); 136 | done(); 137 | }); 138 | appClient.disconnect(); 139 | }); 140 | 141 | after("Delete the test device & ensure the the MQTT clients are disconnected", function() { 142 | if (appClient != null && appClient.isConnected()) { 143 | appClient.disconnect(); 144 | } 145 | if (deviceClient != null && deviceClient.isConnected()) { 146 | deviceClient.disconnect(); 147 | } 148 | 149 | appClient.registry.unregisterDevice(testTypeId, testDeviceId); 150 | }); 151 | }); 152 | 153 | describe("Command Subscription", function() { 154 | let testTypeId = "iotnodejs-test"; 155 | let testDeviceId = uuidv4(); 156 | let testCommandId = "testCommand"; 157 | let testCommandFormat = "json"; 158 | let testCommandData = "{'foo': 'bar'}"; 159 | let testQos = 1; 160 | 161 | let appClient = null; 162 | let deviceConfig = null; 163 | let deviceClient = null; 164 | 165 | before("Register the test device", function(done){ 166 | this.timeout(10000); 167 | appClient = new ApplicationClient(appConfig); 168 | 169 | // Register the device type 170 | appClient.registry.registerDeviceType(testTypeId) 171 | .catch(function(err){ 172 | if (err.response.status != 409) { 173 | // Anything other than conflict is bad! 174 | throw err; 175 | } 176 | console.info("device type already exists, carry on!") 177 | }) 178 | .then(function(result) { 179 | // Register the device 180 | return appClient.registry.registerDevice(testTypeId, testDeviceId); 181 | }) 182 | .then(function(deviceDetails) { 183 | console.info(deviceDetails); 184 | let identity = {orgId: appConfig.getOrgId(), typeId: deviceDetails.typeId, deviceId: deviceDetails.deviceId}; 185 | let auth = {token: deviceDetails.authToken} 186 | let options = {logLevel: "info"}; 187 | deviceConfig = new DeviceConfig(identity, auth, options); 188 | deviceClient = new DeviceClient(deviceConfig); 189 | done(); 190 | }) 191 | .catch(function(err){ 192 | done(err); 193 | }); 194 | }) 195 | 196 | afterEach("Remove error listener(s)", function() { 197 | appClient.removeAllListeners("error"); 198 | deviceClient.removeAllListeners("error"); 199 | }); 200 | 201 | step("Connect application within 5 seconds", function(done){ 202 | this.timeout(5000); 203 | appClient.on("connect", done); 204 | appClient.on("error", done); 205 | appClient.connect(); 206 | }); 207 | 208 | step("Connect device within 5 seconds", function(done){ 209 | this.timeout(5000); 210 | deviceClient.on("connect", done); 211 | deviceClient.on("error", done); 212 | deviceClient.connect(); 213 | }); 214 | 215 | step("Publish & recieve device command within 10 seconds", function(done){ 216 | this.timeout(10000); 217 | let onCommandSent = function(err) { 218 | if (err != null) { 219 | done(err); 220 | } 221 | } 222 | 223 | let onCommandReceived = function(commandId, format, payload) { 224 | assert(commandId == testCommandId, "Command ID does not match"); 225 | assert(format == testCommandFormat, "Format does not match"); 226 | assert(payload == testCommandData, "Payload does not match"); 227 | done(); 228 | } 229 | 230 | deviceClient.on("error", done); 231 | appClient.on("error", done); 232 | deviceClient.on("command", onCommandReceived); 233 | 234 | appClient.publishCommand(testTypeId, testDeviceId, testCommandId, testCommandFormat, testCommandData, testQos, onCommandSent); 235 | }); 236 | 237 | step("Disconnect the device MQTT client", function(done) { 238 | deviceClient.on("error", done); 239 | deviceClient.on("close", function() { 240 | assert(deviceClient.isConnected() == false, "Device is still connected"); 241 | done(); 242 | }); 243 | deviceClient.disconnect(); 244 | }); 245 | 246 | step("Disconnect the application MQTT client", function(done) { 247 | appClient.on("error", done); 248 | appClient.on("close", function() { 249 | assert(appClient.isConnected() == false, "Application is still connected"); 250 | done(); 251 | }); 252 | appClient.disconnect(); 253 | }); 254 | 255 | after("Delete the test device & ensure the the MQTT clients are disconnected", function() { 256 | if (appClient != null && appClient.isConnected()) { 257 | appClient.disconnect(); 258 | } 259 | if (deviceClient != null && deviceClient.isConnected()) { 260 | deviceClient.disconnect(); 261 | } 262 | 263 | appClient.registry.unregisterDevice(testTypeId, testDeviceId); 264 | }); 265 | }); 266 | 267 | }); 268 | -------------------------------------------------------------------------------- /test/DeviceConfig.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | ***************************************************************************** 3 | Copyright (c) 2014, 2019 IBM Corporation and other Contributors. 4 | All rights reserved. This program and the accompanying materials 5 | are made available under the terms of the Eclipse Public License v1.0 6 | which accompanies this distribution, and is available at 7 | http://www.eclipse.org/legal/epl-v10.html 8 | ***************************************************************************** 9 | * 10 | */ 11 | 12 | import { DeviceConfig } from '../src/device'; 13 | import { expect } from 'chai'; 14 | 15 | // Turn off console output 16 | console.info = () => {}; 17 | 18 | describe('WIoTP Device Configuration', () => { 19 | 20 | it('Initialise with the minimum required configuration', () => { 21 | let identity = {orgId: "myOrg", typeId: "myType", deviceId: "myDevice"}; 22 | let auth = {token: "myToken"}; 23 | let options = null; 24 | 25 | let config = new DeviceConfig(identity, auth, options); 26 | expect(config.getOrgId()).to.equal("myOrg"); 27 | expect(config.options.logLevel).to.equal("info"); 28 | expect(config.options.domain).to.equal("internetofthings.ibmcloud.com"); 29 | expect(config.options.mqtt.transport).to.equal("tcp"); 30 | expect(config.options.mqtt.port).to.equal(8883); 31 | }); 32 | 33 | it('Load mimimal configuration from environment variables', () => { 34 | process.env['WIOTP_IDENTITY_ORGID'] = 'myOrg'; 35 | process.env['WIOTP_IDENTITY_DEVICEID'] = 'MyDevice'; 36 | process.env['WIOTP_IDENTITY_TYPEID'] = 'myType'; 37 | let config = DeviceConfig.parseEnvVars(); 38 | expect(config.options.logLevel).to.equal("info"); 39 | expect(config.options.domain).to.equal("internetofthings.ibmcloud.com"); 40 | expect(config.options.mqtt.transport).to.equal("tcp"); 41 | expect(config.options.mqtt.port).to.equal(8883); 42 | delete process.env['WIOTP_IDENTITY_ORGID']; 43 | delete process.env['WIOTP_IDENTITY_DEVICEID']; 44 | delete process.env['WIOTP_IDENTITY_TYPEID']; 45 | 46 | }); 47 | 48 | it('Load configuration from yaml config file', () => { 49 | let config = DeviceConfig.parseConfigFile("./test/DeviceConfigFile.spec.yaml"); 50 | expect(config.identity.orgId).to.equal("myOrg") 51 | expect(config.identity.typeId).to.equal("myType") 52 | expect(config.identity.deviceId).to.equal("myDevice") 53 | expect(config.auth.token).to.equal("myToken") 54 | expect(config.options.domain).to.equal("internetofthings.ibmcloud.com"); 55 | expect(config.options.logLevel).to.equal("info"); 56 | expect(config.options.mqtt.port).to.equal(8883); 57 | expect(config.options.mqtt.transport).to.equal("tcp"); 58 | expect(config.options.mqtt.cleanStart).to.equal(true); 59 | expect(config.options.mqtt.sessionExpiry).to.equal(3600); 60 | expect(config.options.mqtt.keepAlive).to.equal(60); 61 | expect(config.options.mqtt.caFile).to.equal("myPath"); 62 | }); 63 | 64 | it('Missing identity throws error', () => { 65 | let identity = null; 66 | let auth = null; 67 | let options = null; 68 | var deviceConfigTest = function(){new DeviceConfig(identity, auth, options)}; 69 | expect(deviceConfigTest).to.throw('Missing identity from configuration'); 70 | }); 71 | 72 | it('Missing orgID throws error', () => { 73 | let identity = {orgId: null}; 74 | let auth = null; 75 | let options = null; 76 | var deviceConfigTest = function(){new DeviceConfig(identity, auth, options)}; 77 | expect(deviceConfigTest).to.throw('Missing identity.orgId from configuration'); 78 | }); 79 | 80 | it('Missing typeId throws error', () => { 81 | let identity = {orgId: "myOrg", typeId: null}; 82 | let auth = null; 83 | let options = null; 84 | var deviceConfigTest = function(){new DeviceConfig(identity, auth, options)}; 85 | expect(deviceConfigTest).to.throw('Missing identity.typeId from configuration'); 86 | }); 87 | 88 | it('Missing deviceId throws error', () => { 89 | let identity = {orgId: "myOrg", typeId: "myType", deviceId: null}; 90 | let auth = null; 91 | let options = null; 92 | var deviceConfigTest = function(){new DeviceConfig(identity, auth, options)}; 93 | expect(deviceConfigTest).to.throw('Missing identity.deviceId from configuration'); 94 | }); 95 | 96 | it('Quickstart with an auth throws error', () => { 97 | let identity = {orgId: "quickstart", typeId: "myType", deviceId: "MyDevice"}; 98 | let auth = {token: "myToken"}; 99 | let options = null; 100 | var deviceConfigTest = function(){new DeviceConfig(identity, auth, options)}; 101 | expect(deviceConfigTest).to.throw('Quickstart service does not support device authentication'); 102 | }); 103 | 104 | it('Missing auth throws error', () => { 105 | let identity = {orgId: "myOrg", typeId: "myType", deviceId: "MyDevice"}; 106 | let auth = null; 107 | let options = null; 108 | var deviceConfigTest = function(){new DeviceConfig(identity, auth, options)}; 109 | expect(deviceConfigTest).to.throw('Missing auth from configuration'); 110 | }); 111 | 112 | it('Missing auth token throws error', () => { 113 | let identity = {orgId: "myOrg", typeId: "myType", deviceId: "MyDevice"}; 114 | let auth = {token: null}; 115 | let options = null; 116 | var deviceConfigTest = function(){new DeviceConfig(identity, auth, options)}; 117 | expect(deviceConfigTest).to.throw('Missing auth.token from configuration'); 118 | }); 119 | 120 | it('Incorrect logLevel in config file throws error', () => { 121 | var deviceConfigTest = function(){new DeviceConfig.parseConfigFile("./test/incorrectLogLevelTest.spec.yaml")}; 122 | expect(deviceConfigTest).to.throw('Optional setting options.logLevel (Currently: notALogLevel) must be one of error, warning, info, debug'); 123 | }); 124 | 125 | 126 | }); -------------------------------------------------------------------------------- /test/DeviceConfigFile.spec.yaml: -------------------------------------------------------------------------------- 1 | 2 | identity: 3 | orgId: myOrg 4 | typeId: myType 5 | deviceId: myDevice 6 | auth: 7 | token: myToken 8 | options: 9 | domain: internetofthings.ibmcloud.com 10 | logLevel: info 11 | mqtt: 12 | port: 8883 13 | transport: tcp 14 | cleanStart: true 15 | sessionExpiry: 3600 16 | keepAlive: 60 17 | caFile: myPath 18 | -------------------------------------------------------------------------------- /test/DscClient.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | ***************************************************************************** 3 | Copyright (c) 2019 IBM Corporation and other Contributors. 4 | All rights reserved. This program and the accompanying materials 5 | are made available under the terms of the Eclipse Public License v1.0 6 | which accompanies this distribution, and is available at 7 | http://www.eclipse.org/legal/epl-v10.html 8 | ***************************************************************************** 9 | * 10 | */ 11 | const { v4: uuidv4 } = require('uuid'); 12 | 13 | import { expect, use } from 'chai'; 14 | const chaiAsPromised = require('chai-as-promised'); 15 | use(chaiAsPromised); 16 | 17 | const { CloudantV1: Cloudant } = require('@ibm-cloud/cloudant'); 18 | 19 | import { ApplicationConfig, ApplicationClient } from '../src/application'; 20 | import * as errors from '../src/api/ApiErrors'; 21 | 22 | // Turn off console output 23 | console.info = () => {}; 24 | 25 | 26 | 27 | const generateCloudantServiceConfigDbName = (orgId, serviceId) => { 28 | return `iotp_${orgId}_${serviceId}_configuration`; 29 | } 30 | 31 | const generateCloudantDestinationConfigDbName = (orgId, destinationName) => { 32 | return `iotp_${orgId}_${destinationName}_configuration`; 33 | } 34 | 35 | const generateCloudantDestinationBucketDbNamePattern = (orgId, destinationName, bucketInterval) => { 36 | let bucketPattern; 37 | switch(bucketInterval.toUpperCase()) { 38 | case 'DAY': bucketPattern = '\\d{4}-\\d{2}-\\d{1,2}'; break; 39 | case 'WEEK': bucketPattern = '\\d{4}-w\\d{1,2}'; break; 40 | case 'MONTH': bucketPattern = '\\d{4}-\\d{2}'; break; 41 | default: throw new Error(`Unknown bucketInterval: ${bucketInterval}`); 42 | } 43 | return new RegExp(`iotp_${orgId}_${destinationName}_${bucketPattern}`); 44 | } 45 | 46 | 47 | /* 48 | * Wrap cloudant:2.1.0 (OSS pre-approved) to callbacks to promises 49 | */ 50 | 51 | 52 | const cloudantDestroyDb = (cloudant, dbName) => { 53 | return new Promise( 54 | (resolve, reject) => { 55 | cloudant.db.destroy( 56 | dbName, 57 | function(err, data) { 58 | if(err){ 59 | console.warn(`Failed to delete cloudant DB ${dbName}: ${err}`); 60 | reject(err); 61 | } else { 62 | resolve(data); 63 | } 64 | } 65 | ); 66 | } 67 | ); 68 | } 69 | 70 | 71 | const cloudantListDbs = (cloudant) => { 72 | return new Promise( 73 | (resolve, reject) => cloudant.db.list(function(err, data) { 74 | if(err) { 75 | console.warn(`Failed to list cloudant DBs`); 76 | reject(err); 77 | } else { 78 | resolve(data); 79 | } 80 | }) 81 | ) 82 | } 83 | 84 | describe('WIoTP DSC Client Capabilities', function() { 85 | 86 | let orgId = null; 87 | let dscClient = null; 88 | 89 | before("Initialize the application", function(){ 90 | const appConfig = ApplicationConfig.parseEnvVars(); 91 | orgId = appConfig.getOrgId(); 92 | const appClient = new ApplicationClient(appConfig); 93 | dscClient = appClient.dsc; 94 | }); 95 | 96 | 97 | 98 | // TODO: Example scenarios - these should both: 99 | // 1. provide coverage of client function 100 | // 2. serve as useful, self-documenting reference material 101 | // e.g. 102 | describe('Create, retrieve, connect and delete a Cloudant service binding', function() { 103 | 104 | this.timeout(10000); 105 | 106 | const description = 'WIoTP DSC Client Capabilities / Create, retrieve, connect and delete a Cloudant service binding'; 107 | 108 | let cloudantUsername; 109 | let cloudantPassword; 110 | let cloudant; 111 | 112 | let createdService; 113 | let createdConnector; 114 | let createdDestination; 115 | let createdForwardingRule; 116 | 117 | 118 | before('Init Cloudant Client', function() { 119 | cloudantUsername = process.env.CLOUDANT_USERNAME || null; 120 | cloudantPassword = process.env.CLOUDANT_PASSWORD || null; 121 | if( cloudantUsername === null) expect.fail('CLOUDANT_USERNAME env variable is required for this test'); 122 | if( cloudantPassword === null) expect.fail('CLOUDANT_PASSWORD env variable is required for this test'); 123 | cloudant = Cloudant({account: cloudantUsername, password: cloudantPassword}) 124 | }) 125 | 126 | step('Create service', () => { 127 | const name = uuidv4(); 128 | return dscClient.createCloudantService({name, description, username: cloudantUsername, password: cloudantPassword}) 129 | .then(_createdService => { 130 | createdService = _createdService; 131 | expect(createdService.type).to.equal('cloudant'); 132 | expect(createdService.name).to.equal(name); 133 | expect(createdService.description).to.equal(description); 134 | }); 135 | }) 136 | 137 | step('Retrieve services', () => { 138 | return dscClient.getServices() 139 | .then(services => { 140 | expect(services).to.have.property('results'); 141 | const filtered = services.results.filter(service => service.id === createdService.id); 142 | expect(filtered).to.have.length(1); 143 | const service = filtered[0]; 144 | expect(service).to.deep.equal(createdService); 145 | }); 146 | }); 147 | 148 | 149 | step('Retrieve service', function() { 150 | return dscClient.getService(createdService.id) 151 | .then(service => { 152 | expect(service).to.deep.equal(createdService); 153 | }) 154 | }); 155 | 156 | 157 | step('Create connector for service', function() { 158 | const name = uuidv4(); 159 | const timezone = "Africa/Casablanca"; 160 | return dscClient.createConnector({name, type:'cloudant', description, serviceId: createdService.id, timezone}) 161 | .then(connector => { 162 | createdConnector = connector; 163 | expect(connector).to.have.property('id'); 164 | expect(connector).to.have.property('name', name); 165 | expect(connector).to.have.property('serviceId', createdService.id); 166 | expect(connector).to.have.property('type', 'cloudant'); 167 | expect(connector).to.have.property('timezone', timezone); 168 | expect(connector).to.have.property('enabled', true); 169 | expect(connector).to.have.property('created'); 170 | expect(connector).to.have.property('createdBy'); 171 | expect(connector).to.have.property('updated'); 172 | expect(connector).to.have.property('updatedBy'); 173 | expect(connector).to.have.property('refs'); 174 | }) 175 | }); 176 | 177 | step('Update connector', function() { 178 | const newName = uuidv4(); 179 | return dscClient.updateConnector({...createdConnector, name: newName}) 180 | .then(updatedConnector => { 181 | expect(updatedConnector).to.have.property('id', createdConnector.id); 182 | expect(updatedConnector).to.have.property('name', newName); 183 | expect(updatedConnector).to.have.property('serviceId', createdService.id); 184 | expect(updatedConnector).to.have.property('type', createdConnector.type); 185 | expect(updatedConnector).to.have.property('timezone', createdConnector.timezone); 186 | expect(updatedConnector).to.have.property('enabled', true); 187 | expect(updatedConnector).to.have.property('created', createdConnector.created); 188 | expect(updatedConnector).to.have.property('createdBy', createdConnector.createdBy); 189 | expect(updatedConnector).to.have.property('updated'); 190 | expect(updatedConnector).to.have.property('updatedBy', createdConnector.updatedBy); 191 | expect(updatedConnector).to.have.property('refs'); 192 | }) 193 | }); 194 | 195 | // TODO: retrieve connector 196 | // TODO: retrieve connectors 197 | 198 | 199 | step('Create connector destination', function() { 200 | const name = uuidv4(); 201 | const bucketInterval = 'MONTH'; 202 | return dscClient.createCloudantDestination(createdConnector.id, {name, bucketInterval}) 203 | .then(destination => { 204 | createdDestination = destination; 205 | expect(destination).to.have.property('name', name); 206 | expect(destination).to.have.property('type', 'cloudant'); 207 | expect(destination).to.have.property('configuration'); 208 | expect(destination.configuration).to.have.property('bucketInterval', bucketInterval); 209 | }) 210 | }); 211 | 212 | step('Attempting to create duplicate destination should throw DestinationAlreadyExistss error', function() { 213 | const bucketInterval = 'MONTH'; 214 | return expect( 215 | dscClient.createCloudantDestination(createdConnector.id, {name: createdDestination.name, bucketInterval}) 216 | ).to.be.rejectedWith(errors.DestinationAlreadyExistss); 217 | }); 218 | 219 | // TODO: retrieve connector destination 220 | // TODO: retrieve connector destinations 221 | 222 | step('Create connector forwarding rule', function() { 223 | const name = uuidv4(); 224 | const deviceType='*'; 225 | const eventId='*'; 226 | return dscClient.createEventForwardingRule(createdConnector.id, {name, destinationName: createdDestination.name, deviceType, eventId}) 227 | .then(forwardingRule => { 228 | createdForwardingRule = forwardingRule; 229 | expect(forwardingRule).to.have.property('name', name); 230 | expect(forwardingRule).to.have.property('type', 'event'); 231 | expect(forwardingRule).to.have.property('destinationName', createdDestination.name); 232 | expect(forwardingRule).to.have.property('selector'); 233 | expect(forwardingRule.selector).to.have.property('deviceType', deviceType); 234 | expect(forwardingRule.selector).to.have.property('eventId', eventId); 235 | }) 236 | }); 237 | 238 | // TODO: retrieve connector forwarding rule 239 | // TODO: retrieve connector forwarding rules 240 | 241 | // TODO: send an event, check it makes it into cloudant 242 | 243 | step('Delete forwarding rule', () => { 244 | return dscClient.deleteForwardingRule(createdConnector.id, createdForwardingRule.id) 245 | }); 246 | 247 | step('Delete destination', () => { 248 | return dscClient.deleteDestination(createdConnector.id, createdDestination.name) 249 | }); 250 | 251 | 252 | step('Delete connector', () => { 253 | return dscClient.deleteConnector(createdConnector.id) 254 | }); 255 | 256 | step('Delete Service', () => { 257 | return dscClient.deleteService(createdService.id) 258 | }); 259 | 260 | 261 | 262 | after('Cleanup forwarding rule', () => { 263 | if(createdConnector && createdForwardingRule) return dscClient.deleteForwardingRule(createdConnector.id, createdForwardingRule.id).catch(err=>{}) 264 | }); 265 | 266 | after('Cleanup destination', () => { 267 | if(createdConnector && createdDestination) return dscClient.deleteDestination(createdConnector.id, createdDestination.name).catch(err=>{}) 268 | }); 269 | 270 | 271 | after('Cleanup connector', () => { 272 | if (createdConnector) return dscClient.deleteConnector(createdConnector.id).catch(err=>{}); 273 | }); 274 | 275 | after('Cleanup service', () => { 276 | if (createdService) return dscClient.deleteService(createdService.id).catch(err=>{}); 277 | }); 278 | 279 | 280 | 281 | // deleting a cloudant destination via WIoTP does not delete the databases it created in cloudant (by design) 282 | // to avoid clogging up our test cloudant instance, we delete all databases that will have been created by WIoTP 283 | // NOTE: this must occur after we've deleted the destination from WIoTP otherwise it may be recreated by WIoTP 284 | 285 | 286 | after('Cleanup service config DB from cloudant', () => { 287 | if(createdService) 288 | return cloudantDestroyDb(cloudant, generateCloudantServiceConfigDbName(orgId, createdService.id)).catch(err=>{}); 289 | }); 290 | 291 | after('Cleanup destination config DB from cloudant', () => { 292 | if(createdDestination) 293 | return cloudantDestroyDb(cloudant, generateCloudantDestinationConfigDbName(orgId, createdDestination.name)).catch(err=>{}); 294 | }); 295 | 296 | after('Cleanup destination bucket databases from cloudant', () => { 297 | if(createdDestination) { 298 | return cloudantListDbs(cloudant) 299 | .then((body) => { 300 | return Promise.all( 301 | body 302 | .filter((db) => { 303 | const pattern = generateCloudantDestinationBucketDbNamePattern(orgId, createdDestination.name, createdDestination.configuration.bucketInterval); 304 | return pattern.test(db); 305 | }) 306 | .map(db => { 307 | return cloudantDestroyDb(cloudant, db).catch(err=>{}) 308 | }) 309 | ); 310 | }) 311 | .catch(err => {}) 312 | } 313 | }); 314 | 315 | 316 | 317 | // ensure created resources: 318 | // 1. have globally-unique IDs (that will not conflict with those used by concurrently-running tests) 319 | // 2. are cleaned up, even in the event of test failure 320 | // 3. have some info associated with them to assist debugging - e.g. in name (if poss) or metadata 321 | }); 322 | 323 | 324 | 325 | /* Self-contained simple unit tests */ 326 | describe('createCloudantService', () => { 327 | it('should throw InvalidServiceCredentials when credentials are invalid', () => { 328 | return expect(dscClient.createCloudantService({name: 'asd', username:'user', password: 'pass'})) 329 | .to.be.rejectedWith(errors.InvalidServiceCredentials); 330 | }); 331 | }); 332 | 333 | describe('getService', () => { 334 | it('should throw ServiceNotFound when service does not exist', function() { 335 | this.timeout(10000); 336 | return expect(dscClient.getService(uuidv4())) 337 | .to.be.rejectedWith(errors.ServiceNotFound); 338 | }); 339 | }); 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | describe('Create, retrieve, connect and delete an EventStreams service binding', function() { 362 | 363 | this.timeout(10000); 364 | 365 | const description = 'WIoTP DSC Client Capabilities / Create, retrieve, connect and delete a EventStreams service binding'; 366 | 367 | let eventstreamsApiKey; 368 | let eventstreamsAdminUrl; 369 | let eventstreamsBrokers; 370 | let eventstreamsUser; 371 | let eventstreamsPassword; 372 | 373 | let createdService; 374 | let createdConnector; 375 | let createdDestination; 376 | let createdForwardingRule; 377 | 378 | 379 | before('Get Eventstreams credentials from environment', function() { 380 | eventstreamsApiKey = process.env.EVENTSTREAMS_API_KEY || expect.fail('EVENTSTREAMS_API_KEY env variable is required for this test'); 381 | eventstreamsAdminUrl = process.env.EVENTSTREAMS_ADMIN_URL || expect.fail('EVENTSTREAMS_ADMIN_URL env variable is required for this test'); 382 | eventstreamsBrokers = process.env.EVENTSTREAMS_BROKER1 ? [process.env.EVENTSTREAMS_BROKER1] : expect.fail('EVENTSTREAMS_BROKER1 env variable is required for this test'); 383 | eventstreamsUser = process.env.EVENTSTREAMS_USER || expect.fail('EVENTSTREAMS_USER env variable is required for this test'); 384 | eventstreamsPassword = process.env.EVENTSTREAMS_PASSWORD || expect.fail('EVENTSTREAMS_PASSWORD env variable is required for this test'); 385 | }) 386 | 387 | step('Create service', () => { 388 | const name = uuidv4(); 389 | return dscClient.createEventstreamsService({name, description, 390 | api_key: eventstreamsApiKey, kafka_admin_url: eventstreamsAdminUrl, kafka_brokers_sasl: eventstreamsBrokers, 391 | user: eventstreamsUser, password: eventstreamsPassword}) 392 | .then(_createdService => { 393 | createdService = _createdService; 394 | expect(createdService.type).to.equal('eventstreams'); 395 | expect(createdService.name).to.equal(name); 396 | expect(createdService.description).to.equal(description); 397 | }); 398 | }) 399 | 400 | step('Retrieve services', () => { 401 | return dscClient.getServices() 402 | .then(services => { 403 | expect(services).to.have.property('results'); 404 | const filtered = services.results.filter(service => service.id === createdService.id); 405 | expect(filtered).to.have.length(1); 406 | const service = filtered[0]; 407 | expect(service).to.deep.equal(createdService); 408 | }); 409 | }); 410 | 411 | 412 | step('Retrieve service', function() { 413 | return dscClient.getService(createdService.id) 414 | .then(service => { 415 | expect(service).to.deep.equal(createdService); 416 | }) 417 | }); 418 | 419 | 420 | step('Create connector for service', function() { 421 | const name = uuidv4(); 422 | const timezone = "Africa/Casablanca"; 423 | return dscClient.createConnector({name, type: 'eventstreams', description, serviceId: createdService.id, timezone}) 424 | .then(connector => { 425 | createdConnector = connector; 426 | expect(connector).to.have.property('id'); 427 | expect(connector).to.have.property('name', name); 428 | expect(connector).to.have.property('serviceId', createdService.id); 429 | expect(connector).to.have.property('type', 'eventstreams'); 430 | expect(connector).to.have.property('timezone', timezone); 431 | expect(connector).to.have.property('enabled', true); 432 | expect(connector).to.have.property('created'); 433 | expect(connector).to.have.property('createdBy'); 434 | expect(connector).to.have.property('updated'); 435 | expect(connector).to.have.property('updatedBy'); 436 | expect(connector).to.have.property('refs'); 437 | }) 438 | }); 439 | 440 | 441 | 442 | step('Update connector', function() { 443 | const newName = uuidv4(); 444 | return dscClient.updateConnector({...createdConnector, name: newName}) 445 | .then(updatedConnector => { 446 | expect(updatedConnector).to.have.property('id', createdConnector.id); 447 | expect(updatedConnector).to.have.property('name', newName); 448 | expect(updatedConnector).to.have.property('serviceId', createdService.id); 449 | expect(updatedConnector).to.have.property('type', createdConnector.type); 450 | expect(updatedConnector).to.have.property('timezone', createdConnector.timezone); 451 | expect(updatedConnector).to.have.property('enabled', true); 452 | expect(updatedConnector).to.have.property('created', createdConnector.created); 453 | expect(updatedConnector).to.have.property('createdBy', createdConnector.createdBy); 454 | expect(updatedConnector).to.have.property('updated'); 455 | expect(updatedConnector).to.have.property('updatedBy', createdConnector.updatedBy); 456 | expect(updatedConnector).to.have.property('refs'); 457 | }) 458 | }); 459 | 460 | step('Create connector destination', function() { 461 | const name = uuidv4(); 462 | const partitions = 2; 463 | return dscClient.createEventstreamsDestination(createdConnector.id, {name, partitions}) 464 | .then(destination => { 465 | createdDestination = destination; 466 | expect(destination).to.have.property('name', name); 467 | expect(destination).to.have.property('type', 'eventstreams'); 468 | expect(destination).to.have.property('configuration'); 469 | expect(destination.configuration).to.have.property('partitions', partitions); 470 | }) 471 | }); 472 | 473 | 474 | step('Create connector forwarding rule', function() { 475 | const name = uuidv4(); 476 | const deviceType='*'; 477 | const eventId='*'; 478 | return dscClient.createEventForwardingRule(createdConnector.id, {name, destinationName: createdDestination.name, deviceType, eventId}) 479 | .then(forwardingRule => { 480 | createdForwardingRule = forwardingRule; 481 | expect(forwardingRule).to.have.property('name', name); 482 | expect(forwardingRule).to.have.property('type', 'event'); 483 | expect(forwardingRule).to.have.property('destinationName', createdDestination.name); 484 | expect(forwardingRule).to.have.property('selector'); 485 | expect(forwardingRule.selector).to.have.property('deviceType', deviceType); 486 | expect(forwardingRule.selector).to.have.property('eventId', eventId); 487 | }) 488 | }); 489 | 490 | step('Delete forwarding rule', () => { 491 | return dscClient.deleteForwardingRule(createdConnector.id, createdForwardingRule.id) 492 | }); 493 | 494 | 495 | step('Delete destination', () => { 496 | return dscClient.deleteDestination(createdConnector.id, createdDestination.name) 497 | }); 498 | 499 | 500 | step('Delete connector', () => { 501 | return dscClient.deleteConnector(createdConnector.id) 502 | }); 503 | 504 | 505 | step('Delete Service', () => { 506 | return dscClient.deleteService(createdService.id) 507 | }); 508 | 509 | after('Cleanup forwarding rule', () => { 510 | if(createdConnector && createdForwardingRule) return dscClient.deleteForwardingRule(createdConnector.id, createdForwardingRule.id).catch(err=>{}) 511 | }); 512 | 513 | 514 | after('Cleanup destination', () => { 515 | if(createdConnector && createdDestination) return dscClient.deleteDestination(createdConnector.id, createdDestination.name).catch(err=>{}) 516 | }); 517 | 518 | after('Cleanup connector', () => { 519 | if (createdConnector) return dscClient.deleteConnector(createdConnector.id).catch(err=>{}); 520 | }); 521 | 522 | after('Cleanup service', () => { 523 | if (createdService) return dscClient.deleteService(createdService.id).catch(err=>{}); 524 | }); 525 | 526 | 527 | // NOTE: no need to cleanup ES config. Unlike for Cloudant destinations, 528 | // WIoTP will delete the ES topic when the associated destination is deleted 529 | 530 | }); 531 | 532 | 533 | 534 | }); -------------------------------------------------------------------------------- /test/GatewayClient.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | ***************************************************************************** 3 | Copyright (c) 2019 IBM Corporation and other Contributors. 4 | All rights reserved. This program and the accompanying materials 5 | are made available under the terms of the Eclipse Public License v1.0 6 | which accompanies this distribution, and is available at 7 | http://www.eclipse.org/legal/epl-v10.html 8 | ***************************************************************************** 9 | * 10 | */ 11 | import { ApplicationConfig, ApplicationClient } from '../src/application'; 12 | import { GatewayConfig, GatewayClient } from '../src/gateway'; 13 | import { assert } from 'chai'; 14 | import { step } from 'mocha-steps'; 15 | 16 | const { v4: uuidv4 } = require('uuid'); 17 | 18 | // Turn off console output 19 | console.info = () => {}; 20 | 21 | describe('WIoTP Gateway Capabilities', function() { 22 | 23 | let testTypeId = "iotnodejs-testgw"; 24 | let testQos = 1; 25 | let appConfig = ApplicationConfig.parseEnvVars(); 26 | 27 | describe("Event Publication", function() { 28 | let testDeviceId = uuidv4(); 29 | let testEventId = "testEvent"; 30 | let testEventFormat = "json"; 31 | let testEventData = "{'foo': 'bar'}"; 32 | 33 | let appClient = null; 34 | let deviceConfig = null; 35 | let deviceClient = null; 36 | 37 | before("Register the test gateway", function(done){ 38 | this.timeout(10000); 39 | appClient = new ApplicationClient(appConfig); 40 | 41 | // Register the device type 42 | appClient.registry.registerDeviceType(testTypeId, null, null, null, "Gateway") 43 | .catch(function(err){ 44 | if (err.response.status != 409) { 45 | // Anything other than conflict is bad! 46 | throw err; 47 | } 48 | console.info("device type already exists, carry on!") 49 | }) 50 | .then(function(result) { 51 | // Register the device 52 | return appClient.registry.registerDevice(testTypeId, testDeviceId); 53 | }) 54 | .then(function(deviceDetails) { 55 | console.info(deviceDetails); 56 | let identity = {orgId: appConfig.getOrgId(), typeId: deviceDetails.typeId, deviceId: deviceDetails.deviceId}; 57 | let auth = {token: deviceDetails.authToken} 58 | let options = {logLevel: "info"}; 59 | deviceConfig = new GatewayConfig(identity, auth, options); 60 | deviceClient = new GatewayClient(deviceConfig); 61 | done(); 62 | }) 63 | .catch(function(err){ 64 | done(err); 65 | }); 66 | }) 67 | 68 | afterEach("Remove error listener(s)", function() { 69 | appClient.removeAllListeners("error"); 70 | deviceClient.removeAllListeners("error"); 71 | }); 72 | 73 | step("Connect application within 5 seconds", function(done){ 74 | this.timeout(5000); 75 | appClient.on("connect", done); 76 | appClient.on("error", done); 77 | appClient.connect(); 78 | }); 79 | 80 | step("Connect gateway within 5 seconds", function(done){ 81 | this.timeout(5000); 82 | deviceClient.on("connect", done); 83 | deviceClient.on("error", done); 84 | deviceClient.connect(); 85 | }); 86 | 87 | step("Subscribe to gateway events", function(done){ 88 | let onSubscribe = function(err, granted) { 89 | if (err != null) { 90 | done(err); 91 | } 92 | done(); 93 | } 94 | appClient.on("error", done); 95 | appClient.subscribeToEvents(testTypeId, testDeviceId, testEventId, testEventFormat, testQos, onSubscribe); 96 | }); 97 | 98 | step("Publish & recieve gateway event within 10 seconds", function(done){ 99 | this.timeout(10000); 100 | 101 | let onEventSent = function(err) { 102 | if (err != null) { 103 | done(err); 104 | } 105 | } 106 | 107 | let onEventReceived = function(typeId, deviceId, eventId, format, payload) { 108 | assert(typeId == testTypeId, "Type ID does not match"); 109 | assert(deviceId == testDeviceId, "Device ID does not match"); 110 | assert(eventId == testEventId, "Event ID does not match"); 111 | assert(format == testEventFormat, "Format does not match"); 112 | assert(payload == testEventData, "Payload does not match"); 113 | done(); 114 | } 115 | 116 | deviceClient.on("error", done); 117 | appClient.on("error", done); 118 | appClient.on("deviceEvent", onEventReceived); 119 | 120 | deviceClient.publishEvent(testEventId, testEventFormat, testEventData, testQos, onEventSent); 121 | }); 122 | 123 | step("Disconnect the gateway MQTT client", function(done) { 124 | deviceClient.on("error", done); 125 | deviceClient.on("close", function() { 126 | assert(deviceClient.isConnected() == false, "Device is still connected"); 127 | done(); 128 | }); 129 | deviceClient.disconnect(); 130 | }); 131 | 132 | step("Disconnect the application MQTT client", function(done) { 133 | appClient.on("error", done); 134 | appClient.on("close", function() { 135 | assert(appClient.isConnected() == false, "Application is still connected"); 136 | done(); 137 | }); 138 | appClient.disconnect(); 139 | }); 140 | 141 | after("Delete the test gateway & ensure the the MQTT clients are disconnected", function() { 142 | if (appClient != null && appClient.isConnected()) { 143 | appClient.disconnect(); 144 | } 145 | if (deviceClient != null && deviceClient.isConnected()) { 146 | deviceClient.disconnect(); 147 | } 148 | 149 | appClient.registry.unregisterDevice(testTypeId, testDeviceId); 150 | }); 151 | }); 152 | 153 | describe("Command Subscription", function() { 154 | let testDeviceId = uuidv4(); 155 | let testCommandId = "testCommand"; 156 | let testCommandFormat = "json"; 157 | let testCommandData = "{'foo': 'bar'}"; 158 | 159 | let appClient = null; 160 | let deviceConfig = null; 161 | let deviceClient = null; 162 | 163 | before("Register the test device", function(done){ 164 | this.timeout(10000); 165 | appClient = new ApplicationClient(appConfig); 166 | 167 | // Register the device type 168 | appClient.registry.registerDeviceType(testTypeId, null, null, null, "Gateway") 169 | .catch(function(err){ 170 | if (err.response.status != 409) { 171 | // Anything other than conflict is bad! 172 | throw err; 173 | } 174 | console.info("device type already exists, carry on!") 175 | }) 176 | .then(function(result) { 177 | // Register the device 178 | return appClient.registry.registerDevice(testTypeId, testDeviceId); 179 | }) 180 | .then(function(deviceDetails) { 181 | console.info(deviceDetails); 182 | let identity = {orgId: appConfig.getOrgId(), typeId: deviceDetails.typeId, deviceId: deviceDetails.deviceId}; 183 | let auth = {token: deviceDetails.authToken} 184 | let options = {logLevel: "info"}; 185 | deviceConfig = new GatewayConfig(identity, auth, options); 186 | deviceClient = new GatewayClient(deviceConfig); 187 | done(); 188 | }) 189 | .catch(function(err){ 190 | done(err); 191 | }); 192 | }) 193 | 194 | afterEach("Remove error listener(s)", function() { 195 | appClient.removeAllListeners("error"); 196 | deviceClient.removeAllListeners("error"); 197 | }); 198 | 199 | step("Connect application within 5 seconds", function(done){ 200 | this.timeout(5000); 201 | appClient.on("connect", done); 202 | appClient.on("error", done); 203 | appClient.connect(); 204 | }); 205 | 206 | step("Connect gateway within 5 seconds", function(done){ 207 | this.timeout(5000); 208 | deviceClient.on("connect", done); 209 | deviceClient.on("error", done); 210 | deviceClient.connect(); 211 | }); 212 | 213 | step("Subscribe to gateway command", function(done){ 214 | let onSubscribe = function(err, granted) { 215 | if (err != null) { 216 | done(err); 217 | } 218 | done(); 219 | } 220 | deviceClient.on("error", done); 221 | deviceClient.subscribeToCommands(testCommandId, testCommandFormat, testQos, onSubscribe); 222 | }); 223 | 224 | 225 | step("Publish & recieve gateway command within 10 seconds", function(done){ 226 | this.timeout(10000); 227 | let onCommandSent = function(err) { 228 | if (err != null) { 229 | done(err); 230 | } 231 | } 232 | 233 | let onCommandReceived = function(typeId, deviceId, commandId, format, payload) { 234 | assert(typeId == testTypeId, "Type ID does not match"); 235 | assert(deviceId == testDeviceId, "Device ID does not match"); 236 | assert(commandId == testCommandId, "Command ID does not match"); 237 | assert(format == testCommandFormat, "Format does not match"); 238 | assert(payload == testCommandData, "Payload does not match"); 239 | done(); 240 | } 241 | 242 | deviceClient.on("error", done); 243 | appClient.on("error", done); 244 | deviceClient.on("command", onCommandReceived); 245 | 246 | appClient.publishCommand(testTypeId, testDeviceId, testCommandId, testCommandFormat, testCommandData, testQos, onCommandSent); 247 | }); 248 | 249 | step("Disconnect the gateway MQTT client", function(done) { 250 | deviceClient.on("error", done); 251 | deviceClient.on("close", function() { 252 | assert(deviceClient.isConnected() == false, "Device is still connected"); 253 | done(); 254 | }); 255 | deviceClient.disconnect(); 256 | }); 257 | 258 | step("Disconnect the application MQTT client", function(done) { 259 | appClient.on("error", done); 260 | appClient.on("close", function() { 261 | assert(appClient.isConnected() == false, "Application is still connected"); 262 | done(); 263 | }); 264 | appClient.disconnect(); 265 | }); 266 | 267 | after("Delete the test gateway & ensure the the MQTT clients are disconnected", function() { 268 | if (appClient != null && appClient.isConnected()) { 269 | appClient.disconnect(); 270 | } 271 | if (deviceClient != null && deviceClient.isConnected()) { 272 | deviceClient.disconnect(); 273 | } 274 | 275 | appClient.registry.unregisterDevice(testTypeId, testDeviceId); 276 | }); 277 | }); 278 | 279 | }); 280 | -------------------------------------------------------------------------------- /test/GatewayConfig.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | ***************************************************************************** 3 | Copyright (c) 2014, 2019 IBM Corporation and other Contributors. 4 | All rights reserved. This program and the accompanying materials 5 | are made available under the terms of the Eclipse Public License v1.0 6 | which accompanies this distribution, and is available at 7 | http://www.eclipse.org/legal/epl-v10.html 8 | ***************************************************************************** 9 | * 10 | */ 11 | 12 | import { GatewayConfig } from '../src/gateway'; 13 | import { expect } from 'chai'; 14 | 15 | // Turn off console output 16 | console.info = () => {}; 17 | 18 | describe('WIoTP Gateway Configuration', () => { 19 | 20 | it('Initialise with the minimum required configuration', () => { 21 | let identity = {orgId: "orgid", typeId: "mytypeid", deviceId: "mydeviceid"}; 22 | let auth = {token: "test"}; 23 | let options = null; 24 | 25 | let config = new GatewayConfig(identity, auth, options); 26 | expect(config.getOrgId()).to.equal("orgid"); 27 | expect(config.getClientId()).to.contain("g:"); 28 | expect(config.identity.appId).to.not.be.null; // Should be a generated UUID 29 | expect(config.options.logLevel).to.equal("info"); 30 | expect(config.options.domain).to.equal("internetofthings.ibmcloud.com"); 31 | expect(config.options.mqtt.transport).to.equal("tcp"); 32 | expect(config.options.mqtt.port).to.equal(8883); 33 | }); 34 | 35 | }); 36 | -------------------------------------------------------------------------------- /test/StateClient.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | ***************************************************************************** 3 | Copyright (c) 2019 IBM Corporation and other Contributors. 4 | All rights reserved. This program and the accompanying materials 5 | are made available under the terms of the Eclipse Public License v1.0 6 | which accompanies this distribution, and is available at 7 | http://www.eclipse.org/legal/epl-v10.html 8 | ***************************************************************************** 9 | * 10 | */ 11 | import { ApplicationConfig, ApplicationClient } from '../src/application'; 12 | import { expect } from 'chai'; 13 | 14 | // Turn off console output 15 | console.info = () => {}; 16 | 17 | 18 | describe('WIoTP State Client Capabilities', function() { 19 | 20 | let stateClient = null; 21 | 22 | before("Initialize the application", function(){ 23 | const appConfig = ApplicationConfig.parseEnvVars(); 24 | const appClient = new ApplicationClient(appConfig); 25 | stateClient = appClient.state; 26 | stateClient.workWithDraft(); 27 | }); 28 | 29 | 30 | // TODO: Example scenarios - these should both: 31 | // 1. provide coverage of client function 32 | // 2. serve as useful, self-documenting reference material 33 | // e.g. 34 | //describe('Create and activate a simple model and verify resulting state', () => { 35 | //step('Create a device type', () => { 36 | //}) 37 | // create pi, ets, li, mappings, activate 38 | // send device event 39 | // verify state over HTTP 40 | // verify state notifications over MQTT 41 | // ... etc 42 | 43 | // ensure created resources: 44 | // 1. have globally-unique IDs (that will not conflict with those used by concurrently-running tests) 45 | // 2. are cleaned up, even in the event of test failure 46 | // 3. have some info associated with them to assist debugging - e.g. in name (if poss) or metadata 47 | //}); 48 | 49 | 50 | /* Self-contained simple unit tests */ 51 | describe('getDraftLogicalInterfaces', () => { 52 | 53 | it('should return a different page for different values of bookmark parameter', () => { 54 | return Promise.all([ 55 | stateClient.getLogicalInterfaces(0, 1), 56 | stateClient.getLogicalInterfaces(1, 1), 57 | ]) 58 | .then( (pages) => { 59 | const [page1, page2] = pages; 60 | 61 | // structural assertions common to both pages 62 | pages.forEach(page => { 63 | expect(page).to.have.property('bookmark'); 64 | expect(page).to.have.property('results'); 65 | expect(page).to.have.property('meta'); 66 | expect(page.meta).to.have.property('total_rows'); 67 | expect(page.meta.total_rows).to.be.at.least(2); 68 | expect(page.results).to.have.length(1); 69 | }); 70 | 71 | // and check the pages returned are actually different 72 | expect(page1.bookmark).to.equal('1'); 73 | expect(page2.bookmark).to.equal('2'); 74 | 75 | expect(page1.results[0].id).to.not.equal(page2.results[0].id); 76 | }) 77 | }); 78 | 79 | it(`should correctly honour name parameter`, () => { 80 | let theName; 81 | return stateClient.getLogicalInterfaces(0, 1) 82 | .then((page) => { 83 | expect(page).to.have.property('results'); 84 | expect(page.results).to.not.be.empty; 85 | theName = page.results[0].name; 86 | return stateClient.getLogicalInterfaces(0, 10, undefined, theName) 87 | }) 88 | .then(page => { 89 | expect(page).to.have.property('results'); 90 | expect(page.results).to.not.be.empty; 91 | expect(page.results.every(r => r.name.includes(theName))).to.be.true; 92 | }) 93 | 94 | }); 95 | 96 | 97 | // TODO: test other sortable fields (NOTE: some of which are not strings) 98 | [ 99 | [{name: false}], 100 | [{name: true}], 101 | 102 | [{description: false}], 103 | [{description: true}], 104 | 105 | [{name: false}, {description: false}], 106 | [{name: false}, {description: true}], 107 | 108 | [{description: false}, {name: true}], 109 | [{description: false}, {name: false}], 110 | 111 | ].forEach((sort) => { 112 | it(`should correctly honour sort parameters: ${JSON.stringify(sort)}`, () => { 113 | return stateClient.getLogicalInterfaces(0, 10, sort) 114 | .then((page) => { 115 | expect(page).to.have.property('results'); 116 | const actual = page.results; 117 | 118 | // sort results by name programatically here and check it aligns with expected results 119 | const expected = [...actual]; 120 | expected.sort( (a,b) => { 121 | 122 | for(let i=0; i `${a}, '${c.description}'(${c.id})`, '')) 148 | console.log(actual.reduce((a,c) => `${a}, '${c.description}'(${c.id})`, '')) 149 | */ 150 | 151 | expect(actual).to.deep.equal(expected); 152 | }); // then 153 | }); // it 154 | 155 | }); // forEach 156 | 157 | }); // describe('getDraftLogicalInterfaces' 158 | 159 | }); -------------------------------------------------------------------------------- /test/incorrectLogLevelTest.spec.yaml: -------------------------------------------------------------------------------- 1 | options: 2 | logLevel: notALogLevel 3 | --------------------------------------------------------------------------------