├── .gitignore ├── LICENSE ├── README.md ├── bin └── cdif ├── contributors.txt ├── framework.js ├── lib ├── cdif-device.js ├── cdif-interface.js ├── cdif-proxy-server.js ├── cdif-util.js ├── cli-options.js ├── connect.js ├── device-auth.js ├── device-db.js ├── device-manager.js ├── error.js ├── logger.js ├── module-manager.js ├── oauth │ └── oauth.js ├── route-manager.js ├── service.js ├── session.js ├── socket-server.js ├── subscriber.js ├── timeout.js ├── timer.js ├── validator.js ├── ws-server.js └── ws-subscriber.js ├── module-manager.js ├── modules.db ├── modules.json ├── package.json ├── spec ├── BasicDevice.json ├── BinaryLight.json ├── DimmableLight.json ├── SensorHub.json ├── onvif.json ├── schema.json ├── spec.json └── tools │ └── onvif │ ├── README.md │ ├── devicemgmt.json │ ├── devicemgmt.wsdl │ ├── media.json │ ├── media.wsdl │ ├── onvif.xsd │ ├── process.js │ ├── ptz.json │ ├── ptz.wsdl │ ├── readxml.js │ └── schema.json └── test ├── loop.sh ├── mocha.opts ├── socket.html ├── test1.js ├── test2.js └── test3.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .framework.js.swp 3 | device_addr.db 4 | node_modules/* 5 | npm-debug.log 6 | OZW_Log.txt 7 | device_model.db 8 | device_store.db 9 | core 10 | examples/ 11 | build/ 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2016 Miaobo Chen 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | CEAMS 2 | ----- 3 | CEAMS, the API management product based on CDIF framework now can be freely downloaded & installed from [our web site](https://www.apemesh.com/cn/download), or using below convenience script: 4 | 5 | ``` 6 | curl -sL https://www.apemesh.com/download/get-ceams.sh | sudo -E bash - 7 | ``` 8 | on Debian / Ubuntu or RHEL / CentOS based Linux OS. Please refer to the above download page for more details. 9 | 10 | Among many others, the product's highlighted features are: 11 | * Multi-thread support, so that each CDIF app runs in independent Node.js worker thread and V8 instance 12 | * Freely switching between traditional Node.js single-thread mode and new worker-thread mode for different usage scenarios (I/O or CPU intensive) 13 | * Service orchestration and high-perf message exchanging support for CDIF apps - You may access a service either from CDIF's HTTP interface or from another service using high-perf message channels between worker threads 14 | * Web based IDE and API spec / JSON schema editor for CDIF based apps 15 | * Auto-form based API testing tools based on schema form technology 16 | * Full NPM support for CDIF based apps - CDIF app may add dependency to any third-party npm packages 17 | * Web based API and data integration tool based on pure JSON data (No we don't like REST API design) transformations and flow designer, along with built-in lodash _ support 18 | 19 | You are welcome to download our product and take a trial. For any questions please contact us at: support@apemesh.com, or create an new issue on CDIF‘s Github repo. 20 | 21 | In the future this open-source repo would serve as concept proof, and help provide reference API specification which align with our product. 22 | 23 | 24 | Common device interconnect framework 25 | ------------------------------------ 26 | 27 | Common device interconnect framework (CDIF) is a web based connectivity framework. Its goal is to create a common description language in JSON format for all kinds of web services and IoT devices, and enable RPC communications in JSON texts to these entities over CDIF's RESTful interface. The common description language and RPC endpoint created by CDIF is equivalent to WSDL / SOAP but much lighter for use by the rich set of JS based web applications. CDIF brings SOA governance to REST APIs, and new scenarios such as REST API flow applications, IoT device rules engine, or a mashup of both, would be easier to be built on top of CDIF. 28 | 29 | The common description language created by CDIF is inspired by UPnP with integral support of JSON schema definition to complex type API arguments. It creates a SOA style description language in JSON format. CDIF's common description language organizes entities such as web services, and IoT smart devices, into abstracted entity called device. Each device would have a JSON document to describe their basic information, full capabilities and API interfaces. Following SOA design style, this JSON document contains definition to a list of interchangeable services, and each of the services contains a list of abstract API definitions and their contracts. Therefore, this JSON based description language may also be suitable for describing the interface of micro-service style architectures. 30 | 31 | CDIF would collect device information from the device driver modules which are managed by it, and take care the processes of device / service discovery, registration, management, and etc. Client applications of CDIF may retrieve this description language from CDIF's RESTful interface, analyze it to create client side model or UI forms. Then API calls made to web service or IoT smart devices, which are managed by CDIF, can be done through CDIF's RESTful interface in pure JSON text messages. With event subscription support, client may also receive event updates from smart device or web services from CDIF, and being able to creates bi-directional data channel for CDIF's client applications. For more information about this JSON based common description language, please refer to spec/ folder in the source repository. 32 | 33 | At the lower level, CDIF provides a set of uniformed device abstraction interface, and group different types of devices into device driver modules. Each module can manage one or more devices in same category, such as Bluetooth LE, ZWave, UPnP and etc. This design hides implementation details, such as different RESTful service design style (e.g. different HTTP methods with payloads in query strings, forms, ajax and etc), and IoT protocol details from the client side, so client applications may see uniform representations for all smart device or web services which are managed by CDIF. 34 | 35 | With this design, CDIF separates web service or IoT device's external interface from its native implementation, and vendor's non-standard, proprietary implementations may also be integrated into CDIF framework as modules, and present to client side this common description language. Client application doesn't have to integrate any specific language SDK in order to access arbitrary IoT devices or web services. However, to avoid the risk of unmanaged I/O which could be exposed by arbitrary implementations, proprietary implementation may need to implement their device modules as sub-modules to the standard protocol modules such as ```cdif-ble-manager```, and left all I/O being managed by it. 36 | 37 | 38 | Demo 39 | ---- 40 | A [demo app](https://github.com/out4b/react-schema-form) which is forked from [react-schema-form](https://github.com/networknt/react-schema-form) project shows an example of connect to a running CDIF instance, grab the common description language CDIF created for its managed REST services, auto-generate JSON schema based input forms on app UI, and invoke CDIF's REST API interface to return the API call data from it. 41 | 42 | 43 | CDIF's common description language in summary 44 | --------------------------------------------- 45 | { 46 | "configId": 1, 47 | "specVersion": { 48 | "major": 1, 49 | "minor": 0 50 | }, 51 | "device": { 52 | "deviceType": "urn:cdif-net:device::1", 53 | "friendlyName": "device name", 54 | "manufacturer": "manufacturer name", 55 | "manufacturerURL": "manufacturer URL", 56 | "modelDescription": "device model description", 57 | "modelName": "device model name", 58 | "modelNumber": "device model number", 59 | "serialNumber": "device serial number", 60 | "UPC": "universal product code", 61 | "userAuth": true | false, 62 | "powerIndex": power consumption index, 63 | "devicePresentation": true | false, 64 | "iconList": [ 65 | { 66 | "mimetype": "image/format", 67 | "width": "image width", 68 | "height": "image height", 69 | "depth": "color depth", 70 | "url": "image URL" 71 | } 72 | ], 73 | "serviceList": { 74 | "urn:cdif-net:serviceID:": { 75 | "serviceType": "urn:cdif-net:service::1", 76 | "actionList": { 77 | "": { 78 | "argumentList": { 79 | "": { 80 | "direction": "in | out", 81 | "retval": false | true, 82 | "relatedStateVariable": "" 83 | } 84 | } 85 | } 86 | }, 87 | "serviceStateTable": { 88 | "": { 89 | "sendEvents": true | false, 90 | "dataType": "", 91 | "allowedValueRange": { 92 | "minimum": "", 93 | "maximum": "", 94 | "step": "" 95 | }, 96 | "defaultValue": "" 97 | } 98 | } 99 | } 100 | }, 101 | "deviceList": [ 102 | "device": { 103 | "" 104 | } 105 | ] 106 | } 107 | } 108 | 109 | In original UPnP's definitions, once device discovery is done, the returned device model would present services as URLs, and it requires additional service discovery step to resolve the full service models. Unlike this, CDIF's common description language tries to put all service models together inside device model object to present the full capabilities of a device. And the service discovery process for each protocol, if exists, is assumed to be conducted by the underlying stack and can be startedfrom CDIF's ```connect``` framework API interface. This design hopes to simplify client code, and also to be better compatible with protocols, or vendor's proprietary implementations which have no service discovery concept. In addition, elements such as services, arguments, state variables in CDIF's common description language are indexed by their keys for easier addressing. 110 | 111 | Due to the design of underlying network protocols such as Z-Wave, it could take hours for the device to report its full capabilities. In this case, framework would progressively update device models to reflect any new capabilities reported from the network. To uncover these new device capabilities, client may need to refresh device's model by invoking CDIF's ```get-spec``` RESTful API interface at different times. please refer to [cdif-openzwave](https://github.com/out4b/cdif-openzwave) for more information on this. 112 | 113 | Features 114 | -------- 115 | This framework now provides basic support to below connectivity protocols or web service APIs: 116 | * [Bluetooth Low Energy](https://github.com/out4b/cdif-ble-manager) 117 | * [ONVIF Profile S camera](https://github.com/out4b/cdif-onvif-manager) 118 | * [Z-Wave](https://github.com/out4b/cdif-openzwave) 119 | * [OAuth based web service APIs](https://github.com/out4b/cdif-oauth-manager) 120 | * [PayPal Payment and payouts APIs](https://github.com/out4b/cdif-paypal) 121 | 122 | We added OAuth supported to CDIF because we believe the future of smart home should seamlessly integrate smart hardware with various kinds of web services to create much more powerful and useful scenarios with the rules engines and data analytics technologies built with CDIF. These scenarios could be: 123 | * Collect data from all BLE based health sensor and send to health service for further analysis and report 124 | * Automatically create new buying order when food is emptied in a fridge, or batteries run out in smart devices. 125 | * Automatically test newly-purchased smart device and call return service immediately if failed 126 | * Provide food expiration notification 127 | * Motion sensor and cameras connected to common security service or send warning to social contacts 128 | * A bread machine which has very basic cooking features but can download infinite new menus from web 129 | * And many more future imagination spaces 130 | 131 | For now the CDIF device models created for all above protocols or web services are considered third party extensions to CDIF's own device vocabularies, and thus taking their own URN namespace within ```deviceType``` or ```serviceType``` keywords. We didn't define CDIF's own vocabularies because it is not very helpful at this moment. If there is such need in the future, more information such as normalized error codes should be annotated to the formal specifications. 132 | 133 | How to run 134 | ---------- 135 | ```sh 136 | npm install -g cdif 137 | cdif 138 | ``` 139 | If the above npm install encounters any error, add ```--unsafe-perm``` option to workaround bcrypt install permission issue 140 | 141 | Command line options 142 | -------------------- 143 | * --debug Enable debug option. In this case morgan logging on console would be activated. 144 | * --dbPath Specify the device database location. If not specified, CDIF will read the local one under its install directory. Note that should not start with tilde symbol, CDIF won't expand it. CDIF uses a small device database to persistent device's UUID across framework restart. The device UUID is generated and persistent into a SQLite database, it is mapped to the device's virtual hardware address, which is specified by device object itself 145 | * --allowDiscover Enable discover route, if not specified, cdif would automatically call module's discover interface on startup 146 | * --heapDump Enable heap profiling 147 | * --wsServer Create WebSocket server on startup 148 | * --sioServer Create Socket.IO server on startup 149 | * --loadModule Load a local device driver module from . If this option is enabled, all device database access will be disabled 150 | 151 | Summary of framework API interface: 152 | ----------------------------------- 153 | 154 | ##### Discover all devices 155 | Start discovery process for all modules 156 | 157 | POST http://server_host_name:3049/discover 158 | response: 200 OK 159 | 160 | ##### Stop all discoveries 161 | Stop all discovery processes 162 | 163 | POST http://server_host_name:3049/stop-discover 164 | response: 200 OK 165 | 166 | ##### Get device list 167 | Retrieve uuid of all discovered devices. To improve security, this API won't expose the services provided by the discovered devices. The full description would be available from get-spec interface after client successfully connect to the device, which may need to provide valid JWT token if this device requires authentication. 168 | 169 | GET http://server_host_name:3049/device-list 170 | request body: empty 171 | response: 172 | { 173 | device-uuid1: {...}, device-uuid2: {...} 174 | } 175 | response: 200 OK 176 | 177 | ##### Connect to device: 178 | Connect to a single device. If a device requires auth (userAuth flag set to true in device description), username / password pair needs to be contained in the request body in JSON format. And in this case, a JWT token would be returned in the response body indexed by ```device_access_token```. Client would need to provide this token in request body for subsequent device access. In another case, if OAuth authorization needs to be applied to the device object, userAuth flag would be set to true in device description and this API must be called by client to start the OAuth authorization flow. 179 | 180 | This API call is optional if a device does not require auth (userAuth flag not set in device description). However underlying IoT protocol module may rely on this call to start the service discovery process. 181 | 182 | POST http://server_host_name:3049/devices//connect 183 | (optional) request body: 184 | { 185 | "username": , 186 | "password": 187 | } 188 | response: 200 OK / 500 Internal error 189 | (optional) response body: 190 | { 191 | "device_access_token": 192 | } 193 | 194 | In order to handle OAuth authorization flow, a url redirect object may be returned from connect API call in following format: 195 | 196 | ``` {"url_redirect":{"href":"https://api.example.com","method":"GET"}} ``` 197 | 198 | Client of CDIF may need to follow this URL to complete the OAuth authentication flow 199 | 200 | ##### Disconnect device: 201 | Disconnect a single device, only successful if device is connected. This API call is optional if a device does not require auth (userAuth flag not set in device description). 202 | 203 | POST http://server_host_name:3049/devices//disconnect 204 | (optional) request body: 205 | { 206 | "device_access_token": 207 | } 208 | response: 200 OK / 500 Internal error 209 | 210 | ##### Get spec of a single device: 211 | Retrieve the spec of a single device, only successful if device is connected 212 | 213 | GET http://server_host_name:3049/devices//get-spec 214 | (optional) request body: 215 | { 216 | "device_access_token": 217 | } 218 | response: 200 OK / 500 Internal error 219 | response body: JSON format of the device spec 220 | 221 | ##### Get current state of a service: 222 | Get current state of a service, only successful if device is connected 223 | Client may use this call to initialize or refresh its device model without calling into device modules 224 | 225 | GET http://server_host_name:3049/devices//get-state 226 | (optional) request body: 227 | { 228 | "serviceID": , 229 | (optional) 230 | "device_access_token": 231 | } 232 | response: 200 OK / 500 Internal error 233 | response body: JSON format containing the current state data in the service object 234 | 235 | ##### Device control 236 | Invoke a device control action, only successful if device is connected 237 | 238 | POST http://server_host_name:3049/devices//invoke-action 239 | request boy: 240 | { 241 | serviceID: , 242 | actionName: , 243 | argumentList: { 244 | : , 245 | : 246 | (optional) 247 | "device_access_token": 248 | } 249 | response: 200 OK / 500 internal error 250 | response body: 251 | { 252 | : , 253 | : 254 | } 255 | Argument names must conform to the device spec that sent to client 256 | 257 | ##### Device module install 258 | Install a device module. CDIF fork and exec npm to complete this request. So user might need to run ```npm login``` before launching CDIF. 259 | 260 | POST http://server_host_name:3049/install-module 261 | request boy: 262 | { 263 | (optional) 264 | registry: "http://registry.apemesh.com:5984/", 265 | name: "module name", 266 | version: "module version" 267 | } 268 | response: 200 OK / 500 internal error 269 | 270 | ##### Device module uninstall 271 | Uninstall a device module 272 | 273 | POST http://server_host_name:3049/uninstall-module 274 | request boy: 275 | { 276 | name: "module name" 277 | } 278 | response: 200 OK / 500 internal error 279 | 280 | ##### Errors 281 | For now the above framework API interface would uniformly return 500 internal error if any error occurs. The error information is contained in response body with below JSON format: 282 | {"topic": error class, "message": error message, "fault": faultObject} 283 | 284 | The optional fault object in the error information is set by device driver code to carry back detail information about the error itself. 285 | 286 | Examples 287 | -------- 288 | 289 | To discover, connect, and read sensor value from TI SensorTag CC2650: 290 | ``` 291 | curl -H "Content-Type: application/json" -X POST http://localhost:3049/discover 292 | curl -H "Content-Type: application/json" -X GET http://localhost:3049/device-list 293 | curl -H "Content-Type: application/json" -X POST http://localhost:3049/stop-discover 294 | curl -H "Content-Type: application/json" -X POST http://localhost:3049/devices/a540d490-c3ab-4a46-98a9-c4a0f074f4d7/connect 295 | curl -H "Content-Type: application/json" -X POST -d '{"serviceID":"urn:cdif-net:serviceID:Illuminance","actionName":"getIlluminanceData","argumentList":{"illuminance":0}} ' http://localhost:3049/devices/a540d490-c3ab-4a46-98a9-c4a0f074f4d7/invoke-action 296 | curl -H "Content-Type: application/json" -X GET http://localhost:3049/devices/a540d490-c3ab-4a46-98a9-c4a0f074f4d7/get-spec 297 | ``` 298 | 299 | To connect to, and issue a PTZ absoluteMove action call to ONVIF camera device: 300 | ``` 301 | curl -H "Content-Type: application/json" -X POST -d '{"username": "admin", "password": "test"}' http://localhost:3049/devices/b7f65ae1-1897-4f52-b1b7-9d5ecd0dd71e/connect 302 | 303 | device access token will be returned in following format: 304 | {"device_access_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaWF0IjoxNDUxMTQwODMwLCJleHAiOjE0NTIyMjA4MzB9.JuYbGpWMhAA7OBr5GtE2_7cZMzKJGDorO8SrVRuU_k8"} 305 | 306 | curl -H "Content-Type: application/json" -X POST -d '{"serviceID":"urn:cdif-net:serviceID:ONVIFPTZService","actionName":"absoluteMove","argumentList":{"options":{"x":-1,"y":-1,"zoom":1,"speed":{"x":0.1,"y":0.1}}},"device_access_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaWF0IjoxNDUwMTQ1Njg3LCJleHAiOjE0NTEyMjU2ODd9.qwPcivmv-Oa-300LIi7eMCQUr9ha5OCZeB04eM0oaUc"}' http://localhost:3049/devices/b7f65ae1-1897-4f52-b1b7-9d5ecd0dd71e/invoke-action 307 | ``` 308 | 309 | To connect to, and get the latest Twitter user timeline from Twitter virtual device: 310 | ``` 311 | curl -H "Content-Type: application/json" -X POST http://localhost:3049/devices/a9878d3e-4a6b-481b-b848-37c6a4c7b901/connect 312 | 313 | (after user completed OAuth authentication flow) 314 | 315 | curl -H "Content-Type: application/json" -X POST -d '{"serviceID":"urn:twitter-com:serviceID:Statuses","actionName":"getUserTimeline", "argumentList":{"options":{"count":1}, "userTimeline":{}}}' http://localhost:3049/devices/a9878d3e-4a6b-481b-b848-37c6a4c7b901/invoke-action 316 | ``` 317 | 318 | To create and execute a PayPal payment: 319 | ``` 320 | curl -H "Content-Type: application/json" -X POST -d '{"serviceID": "urn:paypal-com:serviceID:payment","actionName":"createWithPayPal", "argumentList":{"config":{"mode":"sandbox","client_id":"client_id","client_secret":"client_secret"},"intent":"authorize","payer":{"payment_method":"paypal"},"redirect_urls":{"return_url":"http://return.url","cancel_url":"http://cancel.url"},"transactions":[{"item_list":{"items":[{"name":"item","sku":"item","price":"19.00","currency":"USD","quantity":1}]},"amount":{"currency":"USD","total":"19.00"},"description":"This is the payment description."}], "result":{}}}' http://localhost:3049/devices/9d0b29bd-b25f-4632-9a1a-d62e85d3ad4f/invoke-action 321 | 322 | (after user login to PayPal authorization page and authorized this payment, paymentID and payer_id will be carried back in query string parameters on the return URL) 323 | 324 | curl -H "Content-Type: application/json" -X POST -d '{"serviceID": "urn:paypal-com:serviceID:payment","actionName":"execute", "argumentList":{"config":{"mode":"sandbox","client_id":"client_id","client_secret":"client_secret"},"paymentID":"paymentID", "executeArgs":{"payer_id":"payer_id", "transactions":[{"amount":{"currency":"USD","total":"19.00"}}]}, "result":{}}}' http://localhost:3049/devices/9d0b29bd-b25f-4632-9a1a-d62e85d3ad4f/invoke-action 325 | ``` 326 | 327 | Data types and validation 328 | ------------------------- 329 | Various kinds of protocols or IoT devices profiles would usually define their own set of data types to communicate and exchange data with devices. For example, Bluetooth LE GATT profile would define 40-bit integer type characteristics, and in ONVIF most of arguments to SOAP calls are complex types with multiple nesting level, mandatory or optional fields in each data object. Since data integrity is vital to system security, validation needs to be enforced on each device data communication, including action calls and event notifications. However, clients would still hope to have a simple enough representation to describe all different data types that could be exposed by devices. 330 | 331 | The Original UPnP specification has defined a rich set of primitive types for its state variables, which we map to characteristics or values in other IoT protocols, and also defined keywords such as ```allowedValueRange``` / ```allowedValueList``` to aid data validations. However unfortunately, these are still not sufficient to describe the complex-typed data as defined in other standards. Therefore, to provide a complete solution for data typing and validations would be a real challenge. 332 | 333 | Considering these facts, CDIF would take following approaches trying to offer a common solution for data typing and validations: 334 | 335 | * Data would be considered to be either in primitive or complex types 336 | * ```dataType``` keyword inside state variable's definition would be used to describe its type 337 | * CDIF would follow JSON specification and only defines these primitive types: ```boolean```, ```integer```, ```number```, and ```string``` 338 | * Device modules managed by CDIF is responsible for mapping above primitive type to their native types if needed. 339 | * CDIF's common description language would still utilize ```allowedValueRange``` / ```allowedValueList``` keywords for primitive type data, if any of these keywords are defined. 340 | * If a device exposes any complex-typed variable, it is required to provide a root schema document object containing all schema definitions to its variables. 341 | * For complex types variables, they uniformly takes ```object``` type. The actual ```object``` type variable data can be either a JSON ```array``` or ```object```. 342 | * If a state variable is in ```object``` tpye, a ```schema``` keyword must be annotated to the state variable definition. And its value would be used for validation purpose. 343 | * The value of ```schema``` keyword refer to the formal [JSON schema](http://json-schema.org/) definition to this data object. This value is a [JSON pointer](https://tools.ietf.org/html/rfc6901) refers to the variable's sub-schema definition inside device's root schema document. Authenticated clients, such as client web apps or third party web services may also retrieve the sub-schema definitions associated with this reference through CDIF's RESTful interface and do proper validations if needed. In this case, the device's root schema definitions, and variables' sub-schemas which are defined by ```schema``` keyword can be retrieved from below URL: 344 | ``` 345 | http://server_host_name:3049/devices//schema 346 | ``` 347 | * CDIF would internally dereference the schema definitions associated with this pointer, as either defined by CDIF or its submodules, and do data validations upon action calls or event notifications. 348 | 349 | CDIF and its [cdif-onvif-manager](https://github.com/out4b/cdif-onvif-manager) implementation contains an example of providing schema definitions, and do data validations to complex-typed arguments to ONVIF camera's PTZ action calls. For example, ONVIF PTZ ```absoluteMove``` action call through CDIF's API interface defines its argument with ```object``` type, and value of its ```schema``` keyword would be ```/onvif/ptz/AbsoluteMoveArg```, which is a JSON pointer refering to the sub-schema definitions inside ONVIF device's root schema document. In this case, the fully resolved sub-schema (with no ```$ref``` keyword inside) can be retrieved from this URL: 350 | ``` 351 | http://server_host_name:3049/devices//schema/onvif/ptz/AbsoluteMoveArg 352 | ``` 353 | 354 | Upon a ```absoluteMove``` action call, CDIF would internally dereference the sub-schema associated with this pointer, and validate the input data and output result based on those sub-schema definitions. 355 | 356 | Unlike many of other API modelling language such as WSDL or others, CDIF separates API argument's schema definitions from the its common description language. This design may have following benefits: 357 | * Simplify client application design such that applications won't need to explicitly extract schema info from the common description language 358 | * Saving network bandwidth that client do not need to retrieve the full schema document when access only one API 359 | * The description language stored in http cache won't be invalidated when existing API contract requires a update 360 | 361 | Eventing 362 | -------- 363 | CDIF implemented a simple [socket.io](socket.io) based server to provide pub / sub method of eventing service. For now CDIF chooses socket.io as the eventing interface because its pub / sub based room API simplified our design. CDIF try not to invent its own pub / sub API but try to follow standardized technologies as much as possible. If there is such requirement in the future, we may extend this interface to support more pub / sub protocols such MQTT, AMQP and etc, given we know how to appropriately apply security means to them. 364 | 365 | Event subscriptions in CDIF are service based, which means clients have to subscribe to a specific service ID. If any of the variable state managed by the service are updated, e.g. a sensor value change, or a light bulb is switched on / off, client would receive event updates from CDIF. CDIF would cache device states upon successful action calls, thus devices doesn't have to send event data packets to be able to notify their state updates. This would improve the usage model of eventing feature, but also leads to a result that, the ```sendEvents``` property of a state variable in CDIF's common description language would have less significance, because in theory, all state variables can be evented if they can be written by any connected client. However, CDIF would still respect this property, and if it is set to false by the device drivers, clients are not able to receive any event updates from it. 366 | 367 | Users may refer to [test/socket.html](https://github.com/out4b/cdif/blob/master/test/socket.html) for a very simple use case on CDIF's eventing interface. 368 | 369 | Device presentation 370 | ------------------- 371 | Some kinds of IoT devices, such as IP cameras, may have their own device presentation URL for configuration and management purpose. To support this kind of usage, CDIF implemented a reverse proxy server to help redirect HTTP traffics to this URL. By doing this, the actual device presentation URL would be hidden from external network to help improve security. If the device has a presentation URL, its device description would have "devicePresentation" flag set to true. After the device is successfully connected through CDIF's connect API, its presentation URL is mounted on CDIF's RESTful interface and can be uniformly accessed from below URL: 372 | ``` 373 | http://server_host_name:3049/devices//presentation 374 | ``` 375 | 376 | For now only ONVIF devices support this kind of usage. But this concept should be extensible to any device or manufacturer modules who want to host their own presentation page, given they implemented the internal getDeviceRootUrl() interface which returns the reverse proxy server's root URL. Please refer to [cdif-onvif-manager](https://github.com/out4b/cdif-onvif-manager) module for more information. 377 | 378 | Notes 379 | ----- 380 | Due to the dependencies to native bindings of the underlying network stacks, CDIF now only support node v0.10.x. Currently it is only tested on Ubuntu 14.x system. If you encountered a problem on Mac or other system, please kindly report the issue [here](https://github.com/out4b/cdif/issues). 381 | 382 | Test 383 | ---- 384 | Open a console and run below command: 385 | ```sh 386 | cd cdif 387 | npm test 388 | ``` 389 | The above command will discover, connect all available devices, and then invoke *every* action exposed by its device description. 390 | 391 | ### Acknowlegement 392 | Many thanks to the work contributed by following repositories that made this framework implementation possible: 393 | 394 | * [noble-device](https://github.com/sandeepmistry/noble-device), [yeelight-blue](https://github.com/sandeepmistry/node-yeelight-blue), and [node-sensortag](https://github.com/sandeepmistry/node-sensortag) 395 | * [onvif](https://github.com/agsh/onvif) 396 | * [openzwave-shared](https://www.npmjs.com/package/openzwave-shared) 397 | -------------------------------------------------------------------------------- /bin/cdif: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | basedir=`dirname "$0"` 3 | path=`readlink -f "$0"` 4 | basepath=`dirname $path` 5 | 6 | which bunyan 7 | OUT=$? 8 | 9 | if [ $OUT -eq 0 ];then 10 | NODE_PATH=$basepath/../lib node --expose-gc "$basepath/../framework.js" "$@" | bunyan 11 | else 12 | NODE_PATH=$basepath/../lib node --expose-gc "$basepath/../framework.js" "$@" 13 | fi 14 | -------------------------------------------------------------------------------- /contributors.txt: -------------------------------------------------------------------------------- 1 | out4b 2 | -------------------------------------------------------------------------------- /framework.js: -------------------------------------------------------------------------------- 1 | var ModuleManager = require('./lib/module-manager'); 2 | var RouteManager = require('./lib/route-manager'); 3 | var argv = require('minimist')(process.argv.slice(1)); 4 | var options = require('./lib/cli-options'); 5 | var logger = require('./lib/logger'); 6 | var deviceDB = require('./lib/device-db'); 7 | 8 | global.CdifUtil = require('./lib/cdif-util'); 9 | global.CdifDevice = require('./lib/cdif-device'); 10 | 11 | logger.createLogger(); 12 | options.setOptions(argv); 13 | deviceDB.init(); 14 | 15 | var mm = new ModuleManager(); 16 | var routeManager = new RouteManager(mm); 17 | 18 | 19 | routeManager.installRoutes(); 20 | mm.loadAllModules(); 21 | 22 | 23 | // forever to restart on crash? 24 | -------------------------------------------------------------------------------- /lib/cdif-device.js: -------------------------------------------------------------------------------- 1 | var events = require('events'); 2 | var util = require('util'); 3 | var url = require('url'); 4 | var parser = require('json-schema-ref-parser'); 5 | var Service = require('./service'); 6 | var ConnMan = require('./connect'); 7 | var validator = require('./validator'); 8 | var logger = require('./logger'); 9 | var CdifError = require('./error').CdifError; 10 | var DeviceError = require('./error').DeviceError; 11 | 12 | //warn: try not add event listeners in this class 13 | function CdifDevice(spec) { 14 | this.deviceID = ''; 15 | this.user = ''; 16 | this.secret = ''; 17 | this.connectionState = 'disconnected'; // enum of disconnected, connected, & redirecting 18 | this.connMan = new ConnMan(this); 19 | this.schemaDoc = this.getDeviceRootSchema(); 20 | 21 | this.spec = spec; 22 | this.initServices(); 23 | 24 | this.getDeviceSpec = this.getDeviceSpec.bind(this); 25 | this.connect = this.connect.bind(this); 26 | this.disconnect = this.disconnect.bind(this); 27 | this.getHWAddress = this.getHWAddress.bind(this); 28 | this.deviceControl = this.deviceControl.bind(this); 29 | this.subscribeDeviceEvent = this.subscribeDeviceEvent.bind(this); 30 | this.unSubscribeDeviceEvent = this.unSubscribeDeviceEvent.bind(this); 31 | } 32 | 33 | util.inherits(CdifDevice, events.EventEmitter); 34 | 35 | 36 | CdifDevice.prototype.setAction = function(serviceID, actionName, action) { 37 | if (action === null || typeof(action) !== 'function') { 38 | return logger.error('set incorrect action type for: ' + serviceID + ' ' + actionName); 39 | } 40 | 41 | var service = this.services[serviceID]; 42 | if (service) { 43 | service.actions[actionName].invoke = action.bind(this); 44 | } else { 45 | logger.error('cannot set action: ' + serviceID + ' ' + actionName); 46 | } 47 | }; 48 | 49 | CdifDevice.prototype.initServices = function() { 50 | if (typeof(this.spec) !== 'object' || this.spec.device == null || this.spec.device.serviceList == null) { 51 | return logger.error('no valid device spec: ' + this.constructor.name); 52 | } 53 | 54 | var serviceList = this.spec.device.serviceList; 55 | 56 | if (!this.services) { 57 | this.services = new Object(); 58 | } 59 | for (var i in serviceList) { 60 | var service_spec = serviceList[i]; 61 | if (!this.services[i]) { 62 | this.services[i] = new Service(this, i, service_spec); 63 | } else { 64 | this.services[i].updateSpec(service_spec); 65 | } 66 | } 67 | }; 68 | 69 | CdifDevice.prototype.getDeviceSpec = function(callback) { 70 | if (this.spec === null) { 71 | return callback(new CdifError('cannot get device spec'), null); 72 | } 73 | callback(null, this.spec); 74 | }; 75 | 76 | CdifDevice.prototype.getServiceStates = function(serviceID, callback) { 77 | if (callback && typeof(callback) !== 'function') { 78 | return logger.error('getServiceStates failed, not valid callback'); 79 | } 80 | var service = this.services[serviceID]; 81 | if (service == null) { 82 | if (callback && typeof(callback) === 'function') { 83 | return callback(new CdifError('service not found: ' + serviceID), null); 84 | } else { 85 | return logger.error('getServiceStates failed, service not found: ' + serviceID); 86 | } 87 | } 88 | 89 | service.getServiceStates(callback); 90 | }; 91 | 92 | CdifDevice.prototype.setServiceStates = function(serviceID, values, callback) { 93 | if (callback == null || typeof(callback) !== 'function') { 94 | return logger.error('setServiceStates failed, no valid callback'); 95 | } 96 | var service = this.services[serviceID]; 97 | if (service == null) { 98 | return callback(new CdifError('service not found: ' + serviceID)); 99 | } 100 | 101 | service.setServiceStates(values, callback); 102 | }; 103 | 104 | // now support only one user / pass pair 105 | // TODO: check if no other case than oauth redirect flow needs to temporarily unset connected flag 106 | CdifDevice.prototype.connect = function(user, pass, callback) { 107 | if (this.connectionState === 'redirecting') { 108 | return callback(new CdifError('device in action'), null, null); 109 | } 110 | 111 | if (this.connectionState === 'connected') { 112 | return this.connMan.verifyConnect(user, pass, callback); 113 | } 114 | return this.connMan.processConnect(user, pass, callback); 115 | }; 116 | 117 | CdifDevice.prototype.disconnect = function(callback) { 118 | return this.connMan.processDisconnect(callback); 119 | }; 120 | 121 | CdifDevice.prototype.getHWAddress = function(callback) { 122 | if (this._getHWAddress && typeof(this._getHWAddress) === 'function') { 123 | this._getHWAddress(function(error, data) { 124 | if (error) { 125 | return callback(new DeviceError('get hardware address fail: ' + error.message), null); 126 | } 127 | callback(null, data); 128 | }); 129 | } else { 130 | callback(null, null); 131 | } 132 | }; 133 | 134 | CdifDevice.prototype.deviceControl = function(serviceID, actionName, args, callback) { 135 | var service = this.services[serviceID]; 136 | if (service == null) { 137 | return callback(new CdifError('service not found: ' + serviceID), null); 138 | } 139 | service.invokeAction(actionName, args, callback); 140 | }; 141 | 142 | CdifDevice.prototype.updateDeviceSpec = function(newSpec) { 143 | validator.validateDeviceSpec(newSpec, function(error) { 144 | if (error) { 145 | return logger.error(error.message + ', device spec: ' + JSON.stringify(newSpec)); 146 | } 147 | this.spec = newSpec; 148 | this.initServices(); 149 | }.bind(this)); 150 | }; 151 | 152 | CdifDevice.prototype.setEventSubscription = function(serviceID, subscribe, unsubscribe) { 153 | if (typeof(subscribe) !== 'function' || typeof(unsubscribe) !== 'function') { 154 | return logger.error('type error for event subscribers'); 155 | } 156 | var service = this.services[serviceID]; 157 | if (service) { 158 | service.setEventSubscription(subscribe.bind(this), unsubscribe.bind(this)); 159 | } else { 160 | logger.error('cannot set subscriber for: ' + serviceID); 161 | } 162 | }; 163 | 164 | CdifDevice.prototype.subscribeDeviceEvent = function(subscriber, serviceID, callback) { 165 | var service = this.services[serviceID]; 166 | if (service == null) { 167 | return callback(new CdifError('cannot subscribe to unknown serviceID: ' + serviceID)); 168 | } 169 | 170 | service.subscribeEvent(subscriber.onChange, function(err) { 171 | if (!err) { 172 | service.addListener('serviceevent', subscriber.publish); 173 | } 174 | callback(err); 175 | }); 176 | }; 177 | 178 | CdifDevice.prototype.unSubscribeDeviceEvent = function(subscriber, serviceID, callback) { 179 | var service = this.services[serviceID]; 180 | if (service == null) { 181 | return callback(new CdifError('cannot unsubscribe from unknown serviceID: ' + serviceID)); 182 | } 183 | 184 | service.removeListener('serviceevent', subscriber.publish); 185 | if (service.listeners('serviceevent').length === 0) { 186 | service.unsubscribeEvent(callback); 187 | } else { 188 | callback(null); 189 | } 190 | }; 191 | 192 | // get device root url string 193 | CdifDevice.prototype.getDeviceRootUrl = function(callback) { 194 | if (this.spec.device.devicePresentation !== true || typeof(this._getDeviceRootUrl) !== 'function') { 195 | return callback(new DeviceError('this device do not support presentation'), null); 196 | } 197 | this._getDeviceRootUrl(function(err, data) { 198 | if (err) { 199 | return callback(new DeviceError('get device root url failed: ' + err.message), null); 200 | } 201 | try { 202 | url.parse(data); 203 | } catch(e) { 204 | return callback(new DeviceError('device root url parse failed: ' + e.message), null); 205 | } 206 | callback(null, data); 207 | }); 208 | }; 209 | 210 | // get device root schema document object, must be sync 211 | CdifDevice.prototype.getDeviceRootSchema = function() { 212 | if (typeof(this._getDeviceRootSchema) !== 'function') return null; 213 | try { 214 | return this._getDeviceRootSchema(); 215 | } catch (e) { 216 | logger.error(e); 217 | return null; 218 | } 219 | }; 220 | 221 | // resolve JSON pointer based schema ref and return the schema object associated with it 222 | // For now we only support single doc schema to avoid security risks when resolving external refs 223 | CdifDevice.prototype.resolveSchemaFromPath = function(path, self, callback) { 224 | var schemaDoc = this.schemaDoc; 225 | if (schemaDoc == null || typeof(schemaDoc) !== 'object') { 226 | return callback(new DeviceError('device has no schema doc'), self, null); 227 | } 228 | if (path === '/') { 229 | return callback(null, self, schemaDoc); 230 | } 231 | 232 | var doc = null; 233 | try { 234 | doc = JSON.parse(JSON.stringify(schemaDoc)); 235 | } catch(e) { 236 | return callback(new DeviceError('invalid schema doc: ' + e.message), self, null); 237 | } 238 | 239 | var ref; 240 | 241 | // for now we dont support fragment based pointer 242 | // because it won't be able to be resolved 243 | if (/^\/./.test(path) === false) { 244 | return callback(new CdifError('path is not a valid pointer'), self, null); 245 | } 246 | 247 | ref = '#' + path; 248 | 249 | doc.__ = { 250 | "$ref": ref 251 | }; 252 | 253 | parser.dereference(doc, {$refs: {external: false}}, function(err, out) { 254 | if (err) { 255 | return callback(new CdifError('pointer dereference fail: ' + err.message), self, null); 256 | } 257 | callback(null, self, out.__); 258 | }); 259 | }; 260 | 261 | CdifDevice.prototype.setOAuthAccessToken = function(params, callback) { 262 | if (typeof(this._setOAuthAccessToken) === 'function') { 263 | this._setOAuthAccessToken(params, function(err) { 264 | if (err) { 265 | return callback(new CdifError(err.message)); 266 | } 267 | this.connectionState = 'connected'; 268 | callback(null); 269 | }.bind(this)); 270 | } else { 271 | callback(new CdifError('cannot set device oauth access token: no available device interface')); 272 | } 273 | }; 274 | 275 | module.exports = CdifDevice; 276 | -------------------------------------------------------------------------------- /lib/cdif-interface.js: -------------------------------------------------------------------------------- 1 | var events = require('events'); 2 | var util = require('util'); 3 | 4 | var options = require('./cli-options'); 5 | var DeviceManager = require('./device-manager'); 6 | var logger = require('./logger'); 7 | var CdifError = require('./error').CdifError; 8 | var DeviceError = require('./error').DeviceError; 9 | 10 | function CdifInterface(mm) { 11 | this.deviceManager = new DeviceManager(mm); 12 | 13 | if (options.heapDump === true) { 14 | setInterval(function() { 15 | global.gc(); 16 | logger.info('heap used: ' + process.memoryUsage().heapUsed); 17 | // heapdump.writeSnapshot('./' + Date.now() + '.heapsnapshot'); 18 | }, 1 * 60 * 1000); 19 | } 20 | 21 | this.deviceManager.on('presentation', this.onDevicePresentation.bind(this)); 22 | } 23 | 24 | util.inherits(CdifInterface, events.EventEmitter); 25 | 26 | CdifInterface.prototype.discoverAll = function(session) { 27 | this.deviceManager.emit('discoverall', session.callback); 28 | }; 29 | 30 | CdifInterface.prototype.stopDiscoverAll = function(session) { 31 | this.deviceManager.emit('stopdiscoverall', session.callback); 32 | }; 33 | 34 | CdifInterface.prototype.getDiscoveredDeviceList = function(session) { 35 | this.deviceManager.emit('devicelist', session.callback); 36 | }; 37 | 38 | CdifInterface.prototype.connectDevice = function(deviceID, user, pass, session) { 39 | var _this = this; 40 | var cdifDevice = this.deviceManager.deviceMap[deviceID]; 41 | if (cdifDevice == null) { 42 | return session.callback(new CdifError('device not found: ' + deviceID)); 43 | } 44 | 45 | if (cdifDevice.module.discoverState === 'discovering') { 46 | return session.callback(new CdifError('in discovering', null)); 47 | } 48 | 49 | this.deviceManager.emit('connect', cdifDevice, user, pass, session); 50 | }; 51 | 52 | CdifInterface.prototype.disconnectDevice = function(deviceID, token, session) { 53 | var cdifDevice = this.deviceManager.deviceMap[deviceID]; 54 | 55 | if (cdifDevice == null) { 56 | return session.callback(new CdifError('device not found: ' + deviceID)); 57 | } 58 | 59 | this.deviceManager.ensureDeviceState(deviceID, token, function(err) { 60 | if (err) { 61 | if (cdifDevice.connectionState !== 'redirecting') { 62 | return session.callback(err); 63 | } 64 | } 65 | this.deviceManager.emit('disconnect', cdifDevice, session); 66 | }.bind(this)); 67 | }; 68 | 69 | CdifInterface.prototype.invokeDeviceAction = function(deviceID, serviceID, actionName, args, token, session) { 70 | var cdifDevice = this.deviceManager.deviceMap[deviceID]; 71 | 72 | this.deviceManager.ensureDeviceState(deviceID, token, function(err) { 73 | if (err) { 74 | return session.callback(err); 75 | } 76 | this.deviceManager.emit('invokeaction', cdifDevice, serviceID, actionName, args, session); 77 | }.bind(this)); 78 | }; 79 | 80 | CdifInterface.prototype.getDeviceSpec = function(deviceID, token, session) { 81 | var cdifDevice = this.deviceManager.deviceMap[deviceID]; 82 | 83 | this.deviceManager.ensureDeviceState(deviceID, token, function(err) { 84 | if (err) { 85 | return session.callback(err); 86 | } 87 | this.deviceManager.emit('getspec', cdifDevice, session); 88 | }.bind(this)); 89 | }; 90 | 91 | CdifInterface.prototype.getDeviceState = function(deviceID, serviceID, token, session) { 92 | var cdifDevice = this.deviceManager.deviceMap[deviceID]; 93 | 94 | this.deviceManager.ensureDeviceState(deviceID, token, function(err) { 95 | if (err) { 96 | return session.callback(err); 97 | } 98 | this.deviceManager.emit('getstate', cdifDevice, serviceID, session); 99 | }.bind(this)); 100 | }; 101 | 102 | CdifInterface.prototype.eventSubscribe = function(subscriber, deviceID, serviceID, token, session) { 103 | var cdifDevice = this.deviceManager.deviceMap[deviceID]; 104 | 105 | this.deviceManager.ensureDeviceState(deviceID, token, function(err) { 106 | if (err) { 107 | return session.callback(err); 108 | } 109 | this.deviceManager.emit('subscribe', subscriber, cdifDevice, serviceID, session); 110 | }.bind(this)); 111 | }; 112 | 113 | CdifInterface.prototype.eventUnsubscribe = function(subscriber, deviceID, serviceID, token, session) { 114 | var cdifDevice = this.deviceManager.deviceMap[deviceID]; 115 | 116 | this.deviceManager.ensureDeviceState(deviceID, token, function(err) { 117 | if (err) { 118 | return session.callback(err); 119 | } 120 | this.deviceManager.emit('unsubscribe', subscriber, cdifDevice, serviceID, session); 121 | }.bind(this)); 122 | }; 123 | 124 | CdifInterface.prototype.getDeviceSchema = function(deviceID, path, token, session) { 125 | var cdifDevice = this.deviceManager.deviceMap[deviceID]; 126 | 127 | this.deviceManager.ensureDeviceState(deviceID, token, function(err) { 128 | if (err) { 129 | return session.callback(err); 130 | } 131 | this.deviceManager.emit('getschema', cdifDevice, path, session); 132 | }.bind(this)); 133 | }; 134 | 135 | CdifInterface.prototype.setDeviceOAuthAccessToken = function(deviceID, params, session) { 136 | var cdifDevice = this.deviceManager.deviceMap[deviceID]; 137 | 138 | if (cdifDevice == null) { // check null or undefined 139 | return session.callback(new CdifError('device not found: ' + deviceID)); 140 | } 141 | this.deviceManager.emit('setoauthtoken', cdifDevice, params, session); 142 | }; 143 | 144 | CdifInterface.prototype.getDeviceRootUrl = function(deviceID, session) { 145 | var cdifDevice = this.deviceManager.deviceMap[deviceID]; 146 | 147 | if (cdifDevice == null) { 148 | return session.callback(new CdifError('device not found: ' + deviceID)); 149 | } 150 | this.deviceManager.emit('getrooturl', cdifDevice, session); 151 | }; 152 | 153 | CdifInterface.prototype.onDevicePresentation = function(deviceID) { 154 | this.emit('presentation', deviceID); 155 | }; 156 | 157 | module.exports = CdifInterface; 158 | -------------------------------------------------------------------------------- /lib/cdif-proxy-server.js: -------------------------------------------------------------------------------- 1 | var cp = require('child_process'); 2 | var events = require('events'); 3 | var util = require('util'); 4 | var CdifUtil = require('./cdif-util'); 5 | var CdifError = require('./error').CdifError; 6 | 7 | function ProxyServer() { 8 | this.server = null; 9 | this.proxyUrl = ''; 10 | // For now this is onvif only 11 | this.streamUrl = ''; 12 | } 13 | 14 | util.inherits(ProxyServer, events.EventEmitter); 15 | 16 | ProxyServer.prototype.createServer = function(path, callback) { 17 | try { 18 | this.server = cp.fork(path); 19 | 20 | this.server.on('message', function(msg) { 21 | if (msg.port) { 22 | var port = msg.port; 23 | var protocol = CdifUtil.getHostProtocol(); 24 | var hostIp = CdifUtil.getHostIp(); 25 | this.proxyUrl = protocol + hostIp + ':' + port; 26 | this.emit('proxyurl', this.proxyUrl); 27 | } else if (msg.streamUrl) { 28 | // For now this is onvif only 29 | this.streamUrl = msg.streamUrl; 30 | this.emit('streamurl', this.streamUrl); 31 | } else if (msg.error) { 32 | this.emit('error', msg.error); 33 | } 34 | }.bind(this)); 35 | } catch(e) { 36 | if (typeof(callback) === 'function') { 37 | callback(new CdifError('proxy server create failed: ' + e.message)); 38 | } 39 | return; 40 | } 41 | if (typeof(callback) === 'function') { 42 | callback(null); 43 | } 44 | }; 45 | 46 | ProxyServer.prototype.killServer = function(callback) { 47 | if (this.server) { 48 | this.server.kill('SIGTERM'); 49 | } 50 | if (typeof(callback) === 'function') { 51 | callback(null); 52 | } 53 | }; 54 | 55 | ProxyServer.prototype.setDeviceID = function(id) { 56 | this.server.send({deviceID: id}); 57 | }; 58 | 59 | ProxyServer.prototype.setDeviceRootUrl = function(url) { 60 | this.server.send({deviceRootUrl: url}); 61 | }; 62 | 63 | // For now this is onvif only 64 | ProxyServer.prototype.setDeviceStreamUrl = function(url) { 65 | this.server.send({deviceStreamUrl: url}); 66 | }; 67 | 68 | module.exports = ProxyServer; 69 | -------------------------------------------------------------------------------- /lib/cdif-util.js: -------------------------------------------------------------------------------- 1 | var os = require('os'); 2 | var util = require('util'); 3 | var CdifDevice = require('cdif-device'); 4 | var logger = require('./logger'); 5 | 6 | module.exports = { 7 | // if this host run as router it may need to return its WAN IP address 8 | getHostIp: function() { 9 | var interfaces = os.networkInterfaces(); 10 | for (var k in interfaces) { 11 | for (var k2 in interfaces[k]) { 12 | var address = interfaces[k][k2]; 13 | if (address.family === 'IPv4' && !address.internal) { 14 | // only return the first available IP 15 | return address.address; 16 | } 17 | } 18 | } 19 | }, 20 | getHostProtocol: function() { 21 | // in production return https instead 22 | return 'http://'; 23 | }, 24 | getHostPort: function() { 25 | //TODO: check port availability 26 | return '3049'; 27 | }, 28 | inherits: function(constructor, superConstructor) { 29 | util.inherits(constructor, superConstructor); 30 | 31 | // prevent child override 32 | if (superConstructor === CdifDevice) { 33 | for (var i in superConstructor.prototype) { 34 | constructor.prototype[i] = superConstructor.prototype[i]; 35 | } 36 | } 37 | }, 38 | loadFile: function(name) { 39 | // avoid entering global require cache 40 | // to be used by device modules to reload its impl. files on module reload 41 | try { 42 | return require(name); 43 | } catch (e) { 44 | logger.error(e); 45 | return null; 46 | } 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /lib/cli-options.js: -------------------------------------------------------------------------------- 1 | var logger = require('logger'); 2 | 3 | module.exports = { 4 | setOptions: function(argv) { 5 | this.isDebug = (argv.debug === true) ? true : false; 6 | this.allowDiscover = (argv.allowDiscover === true) ? true : false; 7 | this.heapDump = (argv.heapDump === true) ? true : false; 8 | this.wsServer = (argv.wsServer === true) ? true : false; 9 | this.sioServer = (argv.sioServer === true) ? true : false; 10 | 11 | this.localDBAccess = true; // whether or not access local device DB 12 | this.dbPath = null; // the absolute path of local device DB 13 | 14 | if (argv.dbPath != null && typeof(argv.dbPath) === 'string') { 15 | this.dbPath = argv.dbPath; 16 | } 17 | 18 | this.localModulePath = null; 19 | 20 | //TODO: support specify only one module name now 21 | if (argv.loadModule != null) { 22 | this.localDBAccess = false; 23 | this.localModulePath = argv.loadModule; 24 | } 25 | 26 | if (this.wsServer === true && this.sioServer === true) { 27 | logger.info('trying to start WebSocket and SocketIO server simultanenouesly, start with WebSocket server'); 28 | } 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /lib/connect.js: -------------------------------------------------------------------------------- 1 | var CdifError = require('./error').CdifError; 2 | var DeviceError = require('./error').DeviceError; 3 | 4 | function ConnectManager(cdifDevice) { 5 | this.device = cdifDevice; 6 | } 7 | 8 | //TODO: support multi-user secrets 9 | ConnectManager.prototype.verifyConnect = function(user, pass, callback) { 10 | if (this.device.spec.device.userAuth === true && !this.device.isOAuthDevice) { 11 | if (this.device.secret === '') { 12 | return callback(new CdifError('cannot verify password'), null, null); 13 | } 14 | if (this.device.user !== user) { 15 | return callback(new CdifError('username not match'), null, null); 16 | } 17 | this.device.auth.compareSecret(pass, this.device.secret, function(err, res) { 18 | if (!err && res === true) { 19 | return callback(null, this.device.secret, null); 20 | } 21 | if (!err) { 22 | return callback(new CdifError('password not match'), null, null); 23 | } 24 | return callback(new CdifError(err.message), null, null); 25 | }.bind(this)); 26 | } else { 27 | callback(null, null, null); 28 | } 29 | }; 30 | 31 | //TODO: connect args may contain return and cancel URL from client which we can redirect to after OAuth flow is done 32 | ConnectManager.prototype.processConnect = function(user, pass, callback) { 33 | if (this.device._connect && typeof(this.device._connect) === 'function') { 34 | this.device._connect(user, pass, function(error, redirectObj) { 35 | if (error) { 36 | return callback(new DeviceError('connect fail: ' + error.message), null, null); 37 | } 38 | 39 | if (redirectObj != null) { 40 | if (typeof(redirectObj) !== 'object') { 41 | return callback(new DeviceError('redirect url is not an object'), null, null); 42 | } 43 | if (redirectObj.href == null || redirectObj.method == null) { 44 | return callback(new DeviceError('redirect url malform: ' + JSON.stringify(redirectObj)), null, null); 45 | } 46 | this.device.connectionState = 'redirecting'; 47 | } else { 48 | this.device.connectionState = 'connected'; 49 | } 50 | 51 | if (this.device.spec.device.userAuth === true && !this.device.isOAuthDevice) { 52 | this.device.auth.getSecret(this.device.deviceID, pass, function(err, secret) { 53 | if (!err) { 54 | this.device.user = user; 55 | this.device.secret = secret; 56 | } 57 | callback(err, secret); 58 | }.bind(this)); 59 | } else { 60 | callback(null, null, redirectObj); 61 | } 62 | }.bind(this)); 63 | } else { 64 | if (this.device.spec.device.userAuth === true) { 65 | // TODO: may create auth strategy later on 66 | callback(new CdifError('cannot authenticate this device'), null, null); 67 | } else { 68 | this.device.connectionState = 'connected'; 69 | callback(null, null, null); 70 | } 71 | } 72 | }; 73 | 74 | ConnectManager.prototype.processDisconnect = function(callback) { 75 | if (this.device.connectionState === 'connected' || this.device.connectionState === 'redirecting') { 76 | if (this.device._disconnect && typeof(this.device._disconnect) === 'function') { 77 | this.device._disconnect(function(error) { 78 | if (error) { 79 | return callback(new DeviceError('disconnect fail: ' + error.message)); 80 | } 81 | if (this.device.spec.device.userAuth === true && !this.device.isOAuthDevice) { 82 | this.device.user = ''; this.device.secret = ''; 83 | } 84 | this.device.connectionState = 'disconnected'; 85 | callback(null); 86 | }.bind(this)); 87 | } else { 88 | if (this.device.spec.device.userAuth === true) { 89 | this.device.user = ''; this.device.secret = ''; 90 | } 91 | this.device.connectionState = 'disconnected'; 92 | callback(null); 93 | } 94 | } else { 95 | callback(new CdifError('device not connected')); 96 | } 97 | }; 98 | 99 | module.exports = ConnectManager; 100 | -------------------------------------------------------------------------------- /lib/device-auth.js: -------------------------------------------------------------------------------- 1 | var jwt = require('jsonwebtoken'); 2 | var bcrypt = require('bcrypt'); 3 | var deviceDB = require('./device-db'); 4 | var CdifError = require('./error').CdifError; 5 | 6 | function DeviceAuth() { 7 | } 8 | 9 | DeviceAuth.prototype.generateToken = function(user, secret, callback) { 10 | //TODO: user configurable expire time? 11 | // this alway success? 12 | var token = jwt.sign({ username: user }, secret, { expiresIn: 60 * 60 * 60 * 5 }); 13 | callback(null, token); 14 | }; 15 | 16 | DeviceAuth.prototype.verifyAccess = function(cdifDevice, secret, token, callback) { 17 | if (cdifDevice.isOAuthDevice === true) { 18 | if (cdifDevice.oauth_access_token === '' || cdifDevice.oauth2_access_token === '') { 19 | return callback(new CdifError('oauth access token not available, do connect first')); 20 | } 21 | return callback(null); 22 | } 23 | 24 | if (typeof(token) !== 'string') { 25 | return callback(new CdifError('no valid token')); 26 | } 27 | if (typeof(secret) !== 'string') { 28 | return callback(new CdifError('not able to verify token')); 29 | } 30 | // var decoded = jwt.decode(token, {complete: true}); 31 | // console.log(decoded); 32 | try { 33 | var decoded = jwt.verify(token, secret); 34 | } catch(e) { 35 | return callback(new CdifError(e.message)); 36 | } 37 | callback(null); 38 | }; 39 | 40 | DeviceAuth.prototype.compareSecret = function(pass, secret, callback) { 41 | bcrypt.compare(pass, secret, callback); 42 | }; 43 | 44 | DeviceAuth.prototype.getSecret = function(deviceID, pass, callback) { 45 | deviceDB.loadSecret(deviceID, function(err, data) { 46 | if (err) { 47 | return callback(new CdifError(err.message), null); 48 | } 49 | if (!data) { 50 | bcrypt.hash(pass, 8, function(e, secret) { 51 | if (e) { 52 | callback(new CdifError(e.message), null); 53 | } else { 54 | deviceDB.storeSecret(deviceID, secret, function(error) { 55 | if (error) { 56 | callback(new CdifError(error.message), null); 57 | } else { 58 | callback(null, secret); 59 | } 60 | }); 61 | } 62 | }); 63 | } else { 64 | bcrypt.compare(pass, data.hash, function(err, res) { 65 | if (err) { 66 | callback(new CdifError(err.message), null); 67 | } else if (res === false) { // user changed password or token updated 68 | bcrypt.hash(pass, 8, function(e, s) { 69 | if (e) { 70 | callback(new CdifError(e.message), null); 71 | } else { 72 | deviceDB.storeSecret(deviceID, s, function(err) { 73 | if (err) { 74 | callback(new CdifError(err.message), null); 75 | } else { 76 | callback(null, s); 77 | } 78 | }); 79 | } 80 | }); 81 | } else { 82 | callback(null, data.hash); 83 | } 84 | }); 85 | } 86 | }); 87 | }; 88 | 89 | module.exports = DeviceAuth; 90 | -------------------------------------------------------------------------------- /lib/device-db.js: -------------------------------------------------------------------------------- 1 | var sqlite3 = require('sqlite3'); 2 | var options = require('./cli-options'); 3 | var mkdirp = require('mkdirp'); 4 | var fs = require('fs'); 5 | var logger = require('./logger'); 6 | 7 | module.exports = { 8 | getDeviceUUIDFromHWAddr: function(hwAddr, callback) { 9 | if (options.localDBAccess === false) return callback(null, null); 10 | 11 | if (hwAddr == null) { 12 | return callback(null, null); 13 | } 14 | 15 | this.db.serialize(function() { 16 | this.db.get("SELECT uuid FROM device_db WHERE hwaddr = ?", hwAddr, callback); 17 | }.bind(this)); 18 | }, 19 | 20 | setDeviceUUID: function(hwAddr, deviceUUID, callback) { 21 | if (options.localDBAccess === false) return callback(null, null); 22 | 23 | if (hwAddr == null) { 24 | return callback(null, null); 25 | } 26 | 27 | this.db.serialize(function() { 28 | this.db.run("INSERT OR REPLACE INTO device_db(hwaddr, uuid, spec) VALUES (?, ?, (SELECT spec FROM device_db WHERE hwaddr = ?))", 29 | hwAddr, deviceUUID, hwAddr, callback); 30 | }.bind(this)); 31 | }, 32 | 33 | getDeviceSpecFromHWAddr: function(hwAddr, callback) { 34 | if (options.localDBAccess === false) return callback(null); 35 | 36 | this.db.serialize(function() { 37 | this.db.get("SELECT spec FROM device_db WHERE hwaddr = ?", hwAddr, callback); 38 | }.bind(this)); 39 | }, 40 | 41 | setSpecForDevice: function(hwAddr, spec) { 42 | if (options.localDBAccess === false) return; 43 | 44 | if (hwAddr == null) return; 45 | 46 | this.db.serialize(function() { 47 | this.db.run("INSERT OR REPLACE INTO device_db(hwaddr, uuid, spec) VALUES (?, (SELECT uuid FROM device_db WHERE hwaddr = ?), ?)", 48 | hwAddr, hwAddr, spec); 49 | }.bind(this)); 50 | }, 51 | 52 | getSpecForAllDevices: function(callback) { 53 | if (options.localDBAccess === false) return callback(null, null); 54 | 55 | this.db.parallelize(function() { 56 | this.db.all("SELECT spec FROM device_db", callback); 57 | }.bind(this)); 58 | }, 59 | 60 | deleteDeviceInformation: function(hwAddr, callback) { 61 | if (options.localDBAccess === false) return callback(null); 62 | 63 | this.db.serialize(function() { 64 | this.db.run("DELETE FROM device_db WHERE hwaddr = ?", hwAddr, callback); 65 | }.bind(this)); 66 | }, 67 | 68 | loadSecret: function(deviceUUID, callback) { 69 | if (options.localDBAccess === false) return callback(null); 70 | 71 | this.db.serialize(function() { 72 | this.db.get("SELECT hash FROM device_hash WHERE uuid = ?", deviceUUID, callback); 73 | }.bind(this)); 74 | }, 75 | 76 | storeSecret: function(deviceUUID, hash, callback) { 77 | if (options.localDBAccess === false) return callback(null); 78 | 79 | this.db.serialize(function() { 80 | this.db.run("INSERT OR REPLACE INTO device_hash(uuid, hash) VALUES (?, ?)", 81 | deviceUUID, hash, callback); 82 | }.bind(this)); 83 | }, 84 | 85 | setModuleInfo: function(name, version, callback) { 86 | if (options.localDBAccess === false) return callback(null); 87 | 88 | if (name == null) { 89 | return callback(new Error('setting incorrect module name'), null); 90 | } 91 | 92 | this.moduleDB.serialize(function() { 93 | this.moduleDB.run("INSERT OR REPLACE INTO module_info(name, version) VALUES (?, ?)", 94 | name, version, callback); 95 | }.bind(this)); 96 | }, 97 | 98 | removeModuleInfo: function(name, callback) { 99 | if (options.localDBAccess === false) return callback(null); 100 | 101 | if (name == null) { 102 | return callback(new Error('remove incorrect module name'), null); 103 | } 104 | 105 | this.moduleDB.serialize(function() { 106 | this.moduleDB.run("DELETE FROM module_info WHERE name = ?", name, callback); 107 | }.bind(this)); 108 | }, 109 | 110 | getAllModuleInfo: function(callback) { 111 | if (options.localDBAccess === false) return callback(null); 112 | 113 | this.moduleDB.parallelize(function() { 114 | this.moduleDB.all("SELECT * FROM module_info", callback); 115 | }.bind(this)); 116 | }, 117 | 118 | init: function() { 119 | var deviceDBName, moduleDBName; 120 | 121 | if (options.localDBAccess === false) { 122 | return logger.info('loading local module, device DB access is disabled'); 123 | } 124 | 125 | if (options.dbPath !== null) { 126 | deviceDBName = options.dbPath + '/device_store.db'; 127 | moduleDBName = options.dbPath + '/modules.db'; 128 | //TODO: check write safety of this call, do not crash 129 | try { 130 | mkdirp.sync(options.dbPath); 131 | fs.accessSync(options.dbPath, fs.W_OK); 132 | } catch (e) { 133 | return logger.error('cannot access DB folder: ' + options.dbPath + ' reason: ' + e.message); 134 | process.exit(-1); 135 | } 136 | } else { 137 | // assume local install folder is always writable 138 | deviceDBName = __dirname + '/../device_store.db'; 139 | moduleDBName = __dirname + '/../modules.db'; 140 | } 141 | 142 | this.db = new sqlite3.Database(deviceDBName); 143 | this.moduleDB = new sqlite3.Database(moduleDBName); 144 | 145 | this.db.serialize(function() { 146 | this.db.run("CREATE TABLE IF NOT EXISTS device_db(hwaddr TEXT PRIMARY KEY, uuid TEXT, spec TEXT)"); 147 | this.db.run("CREATE TABLE IF NOT EXISTS device_hash(uuid TEXT PRIMARY KEY, hash TEXT)"); 148 | }.bind(this)); 149 | 150 | this.moduleDB.serialize(function() { 151 | this.moduleDB.run("CREATE TABLE IF NOT EXISTS module_info(name TEXT PRIMARY KEY, version TEXT)"); 152 | }.bind(this)); 153 | } 154 | }; 155 | 156 | -------------------------------------------------------------------------------- /lib/device-manager.js: -------------------------------------------------------------------------------- 1 | var events = require('events'); 2 | var util = require('util'); 3 | var uuid = require('uuid'); 4 | var CdifDevice = require('cdif-device'); 5 | var deviceDB = require('./device-db'); 6 | var DeviceAuth = require('./device-auth'); 7 | var validator = require('./validator'); 8 | var logger = require('./logger'); 9 | var OAuthDevice = require('./oauth/oauth'); 10 | var CdifError = require('./error').CdifError; 11 | var DeviceError = require('./error').DeviceError; 12 | 13 | function DeviceManager(mm) { 14 | this.deviceMap = {}; 15 | this.deviceAuth = new DeviceAuth(); 16 | this.moduleManager = mm; 17 | 18 | this.moduleManager.on('deviceonline', this.onDeviceOnline.bind(this)); 19 | this.moduleManager.on('deviceoffline', this.onDeviceOffline.bind(this)); 20 | this.moduleManager.on('purgedevice', this.onPurgeDevice.bind(this)); 21 | 22 | this.on('discoverall', this.onDiscoverAll.bind(this)); 23 | this.on('stopdiscoverall', this.onStopDiscoverAll.bind(this)); 24 | this.on('devicelist', this.onGetDiscoveredDeviceList.bind(this)); 25 | this.on('connect', this.onConnectDevice.bind(this)); 26 | this.on('disconnect', this.onDisconnectDevice.bind(this)); 27 | this.on('invokeaction', this.onInvokeDeviceAction.bind(this)); 28 | this.on('getspec', this.onGetDeviceSpec.bind(this)); 29 | this.on('devicestate', this.onGetDeviceState.bind(this)); 30 | this.on('subscribe', this.onEventSubscribe.bind(this)); 31 | this.on('unsubscribe', this.onEventUnsubscribe.bind(this)); 32 | this.on('getschema', this.onGetDeviceSchema.bind(this)); 33 | this.on('setoauthtoken', this.onSetDeviceOAuthAccessToken.bind(this)); 34 | this.on('getrooturl', this.onGetDeviceRootUrl.bind(this)); 35 | } 36 | 37 | util.inherits(DeviceManager, events.EventEmitter); 38 | 39 | DeviceManager.prototype.onDeviceOnline = function(cdifDevice, m) { 40 | var _this = this; 41 | 42 | if (cdifDevice.oauth_version === '1.0' || cdifDevice.oauth_version === '2.0') { 43 | var oauth = new OAuthDevice(cdifDevice); 44 | oauth.createOAuthDevice(); 45 | } 46 | 47 | if (this.checkDeviceInterface(cdifDevice) === false) return; 48 | 49 | validator.validateDeviceSpec(cdifDevice.spec, function(error) { 50 | if (error) { 51 | return logger.error(error.message + ', device spec: ' + JSON.stringify(cdifDevice.spec)); 52 | } 53 | if (m == null) { 54 | return logger.error('unknown module for device: ' + cdifDevice.spec.device.friendlyName); 55 | } 56 | 57 | cdifDevice.getHWAddress(function(err, addr) { 58 | var hwAddr; 59 | var deviceUUID; 60 | if (!err) { 61 | hwAddr = addr; 62 | deviceDB.getDeviceUUIDFromHWAddr(hwAddr, function(err, data) { 63 | if (err) { 64 | return logger.error(err); 65 | } 66 | if (!data) { 67 | deviceUUID = uuid.v4(); 68 | deviceDB.setDeviceUUID(hwAddr, deviceUUID, function(err) { 69 | if (err) { 70 | logger.error('cannot insert address record for device:' + deviceUUID); 71 | } 72 | }); 73 | } else { 74 | deviceUUID = data.uuid; 75 | } 76 | deviceDB.setSpecForDevice(hwAddr, JSON.stringify(cdifDevice.spec)); 77 | // TODO: handle device offline and purge dead devices 78 | cdifDevice.module = m; 79 | cdifDevice.auth = _this.deviceAuth; 80 | cdifDevice.hwAddr = hwAddr; 81 | cdifDevice.deviceID = deviceUUID; 82 | cdifDevice.online = true; 83 | 84 | //it could return a new device object instance with initial states here 85 | //module install route rely on this to create new device object 86 | logger.info('new device online: ' + cdifDevice.deviceID); 87 | _this.deviceMap[deviceUUID] = cdifDevice; 88 | }); 89 | } else { 90 | logger.error('cannot get HW address for device: ' + cdifDevice.spec.device.friendlyName); 91 | } 92 | }); 93 | }); 94 | }; 95 | 96 | // for now this is not triggered 97 | DeviceManager.prototype.onDeviceOffline = function(cdifDevice, m) { 98 | logger.error('device offline: ' + cdifDevice.deviceID); 99 | cdifDevice.online = false; 100 | }; 101 | 102 | // purge all device objects which are managed by the unloaded module 103 | DeviceManager.prototype.onPurgeDevice = function(m) { 104 | for (var deviceID in this.deviceMap) { 105 | if (this.deviceMap[deviceID].module === m) { 106 | delete this.deviceMap[deviceID]; 107 | logger.info('device purged: ' + deviceID); 108 | } 109 | } 110 | }; 111 | 112 | DeviceManager.prototype.onDiscoverAll = function(callback) { 113 | this.moduleManager.discoverAllDevices(); 114 | callback(null); 115 | }; 116 | 117 | DeviceManager.prototype.onStopDiscoverAll = function(callback) { 118 | this.moduleManager.stopDiscoverAllDevices(); 119 | callback(null); 120 | }; 121 | 122 | DeviceManager.prototype.onGetDiscoveredDeviceList = function(callback) { 123 | var deviceList = {}; 124 | for (var i in this.deviceMap) { 125 | var cdifDevice = this.deviceMap[i]; 126 | if (cdifDevice.spec) { 127 | //this is ugly but the easiest way to handle this request 128 | var desc = JSON.parse(JSON.stringify(cdifDevice.spec)); 129 | desc.device.serviceList = {}; 130 | deviceList[i] = desc; 131 | } 132 | } 133 | callback(null, deviceList); 134 | }; 135 | 136 | DeviceManager.prototype.ensureDeviceState = function(deviceID, token, callback) { 137 | var cdifDevice = this.deviceMap[deviceID]; 138 | 139 | if (cdifDevice == null) { // check null or undefined 140 | return callback(new CdifError('device not found: ' + deviceID)); 141 | } 142 | if (cdifDevice.module.discoverState === 'discovering') { 143 | return callback(new CdifError('in discovering')); 144 | } 145 | // if (cdifDevice.connectionState !== 'connected') { 146 | // return callback(new CdifError('device not connected')); 147 | // } 148 | if (cdifDevice.online === false) { 149 | return callback(new CdifError('device offlined')); 150 | } 151 | if (cdifDevice.spec.device.userAuth === true) { 152 | // make sure this is sync 153 | this.deviceAuth.verifyAccess(cdifDevice, cdifDevice.secret, token, callback); 154 | } else { 155 | callback(null); 156 | } 157 | }; 158 | 159 | DeviceManager.prototype.onConnectDevice = function(cdifDevice, user, pass, session) { 160 | var _this = this; 161 | 162 | session.setDeviceTimer(cdifDevice, function(error, device, timer) { 163 | try { 164 | device.connect(user, pass, function(err, secret, redirectObj) { 165 | if (this.expired === true) return; 166 | 167 | var sess = this.session; 168 | sess.clearDeviceTimer(this); 169 | if (err) { 170 | return sess.callback(err, null); 171 | } 172 | 173 | if (secret) { 174 | // FIXME: this brings in 'user' from context which could be an issue, bring in from session 175 | _this.deviceAuth.generateToken(user, secret, function(err, token) { 176 | if (err) { 177 | return sess.callback(new CdifError('cannot generate access token'), null); 178 | } 179 | if (redirectObj) { 180 | return sess.callback(null, {'device_access_token': token, 'url_redirect': redirectObj}); 181 | } else { 182 | return sess.callback(null, {'device_access_token': token}); 183 | } 184 | }); 185 | } else { 186 | if (redirectObj != null) { 187 | return sess.callback(null, {'url_redirect': redirectObj}); 188 | } else { 189 | return sess.callback(null, null); 190 | } 191 | } 192 | //FIXME: do not emit presentation event more than once, this brings in from context 193 | if (device.spec.device.devicePresentation === true) { 194 | _this.emit('presentation', cdifDevice.deviceID); 195 | } 196 | }.bind(timer)); 197 | } catch (e) { 198 | if (timer.expired === true) return; 199 | this.clearDeviceTimer(timer); 200 | return this.callback(new DeviceError(e.message), null); 201 | } 202 | }.bind(session)); 203 | }; 204 | 205 | DeviceManager.prototype.onDisconnectDevice = function(cdifDevice, session) { 206 | session.setDeviceTimer(cdifDevice, function(error, device, timer) { 207 | try { 208 | device.disconnect(function(err) { 209 | if (this.expired === true) return; 210 | this.session.clearDeviceTimer(this); 211 | return this.session.callback(err); 212 | }.bind(timer)); 213 | } catch (e) { 214 | if (timer.expired === true) return; 215 | this.clearDeviceTimer(timer); 216 | return this.callback(new DeviceError(e.message), null); 217 | } 218 | }.bind(session)); 219 | }; 220 | 221 | DeviceManager.prototype.onInvokeDeviceAction = function(cdifDevice, serviceID, actionName, args, session) { 222 | session.setDeviceTimer(cdifDevice, function(error, device, timer) { 223 | try { 224 | device.deviceControl(serviceID, actionName, args, function(err, data) { 225 | if (this.expired === true) return; 226 | this.session.clearDeviceTimer(this); 227 | return this.session.callback(err, data); 228 | }.bind(timer)); 229 | } catch (e) { 230 | if (timer.expired === true) return; 231 | this.clearDeviceTimer(timer); 232 | return this.callback(new DeviceError(e.message), null); //framework won't throw 233 | } 234 | }.bind(session)); 235 | }; 236 | 237 | DeviceManager.prototype.onGetDeviceSpec = function(cdifDevice, session) { 238 | session.setDeviceTimer(cdifDevice, function(error, device, timer) { 239 | device.getDeviceSpec(function(err, data) { 240 | if (this.expired === true) return; 241 | this.session.clearDeviceTimer(this); 242 | return this.session.callback(err, data); 243 | }.bind(timer)); 244 | }.bind(session)); 245 | }; 246 | 247 | DeviceManager.prototype.onGetDeviceState = function(cdifDevice, serviceID, session) { 248 | session.setDeviceTimer(cdifDevice, function(error, device, timer) { 249 | device.getServiceStates(serviceID, function(err, data) { 250 | if (this.expired === true) return; 251 | this.session.clearDeviceTimer(this); 252 | return this.session.callback(err, data); 253 | }.bind(timer)); 254 | }.bind(session)); 255 | }; 256 | 257 | DeviceManager.prototype.onEventSubscribe = function(subscriber, cdifDevice, serviceID, session) { 258 | session.setDeviceTimer(cdifDevice, function(error, device, timer) { 259 | device.subscribeDeviceEvent(subscriber, serviceID, function(err) { 260 | if (this.expired === true) return; 261 | this.session.clearDeviceTimer(this); 262 | return this.session.callback(err); 263 | }.bind(timer)); 264 | }.bind(session)); 265 | }; 266 | 267 | DeviceManager.prototype.onEventUnsubscribe = function(subscriber, cdifDevice, serviceID, session) { 268 | session.setDeviceTimer(cdifDevice, function(error, device, timer) { 269 | device.unSubscribeDeviceEvent(subscriber, serviceID, function(err) { 270 | if (this.expired === true) return; 271 | this.session.clearDeviceTimer(this); 272 | return this.session.callback(err); 273 | }.bind(timer)); 274 | }.bind(session)); 275 | }; 276 | 277 | DeviceManager.prototype.onGetDeviceSchema = function(cdifDevice, path, session) { 278 | session.setDeviceTimer(cdifDevice, function(error, device, timer) { 279 | device.resolveSchemaFromPath(path, null, function(err, self, data) { 280 | if (this.expired === true) return; 281 | this.session.clearDeviceTimer(this); 282 | return this.session.callback(err, data); 283 | }.bind(timer)); 284 | }.bind(session)); 285 | }; 286 | 287 | DeviceManager.prototype.onSetDeviceOAuthAccessToken = function(cdifDevice, params, session) { 288 | session.setDeviceTimer(cdifDevice, function(error, device, timer) { 289 | device.setOAuthAccessToken(params, function(err) { 290 | if (this.expired === true) return; 291 | this.session.clearDeviceTimer(this); 292 | return this.session.callback(err); 293 | }.bind(timer)); 294 | }.bind(session)); 295 | }; 296 | 297 | DeviceManager.prototype.onGetDeviceRootUrl = function(cdifDevice, session) { 298 | session.setDeviceTimer(cdifDevice, function(error, device, timer) { 299 | device.getDeviceRootUrl(function(err, data) { 300 | if (this.expired === true) return; 301 | this.session.clearDeviceTimer(this); 302 | return this.session.callback(err, data); 303 | }.bind(timer)); 304 | }.bind(session)); 305 | }; 306 | 307 | DeviceManager.prototype.checkDeviceInterface = function(cdifDevice) { 308 | if (!(cdifDevice instanceof CdifDevice)) { 309 | logger.error('object is not an instance of CDIF device'); 310 | return false; 311 | } 312 | return true; 313 | }; 314 | 315 | module.exports = DeviceManager; 316 | -------------------------------------------------------------------------------- /lib/error.js: -------------------------------------------------------------------------------- 1 | function CdifError(message) { 2 | this.topic = 'cdif error'; 3 | this.message = message; 4 | } 5 | CdifError.prototype = new Error; 6 | 7 | function DeviceError(message) { 8 | this.topic = 'device error'; 9 | this.message = message; 10 | } 11 | DeviceError.prototype = new Error; 12 | 13 | module.exports = { 14 | CdifError: CdifError, 15 | DeviceError: DeviceError 16 | }; 17 | -------------------------------------------------------------------------------- /lib/logger.js: -------------------------------------------------------------------------------- 1 | var bunyan = require('bunyan'); 2 | 3 | //TODO: configurable error log path, per component (child) logging 4 | module.exports = { 5 | createLogger: function() { 6 | this.logger = bunyan.createLogger({ 7 | name: 'cdif', 8 | serializers: bunyan.stdSerializers, 9 | streams: [ 10 | { 11 | level: 'info', 12 | stream: process.stdout 13 | }, 14 | { 15 | level: 'error', 16 | type: 'file', 17 | path: __dirname + '/../cdif-error.log' 18 | } 19 | ] 20 | }); 21 | }, 22 | info: function(logInfo) { 23 | this.logger.info(logInfo); 24 | }, 25 | error: function(errorInfo) { 26 | this.logger.error(errorInfo); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /lib/module-manager.js: -------------------------------------------------------------------------------- 1 | var events = require('events'); 2 | var util = require('util'); 3 | var options = require('../lib/cli-options'); 4 | var exec = require('child_process').exec; 5 | var deviceDB = require('../lib/device-db'); 6 | var CdifError = require('../lib/error').CdifError; 7 | var logger = require('../lib/logger'); 8 | var semver = require('semver'); 9 | 10 | function ModuleManager() { 11 | this.modules = {}; 12 | 13 | this.on('moduleload', this.onModuleLoad.bind(this)); 14 | this.on('moduleunload', this.onModuleUnload.bind(this)); 15 | } 16 | 17 | util.inherits(ModuleManager, events.EventEmitter); 18 | 19 | ModuleManager.prototype.onModuleLoad = function(name, module, version) { 20 | logger.info('module: ' + name + '@' + version + ' loaded'); 21 | 22 | module.discoverState = 'stopped'; 23 | 24 | var m = this.modules[name]; 25 | if (m != null) { 26 | // module reloaded 27 | if (m.discoverState === 'discovering') { 28 | m.discoverState = 'stopped'; 29 | } 30 | this.emit('purgedevice', m); // to be handled by device manager 31 | } 32 | this.modules[name] = module; 33 | 34 | if (options.allowDiscover === false) { 35 | module.emit('discover'); 36 | setTimeout(function() { 37 | this.emit('stopdiscover'); 38 | }.bind(module), 5000); 39 | } 40 | }; 41 | 42 | ModuleManager.prototype.onModuleUnload = function(name) { 43 | logger.info('module: ' + name + ' unloaded'); 44 | 45 | var m = this.modules[name]; 46 | if (m != null) { 47 | if (m.discoverState === 'discovering') { 48 | m.discoverState = 'stopped'; 49 | } 50 | delete this.modules[name]; 51 | this.emit('purgedevice', m); // to be handled by device manager 52 | } 53 | }; 54 | 55 | ModuleManager.prototype.discoverAllDevices = function() { 56 | for (var m in this.modules) { 57 | var module = this.modules[m]; 58 | if (module.discoverState === 'discovering') { 59 | return; 60 | } 61 | module.emit('discover'); 62 | module.discoverState = 'discovering'; 63 | } 64 | }; 65 | 66 | ModuleManager.prototype.stopDiscoverAllDevices = function() { 67 | for (var m in this.modules) { 68 | var module = this.modules[m]; 69 | if (module.discoverState === 'stopped') { 70 | return; 71 | } 72 | module.emit('stopdiscover'); 73 | module.discoverState = 'stopped'; 74 | } 75 | }; 76 | 77 | ModuleManager.prototype.onDeviceOnline = function(device, module) { 78 | this.emit('deviceonline', device, module); 79 | }; 80 | 81 | ModuleManager.prototype.onDeviceOffline = function(device, module) { 82 | this.emit('deviceoffline', device, module); 83 | }; 84 | 85 | ModuleManager.prototype.loadAllModules = function() { 86 | deviceDB.getAllModuleInfo(function(err, data) { 87 | if (err) { 88 | return logger.error(err); 89 | } 90 | 91 | if (data == null) return; 92 | 93 | data.forEach(function(item) { 94 | var mod = null; 95 | try { 96 | mod = require(item.name); 97 | } catch (e) { 98 | return logger.error(e); 99 | } 100 | 101 | var m = new mod(); 102 | m.on('deviceonline', this.onDeviceOnline.bind(this)); 103 | m.on('deviceoffline', this.onDeviceOffline.bind(this)); 104 | this.emit('moduleload', item.name, m, item.version); 105 | }.bind(this)); 106 | }.bind(this)); 107 | 108 | // load any local module specified on command line 109 | if (options.localModulePath != null) { 110 | logger.info('load local module from path: ' + options.localModulePath); 111 | 112 | try { 113 | var moduleConstructor = require(options.localModulePath); 114 | var moduleInstance = new moduleConstructor(); 115 | 116 | moduleInstance.on('deviceonline', this.onDeviceOnline.bind(this)); 117 | moduleInstance.on('deviceoffline', this.onDeviceOffline.bind(this)); 118 | this.emit('moduleload', options.localModulePath, moduleInstance, 'local'); // local module won't need version info 119 | 120 | } catch (e) { 121 | return logger.error('local module load failed: ' + options.localModulePath + ', error: ' + e.message, null); 122 | } 123 | } 124 | }; 125 | 126 | ModuleManager.prototype.loadModule = function(name, version) { 127 | var moduleConstructor = null; 128 | var moduleInstance = null; 129 | 130 | try { 131 | moduleConstructor = require(name); 132 | moduleInstance = new moduleConstructor(); 133 | } catch (e) { 134 | return logger.error(e); 135 | } 136 | 137 | moduleInstance.on('deviceonline', this.onDeviceOnline.bind(this)); 138 | moduleInstance.on('deviceoffline', this.onDeviceOffline.bind(this)); 139 | this.emit('moduleload', name, moduleInstance, version); 140 | }; 141 | 142 | ModuleManager.prototype.unloadModule = function(name) { 143 | this.emit('moduleunload', name); 144 | }; 145 | 146 | //TODO: check module validness, e.g. name start with cdif 147 | ModuleManager.prototype.installModuleFromRegistry = function(registry, name, version, callback) { 148 | if ((registry != null) && (typeof(registry) !== 'string' || 149 | ((/^http:\/\/.{1,256}$/.test(registry) || 150 | /^https:\/\/.{1,256}$/.test(registry)) === false))) { 151 | return callback(new CdifError('module install: invalid registry name')); 152 | } 153 | 154 | if (name == null || typeof(name) !== 'string') { 155 | return callback(new CdifError('invalid package name')); 156 | } 157 | 158 | if (typeof(version) !== 'string' || semver.valid(version) == null) { 159 | return callback(new CdifError('invalid package version: ' + version)); 160 | } 161 | 162 | var command = null; 163 | if (registry == null) { 164 | command = 'npm install ' + name + '@' + version; 165 | } else { 166 | command = 'npm install ' + '--registry=' + registry + ' ' + name + '@' + version; 167 | } 168 | 169 | try { 170 | exec(command, {timeout: 60000}, function(err, stdout, stderr) { 171 | if (err) { 172 | logger.error('module install failed: ' + name + ', error: ' + err.message); 173 | return callback(new CdifError('module install failed: ' + name + ', error: ' + err.message), null); 174 | } 175 | 176 | logger.info('module installed: ' + name + '@' + version); 177 | 178 | this.addModuleInformation(name, version, function(e) { 179 | if (e) { 180 | return callback(new CdifError('add module record failed: ' + name + ', error: ' + e.message), null); 181 | } 182 | this.loadModule(name, version); 183 | return callback(null); 184 | }.bind(this)); 185 | }.bind(this)); 186 | } catch (e) { 187 | return callback(new CdifError('module install failed: ' + name + ', error: ' + e.message), null); 188 | } 189 | }; 190 | 191 | ModuleManager.prototype.uninstallModule = function(name, callback) { 192 | if (name == null || typeof(name) !== 'string') { 193 | return callback(new CdifError('invalid package name')); 194 | } 195 | 196 | var command = 'npm uninstall ' + name; 197 | 198 | try { 199 | exec(command, {timeout: 60000}, function(err, stdout, stderr) { 200 | if (err) { 201 | logger.error('module uninstall failed: ' + name + ', error: ' + err.message); 202 | return callback(new CdifError('module uninstall failed: ' + name + ', error: ' + err.message), null); 203 | } 204 | 205 | logger.info('module uninstalled: ' + name); 206 | 207 | this.removeModuleInformation(name, function(e) { 208 | if (e) { 209 | return callback(new CdifError('remove module record failed: ' + name + ', error: ' + e.message), null); 210 | } 211 | this.unloadModule(name); 212 | return callback(null); 213 | }.bind(this)); 214 | }.bind(this)); 215 | } catch (e) { 216 | return callback(new CdifError('module uninstall failed: ' + name + ', error: ' + e.message), null); 217 | } 218 | }; 219 | 220 | ModuleManager.prototype.addModuleInformation = function(name, version, callback) { 221 | if (version === '') version = 'latest'; 222 | 223 | deviceDB.setModuleInfo(name, version, function(err) { 224 | if (err) { 225 | return callback(new Error('cannot set module info in db: ' + err.message)); 226 | } 227 | return callback(null); 228 | }.bind(this)); 229 | }; 230 | 231 | ModuleManager.prototype.removeModuleInformation = function(name, callback) { 232 | deviceDB.removeModuleInfo(name, function(err) { 233 | if (err) { 234 | return callback(new Error('cannot remove module info in db: ' + err.message)); 235 | } 236 | return callback(null); 237 | }.bind(this)); 238 | }; 239 | 240 | module.exports = ModuleManager; 241 | -------------------------------------------------------------------------------- /lib/oauth/oauth.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | var events = require('events'); 3 | var querystring = require('querystring'); 4 | var OAuth = require('oauth').OAuth; 5 | var OAuth2 = require('oauth').OAuth2; 6 | var CdifUtil = require('cdif-util'); 7 | 8 | function OAuthDevice(cdifDevice) { 9 | this.device = cdifDevice; 10 | } 11 | 12 | OAuthDevice.prototype.setOAuthAccessToken = function(params, callback) { 13 | if (this.oauth_version === '1.0') { 14 | var oauth_verifier = params.oauth_verifier; 15 | 16 | if (oauth_verifier == null) { 17 | callback(new Error('no valid oauth verifier')); 18 | return; 19 | } 20 | this.oauth.getOAuthAccessToken( 21 | this.oauth_token, 22 | this.oauth_token_secret, 23 | oauth_verifier, function(error, oauth_access_token, oauth_access_token_secret, results) { 24 | if (error) { 25 | return callback(new Error('cannot get oauth access token: ' + error.data)); 26 | } else { 27 | this.oauth_access_token = oauth_access_token; 28 | this.oauth_access_token_secret = oauth_access_token_secret; 29 | this.results = results; 30 | return callback(null); 31 | } 32 | }.bind(this)); 33 | } else if (this.oauth_version === '2.0') { 34 | // for oauth 2.0 use 127.0.0.1 instead of real host address 35 | params.redirect_uri = CdifUtil.getHostProtocol() + '127.0.0.1' + ':' + CdifUtil.getHostPort() + '/callback_url'; 36 | this.oauth2.getOAuthAccessToken(params.code, params, function(error, access_token, refresh_token, results) { 37 | if (error) { 38 | return callback(error); 39 | } else if (results.error) { 40 | return callback(new Error(JSON.stringify(results))); 41 | } else { 42 | this.oauth2_access_token = access_token; 43 | this.oauth2_refresh_token = refresh_token; 44 | this.results = results; 45 | return callback(null); 46 | } 47 | }.bind(this)); 48 | } else { 49 | return callback(new Error('cannot set oauth access token, only oauth 1.0 and 2.0 are supported')); 50 | } 51 | }; 52 | 53 | OAuthDevice.prototype.connect = function(user, pass, callback) { 54 | if (this.oauth_version === '1.0') { 55 | //TODO: update this after we mount callback url on reverse proxy server 56 | var requestUrl = this.oauth_requestUrl + '?oauth_callback=' + querystring.escape(CdifUtil.getHostProtocol() + CdifUtil.getHostIp() + ':' + CdifUtil.getHostPort() + '/callback_url?deviceID=' + this.deviceID); 57 | this.oauth = new OAuth(requestUrl, 58 | this.oauth_accessUrl || null, 59 | this.apiKey || '', 60 | this.apiSecret || '', 61 | this.oauth_version, 62 | null, 63 | this.oauth_signatureMethod || 'HMAC-SHA1', 64 | this.oauth_nonceSize || null, 65 | this.oauth_customHeaders || null); 66 | // below fields would be filled by oauth flow 67 | this.oauth_token = ''; 68 | this.oauth_token_secret = ''; 69 | this.oauth_access_token = ''; 70 | this.oauth_access_token_secret = ''; 71 | } else { 72 | this.oauth2 = new OAuth2(this.apiKey || '', 73 | this.apiSecret || '', 74 | this.oauth2_baseSite || '', 75 | this.oauth2_authorizePath || '', 76 | this.oauth2_accessTokenPath || '', 77 | this.oauth2_customHeaders); 78 | // below fields would be filled by oauth flow 79 | this.oauth2_access_token = ''; 80 | this.oauth2_refresh_token = ''; 81 | this.oauth2_results = {}; 82 | } 83 | 84 | var redirectUrl = null; 85 | 86 | if (this.oauth_version === '1.0') { 87 | if (this.oauth_requestUrl == null) { 88 | return callback(new Error('request Url not valid')); 89 | } 90 | 91 | this.oauth.getOAuthRequestToken(function(error, oauth_token, oauth_token_secret, results) { 92 | if (error) { 93 | return callback(new Error('connect failed, reason: ' + error.message)); 94 | } 95 | 96 | this.oauth_token = oauth_token; 97 | this.oauth_token_secret = oauth_token_secret; 98 | this.authorize_redirect_url = this.authorize_redirect_url || ''; 99 | redirectUrl = this.authorize_redirect_url + oauth_token; 100 | return callback(null, {'href': redirectUrl, 'method': 'GET'}); 101 | }.bind(this)); 102 | } else if (this.oauth_version === '2.0') { 103 | var authorize_params = {}; 104 | // for oauth 2.0 use 127.0.0.1 instead of real host address 105 | authorize_params.redirect_uri = CdifUtil.getHostProtocol() + '127.0.0.1' + ':' + CdifUtil.getHostPort() + '/callback_url'; 106 | 107 | // add vendor defined authorize params 108 | for (var i in this.oauth2_authorize_params) { 109 | authorize_params[i] = this.oauth2_authorize_params[i]; 110 | } 111 | // has to use 'state' to bring back deviceID on callback url... 112 | authorize_params.state = this.deviceID; 113 | 114 | redirectUrl = this.oauth2.getAuthorizeUrl(authorize_params); 115 | return callback(null, {'href': redirectUrl, 'method': 'GET'}); 116 | } else { 117 | return callback(new Error('only oauth 1.0 and 2.0 are supported'), null); 118 | } 119 | }; 120 | 121 | OAuthDevice.prototype.disconnect = function(callback) { 122 | // below fields are generated during oauth flow 123 | if (this.oauth_version === '1.0') { 124 | this.oauth_token = ''; 125 | this.oauth_token_secret = ''; 126 | this.oauth_access_token = ''; 127 | this.oauth_access_token_secret = ''; 128 | this.results = null; 129 | } else if (this.oauth_version === '2.0') { 130 | this.oauth2_access_token = ''; 131 | this.oauth2_refresh_token = ''; 132 | this.oauth2_results = null; 133 | } 134 | callback(null); 135 | }; 136 | 137 | OAuthDevice.prototype.createOAuthDevice = function() { 138 | if (this.device.oauth_version === '1.0') { 139 | this.device.oauth_token = ''; 140 | this.device.oauth_token_secret = ''; 141 | this.device.oauth_access_token = ''; 142 | this.device.oauth_access_token_secret = ''; 143 | this.device.results = null; 144 | } else if (this.device.oauth_version === '2.0') { 145 | this.device.oauth2_access_token = ''; 146 | this.device.oauth2_refresh_token = ''; 147 | this.device.oauth2_results = null; 148 | } 149 | 150 | this.device.isOAuthDevice = true; 151 | this.device._connect = this.connect.bind(this.device); 152 | this.device._disconnect = this.disconnect.bind(this.device); 153 | this.device._setOAuthAccessToken = this.setOAuthAccessToken.bind(this.device); 154 | }; 155 | 156 | 157 | module.exports = OAuthDevice; 158 | -------------------------------------------------------------------------------- /lib/route-manager.js: -------------------------------------------------------------------------------- 1 | var events = require('events'); 2 | var util = require('util'); 3 | var http = require('http'); 4 | var express = require('express'); 5 | var url = require('url'); 6 | var bodyParser = require('body-parser'); 7 | var morgan = require('morgan'); 8 | var CdifUtil = require('./cdif-util'); 9 | var SocketServer = require('./socket-server'); 10 | var WSServer = require('./ws-server'); 11 | var CdifInterface = require('./cdif-interface'); 12 | var Session = require('./session'); 13 | var CdifError = require('./error').CdifError; 14 | var options = require('./cli-options'); 15 | var logger = require('./logger'); 16 | 17 | var loginRoute = '/login'; 18 | var discoverRoute = '/discover'; 19 | var stopDiscoverRoute = '/stop-discover'; 20 | var deviceListRoute = '/device-list'; 21 | var deviceControlRoute = '/devices'; 22 | var connectRoute = '/connect'; 23 | var disconnectRoute = '/disconnect'; 24 | var actionInvokeRoute = '/invoke-action'; 25 | var eventSubRoute = '/event-sub'; 26 | var eventUnsubRoute = '/event-unsub'; 27 | var getDeviceSpecRoute = '/get-spec'; 28 | var getStateRoute = '/get-state'; 29 | var deviceSchemaRoute = '/schema'; 30 | var oauthCallbackUrl = '/callback_url'; 31 | 32 | var moduleInstallRoute = '/install-module'; 33 | var moduleUninstallRoute = '/uninstall-module'; 34 | 35 | function RouteManager(mm) { 36 | this.app = express(); 37 | 38 | this.moduleManager = mm; 39 | this.cdifInterface = new CdifInterface(mm); 40 | 41 | this.loginRouter = express.Router(); 42 | this.oauthCallbackRouter = express.Router(); 43 | 44 | if (options.allowDiscover) { 45 | this.discoverRouter = express.Router(); 46 | this.stopDiscoverRouter = express.Router(); 47 | } 48 | 49 | this.deviceListRouter = express.Router(); 50 | this.deviceControlRouter = express.Router(); 51 | this.connectRouter = express.Router({mergeParams: true}); 52 | this.disconnectRouter = express.Router({mergeParams: true}); 53 | this.actionInvokeRouter = express.Router({mergeParams: true}); 54 | this.getDeviceSpecRouter = express.Router({mergeParams: true}); 55 | this.getStateRouter = express.Router({mergeParams: true}); 56 | this.eventSubRouter = express.Router({mergeParams: true}); 57 | this.eventUnsubRouter = express.Router({mergeParams: true}); 58 | this.presentationRouter = express.Router({mergeParams: true}); 59 | this.deviceSchemaRouter = express.Router({mergeParams: true}); 60 | 61 | this.moduleInstallRouter = express.Router(); 62 | this.moduleUninstallRouter = express.Router(); 63 | 64 | this.server = http.createServer(this.app); 65 | 66 | if (options.isDebug === true) { 67 | this.app.use(morgan('dev')); 68 | } 69 | 70 | this.app.use(function(req, res, next) { 71 | res.header("Access-Control-Allow-Origin", "*"); 72 | res.header('Access-Control-Allow-Methods', 'GET, POST'); 73 | res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); 74 | next(); 75 | }); 76 | 77 | this.app.use(bodyParser.json()); 78 | 79 | // global routes 80 | this.app.use(loginRoute, this.loginRouter); 81 | this.app.use(oauthCallbackUrl, this.oauthCallbackRouter); 82 | this.app.use(moduleInstallRoute, this.moduleInstallRouter); 83 | this.app.use(moduleUninstallRoute, this.moduleUninstallRouter); 84 | 85 | if (options.allowDiscover) { 86 | this.app.use(discoverRoute, this.discoverRouter); 87 | this.app.use(stopDiscoverRoute, this.stopDiscoverRouter); 88 | } 89 | 90 | this.app.use(deviceListRoute, this.deviceListRouter); 91 | this.app.use(deviceControlRoute, this.deviceControlRouter); 92 | //per device routes 93 | this.deviceControlRouter.use('/:deviceID' + connectRoute, this.connectRouter); 94 | this.deviceControlRouter.use('/:deviceID' + disconnectRoute, this.disconnectRouter); 95 | this.deviceControlRouter.use('/:deviceID' + actionInvokeRoute, this.actionInvokeRouter); 96 | this.deviceControlRouter.use('/:deviceID' + getDeviceSpecRoute, this.getDeviceSpecRouter); 97 | this.deviceControlRouter.use('/:deviceID' + getStateRoute, this.getStateRouter); 98 | this.deviceControlRouter.use('/:deviceID' + eventSubRoute, this.eventSubRouter); 99 | this.deviceControlRouter.use('/:deviceID' + eventUnsubRoute, this.eventUnsubRouter); 100 | this.deviceControlRouter.use('/:deviceID' + deviceSchemaRoute, this.deviceSchemaRouter); 101 | 102 | this.cdifInterface.on('presentation', this.mountDevicePresentationPage.bind(this)); 103 | 104 | if (options.wsServer === true) { 105 | this.wsServer = new WSServer(this.server, this.cdifInterface); 106 | } else if (options.sioServer === true) { 107 | this.socketServer = new SocketServer(this.server, this.cdifInterface); 108 | this.socketServer.installHandlers(); 109 | } 110 | } 111 | 112 | util.inherits(RouteManager, events.EventEmitter); 113 | 114 | RouteManager.prototype.mountDevicePresentationPage = function(deviceID) { 115 | this.deviceControlRouter.use('/:deviceID/presentation', this.presentationRouter); 116 | 117 | var session = new Session(null, null); 118 | session.callback = function(err, deviceUrl) { 119 | if (!err) { 120 | this.presentationRouter.use('/', function(req, res) { 121 | var redirectedUrl = deviceUrl + req.url; 122 | res.redirect(redirectedUrl); 123 | }); 124 | } else { 125 | logger.error('cannot get device root url, error: ' + err.message); 126 | } 127 | }.bind(this); 128 | 129 | this.cdifInterface.getDeviceRootUrl(deviceID, session); 130 | }; 131 | 132 | //TODO: manage session creation and prevent double callback from drivers 133 | //TODO: sanity check to req data 134 | RouteManager.prototype.installRoutes = function() { 135 | this.loginRouter.route('/').post(function (req, res) { 136 | // to be filled by production code 137 | }); 138 | 139 | this.oauthCallbackRouter.route('/').get(function (req, res) { 140 | var session = new Session(req, res); 141 | var deviceID = null; 142 | var params = req.query; 143 | 144 | // console.log(params); 145 | if (params.state != null) { 146 | deviceID = params.state; // oauth 2.0 bring back device ID in state param 147 | } else { 148 | deviceID = params.deviceID; 149 | } 150 | 151 | this.cdifInterface.setDeviceOAuthAccessToken(deviceID, params, session); 152 | }.bind(this)); 153 | 154 | //TODO: protect this route with npm login information 155 | this.moduleInstallRouter.route('/').post(function (req, res) { 156 | var registry = req.body.registry; 157 | var name = req.body.name; 158 | var version = req.body.version; 159 | var session = new Session(req, res); 160 | 161 | this.moduleManager.installModuleFromRegistry(registry, name, version, session.callback); 162 | }.bind(this)); 163 | 164 | //TODO: protect this route with npm login information 165 | this.moduleUninstallRouter.route('/').post(function (req, res) { 166 | var name = req.body.name; 167 | var session = new Session(req, res); 168 | 169 | this.moduleManager.uninstallModule(name, session.callback); 170 | }.bind(this)); 171 | 172 | if (options.allowDiscover) { 173 | this.discoverRouter.route('/').post(function(req, res) { 174 | var session = new Session(req, res); 175 | 176 | this.cdifInterface.discoverAll(session); 177 | }.bind(this)); 178 | 179 | this.stopDiscoverRouter.route('/').post(function(req, res) { 180 | var session = new Session(req, res); 181 | 182 | this.cdifInterface.stopDiscoverAll(session); 183 | }.bind(this)); 184 | } 185 | 186 | this.deviceListRouter.route('/').get(function(req, res) { 187 | var session = new Session(req, res); 188 | 189 | this.cdifInterface.getDiscoveredDeviceList(session); 190 | }.bind(this)); 191 | 192 | this.connectRouter.route('/').post(function(req, res) { 193 | var session = new Session(req, res); 194 | 195 | var deviceID = req.params.deviceID; 196 | var user = req.body.username; 197 | var pass = req.body.password; 198 | 199 | if (user == null && pass == null) { 200 | user = ''; pass = ''; 201 | } else if (user == null || user === '') { 202 | return session.callback(new CdifError('must provide a username')); 203 | } else if (pass == null || pass === '') { 204 | return session.callback(new CdifError('must provide a password')); 205 | } 206 | 207 | this.cdifInterface.connectDevice(deviceID, user, pass, session); 208 | }.bind(this)); 209 | 210 | this.disconnectRouter.route('/').post(function(req, res) { 211 | var session = new Session(req, res); 212 | 213 | var deviceID = req.params.deviceID; 214 | var token = req.body.device_access_token; 215 | 216 | this.cdifInterface.disconnectDevice(deviceID, token, session); 217 | }.bind(this)); 218 | 219 | this.actionInvokeRouter.route('/').post(function(req, res) { 220 | var session = new Session(req, res); 221 | 222 | var deviceID = req.params.deviceID; 223 | var serviceID = req.body.serviceID; 224 | var actionName = req.body.actionName; 225 | var args = req.body.argumentList; 226 | var token = req.body.device_access_token; 227 | 228 | this.cdifInterface.invokeDeviceAction(deviceID, serviceID, actionName, args, token, session); 229 | }.bind(this)); 230 | 231 | this.getDeviceSpecRouter.route('/').get(function(req, res) { 232 | var session = new Session(req, res); 233 | 234 | var deviceID = req.params.deviceID; 235 | var token = req.body.device_access_token; 236 | 237 | this.cdifInterface.getDeviceSpec(deviceID, token, session); 238 | }.bind(this)); 239 | 240 | this.getStateRouter.route('/').get(function(req, res) { 241 | var session = new Session(req, res); 242 | 243 | var deviceID = req.params.deviceID; 244 | var serviceID = req.body.serviceID; 245 | var token = req.body.device_access_token; 246 | 247 | this.cdifInterface.getDeviceState(deviceID, serviceID, token, session); 248 | }.bind(this)); 249 | 250 | this.eventSubRouter.route('/').post(function(req, res) { 251 | var session = new Session(req, res); 252 | 253 | var deviceID = req.params.deviceID; 254 | var serviceID = req.body.serviceID; 255 | var token = req.body.device_access_token; 256 | 257 | this.cdifInterface.eventSubscribe(this.subscriber, deviceID, serviceID, token, session); 258 | }.bind(this)); 259 | 260 | this.eventUnsubRouter.route('/').post(function(req, res) { 261 | var session = new Session(req, res); 262 | 263 | var deviceID = req.params.deviceID; 264 | var serviceID = req.body.serviceID; 265 | var token = req.body.device_access_token; 266 | 267 | this.cdifInterface.eventUnsubscribe(this.subscriber, deviceID, serviceID, token, session); 268 | }.bind(this)); 269 | 270 | this.deviceSchemaRouter.route('/*').get(function(req, res) { 271 | var session = new Session(req, res); 272 | 273 | var deviceID = req.params.deviceID; 274 | var token = req.body.device_access_token; 275 | var path = req.url; 276 | 277 | this.cdifInterface.getDeviceSchema(deviceID, path, token, session); 278 | }.bind(this)); 279 | 280 | // test subscriber 281 | this.subscriber = new function() { 282 | this.publish = function(updated, data) { 283 | console.log(data); 284 | }; 285 | } 286 | 287 | this.server.listen(CdifUtil.getHostPort()); 288 | }; 289 | 290 | module.exports = RouteManager; 291 | -------------------------------------------------------------------------------- /lib/service.js: -------------------------------------------------------------------------------- 1 | var events = require('events'); 2 | var util = require('util'); 3 | var validator = require('./validator'); 4 | var logger = require('./logger'); 5 | var CdifError = require('./error').CdifError; 6 | var DeviceError = require('./error').DeviceError; 7 | var Domain = require('domain'); 8 | 9 | function Service(device, serviceID, spec) { 10 | this.device = device; 11 | this.serviceID = serviceID; 12 | this.serviceType = spec.serviceType; 13 | this.actions = {}; 14 | this.states = {}; 15 | 16 | this.updateSpec(spec); 17 | this.updateStateFromAction = this.updateStateFromAction.bind(this); 18 | } 19 | 20 | util.inherits(Service, events.EventEmitter); 21 | 22 | Service.prototype.addAction = function(actionName, action) { 23 | this.actions[actionName].invoke = action; 24 | }; 25 | 26 | Service.prototype.updateSpec = function(spec) { 27 | var actionList = spec.actionList; 28 | for (var i in actionList) { 29 | if (!this.actions[i]) { 30 | var action = actionList[i]; 31 | this.actions[i] = {}; 32 | this.actions[i].args = action.argumentList; // save for validation 33 | this.actions[i].invoke = null; // to be filled by device modules 34 | } 35 | } 36 | 37 | // TODO: to save memory usage we can reclaim spec object and dynamically reconstruct it on get-spec call 38 | var stateVariables = spec.serviceStateTable; 39 | for (var i in stateVariables) { 40 | if (!this.states[i]) { 41 | this.states[i] = {}; 42 | 43 | if (stateVariables[i].dataType === 'object') { 44 | this.states[i].variable = JSON.parse(JSON.stringify(stateVariables[i])); // save for schema deref 45 | 46 | var schemaRef = stateVariables[i].schema; 47 | if (schemaRef != null) { 48 | var self = this.states[i].variable; 49 | this.device.resolveSchemaFromPath(schemaRef, self, function(err, s, data) { 50 | if (!err) { 51 | s.schema = JSON.parse(JSON.stringify(data)); // reclaim doc object 52 | } // or else this is still a pointer 53 | }); 54 | } 55 | } else { 56 | this.states[i].variable = stateVariables[i]; 57 | } 58 | 59 | //TODO: need to deep clone this if we reclaim spec obj 60 | if (stateVariables[i].hasOwnProperty('defaultValue')) { 61 | this.states[i].value = stateVariables[i].defaultValue; 62 | } else { 63 | this.states[i].value = ''; 64 | } 65 | } 66 | } 67 | }; 68 | 69 | Service.prototype.getServiceStates = function(callback) { 70 | var output = {}; 71 | for (var i in this.states) { 72 | output[i] = this.states[i].value; 73 | } 74 | callback(null, output); 75 | }; 76 | 77 | Service.prototype.setServiceStates = function(values, callback) { 78 | var _this = this; 79 | var errorMessage = null; 80 | var updated = false; 81 | var sendEvent = false; 82 | var data = {}; 83 | 84 | if (typeof(values) !== 'object') { 85 | errorMessage = 'event data must be object'; 86 | } else { 87 | for (var i in values) { 88 | if (this.states[i] === undefined) { 89 | errorMessage = 'set invalid state for variable name: ' + i; 90 | break; 91 | } 92 | } 93 | } 94 | 95 | if (errorMessage === null) { 96 | for (var i in values) { 97 | validator.validate(i, this.states[i].variable, values[i], function(err) { 98 | if (!err) { 99 | if (typeof(values[i]) === 'object') { 100 | if (JSON.stringify(values[i]) !== JSON.stringify(_this.states[i].value)) { 101 | updated = true; 102 | _this.states[i].value = JSON.parse(JSON.stringify(values[i])); 103 | } 104 | } else { 105 | if (_this.states[i].value !== values[i]) { 106 | _this.states[i].value = values[i]; 107 | updated = true; 108 | } 109 | } 110 | } else { 111 | errorMessage = err.message; 112 | } 113 | }); 114 | if (errorMessage) break; 115 | 116 | // report only eventable data 117 | if (this.states[i].variable.sendEvents === true) { 118 | if (typeof(values[i]) === 'object') { 119 | data[i] = JSON.parse(JSON.stringify(values[i])); 120 | } else { 121 | data[i] = values[i]; 122 | } 123 | } 124 | } 125 | } 126 | 127 | if (errorMessage) { 128 | callback(new CdifError('setServiceStates error: ' + errorMessage)); 129 | } else { 130 | this.emit('serviceevent', updated, this.device.deviceID, this.serviceID, data); 131 | callback(null); 132 | } 133 | }; 134 | 135 | Service.prototype.updateStateFromAction = function(action, input, output, callback) { 136 | var updated = false; 137 | var data = {}; 138 | 139 | for (var i in input) { 140 | var argument = action.args[i]; 141 | if (argument == null) break; 142 | var stateVarName = argument.relatedStateVariable; 143 | if (stateVarName == null) break; 144 | if (argument.direction === 'in') { 145 | if (this.states[stateVarName].variable.sendEvents === true) { 146 | if (this.states[stateVarName].value !== input[i]) { 147 | data[stateVarName] = input[i]; 148 | updated = true; 149 | } 150 | } 151 | this.states[stateVarName].value = input[i]; 152 | } 153 | } 154 | 155 | for (var i in output) { 156 | var argument = action.args[i]; 157 | if (argument == null) break; 158 | var stateVarName = argument.relatedStateVariable; 159 | if (stateVarName == null) break; 160 | if (argument.direction === 'out') { 161 | if (this.states[stateVarName].variable.sendEvents === true) { 162 | if (this.states[stateVarName].value !== output[i]) { 163 | data[stateVarName] = output[i]; 164 | updated = true; 165 | } 166 | } 167 | this.states[stateVarName].value = output[i]; 168 | } 169 | } 170 | 171 | callback(updated, data); 172 | }; 173 | 174 | Service.prototype.validateActionCall = function(action, arguments, isInput, callback) { 175 | var argList = action.args; 176 | var failed = false; 177 | var error = null; 178 | 179 | if (arguments == null) { 180 | return callback(new CdifError('no valid arguments')); 181 | } 182 | 183 | // argument keys must match spec 184 | if (isInput) { 185 | for (var i in argList) { 186 | if (argList[i].direction === 'in') { 187 | if (arguments[i] === undefined) { 188 | failed = true; 189 | error = new CdifError('missing argument: ' + i); 190 | break; 191 | } 192 | } 193 | } 194 | } else { 195 | for (var i in argList) { 196 | if (argList[i].direction === 'out') { 197 | if (arguments[i] === undefined) { 198 | failed = true; 199 | error = new CdifError('missing output argument: ' + i); 200 | break; 201 | } 202 | } 203 | } 204 | } 205 | if (failed) { 206 | return callback(error); 207 | } 208 | // validate data 209 | for (var i in arguments) { 210 | var name = argList[i].relatedStateVariable; 211 | var stateVar = this.states[name].variable; 212 | 213 | if (isInput && argList[i].direction === 'out') { 214 | // only check out args on call return 215 | continue; 216 | } else { 217 | validator.validate(name, stateVar, arguments[i], function(err) { 218 | if (err) { 219 | error = new CdifError(err.message); 220 | failed = true; 221 | } 222 | }); 223 | } 224 | if (failed) break; 225 | } 226 | callback(error); 227 | }; 228 | 229 | Service.prototype.invokeAction = function(actionName, input, callback) { 230 | var _this = this; 231 | var action = this.actions[actionName]; 232 | 233 | if (action === undefined) { 234 | return callback(new CdifError('action not found: ' + actionName), null); 235 | } 236 | if (input === undefined) { 237 | return callback(new CdifError('cannot identify input arguments'), null); 238 | } 239 | if (action.invoke === null) { 240 | return callback(new DeviceError('action: ' + actionName + ' not implemented'), null); 241 | } 242 | this.validateActionCall(action, input, true, function(err) { 243 | if (err) { 244 | return callback(err, null); 245 | } 246 | 247 | var unsafeDomain = Domain.create(); 248 | unsafeDomain.on('error', function(err) { 249 | logger.error(err); 250 | return callback(err, null); 251 | }); 252 | 253 | unsafeDomain.run(function() { 254 | action.invoke(input, function(err, output) { 255 | if (err) { 256 | //TODO: validate the content of fault object according to its optional fault definition in device spec 257 | // API's formal fault definition, which can be in either simple or complex type, would make it more conformant to WSDL 258 | if (output && output.fault) { 259 | return callback(new DeviceError(err.message), output.fault); 260 | } 261 | return callback(new DeviceError(err.message), null); 262 | } 263 | _this.validateActionCall(action, output, false, function(error) { 264 | if (error) { 265 | return callback(error, null); 266 | } 267 | _this.updateStateFromAction(action, input, output, function(updated, data) { 268 | _this.emit('serviceevent', updated, _this.device.deviceID, _this.serviceID, data); 269 | }); 270 | callback(null, output); 271 | }); 272 | }); 273 | }); 274 | }); 275 | }; 276 | 277 | Service.prototype.setEventSubscription = function(subscribe, unsubscribe) { 278 | this.subscribe = subscribe; 279 | this.unsubscribe = unsubscribe; 280 | }; 281 | 282 | Service.prototype.subscribeEvent = function(onChange, callback) { 283 | if (this.subscribe) { 284 | this.subscribe(onChange, function(err) { 285 | if (err) { 286 | return callback(new DeviceError('event subscription failed: ' + err.message)); 287 | } 288 | callback(null); 289 | }); 290 | } else { 291 | // we can still send state change events upon action call 292 | callback(null); 293 | } 294 | }; 295 | 296 | Service.prototype.unsubscribeEvent = function(callback) { 297 | if (this.unsubscribe) { 298 | this.unsubscribe(function(err) { 299 | if (err) { 300 | return callback(new DeviceError('event unsubscription failed: ' + err.message)); 301 | } 302 | callback(null); 303 | }); 304 | } else { 305 | callback(null); 306 | } 307 | }; 308 | 309 | module.exports = Service; 310 | -------------------------------------------------------------------------------- /lib/session.js: -------------------------------------------------------------------------------- 1 | var CdifError = require('./error').CdifError; 2 | var DeviceError = require('./error').DeviceError; 3 | var Timer = require('./timer'); 4 | var uuid = require('uuid'); 5 | var logger = require('./logger'); 6 | 7 | function Session(req, res) { 8 | this.req = req; 9 | this.res = res; 10 | this.timers = {}; 11 | 12 | this.redirect = this.redirect.bind(this); 13 | this.callback = this.callback.bind(this); 14 | this.setDeviceTimer = this.setDeviceTimer.bind(this); 15 | this.clearDeviceTimer = this.clearDeviceTimer.bind(this); 16 | }; 17 | 18 | Session.prototype.redirect = function(url) { 19 | this.res.redirect(url); 20 | }; 21 | 22 | //TODO: consider wrap this with https://www.npmjs.com/package/once 23 | Session.prototype.callback = function(err, data) { 24 | // console.log(new Error().stack); 25 | if (this.res) { 26 | this.res.setHeader('Content-Type', 'application/json'); 27 | if (err) { 28 | if (data != null) { 29 | this.res.status(500).json({topic: err.topic, message: err.message, fault: data}); 30 | } else { 31 | this.res.status(500).json({topic: err.topic, message: err.message}); 32 | } 33 | return logger.error({req: this.req, error: err.message, fault: data}); 34 | } else { 35 | this.res.status(200).json(data); 36 | } 37 | } 38 | }; 39 | 40 | Session.prototype.setDeviceTimer = function(device, callback) { 41 | if (device.online === false) { 42 | return callback(new CdifError('set timer for an offlined device'), device, null); 43 | } 44 | 45 | // TODO: configurable max no. of parallel ops, it can be done by counting numbers of alive timers 46 | this.installTimer(device, function(err, device, timer) { 47 | if (err) { 48 | return callback(err, device, null); 49 | } 50 | callback(null, device, timer); 51 | }); 52 | }; 53 | 54 | Session.prototype.installTimer = function(device, callback) { 55 | var timer = new Timer(this); 56 | this.timers[timer.uuid] = timer; 57 | timer.once('expired', function(timer) { 58 | clearTimeout(timer.timeout); 59 | timer.session = null; 60 | delete this.timers[timer.uuid]; 61 | return this.callback(new DeviceError('device not responding'), null); 62 | }.bind(this)); 63 | callback(null, device, timer); 64 | }; 65 | 66 | Session.prototype.clearDeviceTimer = function(timer) { 67 | var uuid = timer.uuid; 68 | 69 | if (uuid == null) return false; 70 | 71 | clearTimeout(this.timers[uuid].timeout); 72 | delete this.timers[uuid]; 73 | return true; 74 | }; 75 | 76 | module.exports = Session; 77 | -------------------------------------------------------------------------------- /lib/socket-server.js: -------------------------------------------------------------------------------- 1 | var events = require('events'); 2 | var util = require('util'); 3 | var socketio = require('socket.io'); 4 | var SubscriberManager = require('./subscriber'); 5 | var Session = require('./session'); 6 | var logger = require('./logger'); 7 | 8 | function SocketServer(server, cdifInterface) { 9 | this.io = socketio.listen(server); 10 | this.cdifInterface = cdifInterface; 11 | this.subscriberManager = new SubscriberManager(this.io); 12 | } 13 | 14 | util.inherits(SocketServer, events.EventEmitter); 15 | 16 | SocketServer.prototype.installHandlers = function() { 17 | var _this = this; 18 | 19 | //TODO: support event id, cancel and subscription expire time 20 | this.io.sockets.on('connection', function(socket) { 21 | socket.on('subscribe', function (options) { 22 | var info; 23 | try { 24 | info = JSON.parse(options); 25 | } catch (e) { 26 | return; 27 | } 28 | var deviceID = info.deviceID; 29 | var serviceID = info.serviceID; 30 | var device_access_token = info.device_access_token; 31 | 32 | logger.info('client subscribe to events of deviceID: ' + deviceID + ', serviceID: '+ serviceID); 33 | _this.subscriberManager.getSubscriber(socket.id, _this.io, info, function(subscriber) { 34 | var session = new Session(null, null); 35 | 36 | session.callback = function(err) { 37 | if (err) { 38 | _this.io.to(socket.id).emit('error', {topic: err.topic, message: err.message}); 39 | _this.subscriberManager.removeSubscriber(socket.id, function(){}); 40 | } 41 | } 42 | 43 | _this.cdifInterface.eventSubscribe(subscriber, deviceID, serviceID, device_access_token, session); 44 | }); 45 | }); 46 | socket.on('disconnect', function() { 47 | _this.subscriberManager.removeSubscriber(socket.id, function(err, subscriber, info) { 48 | if (!err) { 49 | var deviceID = info.deviceID; 50 | var serviceID = info.serviceID; 51 | var device_access_token = info.device_access_token; 52 | 53 | var session = new Session(null, null); 54 | session.callback = function(err) { 55 | if (err) { 56 | logger.error('unsubscribe failed: ' + err.message); 57 | } 58 | } 59 | 60 | _this.cdifInterface.eventUnsubscribe(subscriber, deviceID, serviceID, device_access_token, session); 61 | } 62 | }); 63 | }); 64 | }); 65 | }; 66 | 67 | 68 | module.exports = SocketServer; 69 | -------------------------------------------------------------------------------- /lib/subscriber.js: -------------------------------------------------------------------------------- 1 | function SubscriberManager(io) { 2 | this.io = io; 3 | this.subscriberList = {}; 4 | } 5 | 6 | SubscriberManager.prototype.getSubscriber = function(id, io, info, callback) { 7 | if (this.subscriberList[id] == null) { 8 | var subscriber = new Subscriber(io, id, info); 9 | this.subscriberList[id] = subscriber; 10 | callback(subscriber); 11 | } else { 12 | callback(this.subscriberList[id]); 13 | } 14 | }; 15 | 16 | SubscriberManager.prototype.removeSubscriber = function(id, callback) { 17 | var subscriber = this.subscriberList[id]; 18 | if (subscriber == null) { 19 | return callback(new Error('cannot remove non existed subscriber')); 20 | } 21 | callback(null, subscriber, subscriber.info); 22 | this.subscriberList[id] = null; 23 | }; 24 | 25 | function Subscriber(io, id, info) { 26 | this.io = io; 27 | this.id = id; 28 | this.info = info; 29 | this.publish = this.publish.bind(this); 30 | } 31 | 32 | Subscriber.prototype.publish = function(updated, deviceID, serviceID, data) { 33 | if (updated || !this.info.onUpdate) { 34 | this.io.to(this.id).emit('event', {timeStamp: Date.now(), deviceID: deviceID, serviceID: serviceID, eventData: data}); 35 | } 36 | }; 37 | 38 | module.exports = SubscriberManager; 39 | -------------------------------------------------------------------------------- /lib/timeout.js: -------------------------------------------------------------------------------- 1 | function Timeout(device, eventName, callback) { 2 | this.device = device; 3 | this.eventName = eventName; 4 | this.callback = callback; 5 | this.expire = function() { 6 | this.device.emit(this.eventName, this.device, this.eventName, this.callback); 7 | }.bind(this); 8 | } 9 | 10 | module.exports = Timeout; 11 | -------------------------------------------------------------------------------- /lib/timer.js: -------------------------------------------------------------------------------- 1 | var events = require('events'); 2 | var util = require('util'); 3 | var CdifError = require('./error').CdifError; 4 | var DeviceError = require('./error').DeviceError; 5 | var uuid = require('uuid'); 6 | 7 | function Timer(session) { 8 | this.session = session; 9 | this.uuid = uuid.v4(); 10 | this.expired = false; 11 | 12 | this.timeout = setTimeout(this.expire.bind(this), 10000); 13 | } 14 | 15 | util.inherits(Timer, events.EventEmitter); 16 | 17 | Timer.prototype.expire = function() { 18 | // if (this.device.connectionState !== 'disconnected') this.device.connectionState = 'disconnected'; 19 | this.expired = true; 20 | this.emit('expired', this); 21 | // delete this.session.timers[this.uuid]; 22 | // this.session.callback(new DeviceError('device not responding'), null); 23 | }; 24 | 25 | module.exports = Timer; 26 | -------------------------------------------------------------------------------- /lib/validator.js: -------------------------------------------------------------------------------- 1 | var options = require('../lib/cli-options'); 2 | var AJV = require('ajv'); 3 | var ajv = new AJV({schemaId: 'auto', jsonPointers: true}); 4 | 5 | ajv.addMetaSchema(require('ajv/lib/refs/json-schema-draft-04.json')); 6 | 7 | var deviceRootSchema = require('../spec/schema.json'); 8 | var deviceSchemaValidator = ajv.compile(deviceRootSchema); 9 | 10 | module.exports = { 11 | getSchemaValidator: function() { 12 | return ajv; 13 | }, 14 | 15 | validate: function(name, varDef, data, callback) { 16 | var type = varDef.dataType; 17 | var range = varDef.allowedValueRange; 18 | var list = varDef.allowedValueList; 19 | 20 | var errorMessage = null; 21 | var errorInfo = null; 22 | 23 | if (type == null) { //check null and undefined 24 | errorMessage = '非法变量类型'; errorInfo = name; 25 | } 26 | if (data === null) { 27 | errorMessage = '空数据'; errorInfo = name; 28 | } 29 | if (errorMessage === null) { 30 | switch (type) { 31 | case 'number': 32 | if (typeof(data) !== 'number') { 33 | errorMessage = '数据不是number类型'; errorInfo = name; 34 | } 35 | break; 36 | case 'integer': 37 | if (typeof(data) !== 'number' || (data % 1) !== 0) { 38 | errorMessage = '数据不是integer类型'; errorInfo = name; 39 | } 40 | break; 41 | case 'string': 42 | if (typeof(data) !== 'string') { 43 | errorMessage = '数据不是string类型'; errorInfo = name; 44 | } 45 | break; 46 | case 'boolean': 47 | if (typeof(data) !== 'boolean') { 48 | errorMessage = '数据不是boolean类型'; errorInfo = name; 49 | } 50 | break; 51 | case 'object': 52 | if (typeof(data) !== 'object' && !Array.isArray(data)) { 53 | errorMessage = '数据不是object类型'; errorInfo = name; 54 | } else { 55 | var schema = varDef.schema; 56 | if (schema == null) { // check both null and undefined 57 | errorMessage = '数据没有schema对象'; errorInfo = name; 58 | } else if (typeof(schema) !== 'object') { 59 | errorMessage = '数据schema对象非法'; errorInfo = name; 60 | } else { 61 | var validator = varDef.validator; 62 | if (validator == null) { 63 | errorMessage = 'schema校验器不可用'; errorInfo = name; 64 | } else { 65 | try { 66 | if (!validator(data)) { 67 | errorMessage = '数据校验失败'; 68 | errorInfo = this.getValidatorErrorInfo(validator.errors[0]); 69 | } 70 | } catch (e) { 71 | errorMessage = '数据校验异常'; errorInfo = name + e.message; 72 | } 73 | } 74 | } 75 | } 76 | break; 77 | default: 78 | errorMessage = '数据校验失败'; errorInfo = name + '未知数据类型: ' + type; 79 | break; 80 | } 81 | } 82 | if (errorMessage === null) { 83 | if (range) { 84 | if (data > range.maximum || data < range.minimum) { 85 | errorMessage = '数据超过允许范围'; errorInfo = name; 86 | } 87 | } 88 | if (list) { 89 | var matched = false; 90 | for (var i in list) { 91 | if (data === list[i]) matched = true; 92 | } 93 | if (matched === false) { 94 | errorMessage = '数据不在允许列表中'; errorInfo = name; 95 | } 96 | } 97 | } 98 | callback(errorMessage, errorInfo); 99 | }, 100 | 101 | getValidatorErrorInfo: function(error) { 102 | return {dataPath: error.dataPath, schemaPath: error.schemaPath, validatorMessage: error.message}; 103 | }, 104 | 105 | validateDeviceSpec: function(spec, callback) { 106 | var errorMessage = null; 107 | 108 | try { 109 | if (!deviceSchemaValidator(spec)) { 110 | errorMessage = deviceSchemaValidator.errors[0].message; 111 | } 112 | } catch (e) { 113 | errorMessage = e.message; 114 | } 115 | 116 | if (errorMessage) { 117 | return callback(new Error('device spec validation failed, reason: ' + errorMessage)); 118 | } 119 | 120 | //find all matching relatedStateVariable 121 | var serviceList = spec.device.serviceList; 122 | 123 | for (var serviceID in serviceList) { 124 | var service = serviceList[serviceID]; 125 | var stateTable = service.serviceStateTable; 126 | var actionList = service.actionList; 127 | 128 | for (var actionName in actionList) { 129 | var action = actionList[actionName]; 130 | 131 | if (options.allowSimpleType !== true) { 132 | if (Object.keys(action.argumentList).length > 2) { 133 | return callback(new Error('argumentList cannot contain arguments other than input and output. Service: ' + serviceID + ', action: ' + actionName)); 134 | } 135 | if (action.argumentList.input == null) { 136 | return callback(new Error('missing input argument. Service: ' + serviceID + ', action: ' + actionName)); 137 | } 138 | if (action.argumentList.output == null) { 139 | return callback(new Error('missing output argument. Service: ' + serviceID + ', action: ' + actionName)); 140 | } 141 | } 142 | 143 | for (var argumentName in action.argumentList) { 144 | var argument = action.argumentList[argumentName]; 145 | var stateVariableName = argument.relatedStateVariable; 146 | var stateVariable = stateTable[stateVariableName]; 147 | 148 | if (stateVariable == null || typeof(stateVariable) !== 'object') { 149 | return callback(new Error('device spec validation failed, no matching state variable definition for service: ' + serviceID + ', action: ' + actionName + ', argument: ' + argumentName)); 150 | } 151 | if (stateVariable.dataType !== 'object' && options.allowSimpleType !== true) { 152 | return callback(new Error('state variable dataType must be object. Service: ' + serviceID + ' State variable name: ' + stateVariableName)); 153 | } 154 | } 155 | } 156 | } 157 | callback(null); 158 | } 159 | }; 160 | -------------------------------------------------------------------------------- /lib/ws-server.js: -------------------------------------------------------------------------------- 1 | var events = require('events'); 2 | var util = require('util'); 3 | var url = require('url'); 4 | var CdifError = require('./error').CdifError; 5 | var WebSocketServer = require('ws').Server; 6 | var WSSubscriberManager = require('./ws-subscriber'); 7 | var logger = require('./logger'); 8 | 9 | function WSServer(server, cdifInterface) { 10 | this.wss = new WebSocketServer( 11 | { server: server, 12 | // TODO: improve security check in production environment 13 | // See https://github.com/websockets/ws/blob/1242a8ca0de7668fc5fe1ddbfba09d42e95aa7cc/doc/ws.md 14 | verifyClient: function(info) { 15 | // console.log(info); 16 | return true; 17 | } 18 | } 19 | ); 20 | this.cdifInterface = cdifInterface; 21 | this.subscriberManager = new WSSubscriberManager(); 22 | 23 | this.wss.on('connection', this.onNewConnection.bind(this)); 24 | this.wss.on('error', this.onServerError.bind(this)); 25 | } 26 | 27 | WSServer.prototype.getEventSubscriber = function(ws, clientKey, options, callback) { 28 | this.subscriberManager.getEventSubscriber(ws, clientKey, options, callback); 29 | }; 30 | 31 | WSServer.prototype.findEventSubscriber = function(ws, clientKey, options, callback) { 32 | this.subscriberManager.findEventSubscriber(ws, clientKey, options, callback); 33 | }; 34 | 35 | WSServer.prototype.removeEventSubscriber = function(subscriber, callback) { 36 | this.subscriberManager.removeEventSubscriber(subscriber, callback); 37 | }; 38 | 39 | WSServer.prototype.findAllEventSubsribers = function(ws, clientKey, callback) { 40 | this.subscriberManager.findAllEventSubsribers(ws, clientKey, callback); 41 | }; 42 | 43 | //TODO: fix callback by using session object 44 | WSServer.prototype.eventSubscribe = function(subscriber, deviceID, serviceID, device_access_token, callback) { 45 | this.cdifInterface.eventSubscribe(subscriber, deviceID, serviceID, device_access_token, callback); 46 | }; 47 | 48 | WSServer.prototype.eventUnsubscribe = function(subscriber, deviceID, serviceID, device_access_token, callback) { 49 | this.cdifInterface.eventUnsubscribe(subscriber, deviceID, serviceID, device_access_token, callback); 50 | }; 51 | 52 | WSServer.prototype.onNewConnection = function(ws) { 53 | // TODO: see http://stackoverflow.com/a/16395220/151312 on how to parse cookie in production environment 54 | // var location = url.parse(ws.upgradeReq.url, true); 55 | // it seems we have to use client key to uniquely identify a client connection.. 56 | if (ws.upgradeReq.headers['sec-websocket-key'] == null) { 57 | logger.error('no valid client key'); 58 | return ws.terminate(); 59 | } 60 | 61 | ws.wsServer = this; 62 | 63 | ws.on('open', this.onSocketOpen.bind(ws)); 64 | ws.on('close', this.onSocketClose.bind(ws)); 65 | ws.on('message', this.onInputMessage.bind(ws)); 66 | ws.on('error', this.onSocketError.bind(ws)); 67 | ws.on('ping', this.onPing.bind(ws)); 68 | ws.on('pong', this.onPong.bind(ws)); 69 | }; 70 | 71 | WSServer.prototype.onServerError = function(error) { 72 | logger.error('websocket server error: ' + error); 73 | }; 74 | 75 | // TODO: better not cache access_token in memory considering multi-user support 76 | // below code belongs to ws instance 77 | WSServer.prototype.onSocketOpen = function() { 78 | }; 79 | 80 | WSServer.prototype.onSocketClose = function(code, message) { 81 | var clientKey = this.upgradeReq.headers['sec-websocket-key']; 82 | 83 | this.wsServer.findAllEventSubsribers(this, clientKey, function(err, subscribers) { 84 | if (subscribers == null) return; 85 | 86 | for (var s in subscribers) { 87 | var subscriber = subscribers[s]; 88 | var opt = subscriber.options; 89 | 90 | this.wsServer.eventUnsubscribe(subscriber, opt.deviceID, opt.serviceID, opt.device_access_token, function(err) { 91 | if (err) { 92 | logger.error(err); 93 | } 94 | this.wsServer.removeEventSubscriber(subscriber, function(error, subscriber, options) { 95 | if (error) { 96 | logger.error(error); 97 | } 98 | }.bind(this)); 99 | }.bind(this)); 100 | } 101 | }.bind(this)); 102 | 103 | this.terminate(); 104 | }; 105 | 106 | // TODO: support event id, subscription renew 107 | WSServer.prototype.onInputMessage = function(message, flags) { 108 | // assume no need to check mask flag 109 | if (flags.binary) return; 110 | 111 | var inputMessage = null; 112 | 113 | try { 114 | inputMessage = JSON.parse(message); 115 | } catch (e) { 116 | return this.send(JSON.stringify({topic: 'cdif error', message: 'socket input message not in JSON format'})); 117 | } 118 | 119 | switch(inputMessage.topic) { 120 | case 'subscribe': 121 | var options = inputMessage.options; 122 | if (options == null) { 123 | return this.send(JSON.stringify({topic: 'cdif error', message: 'socket subscription options invalid'})); 124 | } 125 | 126 | var deviceID = options.deviceID; 127 | var serviceID = options.serviceID; 128 | var device_access_token = options.device_access_token; 129 | 130 | if (deviceID == null || serviceID == null) { 131 | return this.send(JSON.stringify({topic: 'cdif error', message: 'unknown socket subscription options'})); 132 | } 133 | 134 | var clientKey = this.upgradeReq.headers['sec-websocket-key']; 135 | 136 | this.wsServer.getEventSubscriber(this, clientKey, options, function(subscriber, created) { 137 | if (!created) return; 138 | 139 | this.wsServer.eventSubscribe(subscriber, deviceID, serviceID, device_access_token, function(err) { 140 | if (err) { 141 | this.send(JSON.stringify({topic: err.topic, message: err.message})); 142 | this.wsServer.removeEventSubscriber(subscriber, function(){}); 143 | } 144 | }.bind(this)); 145 | }.bind(this)); 146 | break; 147 | case 'unsubscribe': 148 | var options = inputMessage.options; 149 | if (options == null) { 150 | return this.send(JSON.stringify({topic: 'cdif error', message: 'socket unsubscription options invalid'})); 151 | } 152 | 153 | var deviceID = options.deviceID; 154 | var serviceID = options.serviceID; 155 | var device_access_token = options.device_access_token; 156 | 157 | if (deviceID == null || serviceID == null) { 158 | return this.send(JSON.stringify({topic: 'cdif error', message: 'unknown socket unsubscription options'})); 159 | } 160 | 161 | var clientKey = this.upgradeReq.headers['sec-websocket-key']; 162 | 163 | this.wsServer.findEventSubscriber(this, clientKey, options, function(subscriber) { 164 | if (subscriber === null) { 165 | return this.send(JSON.stringify({topic: 'cdif error', message: 'cannot find existing event subscription'})); 166 | } 167 | var opt = subscriber.options; 168 | this.wsServer.eventUnsubscribe(subscriber, opt.deviceID, opt.serviceID, opt.device_access_token, function(err) { 169 | if (err) { 170 | return this.send(JSON.stringify({topic: 'cdif error', message: err.message})); 171 | } 172 | this.wsServer.removeEventSubscriber(subscriber, function(error, subscriber, options) { 173 | if (error) { 174 | return this.send(JSON.stringify({topic: 'cdif error', message: error.message})); 175 | } 176 | }.bind(this)); 177 | }.bind(this)); 178 | }.bind(this)); 179 | break; 180 | default: 181 | return this.send(JSON.stringify({topic: 'cdif error', message: 'unknown socket input message'})); 182 | break; 183 | } 184 | }; 185 | 186 | WSServer.prototype.onSocketError = function(error) { 187 | logger.error('websocket error: ' + error); 188 | }; 189 | 190 | WSServer.prototype.onPing = function(data, flags) { 191 | 192 | }; 193 | 194 | WSServer.prototype.onPong = function(data, flags) { 195 | 196 | }; 197 | 198 | module.exports = WSServer; 199 | -------------------------------------------------------------------------------- /lib/ws-subscriber.js: -------------------------------------------------------------------------------- 1 | function Subscriber(ws, clientKey, options) { 2 | this.ws = ws; 3 | this.clientKey = clientKey; 4 | this.options = options; 5 | this.publish = this.publish.bind(this); 6 | } 7 | 8 | Subscriber.prototype.publish = function(updated, deviceID, serviceID, data) { 9 | // only send updated data, subject to change if we need to do api logging 10 | if (updated) { 11 | this.ws.send(JSON.stringify({timeStamp: Date.now(), deviceID: deviceID, serviceID: serviceID, eventData: data})); 12 | } 13 | }; 14 | 15 | function WSSubscriberManager() { 16 | this.subscriberList = {}; 17 | } 18 | 19 | WSSubscriberManager.prototype.getEventSubscriber = function(ws, clientKey, options, callback) { 20 | var deviceID = options.deviceID; 21 | var serviceID = options.serviceID; 22 | var optionKey = deviceID + '/' + serviceID; 23 | 24 | if (this.subscriberList[clientKey] && this.subscriberList[clientKey][optionKey]) { 25 | return callback(this.subscriberList[clientKey][optionKey], false); 26 | } 27 | 28 | var subscriber = new Subscriber(ws, clientKey, options); 29 | 30 | if (this.subscriberList[clientKey] == null) { 31 | this.subscriberList[clientKey] = {}; 32 | } 33 | 34 | this.subscriberList[clientKey][optionKey] = subscriber; 35 | return callback(subscriber, true); 36 | }; 37 | 38 | WSSubscriberManager.prototype.findEventSubscriber = function(ws, clientKey, options, callback) { 39 | var deviceID = options.deviceID; 40 | var serviceID = options.serviceID; 41 | var optionKey = deviceID + '/' + serviceID; 42 | 43 | if (!this.subscriberList[clientKey] || !this.subscriberList[clientKey][optionKey]) { 44 | return callback(null); 45 | } 46 | return callback(this.subscriberList[clientKey][optionKey]); 47 | }; 48 | 49 | WSSubscriberManager.prototype.removeEventSubscriber = function(subscriber, callback) { 50 | var clientKey = subscriber.clientKey; 51 | var deviceID = subscriber.options.deviceID; 52 | var serviceID = subscriber.options.serviceID; 53 | var optionKey = deviceID + '/' + serviceID; 54 | 55 | delete this.subscriberList[clientKey][optionKey]; 56 | 57 | callback(null, subscriber, subscriber.options); 58 | }; 59 | 60 | WSSubscriberManager.prototype.findAllEventSubsribers = function(ws, clientKey, callback) { 61 | callback(null, this.subscriberList[clientKey]); 62 | } 63 | 64 | module.exports = WSSubscriberManager; 65 | -------------------------------------------------------------------------------- /module-manager.js: -------------------------------------------------------------------------------- 1 | var events = require('events'); 2 | var util = require('util'); 3 | var supported_modules = require('./modules.json'); 4 | 5 | //var forever = require('forever-monitor'); 6 | 7 | var modules = {}; 8 | 9 | function ModuleManager() { 10 | this.on('moduleload', this.onModuleLoad.bind(this)); 11 | this.on('moduleunload', this.onModuleUnload.bind(this)); 12 | } 13 | 14 | util.inherits(ModuleManager, events.EventEmitter); 15 | 16 | ModuleManager.prototype.onModuleLoad = function(name, module) { 17 | console.log('module: ' + name + ' loaded'); 18 | var m = modules[name]; 19 | if (m == null) { 20 | modules[name] = {}; 21 | } 22 | modules[name].module = module; 23 | modules[name].state = 'loaded'; 24 | //module.discoverDevices(); 25 | }; 26 | 27 | ModuleManager.prototype.onModuleUnload = function(name) { 28 | console.log('module: ' + name + ' unloaded'); 29 | var m = modules[name]; 30 | if (m != null) { 31 | modules[name].module = null; 32 | modules[name].state = 'unloaded'; 33 | } 34 | }; 35 | 36 | ModuleManager.prototype.discoverAllDevices = function() { 37 | var map = modules; 38 | 39 | for (var i in map) { 40 | if (map[i].state === 'loaded') { 41 | map[i].module.discoverDevices(); 42 | } 43 | } 44 | }; 45 | 46 | ModuleManager.prototype.stopDiscoverAllDevices = function() { 47 | var map = modules; 48 | 49 | for (var i in map) { 50 | if (map[i].state === 'loaded') { 51 | map[i].module.stopDiscoverDevices(); 52 | } 53 | } 54 | }; 55 | 56 | ModuleManager.prototype.onDeviceOnline = function(device, module) { 57 | this.emit('deviceonline', device, module); 58 | }; 59 | 60 | ModuleManager.prototype.onDeviceOffline = function(device, module) { 61 | this.emit('deviceoffline', device, module); 62 | }; 63 | 64 | function checkModuleExports(module) { 65 | // TODO: check module actually inherits from EventEmitter 66 | var proto = module.prototype; 67 | return proto.hasOwnProperty('discoverDevices'); 68 | } 69 | 70 | ModuleManager.prototype.loadModules = function() { 71 | var _mm = this; 72 | 73 | supported_modules.forEach(function(item) { 74 | var mod = require(item); 75 | var m = new mod(); 76 | m.on('deviceonline', _mm.onDeviceOnline.bind(_mm)); 77 | m.on('deviceoffline', _mm.onDeviceOffline.bind(_mm)); 78 | _mm.emit('moduleload', item, m); 79 | }); 80 | } 81 | 82 | module.exports = ModuleManager; 83 | -------------------------------------------------------------------------------- /modules.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/out4b/cdif/d8c9b706d7835151bc709d8a277d2716b6e3cafb/modules.db -------------------------------------------------------------------------------- /modules.json: -------------------------------------------------------------------------------- 1 | [ 2 | "cdif-ble-manager", 3 | "cdif-onvif-manager", 4 | "cdif-openzwave", 5 | "cdif-oauth-manager", 6 | "cdif-paypal" 7 | ] 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cdif", 3 | "version": "0.4.4", 4 | "description": "Common device interconnect framework", 5 | "bin": "./bin/cdif", 6 | "main": "./bin/framework.js", 7 | "scripts": { 8 | "start": "./bin/cdif", 9 | "start-allow-discover": "NODE_PATH=./lib node ./framework.js --allowDiscover", 10 | "start-heap-dump": "NODE_PATH=./lib node --expose-gc ./framework.js --heapDump", 11 | "test": "mocha" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/out4b/cdif.git" 16 | }, 17 | "keywords": [ 18 | "smart home", 19 | "IoT", 20 | "Bluetooth", 21 | "ONVIF", 22 | "Z-Wave", 23 | "OAuth", 24 | "SOA", 25 | "WSDL" 26 | ], 27 | "author": "out4b", 28 | "license": "Apache-2.0", 29 | "dependencies": { 30 | "ajv": "^6.10.0", 31 | "bcrypt": "^3.0.6", 32 | "body-parser": "^1.19.0", 33 | "bunyan": "^1.8.12", 34 | "express": "^4.16.4", 35 | "json-schema-ref-parser": "^6.1.0", 36 | "jsonwebtoken": "^8.5.1", 37 | "minimist": "^1.2.0", 38 | "mkdirp": "^0.5.1", 39 | "morgan": "^1.9.1", 40 | "oauth": "^0.9.15", 41 | "semver": "^6.0.0", 42 | "socket.io": "^2.2.0", 43 | "sqlite3": "^4.0.6", 44 | "uuid": "^3.3.2" 45 | }, 46 | "devDependencies": { 47 | "async": "^2.6.2", 48 | "json-schema-faker": "^0.5.0-rc16", 49 | "should": "^13.2.3", 50 | "supertest": "^4.0.2" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /spec/BasicDevice.json: -------------------------------------------------------------------------------- 1 | { 2 | "configId": 1, 3 | "specVersion": { 4 | "major": 1, 5 | "minor": 0 6 | }, 7 | "device": { 8 | "deviceType": "urn:cdif-net:device:Basic:1", 9 | "friendlyName": "device name", 10 | "manufacturer": "manufacturer name", 11 | "manufacturerURL": "manufacturer URL", 12 | "modelDescription": "device full description", 13 | "modelName": "device model name", 14 | "modelNumber": "device model number", 15 | "serialNumber": "device serial number", 16 | "UPC": "universal product code", 17 | "userAuth": false, 18 | "powerIndex": 20, 19 | "iconList": [ 20 | { 21 | "mimetype": "image/format", 22 | "width": 80, 23 | "height": 100, 24 | "depth": 16, 25 | "url": "icon URL" 26 | } 27 | ] 28 | } 29 | } -------------------------------------------------------------------------------- /spec/BinaryLight.json: -------------------------------------------------------------------------------- 1 | { 2 | "configId": 1, 3 | "specVersion": { 4 | "major": 1, 5 | "minor": 0 6 | }, 7 | "device": { 8 | "deviceType": "urn:cdif-net:device:BinaryLight:1", 9 | "friendlyName": "device name", 10 | "manufacturer": "manufacturer name", 11 | "manufacturerURL": "manufacturer URL", 12 | "modelDescription": "device full description", 13 | "modelName": "device model name", 14 | "modelNumber": "device model number", 15 | "serialNumber": "device serial number", 16 | "UPC": "universal product code", 17 | "userAuth": false, 18 | "powerIndex": 40, 19 | "iconList": [ 20 | { 21 | "mimetype": "image/format", 22 | "width": 80, 23 | "height": 100, 24 | "depth": 16, 25 | "url": "icon URL" 26 | } 27 | ], 28 | "serviceList": { 29 | "urn:cdif-net:serviceID:BinarySwitch": { 30 | "serviceType": "urn:cdif-net:service:SwitchPower:1", 31 | "actionList": { 32 | "getState": { 33 | "argumentList": { 34 | "stateValue": { 35 | "retval": true, 36 | "direction": "out", 37 | "relatedStateVariable": "state" 38 | } 39 | } 40 | }, 41 | "setState": { 42 | "argumentList": { 43 | "stateValue": { 44 | "direction": "in", 45 | "relatedStateVariable": "state" 46 | } 47 | } 48 | } 49 | }, 50 | "serviceStateTable": { 51 | "state": { 52 | "sendEvents": true, 53 | "dataType": "boolean", 54 | "defaultValue": true 55 | } 56 | } 57 | } 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /spec/DimmableLight.json: -------------------------------------------------------------------------------- 1 | { 2 | "configId": 1, 3 | "specVersion": { 4 | "major": 1, 5 | "minor": 0 6 | }, 7 | "device": { 8 | "deviceType": "urn:cdif-net:device:DimmableLight:1", 9 | "friendlyName": "device name", 10 | "manufacturer": "manufacturer name", 11 | "manufacturerURL": "manufacturer URL", 12 | "modelDescription": "device full description", 13 | "modelName": "device model name", 14 | "modelNumber": "device model number", 15 | "serialNumber": "device serial number", 16 | "UPC": "universal product code", 17 | "userAuth": false, 18 | "powerIndex": 40, 19 | "iconList": [ 20 | { 21 | "mimetype": "image/format", 22 | "width": 80, 23 | "height": 100, 24 | "depth": 16, 25 | "url": "icon URL" 26 | } 27 | ], 28 | "serviceList": { 29 | "urn:cdif-net:serviceID:BinarySwitch": { 30 | "serviceType": "urn:cdif-net:service:SwitchPower:1", 31 | "actionList": { 32 | "getState": { 33 | "argumentList": { 34 | "stateValue": { 35 | "retval": true, 36 | "direction": "out", 37 | "relatedStateVariable": "state" 38 | } 39 | } 40 | }, 41 | "setState": { 42 | "argumentList": { 43 | "stateValue": { 44 | "direction": "in", 45 | "relatedStateVariable": "state" 46 | } 47 | } 48 | } 49 | }, 50 | "serviceStateTable": { 51 | "state": { 52 | "sendEvents": true, 53 | "dataType": "boolean", 54 | "defaultValue": true 55 | } 56 | } 57 | }, 58 | "urn:cdif-net:serviceID:Dimming": { 59 | "serviceType": "urn:cdif-net:service:Dimming:1", 60 | "actionList": { 61 | "setLoadLevelState": { 62 | "argumentList": { 63 | "newLoadLevelState": { 64 | "direction": "in", 65 | "relatedStateVariable": "loadLevelState" 66 | } 67 | } 68 | }, 69 | "getLoadLevelState": { 70 | "argumentList": { 71 | "loadLevelState": { 72 | "direction": "out", 73 | "retval": true, 74 | "relatedStateVariable": "loadLevelState" 75 | } 76 | } 77 | }, 78 | "setOnEffectLevel": { 79 | "argumentList": { 80 | "newOnEffectLevel": { 81 | "direction": "in", 82 | "relatedStateVariable": "onEffectLevel" 83 | } 84 | } 85 | }, 86 | "getOnEffectLevel": { 87 | "argumentList": { 88 | "onEffectLevel": { 89 | "direction": "out", 90 | "retval": true, 91 | "relatedStateVariable": "onEffectLevel" 92 | } 93 | } 94 | } 95 | }, 96 | "serviceStateTable": { 97 | "loadLevelState": { 98 | "sendEvents": true, 99 | "dataType": "number", 100 | "allowedValueRange": { 101 | "minimum": 0, 102 | "maximum": 100, 103 | "step": 1 104 | }, 105 | "defaultValue": 100 106 | }, 107 | "onEffectLevel": { 108 | "sendEvents": false, 109 | "dataType": "number", 110 | "allowedValueRange": { 111 | "minimum": 0, 112 | "maximum": 100, 113 | "step": 1 114 | }, 115 | "defaultValue": 100 116 | } 117 | } 118 | } 119 | } 120 | } 121 | } -------------------------------------------------------------------------------- /spec/SensorHub.json: -------------------------------------------------------------------------------- 1 | { 2 | "configId": 1, 3 | "specVersion": { 4 | "major": 1, 5 | "minor": 0 6 | }, 7 | "device": { 8 | "deviceType": "urn:cdif-net:device:SensorHub:1", 9 | "friendlyName": "device name", 10 | "manufacturer": "manufacturer name", 11 | "manufacturerURL": "manufacturer URL", 12 | "modelDescription": "device full description", 13 | "modelName": "device model name", 14 | "modelNumber": "device model number", 15 | "serialNumber": "device serial number", 16 | "UPC": "universal product code", 17 | "userAuth": false, 18 | "iconList": [ 19 | { 20 | "mimetype": "image/format", 21 | "width": 80, 22 | "height": 100, 23 | "depth": 16, 24 | "url": "icon URL" 25 | } 26 | ], 27 | "serviceList": { 28 | "urn:cdif-net:serviceID:Illuminance": { 29 | "serviceType": "urn:cdif-net:service:IlluminanceSensor:1", 30 | "actionList": { 31 | "getIlluminanceData": { 32 | "argumentList": { 33 | "illuminance": { 34 | "direction": "out", 35 | "retval": true, 36 | "relatedStateVariable": "illuminance" 37 | } 38 | } 39 | } 40 | }, 41 | "serviceStateTable": { 42 | "illuminance": { 43 | "sendEvents": true, 44 | "dataType": "number", 45 | "allowedValueRange": { 46 | "minimum": 0, 47 | "maximum": 1000 48 | }, 49 | "defaultValue": 0 50 | } 51 | } 52 | }, 53 | "urn:cdif-net:serviceID:Temperature": { 54 | "serviceType": "urn:cdif-net:service:TemperatureSensor:1", 55 | "actionList": { 56 | "getTemperatureData": { 57 | "argumentList": { 58 | "temperature": { 59 | "direction": "out", 60 | "retval": true, 61 | "relatedStateVariable": "temperature" 62 | } 63 | } 64 | } 65 | }, 66 | "serviceStateTable": { 67 | "temperature": { 68 | "sendEvents": true, 69 | "dataType": "number", 70 | "allowedValueRange": { 71 | "minimum": -273.15, 72 | "maximum": 1000000 73 | }, 74 | "defaultValue": 0 75 | } 76 | } 77 | }, 78 | "urn:cdif-net:serviceID:Humidity": { 79 | "serviceType": "urn:cdif-net:service:HumiditySensor:1", 80 | "actionList": { 81 | "getHumidityData": { 82 | "argumentList": { 83 | "humidity": { 84 | "direction": "out", 85 | "retval": true, 86 | "relatedStateVariable": "humidity" 87 | } 88 | } 89 | } 90 | }, 91 | "serviceStateTable": { 92 | "humidity": { 93 | "sendEvents": true, 94 | "dataType": "number", 95 | "allowedValueRange": { 96 | "minimum": 0, 97 | "maximum": 100 98 | }, 99 | "defaultValue": 0 100 | } 101 | } 102 | }, 103 | "urn:cdif-net:serviceID:Accelerometer": { 104 | "serviceType": "urn:cdif-net:service:Accelerometer:1", 105 | "actionList": { 106 | "getAccelerometerData": { 107 | "argumentList": { 108 | "x": { 109 | "direction": "out", 110 | "retval": true, 111 | "relatedStateVariable": "x" 112 | }, 113 | "y": { 114 | "direction": "out", 115 | "retval": true, 116 | "relatedStateVariable": "y" 117 | }, 118 | "z": { 119 | "direction": "out", 120 | "retval": true, 121 | "relatedStateVariable": "z" 122 | } 123 | } 124 | } 125 | }, 126 | "serviceStateTable": { 127 | "x": { 128 | "sendEvents": true, 129 | "dataType": "number", 130 | "allowedValueRange": { 131 | "minimum": -100, 132 | "maximum": 100 133 | }, 134 | "defaultValue": 0 135 | }, 136 | "y": { 137 | "sendEvents": true, 138 | "dataType": "number", 139 | "allowedValueRange": { 140 | "minimum": -100, 141 | "maximum": 100 142 | }, 143 | "defaultValue": 0 144 | }, 145 | "z": { 146 | "sendEvents": true, 147 | "dataType": "number", 148 | "allowedValueRange": { 149 | "minimum": -100, 150 | "maximum": 100 151 | }, 152 | "defaultValue": 0 153 | } 154 | } 155 | }, 156 | "urn:cdif-net:serviceID:Magnetometer": { 157 | "serviceType": "urn:cdif-net:service:Magnetometer:1", 158 | "actionList": { 159 | "getMagnetometerData": { 160 | "argumentList": { 161 | "x": { 162 | "direction": "out", 163 | "retval": true, 164 | "relatedStateVariable": "x" 165 | }, 166 | "y": { 167 | "direction": "out", 168 | "retval": true, 169 | "relatedStateVariable": "y" 170 | }, 171 | "z": { 172 | "direction": "out", 173 | "retval": true, 174 | "relatedStateVariable": "z" 175 | } 176 | } 177 | } 178 | }, 179 | "serviceStateTable": { 180 | "x": { 181 | "sendEvents": true, 182 | "dataType": "number", 183 | "defaultValue": 0 184 | }, 185 | "y": { 186 | "sendEvents": true, 187 | "dataType": "number", 188 | "defaultValue": 0 189 | }, 190 | "z": { 191 | "sendEvents": true, 192 | "dataType": "number", 193 | "defaultValue": 0 194 | } 195 | } 196 | }, 197 | "urn:cdif-net:serviceID:Barometer": { 198 | "serviceType": "urn:cdif-net:service:Barometer:1", 199 | "actionList": { 200 | "getBarometerData": { 201 | "argumentList": { 202 | "pressure": { 203 | "direction": "out", 204 | "retval": true, 205 | "relatedStateVariable": "pressure" 206 | } 207 | } 208 | } 209 | }, 210 | "serviceStateTable": { 211 | "pressure": { 212 | "sendEvents": true, 213 | "dataType": "number", 214 | "allowedValueRange": { 215 | "minimum": 0, 216 | "maximum": 1013.25 217 | }, 218 | "defaultValue": 0 219 | } 220 | } 221 | }, 222 | "urn:cdif-net:serviceID:Gyroscope": { 223 | "serviceType": "urn:cdif-net:service:Gyroscope:1", 224 | "actionList": { 225 | "getGyroscopeData": { 226 | "argumentList": { 227 | "x": { 228 | "direction": "out", 229 | "retval": true, 230 | "relatedStateVariable": "x" 231 | }, 232 | "y": { 233 | "direction": "out", 234 | "retval": true, 235 | "relatedStateVariable": "y" 236 | }, 237 | "z": { 238 | "direction": "out", 239 | "retval": true, 240 | "relatedStateVariable": "z" 241 | } 242 | } 243 | } 244 | }, 245 | "serviceStateTable": { 246 | "x": { 247 | "sendEvents": true, 248 | "dataType": "number", 249 | "defaultValue": 0 250 | }, 251 | "y": { 252 | "sendEvents": true, 253 | "dataType": "number", 254 | "defaultValue": 0 255 | }, 256 | "z": { 257 | "sendEvents": true, 258 | "dataType": "number", 259 | "defaultValue": 0 260 | } 261 | } 262 | } 263 | } 264 | } 265 | } -------------------------------------------------------------------------------- /spec/onvif.json: -------------------------------------------------------------------------------- 1 | { 2 | "configId": 1, 3 | "specVersion": { 4 | "major": 1, 5 | "minor": 0 6 | }, 7 | "device": { 8 | "deviceType": "urn:cdif-net:device:ONVIFCamera:1", 9 | "friendlyName": "", 10 | "manufacturer": "", 11 | "modelName": "", 12 | "userAuth": true, 13 | "devicePresentation": true, 14 | "serviceList": { 15 | "urn:cdif-net:serviceID:ONVIFMediaService": { 16 | "serviceType": "urn:cdif-net:service:ONVIFMedia:1", 17 | "actionList": { 18 | "getStreamUri": { 19 | "argumentList": { 20 | "streamType": { 21 | "direction": "in", 22 | "relatedStateVariable": "streamType" 23 | }, 24 | "transport": { 25 | "direction": "in", 26 | "relatedStateVariable": "transport" 27 | }, 28 | "streamUri": { 29 | "direction": "out", 30 | "retval": true, 31 | "relatedStateVariable": "streamUri" 32 | } 33 | } 34 | }, 35 | "getSnapshotUri": { 36 | "argumentList": { 37 | "snapshotUri": { 38 | "direction": "out", 39 | "retval": true, 40 | "relatedStateVariable": "snapshotUri" 41 | } 42 | } 43 | } 44 | }, 45 | "serviceStateTable": { 46 | "streamType": { 47 | "sendEvents": false, 48 | "dataType": "string", 49 | "allowedValueList": [ 50 | "RTP-Unicast", 51 | "RTP-Multicast", 52 | "MPEG" 53 | ], 54 | "defaultValue": "MPEG" 55 | }, 56 | "transport": { 57 | "sendEvents": false, 58 | "dataType": "string", 59 | "allowedValueList": [ 60 | "UDP", 61 | "TCP", 62 | "RTSP", 63 | "HTTP", 64 | "WebSocket" 65 | ], 66 | "defaultValue": "WebSocket" 67 | }, 68 | "streamUri": { 69 | "sendEvents": false, 70 | "dataType": "string" 71 | }, 72 | "snapshotUri": { 73 | "sendEvents": false, 74 | "dataType": "string" 75 | } 76 | } 77 | }, 78 | "urn:cdif-net:serviceID:ONVIFPTZService": { 79 | "serviceType": "urn:cdif-net:service:ONVIFPTZ:1", 80 | "actionList": { 81 | "absoluteMove": { 82 | "argumentList": { 83 | "options": { 84 | "direction": "in", 85 | "relatedStateVariable": "A_ARG_TYPE_AbsoluteMove" 86 | } 87 | } 88 | }, 89 | "relativeMove": { 90 | "argumentList": { 91 | "options": { 92 | "direction": "in", 93 | "relatedStateVariable": "A_ARG_TYPE_RelativeMove" 94 | } 95 | } 96 | }, 97 | "continuousMove": { 98 | "argumentList": { 99 | "options": { 100 | "direction": "in", 101 | "relatedStateVariable": "A_ARG_TYPE_ContinuousMove" 102 | } 103 | } 104 | }, 105 | "getPresets": { 106 | "argumentList": { 107 | "presets": { 108 | "direction": "out", 109 | "retval": true, 110 | "relatedStateVariable": "A_ARG_TYPE_GetPresets" 111 | } 112 | } 113 | }, 114 | "gotoPreset": { 115 | "argumentList": { 116 | "options": { 117 | "direction": "in", 118 | "relatedStateVariable": "A_ARG_TYPE_GotoPreset" 119 | } 120 | } 121 | }, 122 | "getNodes": { 123 | "argumentList": { 124 | "nodes": { 125 | "direction": "out", 126 | "relatedStateVariable": "A_ARG_TYPE_GetNodes" 127 | } 128 | } 129 | }, 130 | "stop": { 131 | "argumentList": { 132 | "options": { 133 | "direction": "in", 134 | "relatedStateVariable": "A_ARG_TYPE_Stop" 135 | } 136 | } 137 | } 138 | }, 139 | "serviceStateTable": { 140 | "A_ARG_TYPE_AbsoluteMove": { 141 | "sendEvents": false, 142 | "dataType": "object", 143 | "schema": "#ptz/AbsoluteMoveArg" 144 | }, 145 | "A_ARG_TYPE_RelativeMove": { 146 | "sendEvents": false, 147 | "dataType": "object", 148 | "schema": "#ptz/RelativeMoveArg" 149 | }, 150 | "A_ARG_TYPE_ContinuousMove": { 151 | "sendEvents": false, 152 | "dataType": "object", 153 | "schema": "#ptz/ContinuousMoveArg" 154 | }, 155 | "A_ARG_TYPE_Stop": { 156 | "sendEvents": false, 157 | "dataType": "object", 158 | "schema": "#ptz/StopArg" 159 | }, 160 | "A_ARG_TYPE_GetPresets": { 161 | "sendEvents": false, 162 | "dataType": "object", 163 | "schema": "#ptz/GetPresetsArg" 164 | }, 165 | "A_ARG_TYPE_GotoPreset": { 166 | "sendEvents": false, 167 | "dataType": "object", 168 | "schema": "#ptz/GotoPresetArg" 169 | }, 170 | "A_ARG_TYPE_GetNodes": { 171 | "sendEvents": false, 172 | "dataType": "object", 173 | "schema": "#ptz/GetNodesArg" 174 | } 175 | } 176 | } 177 | } 178 | } 179 | } -------------------------------------------------------------------------------- /spec/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "type": "object", 4 | "title": "CDIF device root schema", 5 | "description": "Schema specification for CDIF device model", 6 | "properties": { 7 | "configId": { 8 | "type": "integer", 9 | "maximum": 16777216, 10 | "minimum": 0 11 | }, 12 | "specVersion": { 13 | "type": "object", 14 | "properties": { 15 | "major": { 16 | "type": "integer", 17 | "enum": [ 18 | 1 19 | ] 20 | }, 21 | "minor": { 22 | "type": "integer", 23 | "enum": [ 24 | 0 25 | ] 26 | } 27 | }, 28 | "additionalProperties": false, 29 | "required": [ 30 | "major", 31 | "minor" 32 | ] 33 | }, 34 | "device": { 35 | "description": "Schema for device object", 36 | "type": "object", 37 | "properties": { 38 | "deviceType": { 39 | "type": "string", 40 | "pattern": "^urn\\:[ \\S]{1,64}\\:device\\:[ \\S]{1,64}\\:[0-9]{1,8}$" 41 | }, 42 | "friendlyName": { 43 | "type": "string", 44 | "maxLength": 64, 45 | "minLength": 0 46 | }, 47 | "manufacturer": { 48 | "type": "string", 49 | "maxLength": 64, 50 | "minLength": 0 51 | }, 52 | "manufacturerURL": { 53 | "type": "string", 54 | "format": "uri" 55 | }, 56 | "modelDescription": { 57 | "type": "string", 58 | "maxLength": 256, 59 | "minLength": 0 60 | }, 61 | "rateLimit": { 62 | "description": "the total api requests to this device allowed in one second, must be a positive integer", 63 | "type": "integer", 64 | "minimum": 1 65 | }, 66 | "modelName": { 67 | "type": "string", 68 | "maxLength": 128, 69 | "minLength": 0 70 | }, 71 | "modelNumber": { 72 | "type": "string", 73 | "maxLength": 128, 74 | "minLength": 0 75 | }, 76 | "serialNumber": { 77 | "type": "string", 78 | "maxLength": 128, 79 | "minLength": 0 80 | }, 81 | "price": { 82 | "type": "string", 83 | "maxLength": 256, 84 | "minLength": 1 85 | }, 86 | "UDN": { 87 | "title": "Schema for device UUID", 88 | "type": "string", 89 | "maxLength": 36, 90 | "minLength": 36, 91 | "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" 92 | }, 93 | "UPC": { 94 | "type": "string", 95 | "maxLength": 32, 96 | "minLength": 0 97 | }, 98 | "userAuth": { 99 | "type": "boolean" 100 | }, 101 | "devicePresentation": { 102 | "type": "boolean" 103 | }, 104 | "powerIndex": { 105 | "type": "number", 106 | "minimum": 0 107 | }, 108 | "iconList": { 109 | "type": "array", 110 | "maxItems": 5, 111 | "minItems": 1, 112 | "uniqueItems": true, 113 | "items": { 114 | "type": "object", 115 | "properties": { 116 | "mimetype": { 117 | "type": "string", 118 | "maxLength": 18, 119 | "minLength": 7, 120 | "pattern": "^image/[a-zA-Z0-9\\+\\.]{1,12}$" 121 | }, 122 | "width": { 123 | "type": "integer", 124 | "maximum": 9999, 125 | "minimum": 1 126 | }, 127 | "height": { 128 | "type": "integer", 129 | "maximum": 9999, 130 | "minimum": 1 131 | }, 132 | "depth": { 133 | "type": "integer", 134 | "maximum": 99, 135 | "minimum": 1 136 | }, 137 | "url": { 138 | "type": "string", 139 | "format": "uri" 140 | } 141 | }, 142 | "additionalProperties": false, 143 | "required": [ 144 | "mimetype", 145 | "width", 146 | "height", 147 | "depth", 148 | "url" 149 | ] 150 | } 151 | }, 152 | "serviceList": { 153 | "type": "object", 154 | "maxProperties": 32, 155 | "minProperties": 0, 156 | "patternProperties": { 157 | "^urn\\:[\\S]{1,64}\\:serviceID\\:[\\S]{1,64}$": { 158 | "type": "object", 159 | "properties": { 160 | "serviceType": { 161 | "type": "string", 162 | "pattern": "^urn\\:[ \\S]{1,64}\\:service\\:[ \\S]{1,64}\\:[0-9]{1,8}$" 163 | }, 164 | "actionList": { 165 | "type": "object", 166 | "maxProperties": 64, 167 | "minProperties": 0, 168 | "patternProperties": { 169 | "^[\\S]{1,128}$": { 170 | "type": "object", 171 | "properties": { 172 | "argumentList": { 173 | "type": "object", 174 | "maxProperties": 32, 175 | "minProperties": 1, 176 | "patternProperties": { 177 | "^[\\S]{1,128}$": { 178 | "type": "object", 179 | "properties": { 180 | "direction": { 181 | "type": "string", 182 | "enum": [ 183 | "in", 184 | "out" 185 | ] 186 | }, 187 | "retval": { 188 | "type": "boolean" 189 | }, 190 | "relatedStateVariable": { 191 | "type": "string", 192 | "maxLength": 128, 193 | "minLength": 1 194 | } 195 | }, 196 | "required": [ 197 | "direction", 198 | "relatedStateVariable" 199 | ] 200 | } 201 | }, 202 | "additionalProperties": false 203 | }, 204 | "realPrice": { 205 | "type": "number" 206 | }, 207 | "priceInfo": { 208 | "type": "array", 209 | "items": { 210 | "type": "object", 211 | "properties": { 212 | "price": { 213 | "type": "number" 214 | }, 215 | "count": { 216 | "type": "number" 217 | } 218 | }, 219 | "required": [ 220 | "price", 221 | "count" 222 | ] 223 | } 224 | }, 225 | "freeCount": { 226 | "type": "integer" 227 | }, 228 | "apiCache": { 229 | "type": "number" 230 | }, 231 | "apiLog": { 232 | "type": "boolean" 233 | }, 234 | "fault": { 235 | "type": "object", 236 | "properties": { 237 | "schema": { 238 | "type": "string" 239 | } 240 | }, 241 | "required": [ 242 | "schema" 243 | ] 244 | } 245 | }, 246 | "required": [ 247 | "argumentList" 248 | ] 249 | } 250 | }, 251 | "additionalProperties": false 252 | }, 253 | "serviceStateTable": { 254 | "type": "object", 255 | "maxProperties": 256, 256 | "minProperties": 1, 257 | "patternProperties": { 258 | "^[\\S]{1,128}$": { 259 | "type": "object", 260 | "properties": { 261 | "sendEvents": { 262 | "type": "boolean" 263 | }, 264 | "dataType": { 265 | "type": "string", 266 | "enum": [ 267 | "string", 268 | "boolean", 269 | "integer", 270 | "number", 271 | "object" 272 | ] 273 | }, 274 | "schema": { 275 | "type": "string" 276 | }, 277 | "defaultValue": { 278 | "type": [ 279 | "boolean", 280 | "integer", 281 | "number", 282 | "string" 283 | ], 284 | "anyOf": [ 285 | { 286 | "type": "string", 287 | "maxLength": 1024, 288 | "minLength": 1 289 | }, 290 | { 291 | "type": "boolean" 292 | }, 293 | { 294 | "type": "integer" 295 | }, 296 | { 297 | "type": "number" 298 | } 299 | ] 300 | }, 301 | "allowedValueRange": { 302 | "type": "object", 303 | "properties": { 304 | "minimum": { 305 | "type": "number" 306 | }, 307 | "maximum": { 308 | "type": "number" 309 | }, 310 | "step": { 311 | "type": "number" 312 | } 313 | }, 314 | "additionalProperties": false, 315 | "required": [ 316 | "minimum", 317 | "maximum" 318 | ] 319 | }, 320 | "allowedValueList": { 321 | "type": "array", 322 | "maxItems": 256, 323 | "minItems": 1, 324 | "uniqueItems": true, 325 | "items": { 326 | "oneOf": [ 327 | { 328 | "type": "string" 329 | }, 330 | { 331 | "type": "number" 332 | } 333 | ] 334 | } 335 | } 336 | }, 337 | "required": [ 338 | "dataType" 339 | ] 340 | } 341 | }, 342 | "additionalProperties": false 343 | } 344 | }, 345 | "required": [ 346 | "actionList", 347 | "serviceStateTable" 348 | ] 349 | } 350 | }, 351 | "additionalProperties": false 352 | }, 353 | "deviceList": { 354 | "type": "array", 355 | "minItems": 1, 356 | "uniqueItems": true, 357 | "items": { 358 | "$ref": "#/properties/device" 359 | } 360 | } 361 | }, 362 | "required": [ 363 | "friendlyName", 364 | "manufacturer", 365 | "modelDescription" 366 | ] 367 | } 368 | }, 369 | "additionalProperties": false, 370 | "required": [ 371 | "specVersion", 372 | "device" 373 | ] 374 | } -------------------------------------------------------------------------------- /spec/spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "configId": 1, 3 | "specVersion": { 4 | "major": 1, 5 | "minor": 0 6 | }, 7 | "device": { 8 | "deviceType": "urn:cdif-net:device:deviceType:1", 9 | "friendlyName": "device friendly name", 10 | "manufacturer": "manufacturer", 11 | "manufacturerURL": "manufacturer url", 12 | "modelDescription": "model desc", 13 | "modelName": "model name", 14 | "modelNumber": "model number", 15 | "serialNumber": "serial number", 16 | "UPC": "universal product code", 17 | "userAuth": true, 18 | "powerIndex": 40, 19 | "devicePresentation": false, 20 | "iconList": [ 21 | { 22 | "mimetype": "image/png", 23 | "width": "80", 24 | "height": "80", 25 | "depth": "16", 26 | "url": "icon URL" 27 | } 28 | ], 29 | "serviceList": { 30 | "urn:cdif-net:serviceID:serviceID": { 31 | "serviceType": "urn:cdif-net:service:serviceType:1", 32 | "actionList": { 33 | "actionName": { 34 | "argumentList": { 35 | "argumentName": { 36 | "direction": "in | out", 37 | "retval": false, 38 | "relatedStateVariable": "state variable name" 39 | } 40 | } 41 | } 42 | }, 43 | "serviceStateTable": { 44 | "state variable 1": { 45 | "sendEvents": true, 46 | "dataType": "number", 47 | "allowedValueRange": { 48 | "minimum": "1", 49 | "maximum": "100", 50 | "step": "1" 51 | }, 52 | "defaultValue": 100 53 | }, 54 | "state variable 2": { 55 | "sendEvents": false, 56 | "dataType": { 57 | "type": "string", 58 | "value": "custom data type" 59 | }, 60 | "allowedValueList": [ 61 | "value1", 62 | "value2" 63 | ], 64 | "defaultValue": "default value" 65 | } 66 | } 67 | } 68 | }, 69 | "deviceList": [ 70 | ] 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /spec/tools/onvif/README.md: -------------------------------------------------------------------------------- 1 | The ONVIF WSDL specs and schema definitions are retrieved from below url: 2 | 3 | http://www.onvif.org/onvif/ver10/device/wsdl/devicemgmt.wsdl 4 | http://www.onvif.org/ver20/ptz/wsdl/ptz.wsdl 5 | http://www.onvif.org/onvif/ver10/media/wsdl/media.wsdl 6 | http://www.onvif.org/ver10/schema/onvif.xsd 7 | 8 | Currently the converted spec doesn't contain schema definition to variables in object type, user should consult original ONVIF WSDL specs in order to dereference them 9 | -------------------------------------------------------------------------------- /spec/tools/onvif/process.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var services = [{'PTZ': './ptz.json'}, {'MEDIA': './media.json'}, {'DEVICEMGMT': './devicemgmt.json'}]; 4 | 5 | var spec = {}; 6 | spec.configId = 1; 7 | spec.specVersion = {major: 1, minor: 0}; 8 | spec.device = {}; 9 | spec.device.deviceType = "urn:cdif-net:device:ONVIFCamera:1"; 10 | spec.device.friendlyName = "onvif"; 11 | spec.device.manufacturer = "manufacturer name"; 12 | spec.device.manufacturerURL = "manufacturer URL"; 13 | spec.device.modelDescription = "model description"; 14 | spec.device.modelName = "model name"; 15 | spec.device.modelNumber = "model number"; 16 | spec.device.serialNumber = "serial no"; 17 | spec.device.UPC = "universal product code"; 18 | spec.device.userAuth = false; 19 | spec.device.presentationURL = "presentation URL"; 20 | spec.device.iconList = [{ 21 | "mimetype": "image/png", 22 | "width": 100, 23 | "height": 80, 24 | "depth": 32, 25 | "url": "icon URL" 26 | }]; 27 | spec.device.serviceList = {}; 28 | 29 | services.forEach(function(service) { 30 | var key = Object.keys(service); 31 | var value = service[key]; 32 | var o = require(value); 33 | 34 | var service_name = 'urn:cdif-net:serviceID:ONVIF' + key + 'Service'; 35 | spec.device.serviceList[service_name] = {}; 36 | spec.device.serviceList[service_name].actionList = {}; 37 | spec.device.serviceList[service_name].serviceStateTable = {}; 38 | 39 | var actions = o['wsdl:definitions']['wsdl:portType'][0]['wsdl:operation']; 40 | var types = o['wsdl:definitions']['wsdl:types'][0]['xs:schema'][0]['xs:element']; 41 | var messages = o['wsdl:definitions']['wsdl:message']; 42 | 43 | 44 | var result_actions = {}; 45 | 46 | var input_message_names = {}; 47 | var output_message_names = {}; 48 | 49 | actions.forEach(function(action) { 50 | var name = action.$.name; 51 | spec.device.serviceList[service_name].actionList[name] = {}; 52 | spec.device.serviceList[service_name].actionList[name].argumentList = {}; 53 | result_actions[name] = {}; 54 | result_actions[name].inputArgumentList = []; 55 | result_actions[name].outputArgumentList = []; 56 | }); 57 | 58 | 59 | actions.forEach(function(action) { 60 | var actionName = action.$.name; 61 | var input_messages = action['wsdl:input']; 62 | var output_messages = action['wsdl:output']; 63 | input_messages.forEach(function(input_message) { 64 | var arr = input_message.$.message.split(':'); 65 | var name = arr[arr.length - 1]; 66 | messages.forEach(function(message) { 67 | if(name == message.$.name) { 68 | var element = message['wsdl:part'][0].$.element; 69 | var _arr = element.split(':'); 70 | var elementName = _arr[_arr.length - 1]; 71 | types.forEach(function(type) { 72 | if (elementName == type.$.name) { 73 | if (type['xs:complexType'][0]['xs:sequence'] == null) { 74 | return; 75 | } else { 76 | var elems = type['xs:complexType'][0]['xs:sequence'][0]['xs:element']; 77 | if (elems != null) { 78 | elems.forEach(function(elem) { 79 | spec.device.serviceList[service_name].actionList[actionName].argumentList[elem.$.name] = {'direction': 'in', 'relatedStateVariable': elem.$.name}; 80 | if (spec.device.serviceList[service_name].serviceStateTable[elem.$.name] == null) { 81 | var data_type = 'object'; 82 | if (elem.$.type === 'xs:boolean') { 83 | data_type = 'boolean'; 84 | } else if (elem.$.type === 'xs:string') { 85 | data_type = 'string'; 86 | } else if (elem.$.type === 'xs:int') { 87 | data_type = 'number'; 88 | } 89 | spec.device.serviceList[service_name].serviceStateTable[elem.$.name] = {sendEvents: false, 'dataType': data_type}; 90 | } 91 | result_actions[actionName].inputArgumentList.push(elem.$.name); 92 | }); 93 | } 94 | } 95 | } 96 | }); 97 | } 98 | }); 99 | }); 100 | output_messages.forEach(function(output_message) { 101 | var arr = output_message.$.message.split(':'); 102 | var name = arr[arr.length - 1]; 103 | messages.forEach(function(message) { 104 | if(name == message.$.name) { 105 | var element = message['wsdl:part'][0].$.element; 106 | var _arr = element.split(':'); 107 | var elementName = _arr[_arr.length - 1]; 108 | types.forEach(function(type) { 109 | if (elementName == type.$.name) { 110 | if (type['xs:complexType'][0]['xs:sequence'] == null) { 111 | return; 112 | } else { 113 | var elems = type['xs:complexType'][0]['xs:sequence'][0]['xs:element']; 114 | if (elems != null) { 115 | elems.forEach(function(elem) { 116 | spec.device.serviceList[service_name].actionList[actionName].argumentList[elem.$.name] = {'direction': 'out', 'retval': true, 'relatedStateVariable': elem.$.name}; 117 | if (spec.device.serviceList[service_name].serviceStateTable[elem.$.name] == null) { 118 | var data_type = 'object'; 119 | if (elem.$.type === 'xs:boolean') { 120 | data_type = 'boolean'; 121 | } else if (elem.$.type === 'xs:string') { 122 | data_type = 'string'; 123 | } else if (elem.$.type === 'xs:int') { 124 | data_type = 'number'; 125 | } 126 | spec.device.serviceList[service_name].serviceStateTable[elem.$.name] = {sendEvents: false, 'dataType': data_type}; 127 | } 128 | result_actions[actionName].outputArgumentList.push(elem.$.name); 129 | }); 130 | } 131 | } 132 | } 133 | }); 134 | } 135 | }); 136 | }); 137 | }); 138 | }); 139 | 140 | console.log(JSON.stringify(spec)); 141 | 142 | 143 | -------------------------------------------------------------------------------- /spec/tools/onvif/readxml.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | xml2js = require('xml2js'); 3 | 4 | var parser = new xml2js.Parser(); 5 | fs.readFile(__dirname + '/onvif.xsd', function(err, data) { 6 | if (err) { console.error(err);} 7 | parser.parseString(data, function (err, result) { 8 | if (err) { console.error(err);} 9 | console.log(JSON.stringify(result)); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /test/loop.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | while true; do 3 | curl -H "Content-Type: application/json" -X POST -d '{"serviceID":"urn:qunar-com:serviceID:车站搜索","actionName":"车站搜索","argumentList":{"input":{"station": "漯河"}}}' http://localhost:3049/device-control/f50e8254-c766-419c-af23-80e6eed1823b/invoke-action & 4 | sleep 1 5 | done 6 | 7 | #curl -H "Content-Type: application/json" -X POST -d '{"registry":"http://121.43.107.95:5984/", "name":"cdif-qunar-train-service", "version":"0.0.5"}' http://121.43.107.95:3049/module-install 8 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require should 2 | --reporter spec 3 | --ui bdd 4 | --recursive 5 | -------------------------------------------------------------------------------- /test/socket.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 95 | 96 | 97 |
    98 | 99 | 100 | -------------------------------------------------------------------------------- /test/test1.js: -------------------------------------------------------------------------------- 1 | var request = require('supertest'); 2 | 3 | var url = 'http://localhost:3049'; 4 | 5 | describe('discover all devices', function() { 6 | this.timeout(6000); 7 | it('discover OK', function(done) { 8 | request(url).post('/discover').expect(200).end(function(err, res) { 9 | if(err) throw err; 10 | setTimeout(function() { 11 | request(url).post('/stop-discover').expect(200).end(function(err, res) { 12 | if(err) throw err; 13 | done(); 14 | }); 15 | }, 5000); 16 | }); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /test/test2.js: -------------------------------------------------------------------------------- 1 | var should = require('should'); 2 | var request = require('supertest'); 3 | 4 | var url = 'http://localhost:3049'; 5 | 6 | var deviceList; 7 | 8 | describe('get device list', function() { 9 | it('get device list OK', function(done) { 10 | request(url).get('/device-list') 11 | .expect('Content-Type', /json/) 12 | .expect(200).end(function(err, res) { 13 | if(err) throw err; 14 | for (var i in res.body) { 15 | res.body[i].should.have.property('configId').which.is.a.Number(); 16 | res.body[i].should.have.property('specVersion').and.have.property('major', 1); 17 | res.body[i].should.have.property('specVersion').and.have.property('minor', 0); 18 | res.body[i].should.have.property('device'); 19 | var device = res.body[i].device; 20 | device.should.have.property('deviceType'); 21 | device.should.have.property('friendlyName'); 22 | device.should.have.property('manufacturer'); 23 | // device.should.have.property('modelName'); 24 | device.should.have.property('userAuth'); 25 | device.should.have.property('serviceList', {}); 26 | // if (device.deviceType != 'urn:cdif-net:device:BinaryLight:1' && 27 | // device.deviceType != 'urn:cdif-net:device:DimmableLight:1' && 28 | // device.deviceType != 'urn:cdif-net:device:SensorHub:1' && 29 | // device.deviceType != 'urn:cdif-net:device:ONVIFCamera:1') { 30 | // throw(new Error('unknown device type: ' + device.deviceType)); 31 | // } 32 | } 33 | deviceList = JSON.parse(JSON.stringify(res.body)); 34 | done(); 35 | }); 36 | }); 37 | }); 38 | 39 | -------------------------------------------------------------------------------- /test/test3.js: -------------------------------------------------------------------------------- 1 | var should = require('should'); 2 | var request = require('supertest'); 3 | var async = require('async'); 4 | var io = require('socket.io-client'); 5 | var faker = require('json-schema-faker'); 6 | 7 | var url = 'http://localhost:3049'; 8 | 9 | var deviceList; 10 | 11 | describe('connect all devices', function() { 12 | this.timeout(0); 13 | 14 | it('connect OK', function(done) { 15 | request(url).get('/device-list') 16 | .expect('Content-Type', /json/) 17 | .expect(200).end(function(err, res) { 18 | if(err) throw err; 19 | deviceList = JSON.parse(JSON.stringify(res.body)); 20 | 21 | var list = Object.keys(deviceList); 22 | var cred = {"username": "admin", "password": "test"}; 23 | async.eachSeries(list, function(deviceID, callback) { 24 | var device = deviceList[deviceID].device; 25 | if (device.userAuth === true) { 26 | request(url).post('/device-control/' + deviceID + '/connect') 27 | .send(cred).expect(200, function(err, res) { 28 | if (err) throw err; 29 | var device_access_token = res.body.device_access_token; 30 | deviceList[deviceID].device_access_token = device_access_token; 31 | callback(); 32 | }); 33 | } else { 34 | request(url).post('/device-control/' + deviceID + '/connect') 35 | .expect(200, callback); 36 | } 37 | }, done); 38 | }); 39 | }); 40 | }); 41 | 42 | describe('subscribe events from all devices', function() { 43 | this.timeout(0); 44 | var sock = io.connect(url); 45 | 46 | sock.on('event', function(data) { 47 | console.log('socket client received: ' + JSON.stringify(data)); 48 | }); 49 | sock.on('error', function(data) { 50 | console.log('socket client received error: ' + JSON.stringify(data)); 51 | }); 52 | 53 | it('subscribe OK', function(done) { 54 | var list = Object.keys(deviceList); 55 | async.eachSeries(list, function(deviceID, callback) { 56 | request(url).get('/device-control/' + deviceID + '/get-spec') 57 | .send({"device_access_token": deviceList[deviceID].device_access_token}) 58 | .expect(200, function(err, res) { 59 | if (err) throw err; 60 | var device = res.body.device; 61 | var serviceList = Object.keys(device.serviceList); 62 | 63 | async.eachSeries(serviceList, function(serviceID, cb) { 64 | var room = new Object(); 65 | room.deviceID = deviceID; 66 | room.serviceID = serviceID; 67 | room.device_access_token = deviceList[deviceID].device_access_token; 68 | room.onUpdate = true; 69 | sock.emit('subscribe', JSON.stringify(room)); 70 | cb(); 71 | }, callback); 72 | }); 73 | }, done); 74 | }); 75 | }); 76 | 77 | describe('invoke all actions', function() { 78 | this.timeout(0); 79 | 80 | it('invoke OK', function(done) { 81 | var list = Object.keys(deviceList); 82 | async.eachSeries(list, function(deviceID, callback) { 83 | request(url).get('/device-control/' + deviceID + '/get-spec') 84 | .send({"device_access_token": deviceList[deviceID].device_access_token}) 85 | .expect(200, function(err, res) { 86 | if (err) throw err; 87 | var device = res.body.device; 88 | device.should.have.property('serviceList'); 89 | device.serviceList.should.be.an.Object; 90 | device.serviceList.should.be.not.empty; 91 | var serviceList = Object.keys(device.serviceList); 92 | 93 | async.eachSeries(serviceList, function(serviceID, cb) { 94 | testInvokeActions(deviceID, serviceID, res.body.device.serviceList, cb); 95 | }, callback); 96 | }); 97 | }, done); 98 | }); 99 | }); 100 | 101 | function testInvokeActions(deviceID, serviceID, serviceList, callback) { 102 | var actionList = serviceList[serviceID].actionList; 103 | actionList.should.be.an.Object; 104 | actionList.should.be.not.empty; 105 | 106 | var list = Object.keys(actionList); 107 | 108 | async.eachSeries(list, function(name, cb) { 109 | setTimeout(function() { 110 | var action = actionList[name]; 111 | action.should.be.an.Object; 112 | action.should.be.not.empty; 113 | var args = action.argumentList; 114 | 115 | var argList = Object.keys(action.argumentList); 116 | var req = { serviceID: serviceID, 117 | actionName: name, 118 | argumentList: {}, 119 | device_access_token: deviceList[deviceID].device_access_token 120 | }; 121 | async.eachSeries(argList, function(arg, call_back) { 122 | arg.should.not.be.empty; 123 | var stateVarName = action.argumentList[arg].relatedStateVariable; 124 | var stateVarTable = serviceList[serviceID].serviceStateTable; 125 | stateVarTable.should.be.an.Object; 126 | stateVarTable.should.be.not.empty; 127 | var stateVar = stateVarTable[stateVarName]; 128 | stateVar.should.be.an.Object; 129 | stateVar.should.be.not.empty; 130 | if (stateVar.dataType === 'number' || 131 | stateVar.dataType === 'integer' || 132 | stateVar.dataType === 'uint8' || 133 | stateVar.dataType === 'uint16' || 134 | stateVar.dataType === 'uint32' || 135 | stateVar.dataType === 'sint8' || 136 | stateVar.dataType === 'sint16' || 137 | stateVar.dataType === 'sint32') { 138 | var min = 0; var max = 100; 139 | if (stateVar.allowedValueRange) { 140 | stateVar.allowedValueRange.minimum.should.be.a.Number; 141 | stateVar.allowedValueRange.maximum.should.be.a.Number; 142 | min = stateVar.allowedValueRange.minimum; 143 | max = stateVar.allowedValueRange.maximum; 144 | } 145 | if (stateVar.defaultValue) { 146 | req.argumentList[arg] = stateVar.defaultValue; 147 | } else { 148 | req.argumentList[arg] = Math.floor(Math.random() * max) + min; 149 | } 150 | call_back(); 151 | } else if (stateVar.dataType === 'boolean') { 152 | req.argumentList[arg] = Math.random() >= 0.5; 153 | call_back(); 154 | } else if (stateVar.dataType === 'string') { 155 | if (stateVar.defaultValue) { 156 | req.argumentList[arg] = stateVar.defaultValue; 157 | } else { 158 | req.argumentList[arg] = 'test'; 159 | } 160 | call_back(); 161 | } else if (stateVar.dataType === 'object') { 162 | var schemaRef = stateVar.schema; 163 | schemaRef.should.be.a.String; 164 | request(url).get('/device-control/' + deviceID + '/schema' + schemaRef) 165 | .send({"device_access_token": deviceList[deviceID].device_access_token}) 166 | .expect(200, function(err, res) { 167 | if (err) throw err; 168 | var variableSchema = res.body; 169 | variableSchema.should.be.an.Object; 170 | variableSchema.should.be.not.empty; 171 | var fake_data = faker(variableSchema); 172 | console.log(fake_data); 173 | req.argumentList[arg] = fake_data; 174 | call_back(); 175 | }); 176 | } 177 | }, function() { 178 | console.log('Request:' + JSON.stringify(req)); 179 | request(url).post('/device-control/' + deviceID + '/invoke-action') 180 | .send(req) 181 | .expect('Content-Type', /[json | text]/) 182 | .expect(200, function(err, res) { 183 | if (err) { 184 | console.error(err); 185 | } 186 | console.log('Response: ' + JSON.stringify(res.body)); 187 | cb(); 188 | }); 189 | }); 190 | }, 5000); 191 | }, callback); 192 | } 193 | 194 | --------------------------------------------------------------------------------