├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── index.js ├── lib ├── components │ ├── af.js │ ├── controller.js │ ├── event_bridge.js │ ├── event_handlers.js │ ├── loader.js │ ├── querie.js │ ├── zcl.js │ ├── zdo.js │ ├── zdo_helper.js │ └── zutils.js ├── config │ └── nv_start_options.js ├── initializers │ ├── init_controller.js │ └── init_shepherd.js ├── model │ ├── coord.js │ ├── coordpoint.js │ ├── device.js │ └── endpoint.js └── shepherd.js ├── package.json └── test ├── af.test.js ├── controller.test.js ├── database ├── dev.db └── dev1.db ├── shepherd.test.js └── zcl.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.12.10" 4 | - "4.1.0" 5 | - "6.9.2" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Jack Wu , Simen Li , and Hedy Wang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test-all: 2 | @./node_modules/.bin/mocha -u bdd --reporter spec 3 | 4 | .PHONY: test-all -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zigbee-shepherd 2 | An open source ZigBee gateway solution with node.js 3 | 4 | [![NPM](https://nodei.co/npm/zigbee-shepherd.png?downloads=true)](https://nodei.co/npm/zigbee-shepherd/) 5 | 6 | [![Travis branch](https://img.shields.io/travis/zigbeer/zigbee-shepherd/master.svg?maxAge=2592000)](https://travis-ci.org/zigbeer/zigbee-shepherd) 7 | [![npm](https://img.shields.io/npm/v/zigbee-shepherd.svg?maxAge=2592000)](https://www.npmjs.com/package/zigbee-shepherd) 8 | [![npm](https://img.shields.io/npm/l/zigbee-shepherd.svg?maxAge=2592000)](https://www.npmjs.com/package/zigbee-shepherd) 9 | 10 |
11 | 12 | ## Documentation 13 | 14 | Please visit the [Wiki](https://github.com/zigbeer/zigbee-shepherd/wiki). 15 | 16 |
17 | 18 | ## Overview 19 | 20 | **zigbee-shepherd** is an open source ZigBee gateway solution with node.js. It uses TI's [CC253X](http://www.ti.com/lsds/ti/wireless_connectivity/zigbee/overview.page) wireless SoC as a [zigbee network processor (ZNP)](http://www.ti.com/lit/an/swra444/swra444.pdf), and takes the ZNP approach with [cc-znp](https://github.com/zigbeer/cc-znp) to run the CC253X as a coordinator and to run zigbee-shepherd as the host. 21 | 22 | * [**A simple demo webapp**](https://github.com/zigbeer/zigbee-demo#readme) 23 | 24 | ![ZigBee Network](https://raw.githubusercontent.com/zigbeer/documents/master/zigbee-shepherd/zigbee_net.png) 25 | 26 |
27 | 28 | ## Installation 29 | 30 | * Install zigbee-shepherd 31 | 32 | > $ npm install zigbee-shepherd --save 33 | 34 | * Hardware 35 | - [SmartRF05EB (with CC2530EM)](http://www.ti.com/tool/cc2530dk) 36 | - [CC2531 USB Stick](http://www.ti.com/tool/cc2531emk) 37 | - CC2538 (Not tested yet. I don't have the kit.) 38 | - CC2630/CC2650 (Not tested yet. I don't have the kit.) 39 | 40 | * Firmware 41 | - To use CC2530/31 as the coordinator, please download the [**pre-built ZNP image**](https://github.com/zigbeer/documents/tree/master/zigbee-shepherd) to your chip first. The pre-built image has been compiled as a ZNP with ZDO callback, ZCL supports, and functions we need. 42 | 43 |
44 | 45 | ## Usage 46 | 47 | ```js 48 | var ZShepherd = require('zigbee-shepherd'); 49 | var shepherd = new ZShepherd('/dev/ttyUSB0'); // create a ZigBee server 50 | 51 | shepherd.on('ready', function () { 52 | console.log('Server is ready.'); 53 | 54 | // allow devices to join the network within 60 secs 55 | shepherd.permitJoin(60, function (err) { 56 | if (err) 57 | console.log(err); 58 | }); 59 | }); 60 | 61 | shepherd.start(function (err) { // start the server 62 | if (err) 63 | console.log(err); 64 | }); 65 | ``` 66 | 67 |
68 | 69 | ## License 70 | 71 | Licensed under [MIT](https://github.com/zigbeer/zigbee-shepherd/blob/master/LICENSE). 72 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('./lib/shepherd.js'); 4 | -------------------------------------------------------------------------------- /lib/components/af.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 'use strict'; 3 | 4 | var EventEmitter = require('events'); 5 | 6 | var Q = require('q'), 7 | _ = require('busyman'), 8 | Areq = require('areq'), 9 | zclId = require('zcl-id'), 10 | proving = require('proving'), 11 | ZSC = require('zstack-constants'); 12 | 13 | var zcl = require('./zcl'), 14 | zutils = require('./zutils'), 15 | Endpoint = require('../model/endpoint'), 16 | Coordpoint = require('../model/coordpoint'), 17 | seqNumber = 0, 18 | rebornDevs = {}; // { nwkAddr: [ { type, msg } ], ... }; 19 | 20 | var af = { 21 | controller: null, 22 | areq: null, 23 | _seq: 0 24 | }; 25 | 26 | af.send = function (srcEp, dstEp, cId, rawPayload, opt, callback) { 27 | // srcEp maybe a local app ep, or a remote ep 28 | var deferred = Q.defer(), 29 | controller = af.controller, 30 | areq = af.areq, 31 | areqTimeout, 32 | profId = srcEp.getProfId(), 33 | afParams, 34 | afEventCnf, 35 | apsAck = false, 36 | senderEp; 37 | 38 | if (!((srcEp instanceof Endpoint) || (srcEp instanceof Coordpoint))) 39 | throw new TypeError('srcEp should be an instance of Endpoint class.'); 40 | 41 | if (_.isString(cId)) { 42 | var cIdItem = zclId.cluster(cId); 43 | if (_.isUndefined(cIdItem)) { 44 | deferred.reject(new Error('Invalid cluster id: ' + cId + '.')); 45 | return deferred.promise.nodeify(callback); 46 | } else { 47 | cId = cIdItem.value; 48 | } 49 | } 50 | 51 | if (!_.isBuffer(rawPayload)) 52 | throw new TypeError('Af rawPayload should be a buffer.'); 53 | 54 | if (typeof opt === 'function') { 55 | callback = opt; 56 | opt = undefined; 57 | } 58 | 59 | opt = opt || {}; 60 | 61 | if (opt.hasOwnProperty('timeout')) 62 | proving.number(opt.timeout, 'opt.timeout should be a number.'); 63 | 64 | areqTimeout = opt.hasOwnProperty('timeout') ? opt.timeout : undefined; 65 | 66 | senderEp = srcEp.isLocal() ? srcEp : controller.getCoord().getDelegator(profId); 67 | 68 | if (!senderEp) 69 | senderEp = srcEp.isLocal() ? srcEp : controller.getCoord().getDelegator(0x0104); 70 | 71 | // if (!senderEp) { 72 | // // only occurs if srcEp is a remote one 73 | // deferred.reject(new Error('Profile: ' + profId + ' is not supported at this moment.')); 74 | // return deferred.promise.nodeify(callback); 75 | // } 76 | 77 | afParams = makeAfParams(senderEp, dstEp, cId, rawPayload, opt); 78 | afEventCnf = 'AF:dataConfirm:' + senderEp.getEpId() + ':' + afParams.transid; 79 | apsAck = afParams.options & ZSC.AF.options.ACK_REQUEST; 80 | 81 | while (areq.isEventPending(afEventCnf)) { 82 | afParams.transid = controller.nextTransId(); 83 | afEventCnf = 'AF:dataConfirm:' + senderEp.getEpId() + ':' + afParams.transid; 84 | } 85 | 86 | areq.register(afEventCnf, deferred, function (cnf) { 87 | var errText = 'AF data request fails, status code: '; 88 | 89 | if (cnf.status === 0 || cnf.status === 'SUCCESS') // success 90 | areq.resolve(afEventCnf, cnf); 91 | else if (cnf.status === 0xcd || cnf.status === 'NWK_NO_ROUTE') 92 | areq.reject(afEventCnf, new Error(errText + '205. No network route. Please confirm that the device has (re)joined the network.')); 93 | else if (cnf.status === 0xe9 || cnf.status === 'MAC_NO_ACK') 94 | areq.reject(afEventCnf, new Error(errText + '233. MAC no ack.')); 95 | else if (cnf.status === 0xb7 || cnf.status === 'APS_NO_ACK') // ZApsNoAck period is 20 secs 96 | areq.reject(afEventCnf, new Error(errText + '183. APS no ack.')); 97 | else if (cnf.status === 0xf0 || cnf.status === 'MAC_TRANSACTION_EXPIRED') // ZMacTransactionExpired is 8 secs 98 | areq.reject(afEventCnf, new Error(errText + '240. MAC transaction expired.')); 99 | else 100 | areq.reject(afEventCnf, new Error(errText + cnf.status)); 101 | }, areqTimeout); 102 | 103 | controller.request('AF', 'dataRequest', afParams).then(function (rsp) { 104 | if (rsp.status !== 0 && rsp.status !== 'SUCCESS' ) // unsuccessful 105 | areq.reject(afEventCnf, new Error('AF data request failed, status code: ' + rsp.status + '.')); 106 | else if (!apsAck) 107 | areq.resolve(afEventCnf, rsp); 108 | }).fail(function (err) { 109 | areq.reject(afEventCnf, err); 110 | }).done(); 111 | 112 | return deferred.promise.nodeify(callback); 113 | }; 114 | 115 | af.sendExt = function (srcEp, addrMode, dstAddrOrGrpId, cId, rawPayload, opt, callback) { 116 | // srcEp must be a local ep 117 | var deferred = Q.defer(), 118 | controller = af.controller, 119 | areq = af.areq, 120 | areqTimeout, 121 | profId = srcEp.getProfId(), 122 | afParamsExt, 123 | afEventCnf, 124 | apsAck = false, 125 | senderEp = srcEp; 126 | 127 | if (!((srcEp instanceof Endpoint) || (srcEp instanceof Coordpoint))) 128 | throw new TypeError('srcEp should be an instance of Endpoint class.'); 129 | 130 | proving.number(addrMode, 'Af addrMode should be a number.'); 131 | 132 | if (addrMode === ZSC.AF.addressMode.ADDR_16BIT || addrMode === ZSC.AF.addressMode.ADDR_GROUP) 133 | proving.number(dstAddrOrGrpId, 'Af dstAddrOrGrpId should be a number for netwrok address or group id.'); 134 | else if (addrMode === ZSC.AF.addressMode.ADDR_64BIT) 135 | proving.string(dstAddrOrGrpId, 'Af dstAddrOrGrpId should be a string for long address.'); 136 | 137 | if (_.isString(cId)) { 138 | var cIdItem = zclId.cluster(cId); 139 | if (_.isUndefined(cIdItem)) { 140 | deferred.reject(new Error('Invalid cluster id: ' + cId + '.')); 141 | return deferred.promise.nodeify(callback); 142 | } else { 143 | cId = cIdItem.value; 144 | } 145 | } 146 | 147 | if (!_.isBuffer(rawPayload)) 148 | throw new TypeError('Af rawPayload should be a buffer.'); 149 | 150 | if (typeof opt === 'function') { 151 | callback = opt; 152 | opt = undefined; 153 | } 154 | 155 | opt = opt || {}; 156 | 157 | if (opt.hasOwnProperty('timeout')) 158 | proving.number(opt.timeout, 'opt.timeout should be a number.'); 159 | 160 | areqTimeout = opt.hasOwnProperty('timeout') ? opt.timeout : undefined; 161 | 162 | if (!senderEp.isLocal()) { 163 | deferred.reject(new Error('Only a local endpoint can groupcast, broadcast, and send extend message.')); 164 | return deferred.promise.nodeify(callback); 165 | } 166 | 167 | afParamsExt = makeAfParamsExt(senderEp, addrMode, dstAddrOrGrpId, cId, rawPayload, opt); 168 | 169 | if (!afParamsExt) { 170 | deferred.reject(new Error('Unknown address mode. Cannot send.')); 171 | return deferred.promise.nodeify(callback); 172 | } 173 | 174 | if (addrMode === ZSC.AF.addressMode.ADDR_GROUP || addrMode === ZSC.AF.addressMode.ADDR_BROADCAST) { 175 | // no ack 176 | controller.request('AF', 'dataRequestExt', afParamsExt).then(function (rsp) { 177 | if (rsp.status !== 0 && rsp.status !== 'SUCCESS') // unsuccessful 178 | deferred.reject(new Error('AF data extend request failed, status code: ' + rsp.status + '.')); 179 | else 180 | deferred.resolve(rsp); // Broadcast (or Groupcast) has no AREQ confirm back, just resolve this transaction. 181 | }).fail(function (err) { 182 | deferred.reject(err); 183 | }).done(); 184 | 185 | } else { 186 | afEventCnf = 'AF:dataConfirm:' + senderEp.getEpId() + ':' + afParamsExt.transid; 187 | apsAck = afParamsExt.options & ZSC.AF.options.ACK_REQUEST; 188 | 189 | while (areq.isEventPending(afEventCnf)) { 190 | afParamsExt.transid = controller.nextTransId(); 191 | afEventCnf = 'AF:dataConfirm:' + senderEp.getEpId() + ':' + afParamsExt.transid; 192 | } 193 | 194 | areq.register(afEventCnf, deferred, function (cnf) { 195 | var errText = 'AF data request fails, status code: '; 196 | 197 | if (cnf.status === 0 || cnf.status === 'SUCCESS') // success 198 | areq.resolve(afEventCnf, cnf); 199 | else if (cnf.status === 0xcd || cnf.status === 'NWK_NO_ROUTE') 200 | areq.reject(afEventCnf, new Error(errText + '205. No network route. Please confirm that the device has (re)joined the network.')); 201 | else if (cnf.status === 0xe9 || cnf.status === 'MAC_NO_ACK') 202 | areq.reject(afEventCnf, new Error(errText + '233. MAC no ack.')); 203 | else if (cnf.status === 0xb7 || cnf.status === 'APS_NO_ACK') // ZApsNoAck period is 20 secs 204 | areq.reject(afEventCnf, new Error(errText + '183. APS no ack.')); 205 | else if (cnf.status === 0xf0 || cnf.status === 'MAC_TRANSACTION_EXPIRED') // ZMacTransactionExpired is 8 secs 206 | areq.reject(afEventCnf, new Error(errText + '240. MAC transaction expired.')); 207 | else 208 | areq.reject(afEventCnf, new Error(errText + cnf.status)); 209 | }, areqTimeout); 210 | 211 | controller.request('AF', 'dataRequestExt', afParamsExt).then(function (rsp) { 212 | if (rsp.status !== 0 && rsp.status !== 'SUCCESS') // unsuccessful 213 | areq.reject(afEventCnf, new Error('AF data request failed, status code: ' + rsp.status + '.')); 214 | else if (!apsAck) 215 | areq.resolve(afEventCnf, rsp); 216 | }).fail(function (err) { 217 | areq.reject(afEventCnf, err); 218 | }).done(); 219 | } 220 | 221 | return deferred.promise.nodeify(callback); 222 | }; 223 | 224 | af.zclFoundation = function (srcEp, dstEp, cId, cmd, zclData, cfg, callback) { 225 | // callback(err[, rsp]) 226 | var deferred = Q.defer(), 227 | areq = af.areq, 228 | dir = (srcEp === dstEp) ? 0 : 1, // 0: client-to-server, 1: server-to-client 229 | manufCode = 0, 230 | frameCntl, 231 | seqNum, 232 | zclBuffer, 233 | mandatoryEvent; 234 | 235 | if (_.isFunction(cfg)) { 236 | if (!_.isFunction(callback)) { 237 | callback = cfg; 238 | cfg = {}; 239 | } 240 | } else { 241 | cfg = cfg || {}; 242 | } 243 | 244 | proving.stringOrNumber(cmd, 'cmd should be a number or a string.'); 245 | proving.object(cfg, 'cfg should be a plain object if given.'); 246 | 247 | frameCntl = { 248 | frameType: 0, // command acts across the entire profile (foundation) 249 | manufSpec: cfg.hasOwnProperty('manufSpec') ? cfg.manufSpec : 0, 250 | direction: cfg.hasOwnProperty('direction') ? cfg.direction : dir, 251 | disDefaultRsp: cfg.hasOwnProperty('disDefaultRsp') ? cfg.disDefaultRsp : 0 // enable deafult response command 252 | }; 253 | 254 | if (frameCntl.manufSpec === 1) 255 | manufCode = dstEp.getManufCode(); 256 | 257 | // .frame(frameCntl, manufCode, seqNum, cmd, zclPayload[, clusterId]) 258 | seqNum = cfg.hasOwnProperty('seqNum') ? cfg.seqNum : nextZclSeqNum(); 259 | 260 | try { 261 | zclBuffer = zcl.frame(frameCntl, manufCode, seqNum, cmd, zclData); 262 | } catch (e) { 263 | if (e.message === 'Unrecognized command') { 264 | deferred.reject(e); 265 | return deferred.promise.nodeify(callback); 266 | } else { 267 | throw e; 268 | } 269 | } 270 | 271 | if (frameCntl.direction === 0) { // client-to-server, thus require getting the feedback response 272 | 273 | if (srcEp === dstEp) // from remote to remote itself 274 | mandatoryEvent = 'ZCL:incomingMsg:' + dstEp.getNwkAddr() + ':' + dstEp.getEpId() + ':' + seqNum; 275 | else // from local ep to remote ep 276 | mandatoryEvent = 'ZCL:incomingMsg:' + dstEp.getNwkAddr() + ':' + dstEp.getEpId() + ':' + srcEp.getEpId() + ':' + seqNum; 277 | 278 | areq.register(mandatoryEvent, deferred, function (msg) { 279 | // { groupid, clusterid, srcaddr, srcendpoint, dstendpoint, wasbroadcast, linkquality, securityuse, timestamp, transseqnumber, zclMsg } 280 | areq.resolve(mandatoryEvent, msg.zclMsg); 281 | }); 282 | } 283 | 284 | af.send(srcEp, dstEp, cId, zclBuffer).fail(function (err) { 285 | if (mandatoryEvent && areq.isEventPending(mandatoryEvent)) 286 | areq.reject(mandatoryEvent, err); 287 | else 288 | deferred.reject(err); 289 | }).then(function (rsp) { 290 | if (!mandatoryEvent) 291 | deferred.resolve(rsp); 292 | }).done(); 293 | 294 | return deferred.promise.nodeify(callback); 295 | }; 296 | 297 | af.zclFunctional = function (srcEp, dstEp, cId, cmd, zclData, cfg, callback) { 298 | // callback(err[, rsp]) 299 | var deferred = Q.defer(), 300 | areq = af.areq, 301 | dir = (srcEp === dstEp) ? 0 : 1, // 0: client-to-server, 1: server-to-client 302 | manufCode = 0, 303 | seqNum, 304 | frameCntl, 305 | zclBuffer, 306 | mandatoryEvent; 307 | 308 | if (_.isFunction(cfg)) { 309 | if (!_.isFunction(callback)) { 310 | callback = cfg; 311 | cfg = {}; 312 | } 313 | } else { 314 | cfg = cfg || {}; 315 | } 316 | 317 | if (!((srcEp instanceof Endpoint) || (srcEp instanceof Coordpoint))) 318 | throw new TypeError('srcEp should be an instance of Endpoint class.'); 319 | 320 | if (!((dstEp instanceof Endpoint) || (dstEp instanceof Coordpoint))) 321 | throw new TypeError('dstEp should be an instance of Endpoint class.'); 322 | 323 | if (typeof zclData !== 'object' || zclData === null) 324 | throw new TypeError('zclData should be an object or an array'); 325 | 326 | proving.stringOrNumber(cId, 'cId should be a number or a string.'); 327 | proving.stringOrNumber(cmd, 'cmd should be a number or a string.'); 328 | proving.object(cfg, 'cfg should be a plain object if given.'); 329 | 330 | frameCntl = { 331 | frameType: 1, // functional command frame 332 | manufSpec: cfg.hasOwnProperty('manufSpec') ? cfg.manufSpec : 0, 333 | direction: cfg.hasOwnProperty('direction') ? cfg.direction : dir, 334 | disDefaultRsp: cfg.hasOwnProperty('disDefaultRsp') ? cfg.disDefaultRsp : 0 // enable deafult response command 335 | }; 336 | 337 | if (frameCntl.manufSpec === 1) 338 | manufCode = dstEp.getManufCode(); 339 | 340 | // .frame(frameCntl, manufCode, seqNum, cmd, zclPayload[, clusterId]) 341 | seqNum = cfg.hasOwnProperty('seqNum') ? cfg.seqNum : nextZclSeqNum(); 342 | 343 | try { 344 | zclBuffer = zcl.frame(frameCntl, manufCode, seqNum, cmd, zclData, cId); 345 | } catch (e) { 346 | if (e.message === 'Unrecognized command' || e.message === 'Unrecognized cluster') { 347 | deferred.reject(e); 348 | return deferred.promise.nodeify(callback); 349 | } else { 350 | deferred.reject(e); 351 | return deferred.promise.nodeify(callback); 352 | } 353 | } 354 | 355 | if (frameCntl.direction === 0) { // client-to-server, thus require getting the feedback response 356 | 357 | if (srcEp === dstEp) // from remote to remote itself 358 | mandatoryEvent = 'ZCL:incomingMsg:' + dstEp.getNwkAddr() + ':' + dstEp.getEpId() + ':' + seqNum; 359 | else // from local ep to remote ep 360 | mandatoryEvent = 'ZCL:incomingMsg:' + dstEp.getNwkAddr() + ':' + dstEp.getEpId() + ':' + srcEp.getEpId() + ':' + seqNum; 361 | 362 | areq.register(mandatoryEvent, deferred, function (msg) { 363 | // { groupid, clusterid, srcaddr, srcendpoint, dstendpoint, wasbroadcast, linkquality, securityuse, timestamp, transseqnumber, zclMsg } 364 | areq.resolve(mandatoryEvent, msg.zclMsg); 365 | }); 366 | } 367 | 368 | // af.send(srcEp, dstEp, cId, rawPayload, opt, callback) 369 | af.send(srcEp, dstEp, cId, zclBuffer).fail(function (err) { 370 | if (mandatoryEvent && areq.isEventPending(mandatoryEvent)) 371 | areq.reject(mandatoryEvent, err); 372 | else 373 | deferred.reject(err); 374 | }).then(function (rsp) { 375 | if (!mandatoryEvent) 376 | deferred.resolve(rsp); 377 | }).done(); 378 | 379 | return deferred.promise.nodeify(callback); 380 | }; 381 | 382 | /*************************************************************************************************/ 383 | /*** ZCL Cluster and Attribute Requests ***/ 384 | /*************************************************************************************************/ 385 | af.zclClustersReq = function (dstEp, eventEmitter, callback) { // callback(err, clusters) 386 | // clusters: { 387 | // genBasic: { dir: 1, attrs: { x1: 0, x2: 3, ... } }, // dir => 0: 'unknown', 1: 'in', 2: 'out' 388 | // fooClstr: { dir: 1, attrs: { x1: 0, x2: 3, ... } }, 389 | // ... 390 | // } 391 | 392 | var epId; 393 | try { 394 | epId = dstEp.getEpId(); 395 | } catch (err){ 396 | epId = null; 397 | } 398 | 399 | // If event emitter is function, consider it callback (legacy) 400 | if (typeof eventEmitter === 'function') callback = eventEmitter; 401 | 402 | var deferred = Q.defer(), 403 | clusters = {}, 404 | clusterList = dstEp.getClusterList(), // [ 1, 2, 3, 4, 5 ] 405 | inClusterList = dstEp.getInClusterList(), // [ 1, 2, 3 ] 406 | outClusterList = dstEp.getOutClusterList(), // [ 1, 3, 4, 5 ] 407 | clusterAttrsReqs = []; // functions 408 | // clusterAttrsRsps = []; // { attr1: x }, ... 409 | 410 | var i = 0; 411 | // each request 412 | _.forEach(clusterList, function (cId) { 413 | var cIdString = zclId.cluster(cId); 414 | cIdString = cIdString ? cIdString.key : cId; 415 | 416 | clusterAttrsReqs.push(function (clusters) { 417 | return af.zclClusterAttrsReq(dstEp, cId).then(function (attrs) { 418 | i++; 419 | if (eventEmitter instanceof EventEmitter) { 420 | eventEmitter.emit('ind:interview', { 421 | endpoint: { 422 | current: epId, 423 | cluster: { 424 | total: clusterList.length, 425 | current: i, 426 | } 427 | } 428 | }); 429 | } 430 | clusters[cIdString] = clusters[cIdString] || { dir: 0, attrs: null }; 431 | clusters[cIdString].dir = _.includes(inClusterList, cId) ? (clusters[cIdString].dir | 0x01) : clusters[cIdString].dir; 432 | clusters[cIdString].dir = _.includes(outClusterList, cId) ? (clusters[cIdString].dir | 0x02) : clusters[cIdString].dir; 433 | clusters[cIdString].attrs = attrs; 434 | return clusters; 435 | }).fail(function (err) { 436 | i++; 437 | if (eventEmitter instanceof EventEmitter) { 438 | eventEmitter.emit('ind:interview', { 439 | endpoint:{ 440 | current: epId, 441 | cluster: { 442 | total: clusterList.length, 443 | current: i, 444 | } 445 | } 446 | }); 447 | } 448 | clusters[cIdString] = clusters[cIdString] || { dir: 0, attrs: null }; 449 | clusters[cIdString].dir = _.includes(inClusterList, cId) ? (clusters[cIdString].dir | 0x01) : clusters[cIdString].dir; 450 | clusters[cIdString].dir = _.includes(outClusterList, cId) ? (clusters[cIdString].dir | 0x02) : clusters[cIdString].dir; 451 | clusters[cIdString].attrs = {}; 452 | return clusters; 453 | }); 454 | }); 455 | }); 456 | 457 | // all clusters 458 | var allReqs = clusterAttrsReqs.reduce(function (soFar, f) { 459 | return soFar.then(f); 460 | }, Q(clusters)); 461 | 462 | allReqs.then(function (clusters) { 463 | deferred.resolve(clusters); 464 | }).fail(function (err) { 465 | deferred.reject(err); 466 | }).done(); 467 | 468 | return deferred.promise.nodeify(callback); 469 | }; 470 | 471 | af.zclClusterAttrsReq = function (dstEp, cId, callback) { 472 | var deferred = Q.defer(); 473 | 474 | af.zclClusterAttrIdsReq(dstEp, cId).then(function (attrIds) { 475 | var readReq = [], 476 | attrsReqs = [], 477 | attributes = [], 478 | attrIdsLen = attrIds.length; 479 | 480 | _.forEach(attrIds, function (id) { 481 | readReq.push({ attrId: id }); 482 | 483 | if (readReq.length === 5 || readReq.length === attrIdsLen) { 484 | var req = _.cloneDeep(readReq); 485 | attrsReqs.push(function (attributes) { 486 | return af.zclFoundation(dstEp, dstEp, cId, 'read', req).then(function (readStatusRecsRsp) { 487 | attributes = _.concat(attributes, readStatusRecsRsp.payload); 488 | return attributes; 489 | }); 490 | }); 491 | attrIdsLen -= 5; 492 | readReq = []; 493 | } 494 | }); 495 | 496 | return attrsReqs.reduce(function (soFar, f) { 497 | return soFar.then(f); 498 | }, Q(attributes)); 499 | }).then(function (attributes) { 500 | var attrs = {}; 501 | _.forEach(attributes, function (rec) { // { attrId, status, dataType, attrData } 502 | var attrIdString = zclId.attr(cId, rec.attrId); 503 | 504 | attrIdString = attrIdString ? attrIdString.key : rec.attrId; 505 | 506 | attrs[attrIdString] = null; 507 | 508 | if (rec.status === 0) 509 | attrs[attrIdString] = rec.attrData; 510 | }); 511 | 512 | return attrs; 513 | }).then(function (attrs) { 514 | deferred.resolve(attrs); 515 | }).fail(function (err) { 516 | deferred.reject(err); 517 | }).done(); 518 | 519 | return deferred.promise.nodeify(callback); 520 | }; 521 | 522 | af.zclClusterAttrIdsReq = function (dstEp, cId, callback) { 523 | var deferred = Q.defer(), 524 | attrsToRead = []; 525 | 526 | if (!((dstEp instanceof Endpoint) || (dstEp instanceof Coordpoint))) 527 | throw new TypeError('dstEp should be an instance of Endpoint class.'); 528 | 529 | var discAttrs = function (startAttrId, defer) { 530 | af.zclFoundation(dstEp, dstEp, cId, 'discover', { 531 | startAttrId: startAttrId, 532 | maxAttrIds: 240 533 | }).then(function (discoverRsp) { 534 | // discoverRsp.payload: { discComplete, attrInfos: [ { attrId, dataType }, ... ] } 535 | var payload = discoverRsp.payload, 536 | discComplete = payload.discComplete, 537 | attrInfos = payload.attrInfos, 538 | nextReqIndex; 539 | 540 | _.forEach(attrInfos, function (info) { 541 | if (_.indexOf(attrsToRead, info.attrId) === -1) 542 | attrsToRead.push(info.attrId); 543 | }); 544 | 545 | if (discComplete === 0) { 546 | nextReqIndex = attrInfos[attrInfos.length - 1].attrId + 1; 547 | discAttrs(nextReqIndex, defer); 548 | } else { 549 | defer.resolve(attrsToRead); 550 | } 551 | }).fail(function (err) { 552 | defer.reject(err); 553 | }).done(); 554 | }; 555 | 556 | discAttrs(0, deferred); 557 | 558 | return deferred.promise.nodeify(callback); 559 | }; 560 | 561 | /*************************************************************************************************/ 562 | /*** Private Functions: Message Dispatcher ***/ 563 | /*************************************************************************************************/ 564 | // 4 types of message: dataConfirm, reflectError, incomingMsg, incomingMsgExt, zclIncomingMsg 565 | function dispatchIncomingMsg(type, msg) { 566 | var targetEp, // remote ep, or a local ep (maybe a delegator) 567 | remoteEp, 568 | dispatchTo, // which callback on targetEp 569 | zclHeader, 570 | frameType, // check whether the msg is foundation(0) or functional(1) 571 | mandatoryEvent; // bridged event 572 | 573 | if (msg.hasOwnProperty('endpoint')) { // dataConfirm, reflectError 574 | targetEp = af.controller.getCoord().getEndpoint(msg.endpoint); // => find local ep, such a message is going to local ep 575 | } else if (msg.hasOwnProperty('srcaddr') && msg.hasOwnProperty('srcendpoint')) { // incomingMsg, incomingMsgExt, zclIncomingMsg 576 | targetEp = af.controller.getCoord().getEndpoint(msg.dstendpoint); // => find local ep 577 | 578 | if (targetEp) { // local 579 | remoteEp = af.controller.findEndpoint(msg.srcaddr, msg.srcendpoint); 580 | 581 | if (targetEp.isDelegator()) { // delegator, pass message to remote endpoint 582 | targetEp = remoteEp; 583 | } else if (!remoteEp) { // local zApp not found, get ieeeaddr and emit fake 'endDeviceAnnceInd' msg 584 | var msgBuffer = rebornDevs[msg.srcaddr]; 585 | 586 | if (_.isArray(msgBuffer)) { 587 | msgBuffer.push({ type: type, msg: msg }); 588 | } else if (_.isUndefined(msgBuffer)) { 589 | msgBuffer = rebornDevs[msg.srcaddr] = [ { type: type, msg: msg } ]; 590 | 591 | af.controller.request('ZDO', 'ieeeAddrReq', { shortaddr: msg.srcaddr, reqtype: 0, startindex:0 }).then(function (rsp) { 592 | // rsp: { status, ieeeaddr, nwkaddr, startindex, numassocdev, assocdevlist } 593 | af.controller.once('ind:incoming' + ':' + rsp.ieeeaddr, function () { 594 | if (af.controller.findEndpoint(msg.srcaddr, msg.srcendpoint) && _.isArray(msgBuffer)) 595 | _.forEach(msgBuffer, function(item) { 596 | dispatchIncomingMsg(item.type, item.msg); 597 | }); 598 | else 599 | delete rebornDevs[msg.srcaddr]; 600 | }); 601 | af.controller.emit('ZDO:endDeviceAnnceInd', { srcaddr: rsp.nwkaddr, nwkaddr: rsp.nwkaddr, ieeeaddr: rsp.ieeeaddr, capabilities: {} }); 602 | }).fail(function (err) { 603 | delete rebornDevs[msg.srcaddr]; 604 | }).done(); 605 | } 606 | 607 | return; 608 | } 609 | } 610 | } 611 | 612 | if (!targetEp) // if target not found, ignore this message 613 | return; 614 | 615 | switch (type) { 616 | case 'dataConfirm': 617 | // msg: { status, endpoint, transid } 618 | mandatoryEvent = 'AF:dataConfirm:' + msg.endpoint + ':' + msg.transid; // sender(loEp) is listening, see af.send() and af.sendExt() 619 | dispatchTo = targetEp.onAfDataConfirm; 620 | break; 621 | case 'reflectError': 622 | // msg: { status, endpoint, transid, dstaddrmode, dstaddr } 623 | mandatoryEvent = 'AF:reflectError:' + msg.endpoint + ':' + msg.transid; 624 | dispatchTo = targetEp.onAfReflectError; 625 | break; 626 | case 'incomingMsg': 627 | // msg: { groupid, clusterid, srcaddr, srcendpoint, dstendpoint, wasbroadcast, linkquality, securityuse, timestamp, transseqnumber, len, data } 628 | zclHeader = zcl.header(msg.data); // a zcl packet maybe, pre-parse it to get the header 629 | dispatchTo = targetEp.onAfIncomingMsg; 630 | break; 631 | case 'incomingMsgExt': 632 | // msg: { groupid, clusterid, srcaddr, srcendpoint, dstendpoint, wasbroadcast, linkquality, securityuse, timestamp, transseqnumber, len, data } 633 | zclHeader = zcl.header(msg.data); // a zcl packet maybe, pre-parse it to get the header 634 | dispatchTo = targetEp.onAfIncomingMsgExt; 635 | break; 636 | case 'zclIncomingMsg': 637 | // { groupid, clusterid, srcaddr, srcendpoint, dstendpoint, wasbroadcast, linkquality, securityuse, timestamp, transseqnumber, zclMsg } 638 | if (targetEp.isLocal()) { 639 | // to local app ep, receive zcl command or zcl command response. see af.zclFoudation() and af.zclFunctional() 640 | if (!targetEp.isDelegator()) 641 | mandatoryEvent = 'ZCL:incomingMsg:' + msg.srcaddr + ':' + msg.srcendpoint + ':' + msg.dstendpoint + ':' + msg.zclMsg.seqNum; 642 | } else { 643 | var localEp = af.controller.findEndpoint(0, msg.dstendpoint), 644 | toLocalApp = false; 645 | 646 | if (localEp) 647 | toLocalApp = localEp.isLocal() ? !localEp.isDelegator() : false; 648 | 649 | if (toLocalApp) { 650 | mandatoryEvent = 'ZCL:incomingMsg:' + msg.srcaddr + ':' + msg.srcendpoint + ':' + msg.dstendpoint + ':' + msg.zclMsg.seqNum; 651 | } else { 652 | // to remote ep, receive the zcl command response 653 | mandatoryEvent = 'ZCL:incomingMsg:' + msg.srcaddr + ':' + msg.srcendpoint + ':' + msg.zclMsg.seqNum; 654 | } 655 | } 656 | 657 | // msg.data is now msg.zclMsg 658 | frameType = msg.zclMsg.frameCntl.frameType; 659 | if (frameType === 0 && msg.zclMsg.cmdId === 'report') 660 | af.controller.getShepherd().emit('ind:reported', targetEp, msg.clusterid, msg.zclMsg.payload); 661 | 662 | if (frameType === 0) // foundation 663 | dispatchTo = targetEp.onZclFoundation; 664 | else if (frameType === 1) // functional 665 | dispatchTo = targetEp.onZclFunctional; 666 | break; 667 | } 668 | 669 | if (_.isFunction(dispatchTo)) { 670 | setImmediate(function () { 671 | dispatchTo(msg, remoteEp); 672 | }); 673 | } 674 | 675 | if (mandatoryEvent) 676 | af.controller.emit(mandatoryEvent, msg); 677 | 678 | if (type === 'zclIncomingMsg') // no need for further parsing 679 | return; 680 | 681 | // further parse for ZCL packet from incomingMsg and incomingMsgExt 682 | if (zclHeader) { // if (zclHeader && targetEp.isZclSupported()) { 683 | if (zclHeader.frameCntl.frameType === 0) { // foundation 684 | zcl.parse(msg.data, function (err, zclData) { 685 | if (!err) 686 | zclIncomingParsedMsgEmitter(msg, zclData); 687 | }); 688 | } else if (zclHeader.frameCntl.frameType === 1) { // functional 689 | zcl.parse(msg.data, msg.clusterid, function (err, zclData) { 690 | if (!err) 691 | zclIncomingParsedMsgEmitter(msg, zclData); 692 | }); 693 | } 694 | } 695 | } 696 | 697 | /*************************************************************************************************/ 698 | /*** Private Functions: Af and Zcl Incoming Message Handlers ***/ 699 | /*************************************************************************************************/ 700 | function dataConfirmHandler(msg) { 701 | return dispatchIncomingMsg('dataConfirm', msg); 702 | } 703 | 704 | function reflectErrorHandler(msg) { 705 | return dispatchIncomingMsg('reflectError', msg); 706 | } 707 | 708 | function incomingMsgHandler(msg) { 709 | return dispatchIncomingMsg('incomingMsg', msg); 710 | } 711 | 712 | function incomingMsgExtHandler(msg) { 713 | return dispatchIncomingMsg('incomingMsgExt', msg); 714 | } 715 | 716 | function zclIncomingMsgHandler(msg) { 717 | return dispatchIncomingMsg('zclIncomingMsg', msg); 718 | } 719 | 720 | /*************************************************************************************************/ 721 | /*** Private Functions ***/ 722 | /*************************************************************************************************/ 723 | function zclIncomingParsedMsgEmitter(msg, zclData) { // after zcl packet parsed, re-emit it 724 | var parsedMsg = _.cloneDeep(msg); 725 | parsedMsg.zclMsg = zclData; 726 | 727 | setImmediate(function () { 728 | af.controller.emit('ZCL:incomingMsg', parsedMsg); 729 | }); 730 | } 731 | 732 | function makeAfParams(loEp, dstEp, cId, rawPayload, opt) { 733 | opt = opt || {}; // opt = { options, radius } 734 | 735 | proving.number(cId, 'cId should be a number.'); 736 | 737 | if (opt.hasOwnProperty('options')) 738 | proving.number(opt.options, 'opt.options should be a number.'); 739 | 740 | if (opt.hasOwnProperty('radius')) 741 | proving.number(opt.radius, 'opt.radius should be a number.'); 742 | 743 | var afOptions = ZSC.AF.options.ACK_REQUEST | ZSC.AF.options.DISCV_ROUTE, // ACK_REQUEST (0x10), DISCV_ROUTE (0x20) 744 | afParams = { 745 | dstaddr: dstEp.getNwkAddr(), 746 | destendpoint: dstEp.getEpId(), 747 | srcendpoint: loEp.getEpId(), 748 | clusterid: cId, 749 | transid: af.controller ? af.controller.nextTransId() : null, 750 | options: opt.hasOwnProperty('options') ? opt.options : afOptions, 751 | radius: opt.hasOwnProperty('radius') ? opt.radius : ZSC.AF_DEFAULT_RADIUS, 752 | len: rawPayload.length, 753 | data: rawPayload 754 | }; 755 | 756 | return afParams; 757 | } 758 | 759 | function makeAfParamsExt(loEp, addrMode, dstAddrOrGrpId, cId, rawPayload, opt) { 760 | opt = opt || {}; // opt = { options, radius, dstEpId, dstPanId } 761 | 762 | proving.number(cId, 'cId should be a number.'); 763 | 764 | proving.defined(loEp, 'loEp should be defined'); 765 | 766 | if (opt.hasOwnProperty('options')) 767 | proving.number(opt.options, 'opt.options should be a number.'); 768 | 769 | if (opt.hasOwnProperty('radius')) 770 | proving.number(opt.radius, 'opt.radius should be a number.'); 771 | 772 | var afOptions = ZSC.AF.options.DISCV_ROUTE, 773 | afParamsExt = { 774 | dstaddrmode: addrMode, 775 | dstaddr: zutils.toLongAddrString(dstAddrOrGrpId), 776 | destendpoint: 0xFF, 777 | dstpanid: opt.hasOwnProperty('dstPanId') ? opt.dstPanId : 0, 778 | srcendpoint: loEp.getEpId(), 779 | clusterid: cId, 780 | transid: af.controller ? af.controller.nextTransId() : null, 781 | options: opt.hasOwnProperty('options') ? opt.options : afOptions, 782 | radius: opt.hasOwnProperty('radius') ? opt.radius : ZSC.AF_DEFAULT_RADIUS, 783 | len: rawPayload.length, 784 | data: rawPayload 785 | }; 786 | 787 | switch (addrMode) { 788 | case ZSC.AF.addressMode.ADDR_NOT_PRESENT: 789 | break; 790 | case ZSC.AF.addressMode.ADDR_GROUP: 791 | afParamsExt.destendpoint = 0xFF; 792 | break; 793 | case ZSC.AF.addressMode.ADDR_16BIT: 794 | case ZSC.AF.addressMode.ADDR_64BIT: 795 | afParamsExt.destendpoint = opt.hasOwnProperty('dstEpId') ? opt.dstEpId : 0xFF; 796 | afParamsExt.options = opt.hasOwnProperty('options') ? opt.options : afOptions | ZSC.AF.options.ACK_REQUEST; 797 | break; 798 | case ZSC.AF.addressMode.ADDR_BROADCAST: 799 | afParamsExt.destendpoint = 0xFF; 800 | afParamsExt.dstaddr = zutils.toLongAddrString(0xFFFF); 801 | break; 802 | default: 803 | afParamsExt = null; 804 | break; 805 | } 806 | 807 | return afParamsExt; 808 | } 809 | 810 | function nextZclSeqNum() { 811 | seqNumber += 1; // seqNumber is a private var on the top of this module 812 | if (seqNumber > 255 || seqNumber < 0 ) 813 | seqNumber = 0; 814 | 815 | af._seq = seqNumber; 816 | return seqNumber; 817 | } 818 | 819 | /*************************************************************************************************/ 820 | /*** module.exports ***/ 821 | /*************************************************************************************************/ 822 | module.exports = function (controller) { 823 | var msgHandlers = [ 824 | { evt: 'AF:dataConfirm', hdlr: dataConfirmHandler }, 825 | { evt: 'AF:reflectError', hdlr: reflectErrorHandler }, 826 | { evt: 'AF:incomingMsg', hdlr: incomingMsgHandler }, 827 | { evt: 'AF:incomingMsgExt', hdlr: incomingMsgExtHandler }, 828 | { evt: 'ZCL:incomingMsg', hdlr: zclIncomingMsgHandler } 829 | ]; 830 | 831 | if (!(controller instanceof EventEmitter)) 832 | throw new TypeError('Controller should be an EventEmitter.'); 833 | 834 | af.controller = controller; 835 | af.areq = new Areq(controller); 836 | 837 | function isAttached(evt, lsn) { 838 | var has = false, 839 | lsns = af.controller.listeners(evt); 840 | 841 | if (_.isArray(lsns) && lsns.length) { 842 | has = _.find(lsns, function (n) { 843 | return n === lsn; 844 | }); 845 | } else if (_.isFunction(lsns)) { 846 | has = (lsns === lsn); 847 | } 848 | return !!has; 849 | } 850 | 851 | // attach event listeners 852 | _.forEach(msgHandlers, function (rec) { 853 | if (!isAttached(rec.evt, rec.hdlr)) 854 | af.controller.on(rec.evt, rec.hdlr); 855 | }); 856 | 857 | return af; 858 | }; 859 | -------------------------------------------------------------------------------- /lib/components/controller.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 'use strict'; 3 | 4 | var util = require('util'), 5 | EventEmitter = require('events'); 6 | 7 | var Q = require('q'), 8 | _ = require('busyman'), 9 | znp = require('cc-znp'), 10 | proving = require('proving'), 11 | ZSC = require('zstack-constants'), 12 | debug = { 13 | shepherd: require('debug')('zigbee-shepherd'), 14 | init: require('debug')('zigbee-shepherd:init'), 15 | request: require('debug')('zigbee-shepherd:request'), 16 | response: require('debug')('zigbee-shepherd:response') 17 | }; 18 | 19 | var Zdo = require('./zdo'), 20 | querie = require('./querie'), 21 | bridge = require('./event_bridge.js'), 22 | init = require('../initializers/init_controller'), 23 | nvParams = require('../config/nv_start_options.js'); 24 | 25 | var Device = require('../model/device'), 26 | Coordpoint = require('../model/coordpoint'); 27 | 28 | function Controller(shepherd, cfg) { 29 | // cfg is serial port config 30 | var self = this, 31 | transId = 0; 32 | 33 | EventEmitter.call(this); 34 | 35 | if (!_.isPlainObject(cfg)) 36 | throw new TypeError('cfg should be an object.'); 37 | 38 | /***************************************************/ 39 | /*** Protected Members ***/ 40 | /***************************************************/ 41 | this._shepherd = shepherd; 42 | this._coord = null; 43 | this._znp = znp; 44 | this._cfg = cfg; 45 | this._zdo = new Zdo(this); 46 | this._resetting = false; 47 | this._spinLock = false; 48 | this._joinQueue = []; 49 | this._permitJoinTime = 0; 50 | this._permitJoinInterval; 51 | 52 | this._net = { 53 | state: null, 54 | channel: null, 55 | panId: null, 56 | extPanId: null, 57 | ieeeAddr: null, 58 | nwkAddr: null, 59 | joinTimeLeft: 0 60 | }; 61 | 62 | /***************************************************/ 63 | /*** Public Members ***/ 64 | /***************************************************/ 65 | this.querie = querie(this); 66 | 67 | this.nextTransId = function () { // zigbee transection id 68 | if (++transId > 255) 69 | transId = 1; 70 | return transId; 71 | }; 72 | 73 | this.permitJoinCountdown = function () { 74 | return self._permitJoinTime -= 1; 75 | }; 76 | 77 | this.isResetting = function () { 78 | return self._resetting; 79 | }; 80 | 81 | /***************************************************/ 82 | /*** Event Handlers ***/ 83 | /***************************************************/ 84 | this._znp.on('ready', function () { 85 | init.setupCoord(self).then(function () { 86 | self.emit('ZNP:INIT'); 87 | }).fail(function (err) { 88 | self.emit('ZNP:INIT', err); 89 | debug.init('Coordinator initialize had an error:', err); 90 | }).done(); 91 | }); 92 | 93 | this._znp.on('close', function () { 94 | self.emit('ZNP:CLOSE'); 95 | }); 96 | 97 | this._znp.on('AREQ', function (msg) { 98 | bridge._areqEventBridge(self, msg); 99 | }); 100 | 101 | this.on('ZDO:endDeviceAnnceInd', function (data) { 102 | console.log('spinlock:', self._spinLock, self._joinQueue); 103 | if (self._spinLock) { 104 | // Check if joinQueue already has this device 105 | for (var i = 0; i < self._joinQueue.length; i++) { 106 | if (self._joinQueue[i].ieeeAddr == data.ieeeaddr) { 107 | console.log('already in joinqueue'); 108 | return; 109 | } 110 | } 111 | 112 | self._joinQueue.push({ 113 | func: function () { 114 | self.endDeviceAnnceHdlr(data); 115 | }, 116 | ieeeAddr: data.ieeeaddr 117 | }); 118 | } else { 119 | self._spinLock = true; 120 | self.endDeviceAnnceHdlr(data); 121 | } 122 | }); 123 | } 124 | 125 | util.inherits(Controller, EventEmitter); 126 | 127 | /*************************************************************************************************/ 128 | /*** Public ZigBee Utility APIs ***/ 129 | /*************************************************************************************************/ 130 | Controller.prototype.getShepherd = function () { 131 | return this._shepherd; 132 | }; 133 | 134 | Controller.prototype.getCoord = function () { 135 | return this._coord; 136 | }; 137 | 138 | Controller.prototype.getNetInfo = function () { 139 | var net = _.cloneDeep(this._net); 140 | 141 | if (net.state === ZSC.ZDO.devStates.ZB_COORD) 142 | net.state = 'Coordinator'; 143 | 144 | net.joinTimeLeft = this._permitJoinTime; 145 | 146 | return net; 147 | }; 148 | 149 | Controller.prototype.setNetInfo = function (netInfo) { 150 | var self = this; 151 | 152 | _.forEach(netInfo, function (val, key) { 153 | if (_.has(self._net, key)) 154 | self._net[key] = val; 155 | }); 156 | }; 157 | 158 | /*************************************************************************************************/ 159 | /*** Mandatory Public APIs ***/ 160 | /*************************************************************************************************/ 161 | Controller.prototype.start = function (callback) { 162 | var self = this, 163 | deferred = Q.defer(), 164 | readyLsn; 165 | 166 | readyLsn = function (err) { 167 | return err ? deferred.reject(err) : deferred.resolve(); 168 | }; 169 | 170 | this.once('ZNP:INIT', readyLsn); 171 | 172 | Q.ninvoke(this._znp, 'init', this._cfg).fail(function (err) { 173 | self.removeListener('ZNP:INIT', readyLsn); 174 | deferred.reject(err); 175 | }).done(); 176 | 177 | return deferred.promise.nodeify(callback); 178 | }; 179 | 180 | Controller.prototype.close = function (callback) { 181 | var self = this, 182 | deferred = Q.defer(), 183 | closeLsn; 184 | 185 | closeLsn = function () { 186 | deferred.resolve(); 187 | }; 188 | 189 | this.once('ZNP:CLOSE', closeLsn); 190 | 191 | Q.ninvoke(this._znp, 'close').fail(function (err) { 192 | self.removeListener('ZNP:CLOSE', closeLsn); 193 | deferred.reject(err); 194 | }).done(); 195 | 196 | return deferred.promise.nodeify(callback); 197 | }; 198 | 199 | Controller.prototype.reset = function (mode, callback) { 200 | var self = this, 201 | deferred = Q.defer(), 202 | startupOption = nvParams.startupOption.value[0]; 203 | 204 | proving.stringOrNumber(mode, 'mode should be a number or a string.'); 205 | 206 | Q.fcall(function () { 207 | if (mode === 'soft' || mode === 1) { 208 | debug.shepherd('Starting a software reset...'); 209 | self._resetting = true; 210 | 211 | return self.request('SYS', 'resetReq', { type: 0x01 }); 212 | } else if (mode === 'hard' || mode === 0) { 213 | debug.shepherd('Starting a hardware reset...'); 214 | self._resetting = true; 215 | 216 | if (self._nvChanged && startupOption !== 0x02) 217 | nvParams.startupOption.value[0] = 0x02; 218 | 219 | var steps = [ 220 | function () { return self.request('SYS', 'resetReq', { type: 0x01 }).delay(0); }, 221 | function () { return self.request('SAPI', 'writeConfiguration', nvParams.startupOption).delay(10); }, 222 | function () { return self.request('SYS', 'resetReq', { type: 0x01 }).delay(10); }, 223 | function () { return self.request('SAPI', 'writeConfiguration', nvParams.panId).delay(10); }, 224 | function () { return self.request('SAPI', 'writeConfiguration', nvParams.extPanId).delay(10); }, 225 | function () { return self.request('SAPI', 'writeConfiguration', nvParams.channelList).delay(10); }, 226 | function () { return self.request('SAPI', 'writeConfiguration', nvParams.logicalType).delay(10); }, 227 | function () { return self.request('SAPI', 'writeConfiguration', nvParams.precfgkey).delay(10); }, 228 | function () { return self.request('SAPI', 'writeConfiguration', nvParams.precfgkeysEnable).delay(10); }, 229 | function () { return self.request('SYS', 'osalNvWrite', nvParams.securityMode).delay(10); }, 230 | function () { return self.request('SAPI', 'writeConfiguration', nvParams.zdoDirectCb).delay(10); }, 231 | function () { return self.request('SYS', 'osalNvItemInit', nvParams.znpCfgItem).delay(10).fail(function (err) { 232 | return (err.message === 'rsp error: 9') ? null : Q.reject(err); // Success, item created and initialized 233 | }); }, 234 | function () { return self.request('SYS', 'osalNvWrite', nvParams.znpHasConfigured).delay(10); } 235 | ]; 236 | 237 | return steps.reduce(function (soFar, fn) { 238 | return soFar.then(fn); 239 | }, Q(0)); 240 | } else { 241 | return Q.reject(new Error('Unknown reset mode.')); 242 | } 243 | }).then(function () { 244 | self._resetting = false; 245 | if (self._nvChanged) { 246 | nvParams.startupOption.value[0] = startupOption; 247 | self._nvChanged = false; 248 | deferred.resolve(); 249 | } else { 250 | self.once('_reset', function (err) { 251 | return err ? deferred.reject(err) : deferred.resolve(); 252 | }); 253 | self.emit('SYS:resetInd', '_reset'); 254 | } 255 | }).fail(function (err) { 256 | deferred.reject(err); 257 | }).done(); 258 | 259 | return deferred.promise.nodeify(callback); 260 | }; 261 | 262 | Controller.prototype.request = function (subsys, cmdId, valObj, callback) { 263 | var deferred = Q.defer(), 264 | rspHdlr; 265 | 266 | proving.stringOrNumber(subsys, 'subsys should be a number or a string.'); 267 | proving.stringOrNumber(cmdId, 'cmdId should be a number or a string.'); 268 | 269 | if (!_.isPlainObject(valObj) && !_.isArray(valObj)) 270 | throw new TypeError('valObj should be an object or an array.'); 271 | 272 | if (_.isString(subsys)) 273 | subsys = subsys.toUpperCase(); 274 | 275 | rspHdlr = function (err, rsp) { 276 | if (subsys !== 'ZDO' && subsys !== 5) { 277 | if (rsp && rsp.hasOwnProperty('status')) 278 | debug.request('RSP <-- %s, status: %d', subsys + ':' + cmdId, rsp.status); 279 | else 280 | debug.request('RSP <-- %s', subsys + ':' + cmdId); 281 | } 282 | 283 | if (err) 284 | deferred.reject(err); 285 | else if ((subsys !== 'ZDO' && subsys !== 5) && rsp && rsp.hasOwnProperty('status') && rsp.status !== 0) // unsuccessful 286 | deferred.reject(new Error('rsp error: ' + rsp.status)); 287 | else 288 | deferred.resolve(rsp); 289 | }; 290 | 291 | if ((subsys === 'AF' || subsys === 4) && valObj.hasOwnProperty('transid')) 292 | debug.request('REQ --> %s, transId: %d', subsys + ':' + cmdId, valObj.transid); 293 | else 294 | debug.request('REQ --> %s', subsys + ':' + cmdId); 295 | 296 | if (subsys === 'ZDO' || subsys === 5) 297 | this._zdo.request(cmdId, valObj, rspHdlr); // use wrapped zdo as the exported api 298 | else 299 | this._znp.request(subsys, cmdId, valObj, rspHdlr); // SREQ has timeout inside znp 300 | 301 | return deferred.promise.nodeify(callback); 302 | }; 303 | 304 | Controller.prototype.permitJoin = function (time, type, callback) { 305 | // time: seconds, 0x00 disable, 0xFF always enable 306 | // type: 0 (coord) / 1 (all) 307 | var self = this, 308 | addrmode, 309 | dstaddr; 310 | 311 | proving.number(time, 'time should be a number.'); 312 | proving.stringOrNumber(type, 'type should be a number or a string.'); 313 | 314 | return Q.fcall(function () { 315 | if (type === 0 || type === 'coord') { 316 | addrmode = 0x02; 317 | dstaddr = 0x0000; 318 | } else if (type === 1 || type === 'all') { 319 | addrmode = 0x0F; 320 | dstaddr = 0xFFFC; // all coord and routers 321 | } else { 322 | return Q.reject(new Error('Not a valid type.')); 323 | } 324 | }).then(function () { 325 | if (time > 255 || time < 0) 326 | return Q.reject(new Error('Jointime can only range from 0 to 255.')); 327 | else 328 | self._permitJoinTime = Math.floor(time); 329 | }).then(function () { 330 | return self.request('ZDO', 'mgmtPermitJoinReq', { addrmode: addrmode, dstaddr: dstaddr , duration: time, tcsignificance: 0 }); 331 | }).then(function (rsp) { 332 | self.emit('permitJoining', self._permitJoinTime); 333 | 334 | if (time !== 0 && time !== 255) { 335 | clearInterval(self._permitJoinInterval); 336 | self._permitJoinInterval = setInterval(function () { 337 | if (self.permitJoinCountdown() === 0) 338 | clearInterval(self._permitJoinInterval); 339 | self.emit('permitJoining', self._permitJoinTime); 340 | }, 1000); 341 | } 342 | return rsp; 343 | }).nodeify(callback); 344 | }; 345 | 346 | Controller.prototype.remove = function (dev, cfg, callback) { 347 | // cfg: { reJoin, rmChildren } 348 | var self = this, 349 | reqArgObj, 350 | rmChildren_reJoin = 0x00; 351 | 352 | if (!(dev instanceof Device)) 353 | throw new TypeError('dev should be an instance of Device class.'); 354 | else if (!_.isPlainObject(cfg)) 355 | throw new TypeError('cfg should be an object.'); 356 | 357 | cfg.reJoin = cfg.hasOwnProperty('reJoin') ? !!cfg.reJoin : true; // defaults to true 358 | cfg.rmChildren = cfg.hasOwnProperty('rmChildren') ? !!cfg.rmChildren : false; // defaults to false 359 | 360 | rmChildren_reJoin = cfg.reJoin ? (rmChildren_reJoin | 0x01) : rmChildren_reJoin; 361 | rmChildren_reJoin = cfg.rmChildren ? (rmChildren_reJoin | 0x02) : rmChildren_reJoin; 362 | 363 | reqArgObj = { 364 | dstaddr: dev.getNwkAddr(), 365 | deviceaddress: dev.getIeeeAddr(), 366 | removechildren_rejoin: rmChildren_reJoin 367 | }; 368 | 369 | return this.request('ZDO', 'mgmtLeaveReq', reqArgObj).then(function (rsp) { 370 | if (rsp.status !== 0 && rsp.status !== 'SUCCESS') 371 | return Q.reject(rsp.status); 372 | }).nodeify(callback); 373 | }; 374 | 375 | Controller.prototype.registerEp = function (loEp, callback) { 376 | var self = this; 377 | 378 | if (!(loEp instanceof Coordpoint)) 379 | throw new TypeError('loEp should be an instance of Coordpoint class.'); 380 | 381 | return this.request('AF', 'register', makeRegParams(loEp)).then(function (rsp) { 382 | return rsp; 383 | }).fail(function (err) { 384 | return (err.message === 'rsp error: 184') ? self.reRegisterEp(loEp) : Q.reject(err); 385 | }).nodeify(callback); 386 | }; 387 | 388 | Controller.prototype.deregisterEp = function (loEp, callback) { 389 | var self = this, 390 | coordEps = this.getCoord().endpoints; 391 | 392 | if (!(loEp instanceof Coordpoint)) 393 | throw new TypeError('loEp should be an instance of Coordpoint class.'); 394 | 395 | return Q.fcall(function () { 396 | if (!_.includes(coordEps, loEp)) 397 | return Q.reject(new Error('Endpoint not maintained by Coordinator, cannot be removed.')); 398 | else 399 | return self.request('AF', 'delete', { endpoint: loEp.getEpId() }); 400 | }).then(function (rsp) { 401 | delete coordEps[loEp.getEpId()]; 402 | return rsp; 403 | }).nodeify(callback); 404 | }; 405 | 406 | Controller.prototype.reRegisterEp = function (loEp, callback) { 407 | var self = this; 408 | 409 | return this.deregisterEp(loEp).then(function () { 410 | return self.request('AF', 'register', makeRegParams(loEp)); 411 | }).nodeify(callback); 412 | }; 413 | 414 | Controller.prototype.simpleDescReq = function (nwkAddr, ieeeAddr, callback) { 415 | return this.querie.deviceWithEndpoints(nwkAddr, ieeeAddr, callback); 416 | }; 417 | 418 | Controller.prototype.bind = function (srcEp, cId, dstEpOrGrpId, callback) { 419 | return this.querie.setBindingEntry('bind', srcEp, cId, dstEpOrGrpId, callback); 420 | }; 421 | 422 | Controller.prototype.unbind = function (srcEp, cId, dstEpOrGrpId, callback) { 423 | return this.querie.setBindingEntry('unbind', srcEp, cId, dstEpOrGrpId, callback); 424 | }; 425 | 426 | Controller.prototype.findEndpoint = function (addr, epId) { 427 | return this.getShepherd().find(addr, epId); 428 | }; 429 | 430 | Controller.prototype.setNvParams = function (net) { 431 | // net: { panId, channelList, precfgkey, precfgkeysEnable, startoptClearState } 432 | net = net || {}; 433 | proving.object(net, 'opts.net should be an object.'); 434 | 435 | _.forEach(net, function (val, param) { 436 | switch (param) { 437 | case 'panId': 438 | proving.number(val, 'net.panId should be a number.'); 439 | nvParams.panId.value = [ val & 0xFF, (val >> 8) & 0xFF ]; 440 | break; 441 | case 'precfgkey': 442 | if (!_.isArray(val) || val.length !== 16) 443 | throw new TypeError('net.precfgkey should be an array with 16 uint8 integers.'); 444 | nvParams.precfgkey.value = val; 445 | break; 446 | case 'precfgkeysEnable': 447 | proving.boolean(val, 'net.precfgkeysEnable should be a bool.'); 448 | nvParams.precfgkeysEnable.value = val ? [ 0x01 ] : [ 0x00 ]; 449 | break; 450 | case 'startoptClearState': 451 | proving.boolean(val, 'net.startoptClearState should be a bool.'); 452 | nvParams.startupOption.value = val ? [ 0x02 ] : [ 0x00 ]; 453 | break; 454 | case 'channelList': 455 | proving.array(val, 'net.channelList should be an array.'); 456 | var chList = 0; 457 | 458 | _.forEach(val, function (ch) { 459 | if (ch >= 11 && ch <= 26) 460 | chList = chList | ZSC.ZDO.channelMask['CH' + ch]; 461 | }); 462 | 463 | nvParams.channelList.value = [ chList & 0xFF, (chList >> 8) & 0xFF, (chList >> 16) & 0xFF, (chList >> 24) & 0xFF ]; 464 | break; 465 | default: 466 | throw new TypeError('Unkown argument: ' + param + '.'); 467 | } 468 | }); 469 | }; 470 | 471 | Controller.prototype.checkNvParams = function (callback) { 472 | var self = this, 473 | steps; 474 | 475 | function bufToArray(buf) { 476 | var arr = []; 477 | 478 | for (var i = 0; i < buf.length; i += 1) { 479 | arr.push(buf.readUInt8(i)); 480 | } 481 | 482 | return arr; 483 | } 484 | 485 | steps = [ 486 | function () { return self.request('SYS', 'osalNvRead', nvParams.znpHasConfigured).delay(10).then(function (rsp) { 487 | if (!_.isEqual(bufToArray(rsp.value), nvParams.znpHasConfigured.value)) return Q.reject('reset'); 488 | }); }, 489 | function () { return self.request('SAPI', 'readConfiguration', nvParams.panId).delay(10).then(function (rsp) { 490 | if (!_.isEqual(bufToArray(rsp.value), nvParams.panId.value)) return Q.reject('reset'); 491 | }); }, 492 | function () { return self.request('SAPI', 'readConfiguration', nvParams.channelList).delay(10).then(function (rsp) { 493 | if (!_.isEqual(bufToArray(rsp.value), nvParams.channelList.value)) return Q.reject('reset'); 494 | }); }, 495 | function () { return self.request('SAPI', 'readConfiguration', nvParams.precfgkey).delay(10).then(function (rsp) { 496 | if (!_.isEqual(bufToArray(rsp.value), nvParams.precfgkey.value)) return Q.reject('reset'); 497 | }); }, 498 | function () { return self.request('SAPI', 'readConfiguration', nvParams.precfgkeysEnable).delay(10).then(function (rsp) { 499 | if (!_.isEqual(bufToArray(rsp.value), nvParams.precfgkeysEnable.value)) return Q.reject('reset'); 500 | }); } 501 | ]; 502 | 503 | return steps.reduce(function (soFar, fn) { 504 | return soFar.then(fn); 505 | }, Q(0)).fail(function (err) { 506 | if (err === 'reset' || err.message === 'rsp error: 2') { 507 | self._nvChanged = true; 508 | debug.init('Non-Volatile memory is changed.'); 509 | return self.reset('hard'); 510 | } else { 511 | return Q.reject(err); 512 | } 513 | }).nodeify(callback); 514 | }; 515 | 516 | Controller.prototype.checkOnline = function (dev, callback) { 517 | var self = this, 518 | nwkAddr = dev.getNwkAddr(), 519 | ieeeAddr = dev.getIeeeAddr(); 520 | 521 | this.request('ZDO', 'nodeDescReq', { dstaddr: nwkAddr, nwkaddrofinterest: nwkAddr }).timeout(5000).fail(function () { 522 | return self.request('ZDO', 'nodeDescReq', { dstaddr: nwkAddr, nwkaddrofinterest: nwkAddr }).timeout(5000); 523 | }).then(function () { 524 | if (dev.status === 'offline') 525 | self.emit('ZDO:endDeviceAnnceInd', { srcaddr: nwkAddr, nwkaddr: nwkAddr, ieeeaddr: ieeeAddr, capabilities: {} }); 526 | }).fail(function () { 527 | return; 528 | }).done(); 529 | }; 530 | 531 | Controller.prototype.endDeviceAnnceHdlr = function (data) { 532 | var self = this, 533 | joinTimeout, 534 | joinEvent = 'ind:incoming' + ':' + data.ieeeaddr, 535 | dev = this.getShepherd()._findDevByAddr(data.ieeeaddr); 536 | 537 | if (dev && dev.status === 'online'){ // Device has already joined, do next item in queue 538 | console.log('device already in network'); 539 | 540 | if (self._joinQueue.length) { 541 | var next = self._joinQueue.shift(); 542 | 543 | if (next) { 544 | console.log('next item in joinqueue'); 545 | setImmediate(function () { 546 | next.func(); 547 | }); 548 | } else { 549 | console.log('no next item in joinqueue'); 550 | self._spinLock = false; 551 | } 552 | } else { 553 | self._spinLock = false; 554 | } 555 | 556 | return; 557 | } 558 | 559 | joinTimeout = setTimeout(function () { 560 | if (self.listenerCount(joinEvent)) { 561 | self.emit(joinEvent, '__timeout__'); 562 | self.getShepherd().emit('joining', { type: 'timeout', ieeeAddr: data.ieeeaddr }); 563 | } 564 | 565 | joinTimeout = null; 566 | }, 30000); 567 | 568 | this.once(joinEvent, function () { 569 | if (joinTimeout) { 570 | clearTimeout(joinTimeout); 571 | joinTimeout = null; 572 | } 573 | 574 | if (self._joinQueue.length) { 575 | var next = self._joinQueue.shift(); 576 | 577 | if (next){ 578 | setImmediate(function () { 579 | next.func(); 580 | }); 581 | } else { 582 | self._spinLock = false; 583 | } 584 | } else { 585 | self._spinLock = false; 586 | } 587 | }); 588 | 589 | this.getShepherd().emit('joining', { type: 'associating', ieeeAddr: data.ieeeaddr }); 590 | 591 | this.simpleDescReq(data.nwkaddr, data.ieeeaddr).then(function (devInfo) { 592 | return devInfo; 593 | }).fail(function () { 594 | return self.simpleDescReq(data.nwkaddr, data.ieeeaddr); 595 | }).then(function (devInfo) { 596 | // Now that we have the simple description of the device clear joinTimeout 597 | if (joinTimeout) { 598 | clearTimeout(joinTimeout); 599 | joinTimeout = null; 600 | } 601 | 602 | // Defer a promise to wait for the controller to complete the ZDO:devIncoming event! 603 | var processIncoming = Q.defer(); 604 | self.emit('ZDO:devIncoming', devInfo, processIncoming.resolve, processIncoming.reject); 605 | return processIncoming.promise; 606 | }).then(function () { 607 | self.emit(joinEvent, '__timeout__'); 608 | }).fail(function () { 609 | self.getShepherd().emit('error', 'Cannot get the Node Descriptor of the Device: ' + data.ieeeaddr); 610 | self.getShepherd().emit('joining', { type: 'error', ieeeAddr: data.ieeeaddr }); 611 | self.emit(joinEvent, '__timeout__'); 612 | }).done(); 613 | }; 614 | 615 | /*************************************************************************************************/ 616 | /*** Private Functions ***/ 617 | /*************************************************************************************************/ 618 | function makeRegParams(loEp) { 619 | return { 620 | endpoint: loEp.getEpId(), 621 | appprofid: loEp.getProfId(), 622 | appdeviceid: loEp.getDevId(), 623 | appdevver: 0, 624 | latencyreq: ZSC.AF.networkLatencyReq.NO_LATENCY_REQS, 625 | appnuminclusters: loEp.inClusterList.length, 626 | appinclusterlist: loEp.inClusterList, 627 | appnumoutclusters: loEp.outClusterList.length, 628 | appoutclusterlist: loEp.outClusterList 629 | }; 630 | } 631 | 632 | module.exports = Controller; 633 | -------------------------------------------------------------------------------- /lib/components/event_bridge.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 'use strict'; 3 | 4 | var zdoHelper = require('./zdo_helper.js'), 5 | debug = { 6 | msgHdlr: require('debug')('zigbee-shepherd:msgHdlr') 7 | }; 8 | 9 | var bridge = {}; 10 | 11 | bridge._areqEventBridge = function (controller, msg) { 12 | // msg: { subsys: 'ZDO', ind: 'endDeviceAnnceInd', data: { srcaddr: 63536, nwkaddr: 63536, ieeeaddr: '0x00124b0001ce3631', ... } 13 | var mandatoryEvent = msg.subsys + ':' + msg.ind; // 'SYS:resetInd', 'SYS:osalTimerExpired' 14 | 15 | controller.emit(mandatoryEvent, msg.data); // bridge to subsystem events, like 'SYS:resetInd', 'SYS:osalTimerExpired' 16 | 17 | if (msg.subsys === 'AF') 18 | debug.msgHdlr('IND <-- %s, transId: %d', mandatoryEvent, msg.data.transid || msg.data.transseqnumber); 19 | else 20 | debug.msgHdlr('IND <-- %s', mandatoryEvent); 21 | 22 | // dispatch to specific event bridge 23 | if (msg.subsys === 'ZDO') 24 | bridge._zdoIndicationEventBridge(controller, msg); 25 | else if (msg.subsys === 'SAPI') 26 | bridge._sapiIndicationEventBridge(controller, msg); 27 | // else: Do nothing. No need to bridge: SYS, MAC, NWK, UTIL, DBG, APP 28 | }; 29 | 30 | bridge._zdoIndicationEventBridge = function (controller, msg) { 31 | var payload = msg.data, 32 | zdoEventHead = 'ZDO:' + msg.ind, 33 | zdoBridgedEvent; 34 | 35 | if (msg.ind === 'stateChangeInd') { // this is a special event 36 | if (!payload.hasOwnProperty('nwkaddr')) // Coord itself 37 | zdoBridgedEvent = 'coordStateInd'; 38 | else if (payload.state === 0x83 || payload.state === 'NOT_ACTIVE') 39 | zdoBridgedEvent = zdoEventHead + ':' + payload.nwkaddr + ':NOT_ACTIVE'; 40 | else if (payload.state === 0x82 || payload.state === 'INVALID_EP') 41 | zdoBridgedEvent = zdoEventHead + ':' + payload.nwkaddr + ':INVALID_EP'; 42 | } else { 43 | zdoBridgedEvent = zdoHelper.generateEventOfIndication(msg.ind, payload); 44 | } 45 | 46 | if (zdoBridgedEvent) 47 | controller.emit(zdoBridgedEvent, payload); 48 | }; 49 | 50 | bridge._sapiIndicationEventBridge = function (controller, msg) { 51 | var payload = msg.data, 52 | sapiEventHead = 'SAPI:' + msg.ind, 53 | sapiBridgedEvent; 54 | 55 | switch (msg.ind) { 56 | case 'bindConfirm': 57 | sapiBridgedEvent = sapiEventHead + ':' + payload.commandid; 58 | break; 59 | case 'sendDataConfirm': 60 | sapiBridgedEvent = sapiEventHead + ':' + payload.handle; 61 | break; 62 | case 'receiveDataIndication': 63 | sapiBridgedEvent = sapiEventHead + ':' + payload.source + ':' + payload.command; 64 | break; 65 | case 'findDeviceConfirm': 66 | if (payload.hasOwnProperty('result')) 67 | sapiBridgedEvent = sapiEventHead + ':' + payload.result; 68 | break; 69 | default: // startConfirm and allowBindConfirm need no bridging 70 | break; 71 | } 72 | 73 | if (sapiBridgedEvent) 74 | controller.emit(sapiBridgedEvent, payload); 75 | }; 76 | 77 | module.exports = bridge; 78 | -------------------------------------------------------------------------------- /lib/components/event_handlers.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 'use strict'; 3 | 4 | var Q = require('q'), 5 | EventEmitter = require('events'), 6 | _ = require('busyman'), 7 | Ziee = require('ziee'), 8 | ZSC = require('zstack-constants'), 9 | debug = { 10 | shepherd: require('debug')('zigbee-shepherd'), 11 | init: require('debug')('zigbee-shepherd:init'), 12 | request: require('debug')('zigbee-shepherd:request'), 13 | }; 14 | 15 | var Device = require('../model/device'), 16 | Endpoint = require('../model/endpoint'); 17 | 18 | var handlers = {}; 19 | 20 | handlers.attachEventHandlers = function (shepherd) { 21 | var controller = shepherd.controller, 22 | hdls = {}; 23 | 24 | _.forEach(handlers, function (hdl, key) { 25 | if (key !== 'attachEventHandlers') 26 | hdls[key] = hdl.bind(shepherd); 27 | }); 28 | 29 | controller.removeListener('SYS:resetInd', hdls.resetInd); 30 | controller.removeListener('ZDO:devIncoming', hdls.devIncoming); 31 | controller.removeListener('ZDO:tcDeviceInd', hdls.tcDeviceInd); 32 | controller.removeListener('ZDO:stateChangeInd', hdls.stateChangeInd); 33 | controller.removeListener('ZDO:matchDescRspSent', hdls.matchDescRspSent); 34 | controller.removeListener('ZDO:statusErrorRsp', hdls.statusErrorRsp); 35 | controller.removeListener('ZDO:srcRtgInd', hdls.srcRtgInd); 36 | controller.removeListener('ZDO:beacon_notify_ind', hdls.beacon_notify_ind); 37 | controller.removeListener('ZDO:leaveInd', hdls.leaveInd); 38 | controller.removeListener('ZDO:msgCbIncoming', hdls.msgCbIncoming); 39 | controller.removeListener('ZDO:serverDiscRsp', hdls.serverDiscRsp); 40 | // controller.removeListener('ZDO:permitJoinInd', hdls.permitJoinInd); 41 | 42 | controller.on('SYS:resetInd', hdls.resetInd); 43 | controller.on('ZDO:devIncoming', hdls.devIncoming); 44 | controller.on('ZDO:tcDeviceInd', hdls.tcDeviceInd); 45 | controller.on('ZDO:stateChangeInd', hdls.stateChangeInd); 46 | controller.on('ZDO:matchDescRspSent', hdls.matchDescRspSent); 47 | controller.on('ZDO:statusErrorRsp', hdls.statusErrorRsp); 48 | controller.on('ZDO:srcRtgInd', hdls.srcRtgInd); 49 | controller.on('ZDO:beacon_notify_ind', hdls.beacon_notify_ind); 50 | controller.on('ZDO:leaveInd', hdls.leaveInd); 51 | controller.on('ZDO:msgCbIncoming', hdls.msgCbIncoming); 52 | controller.on('ZDO:serverDiscRsp', hdls.serverDiscRsp); 53 | // controller.on('ZDO:permitJoinInd', hdls.permitJoinInd); 54 | }; 55 | 56 | /*************************************************************************************************/ 57 | /*** Event Handlers ***/ 58 | /*************************************************************************************************/ 59 | handlers.resetInd = function (msg) { 60 | var self = this; 61 | 62 | if (this.controller.isResetting()) return; 63 | 64 | if (msg !== '_reset') 65 | debug.shepherd('Starting a software reset...'); 66 | 67 | this.stop().then(function () { 68 | return self.start(); 69 | }).then(function () { 70 | if (msg === '_reset') 71 | return self.controller.emit('_reset'); 72 | }).fail(function (err) { 73 | if (msg === '_reset') { 74 | return self.controller.emit('_reset', err); 75 | } else { 76 | debug.shepherd('Reset had an error', err); 77 | self.emit('error', err); 78 | } 79 | }).done(); 80 | }; 81 | 82 | handlers.devIncoming = function (devInfo, resolve) { 83 | // devInfo: { type, ieeeAddr, nwkAddr, manufId, epList, endpoints: [ simpleDesc, ... ] } 84 | var self = this, 85 | dev = this._findDevByAddr(devInfo.ieeeAddr), 86 | clustersReqs = []; 87 | 88 | function syncEndpoints(dev) { 89 | devInfo.endpoints.forEach(function (simpleDesc) { 90 | var ep = dev.getEndpoint(simpleDesc.epId); 91 | 92 | if (ep) { 93 | ep.update(simpleDesc); 94 | } else { 95 | ep = new Endpoint(dev, simpleDesc); 96 | ep.clusters = new Ziee(); 97 | self._attachZclMethods(ep); 98 | dev.endpoints[ep.getEpId()] = ep; 99 | } 100 | }); 101 | } 102 | 103 | var processDev = Q.fcall(function () { 104 | if (dev) { 105 | dev.update(devInfo); 106 | dev.update({ status: 'online', joinTime: Math.floor(Date.now()/1000) }); 107 | syncEndpoints(dev); 108 | return dev; 109 | } else { 110 | dev = new Device(devInfo); 111 | dev.update({ status: 'online' }); 112 | syncEndpoints(dev); 113 | return self._registerDev(dev).then(function () { 114 | return dev; 115 | }); 116 | } 117 | }).then(function(dev) { 118 | if (!dev || !dev.hasOwnProperty('endpoints')) return dev; 119 | 120 | // Try genBasic interview, not certain if it works for all devices 121 | try { 122 | var attrMap = { 123 | 4: 'manufName', 124 | 5: 'modelId', 125 | 7: 'powerSource' 126 | }; 127 | 128 | var powerSourceMap = { 129 | 0: 'Unknown', 130 | 1: 'Mains (single phase)', 131 | 2: 'Mains (3 phase)', 132 | 3: 'Battery', 133 | 4: 'DC Source', 134 | 5: 'Emergency mains constantly powered', 135 | 6: 'Emergency mains and transfer switch' 136 | }; 137 | 138 | // Loop all endpoints to find genBasic cluster, and get basic endpoint if possible 139 | var basicEpInst; 140 | 141 | for (var i in dev.endpoints) { 142 | var ep = dev.getEndpoint(i), 143 | clusterList = ep.getClusterList(); 144 | 145 | if (_.isArray(clusterList) && clusterList.indexOf(0) > -1) { 146 | // genBasic found 147 | basicEpInst = ep; 148 | break; 149 | } 150 | } 151 | 152 | if (!basicEpInst || basicEpInst instanceof Error) return dev; 153 | 154 | // Get manufName, modelId and powerSource information 155 | return self.af.zclFoundation(basicEpInst, basicEpInst, 0, 'read', [{ attrId: 4 }, { attrId: 5 }, { attrId: 7 }]).then(function (readStatusRecsRsp) { 156 | var data = {}; 157 | if (readStatusRecsRsp && _.isArray(readStatusRecsRsp.payload)) { 158 | readStatusRecsRsp.payload.forEach(function(item){ // { attrId, status, dataType, attrData } 159 | if (item && item.hasOwnProperty('attrId') && item.hasOwnProperty('attrData')) { 160 | if (item.attrId === 7) 161 | data[attrMap[item.attrId]] = powerSourceMap[item.attrData]; 162 | else 163 | data[attrMap[item.attrId]] = item.attrData; 164 | } 165 | }); 166 | } 167 | 168 | // Update dev 169 | dev.update(data); 170 | 171 | debug.shepherd('Identified Device: { manufacturer: %s, product: %s }', data.manufName, data.modelId); 172 | 173 | // Save device 174 | return Q.ninvoke(self._devbox, 'sync', dev._getId()).then(function () { 175 | return dev; 176 | }); 177 | }).catch(function(){ 178 | return dev; 179 | }); 180 | } catch (err) { 181 | return dev; 182 | } 183 | }).then(function (dev) { 184 | var numberOfEndpoints = _.keys(dev.endpoints).length; 185 | 186 | var interviewEvents = new EventEmitter(); 187 | interviewEvents.on('ind:interview', function(status) { 188 | if (status && status.endpoint) status.endpoint.total = numberOfEndpoints; 189 | self.emit('ind:interview', dev.ieeeAddr, status); 190 | }); 191 | 192 | _.forEach(dev.endpoints, function (ep) { 193 | // if (ep.isZclSupported()) 194 | clustersReqs.push(function () { 195 | return self.af.zclClustersReq(ep, interviewEvents).then(function (clusters) { 196 | _.forEach(clusters, function (cInfo, cid) { 197 | ep.clusters.init(cid, 'dir', { value: cInfo.dir }); 198 | ep.clusters.init(cid, 'attrs', cInfo.attrs, false); 199 | }); 200 | }); 201 | }); 202 | }); 203 | 204 | return clustersReqs.reduce(function (soFar, fn) { 205 | return soFar.then(fn); 206 | }, Q(0)); 207 | }).then(function () { 208 | if (_.isFunction(self.acceptDevIncoming)) { 209 | var info = { 210 | ieeeAddr: dev.getIeeeAddr(), 211 | endpoints: [] 212 | }; 213 | 214 | _.forEach(dev.epList, function (epId) { 215 | info.endpoints.push(dev.getEndpoint(epId)); 216 | }); 217 | 218 | return Q.ninvoke(self, 'acceptDevIncoming', info).timeout(60000); 219 | } else { 220 | return true; 221 | } 222 | }).then(function (accepted) { 223 | if (accepted) { 224 | Q.ninvoke(self._devbox, 'sync', dev._getId()); 225 | debug.shepherd('Device: %s join the network.', dev.getIeeeAddr()); 226 | 227 | self.emit('ind:incoming', dev); 228 | self.emit('ind:status', dev, 'online'); 229 | self.controller.emit('ind:incoming' + ':' + dev.getIeeeAddr()); 230 | } else { 231 | self.remove(dev.getIeeeAddr(), { reJoin: false }).then(function () { 232 | Q.ninvoke(self._devbox, 'remove', dev._getId()); 233 | }); 234 | } 235 | }).fail(function (err) { 236 | self.emit('error', err); 237 | }); 238 | 239 | if (typeof resolve === 'function') { 240 | resolve(processDev); 241 | } 242 | 243 | processDev.done(); 244 | }; 245 | 246 | handlers.leaveInd = function (msg) { 247 | // { srcaddr, extaddr, request, removechildren, rejoin } 248 | var dev = this._findDevByAddr(msg.extaddr); 249 | 250 | if (dev) { 251 | var ieeeAddr = dev.getIeeeAddr(), 252 | epList = _.cloneDeep(dev.epList); 253 | 254 | if (msg.request) // request 255 | this._unregisterDev(dev); 256 | else // indication 257 | this._devbox.remove(dev._getId(), function () {}); 258 | 259 | debug.shepherd('Device: %s leave the network.', ieeeAddr); 260 | this.emit('ind:leaving', epList, ieeeAddr); 261 | } 262 | }; 263 | 264 | handlers.stateChangeInd = function (msg) { 265 | // { state[, nwkaddr] } 266 | if (!msg.hasOwnProperty('nwkaddr')) 267 | return; 268 | 269 | var devStates = msg.state; 270 | 271 | _.forEach(ZSC.ZDO.devStates, function (statesCode, states) { 272 | if (msg.state === statesCode) 273 | devStates = states; 274 | }); 275 | 276 | debug.shepherd('Device: %d is now in state: %s', msg.nwkaddr, devStates); 277 | }; 278 | 279 | handlers.statusErrorRsp = function (msg) { 280 | // { srcaddr, status } 281 | debug.shepherd('Device: %d status error: %d', msg.srcaddr, msg.status); 282 | }; 283 | 284 | handlers.tcDeviceInd = function (msg) { 285 | // { nwkaddr, extaddr, parentaddr } 286 | }; 287 | 288 | handlers.matchDescRspSent = function (msg) { 289 | // { nwkaddr, numinclusters, inclusterlist, numoutclusters, outclusterlist } 290 | }; 291 | 292 | handlers.srcRtgInd = function (msg) { 293 | // { dstaddr, relaycount, relaylist } 294 | }; 295 | 296 | handlers.beacon_notify_ind = function (msg) { 297 | // { beaconcount, beaconlist } 298 | }; 299 | 300 | handlers.msgCbIncoming = function (msg) { 301 | // { srcaddr, wasbroadcast, clusterid, securityuse, seqnum, macdstaddr, msgdata } 302 | }; 303 | 304 | handlers.serverDiscRsp = function (msg) { 305 | // { srcaddr, status, servermask } 306 | }; 307 | 308 | module.exports = handlers; 309 | -------------------------------------------------------------------------------- /lib/components/loader.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 'use strict'; 3 | 4 | var Q = require('q'), 5 | _ = require('busyman'), 6 | Ziee = require('ziee'); 7 | 8 | var Device = require('../model/device'), 9 | Endpoint = require('../model/endpoint'); 10 | 11 | var loader = {}; 12 | 13 | loader.reloadSingleDev = function (shepherd, devRec, callback) { 14 | var deferred = Q.defer(), 15 | dev = shepherd._devbox.get(devRec.id); 16 | 17 | if (dev && isSameDevice(dev, devRec)) { 18 | deferred.resolve(null); // same dev exists, do not reload 19 | return deferred.promise.nodeify(callback); 20 | } else if (dev) { 21 | devRec.id = null; // give new id to devRec 22 | } 23 | 24 | var recoveredDev = new Device(devRec); 25 | 26 | _.forEach(devRec.endpoints, function (epRec, epId) { 27 | var recoveredEp = new Endpoint(recoveredDev, epRec); 28 | 29 | recoveredEp.clusters = new Ziee(); 30 | 31 | _.forEach(epRec.clusters, function (cInfo, cid) { 32 | recoveredEp.clusters.init(cid, 'dir', cInfo.dir); 33 | recoveredEp.clusters.init(cid, 'attrs', cInfo.attrs, false); 34 | }); 35 | 36 | shepherd._attachZclMethods(recoveredEp); 37 | recoveredDev.endpoints[epId] = recoveredEp; 38 | }); 39 | 40 | recoveredDev._recoverFromRecord(devRec); 41 | return shepherd._registerDev(recoveredDev, callback); // return (err, id) 42 | }; 43 | 44 | loader.reloadDevs = function (shepherd, callback) { 45 | var deferred = Q.defer(), 46 | recoveredIds = []; 47 | 48 | Q.ninvoke(shepherd._devbox, 'findFromDb', {}).then(function (devRecs) { 49 | var total = devRecs.length; 50 | 51 | devRecs.forEach(function (devRec) { 52 | if (devRec.nwkAddr === 0) { // coordinator 53 | total -= 1; 54 | if (total === 0) // all done 55 | deferred.resolve(recoveredIds); 56 | } else { 57 | loader.reloadSingleDev(shepherd, devRec).then(function (id) { 58 | recoveredIds.push(id); 59 | }).fail(function (err) { 60 | recoveredIds.push(null); 61 | }).done(function () { 62 | total -= 1; 63 | if (total === 0) // all done 64 | deferred.resolve(recoveredIds); 65 | }); 66 | } 67 | }); 68 | }).fail(function (err) { 69 | deferred.reject(err); 70 | }).done(); 71 | 72 | return deferred.promise.nodeify(callback); 73 | }; 74 | 75 | loader.reload = function (shepherd, callback) { 76 | var deferred = Q.defer(); 77 | 78 | loader.reloadDevs(shepherd).then(function (devIds) { 79 | loader.syncDevs(shepherd, function () { 80 | deferred.resolve(); // whether sync or not, return success 81 | }); 82 | }).fail(function (err) { 83 | deferred.reject(err); 84 | }).done(); 85 | 86 | return deferred.promise.nodeify(callback); 87 | }; 88 | 89 | loader.syncDevs = function (shepherd, callback) { 90 | var deferred = Q.defer(), 91 | idsNotInBox = []; 92 | 93 | Q.ninvoke(shepherd._devbox, 'findFromDb', {}).then(function (devRecs) { 94 | devRecs.forEach(function (devRec) { 95 | if (!shepherd._devbox.get(devRec.id)) 96 | idsNotInBox.push(devRec.id); 97 | }); 98 | 99 | if (idsNotInBox.length) { 100 | var ops = devRecs.length; 101 | idsNotInBox.forEach(function (id) { 102 | setImmediate(function () { 103 | shepherd._devbox.remove(id, function () { 104 | ops -= 1; 105 | if (ops === 0) 106 | deferred.resolve(); 107 | }); 108 | }); 109 | }); 110 | } else { 111 | deferred.resolve(); 112 | } 113 | }).fail(function (err) { 114 | deferred.reject(err); 115 | }).done(); 116 | 117 | return deferred.promise.nodeify(callback); 118 | }; 119 | 120 | function isSameDevice(dev, devRec) { 121 | return (dev.getIeeeAddr() === devRec.ieeeAddr) ? true : false; 122 | } 123 | 124 | module.exports = loader; 125 | -------------------------------------------------------------------------------- /lib/components/querie.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 'use strict'; 3 | 4 | var Q = require('q'), 5 | _ = require('busyman'), 6 | zclId = require('zcl-id'), 7 | proving = require('proving'), 8 | ZSC = require('zstack-constants'); 9 | 10 | var Endpoint = require('../model/endpoint'), 11 | Coordpoint = require('../model/coordpoint'), 12 | zutils = require('./zutils'); 13 | 14 | var controller, 15 | querie = {}; 16 | 17 | /*************************************************************************************************/ 18 | /*** Public APIs ***/ 19 | /*************************************************************************************************/ 20 | querie.coordInfo = function (callback) { 21 | var info = controller.getNetInfo(); 22 | return querie.device(info.ieeeAddr, info.nwkAddr, callback); 23 | }; 24 | 25 | querie.coordState = function (callback) { 26 | return querie.network('DEV_STATE', callback); 27 | }; 28 | 29 | querie.network = function (param, callback) { 30 | if (_.isFunction(param)) { 31 | callback = param; 32 | param = null; 33 | } 34 | 35 | if (param) 36 | return querie._network(param, callback); // return value 37 | else 38 | return querie._networkAll(callback); // return { state, channel, panId, extPanId, ieeeAddr, nwkAddr } 39 | }; 40 | 41 | querie.device = function (ieeeAddr, nwkAddr, callback) { 42 | var devInfo = { 43 | type: null, 44 | ieeeAddr: ieeeAddr, 45 | nwkAddr: nwkAddr, 46 | manufId: null, 47 | epList: null 48 | }; 49 | 50 | proving.string(ieeeAddr, 'ieeeAddr should be a string.'); 51 | proving.number(nwkAddr, 'nwkAddr should be a number.'); 52 | 53 | return controller.request('ZDO', 'nodeDescReq', { dstaddr: nwkAddr, nwkaddrofinterest: nwkAddr }).then(function (rsp) { 54 | // rsp: { srcaddr, status, nwkaddr, logicaltype_cmplxdescavai_userdescavai, ..., manufacturercode, ... } 55 | devInfo.type = devType(rsp.logicaltype_cmplxdescavai_userdescavai & 0x07); // logical type: bit0-2 56 | devInfo.manufId = rsp.manufacturercode; 57 | return controller.request('ZDO', 'activeEpReq', { dstaddr: nwkAddr, nwkaddrofinterest: nwkAddr }); 58 | }).then(function(rsp) { 59 | // rsp: { srcaddr, status, nwkaddr, activeepcount, activeeplist } 60 | devInfo.epList = bufToArray(rsp.activeeplist, 'uint8'); 61 | return devInfo; 62 | }).nodeify(callback); 63 | }; 64 | 65 | querie.endpoint = function (nwkAddr, epId, callback) { 66 | proving.number(nwkAddr, 'nwkAddr should be a number.'); 67 | 68 | return controller.request('ZDO', 'simpleDescReq', { dstaddr: nwkAddr, nwkaddrofinterest: nwkAddr, endpoint: epId }).then(function (rsp) { 69 | // rsp: { ..., endpoint, profileid, deviceid, deviceversion, numinclusters, inclusterlist, numoutclusters, outclusterlist } 70 | return { 71 | profId: rsp.profileid || 0, 72 | epId: rsp.endpoint, 73 | devId: rsp.deviceid || 0, 74 | inClusterList: bufToArray(rsp.inclusterlist, 'uint16'), 75 | outClusterList: bufToArray(rsp.outclusterlist, 'uint16') 76 | }; 77 | }).nodeify(callback); 78 | }; 79 | 80 | querie.deviceWithEndpoints = function (nwkAddr, ieeeAddr, callback) { 81 | var deferred = Q.defer(), 82 | epQueries = [], 83 | fullDev; 84 | 85 | querie.device(ieeeAddr, nwkAddr).then(function (devInfo) { 86 | fullDev = devInfo; 87 | var epInfos = []; 88 | 89 | _.forEach(fullDev.epList, function (epId) { 90 | epQueries.push(function (epInfos) { 91 | return querie.endpoint(nwkAddr, epId).then(function (epInfo) { 92 | epInfos.push(epInfo); 93 | return epInfos; 94 | }); 95 | }); 96 | }); 97 | 98 | return epQueries.reduce(function (soFar, fn) { 99 | return soFar.then(fn); 100 | }, Q(epInfos)); 101 | }).then(function (epInfos) { 102 | fullDev.endpoints = epInfos; 103 | deferred.resolve(fullDev); 104 | }).fail(function (err) { 105 | deferred.reject(err); 106 | }).done(); 107 | 108 | return deferred.promise.nodeify(callback); 109 | }; 110 | 111 | querie.setBindingEntry = function (bindMode, srcEp, cId, dstEpOrGrpId, callback) { 112 | var deferred = Q.defer(), 113 | cIdItem = zclId.cluster(cId), 114 | bindParams, 115 | dstEp, 116 | grpId, 117 | req; 118 | 119 | if (!((srcEp instanceof Endpoint) || (srcEp instanceof Coordpoint))) 120 | throw new TypeError('srcEp should be an instance of Endpoint class.'); 121 | 122 | proving.defined(cIdItem, 'Invalid cluster id: ' + cId + '.'); 123 | 124 | if (_.isNumber(dstEpOrGrpId) && !_.isNaN(dstEpOrGrpId)) 125 | grpId = dstEpOrGrpId; 126 | else if (dstEpOrGrpId instanceof Endpoint || dstEpOrGrpId instanceof Coordpoint) 127 | dstEp = dstEpOrGrpId; 128 | else 129 | throw new TypeError('dstEpOrGrpId should be an instance of Endpoint class or a number of group id.'); 130 | 131 | bindParams = { 132 | dstaddr: srcEp.getNwkAddr(), 133 | srcaddr: srcEp.getIeeeAddr(), 134 | srcendpoint: srcEp.getEpId(), 135 | clusterid: cIdItem.value, 136 | dstaddrmode: dstEp ? ZSC.AF.addressMode.ADDR_64BIT : ZSC.AF.addressMode.ADDR_GROUP, 137 | addr_short_long: dstEp ? dstEp.getIeeeAddr() : zutils.toLongAddrString(grpId), 138 | dstendpoint: dstEp ? dstEp.getEpId() : 0xFF 139 | }; 140 | 141 | if (bindMode === 0 || bindMode === 'bind') { 142 | req = function () { return controller.request('ZDO', 'bindReq', bindParams); }; 143 | } else if (bindMode === 1 || bindMode === 'unbind') { 144 | req = function () { return controller.request('ZDO', 'unbindReq', bindParams); }; 145 | } 146 | 147 | req().then(function (rsp) { 148 | deferred.resolve(); 149 | }).fail(function (err) { 150 | deferred.reject(err); 151 | }).done(); 152 | 153 | return deferred.promise.nodeify(callback); 154 | }; 155 | 156 | /*************************************************************************************************/ 157 | /*** Protected Methods ***/ 158 | /*************************************************************************************************/ 159 | querie._network = function (param, callback) { 160 | var prop = ZSC.SAPI.zbDeviceInfo[param]; 161 | 162 | return Q.fcall(function () { 163 | if (_.isNil(prop)) 164 | return Q.reject(new Error('Unknown network property.')); 165 | else 166 | return controller.request('SAPI', 'getDeviceInfo', { param: prop }); 167 | }).then(function (rsp) { 168 | switch (param) { 169 | case 'DEV_STATE': 170 | case 'CHANNEL': 171 | return rsp.value.readUInt8(0); 172 | case 'IEEE_ADDR': 173 | case 'PARENT_IEEE_ADDR': 174 | case 'EXT_PAN_ID': 175 | return addrBuf2Str(rsp.value); 176 | case 'SHORT_ADDR': 177 | case 'PARENT_SHORT_ADDR': 178 | return rsp.value.readUInt16LE(0); 179 | case 'PAN_ID': 180 | return zutils.toHexString(rsp.value.readUInt16LE(0), 'uint16'); 181 | } 182 | }).nodeify(callback); 183 | }; 184 | 185 | querie._networkAll = function (callback) { 186 | var paramsInfo = [ 187 | { param: 'DEV_STATE', name: 'state' }, { param: 'IEEE_ADDR', name: 'ieeeAddr' }, 188 | { param: 'SHORT_ADDR', name: 'nwkAddr' }, { param: 'CHANNEL', name: 'channel' }, 189 | { param: 'PAN_ID', name: 'panId' }, { param: 'EXT_PAN_ID', name: 'extPanId' } 190 | ], 191 | net = { 192 | state: null, 193 | channel: null, 194 | panId: null, 195 | extPanId: null, 196 | ieeeAddr: null, 197 | nwkAddr: null 198 | }, 199 | steps = []; 200 | 201 | _.forEach(paramsInfo, function (paramInfo) { 202 | steps.push(function (net) { 203 | return querie._network(paramInfo.param).then(function (value) { 204 | net[paramInfo.name] = value; 205 | return net; 206 | }); 207 | }); 208 | }); 209 | 210 | return steps.reduce(function (soFar, fn) { 211 | return soFar.then(fn); 212 | }, Q(net)).nodeify(callback); 213 | }; 214 | 215 | function devType(type) { 216 | var DEVTYPE = ZSC.ZDO.deviceLogicalType; 217 | 218 | switch (type) { 219 | case DEVTYPE.COORDINATOR: 220 | return 'Coordinator'; 221 | case DEVTYPE.ROUTER: 222 | return 'Router'; 223 | case DEVTYPE.ENDDEVICE: 224 | return 'EndDevice'; 225 | case DEVTYPE.COMPLEX_DESC_AVAIL: 226 | return 'ComplexDescAvail'; 227 | case DEVTYPE.USER_DESC_AVAIL: 228 | return 'UserDescAvail'; 229 | default: 230 | break; 231 | } 232 | } 233 | 234 | function addrBuf2Str(buf) { 235 | var val, 236 | bufLen = buf.length, 237 | strChunk = '0x'; 238 | 239 | for (var i = 0; i < bufLen; i += 1) { 240 | val = buf.readUInt8(bufLen - i - 1); 241 | 242 | if (val <= 15) 243 | strChunk += '0' + val.toString(16); 244 | else 245 | strChunk += val.toString(16); 246 | } 247 | 248 | return strChunk; 249 | } 250 | 251 | function bufToArray(buf, nip) { 252 | var i, 253 | nipArr = []; 254 | 255 | if (nip === 'uint8') { 256 | for (i = 0; i < buf.length; i += 1) { 257 | nipArr.push(buf.readUInt8(i)); 258 | } 259 | } else if (nip === 'uint16') { 260 | for (i = 0; i < buf.length; i += 2) { 261 | nipArr.push(buf.readUInt16LE(i)); 262 | } 263 | } 264 | 265 | return nipArr.sort(function (a, b) { return a - b; }); 266 | } 267 | 268 | module.exports = function (cntl) { 269 | controller = cntl; 270 | return querie; 271 | }; -------------------------------------------------------------------------------- /lib/components/zcl.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 'use strict'; 3 | 4 | var zclPacket = require('zcl-packet'); 5 | 6 | module.exports = { 7 | frame: zclPacket.frame, 8 | parse: zclPacket.parse, 9 | header: function (rawBuf) { 10 | var header = zclPacket.header(rawBuf); 11 | 12 | if (!header) 13 | return; 14 | else if (header.frameCntl.frameType > 1) // 2, 3 are reserved 15 | return; 16 | 17 | return header; 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /lib/components/zdo.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 'use strict'; 3 | 4 | var Q = require('q'), 5 | Areq = require('areq'), 6 | ZSC = require('zstack-constants'); 7 | 8 | var zdoHelper = require('./zdo_helper'); 9 | 10 | function Zdo(controller) { 11 | this._controller = controller; 12 | this._areq = new Areq(controller, 10000); 13 | } 14 | 15 | /*************************************************************************************************/ 16 | /*** Public APIs ***/ 17 | /*************************************************************************************************/ 18 | Zdo.prototype.request = function (apiName, valObj, callback) { 19 | var requestType = zdoHelper.getRequestType(apiName); 20 | 21 | if (requestType === 'rspless') 22 | return this._rsplessRequest(apiName, valObj, callback); 23 | else if (requestType === 'generic') 24 | return this._genericRequest(apiName, valObj, callback); 25 | else if (requestType === 'concat') 26 | return this._concatRequest(apiName, valObj, callback); 27 | else if (requestType === 'special') 28 | return this._specialRequest(apiName, valObj, callback); 29 | else 30 | callback(new Error('Unknown request type.')); 31 | }; 32 | 33 | /*************************************************************************************************/ 34 | /*** Protected Methods ***/ 35 | /*************************************************************************************************/ 36 | Zdo.prototype._sendZdoRequestViaZnp = function (apiName, valObj, callback) { 37 | var controller = this._controller, 38 | zdoRequest = controller._znp.zdoRequest.bind(controller._znp); // bind zdo._sendZdoRequestViaZnp() to znp.zdoRequest() 39 | 40 | return zdoRequest(apiName, valObj, function (err, rsp) { 41 | var error = null; 42 | 43 | if (err) 44 | error = err; 45 | else if (apiName !== 'startupFromApp' && rsp.status !== 0) 46 | error = new Error('request unsuccess: ' + rsp.status); 47 | 48 | callback(error, rsp); 49 | }); 50 | }; 51 | 52 | Zdo.prototype._rsplessRequest = function (apiName, valObj, callback) { 53 | return this._sendZdoRequestViaZnp(apiName, valObj, callback); 54 | }; 55 | 56 | Zdo.prototype._genericRequest = function (apiName, valObj, callback) { 57 | var deferred = Q.defer(), 58 | areq = this._areq, 59 | areqEvtKey = zdoHelper.generateEventOfRequest(apiName, valObj); 60 | 61 | if (areqEvtKey) 62 | areq.register(areqEvtKey, deferred, function (payload) { 63 | areq.resolve(areqEvtKey, payload); 64 | }); 65 | 66 | this._sendZdoRequestViaZnp(apiName, valObj, function (err, rsp) { 67 | if (err) 68 | areq.reject(areqEvtKey, err); 69 | }); 70 | 71 | return deferred.promise.nodeify(callback); 72 | }; 73 | 74 | Zdo.prototype._specialRequest = function (apiName, valObj, callback) { 75 | if (apiName === 'serverDiscReq') { 76 | // broadcast, remote device may not response when no bits match in mask 77 | // listener at controller.on('ZDO:serverDiscRsp') 78 | return this._rsplessRequest('serverDiscReq', valObj, callback); 79 | } else if (apiName === 'bindReq') { 80 | if (valObj.dstaddrmode === ZSC.AF.addressMode.ADDR_16BIT) 81 | callback(new Error('TI not support address 16bit mode.')); 82 | else 83 | return this._genericRequest('bindReq', valObj, callback); 84 | } else if (apiName === 'mgmtPermitJoinReq') { 85 | if (valObj.dstaddr === 0xFFFC) // broadcast to all routers (and coord), no waiting for AREQ rsp 86 | return this._rsplessRequest('mgmtPermitJoinReq', valObj, callback); 87 | else 88 | return this._genericRequest('mgmtPermitJoinReq', valObj, callback); 89 | } else { 90 | callback(new Error('No such request.')); 91 | } 92 | }; 93 | 94 | Zdo.prototype._concatRequest = function (apiName, valObj, callback) { 95 | if (apiName === 'nwkAddrReq' || apiName === 'ieeeAddrReq') 96 | return this._concatAddrRequest(apiName, valObj, callback); 97 | else if (apiName === 'mgmtNwkDiscReq') 98 | return this._concatListRequest(apiName, valObj, { 99 | entries: 'networkcount', 100 | listcount: 'networklistcount', 101 | list: 'networklist' 102 | }, callback); 103 | else if (apiName === 'mgmtLqiReq') 104 | return this._concatListRequest(apiName, valObj, { 105 | entries: 'neighbortableentries', 106 | listcount: 'neighborlqilistcount', 107 | list: 'neighborlqilist' 108 | }, callback); 109 | else if (apiName === 'mgmtRtgReq') 110 | return this._concatListRequest(apiName, valObj, { 111 | entries: 'routingtableentries', 112 | listcount: 'routingtablelistcount', 113 | list: 'routingtablelist' 114 | }, callback); 115 | else if (apiName === 'mgmtBindRsp') 116 | return this._concatListRequest(apiName, valObj, { 117 | entries: 'bindingtableentries', 118 | listcount: 'bindingtablelistcount', 119 | list: 'bindingtablelist' 120 | }, callback); 121 | else 122 | callback(new Error('No such request.')); 123 | }; 124 | 125 | Zdo.prototype._concatAddrRequest = function (apiName, valObj, callback) { 126 | var self = this, 127 | totalToGet = null, 128 | accum = 0, 129 | nextIndex = valObj.startindex, 130 | reqObj = { 131 | reqtype: valObj.reqtype, 132 | startindex: valObj.startindex // start from 0 133 | }, 134 | finalRsp = { 135 | status: null, 136 | ieeeaddr: null, 137 | nwkaddr: null, 138 | startindex: valObj.startindex, 139 | numassocdev: null, 140 | assocdevlist: [] 141 | }; 142 | 143 | if (apiName === 'nwkAddrReq') 144 | reqObj.ieeeaddr = valObj.ieeeaddr; 145 | else 146 | reqObj.shortaddr = valObj.shortaddr; 147 | 148 | var recursiveRequest = function () { 149 | self._genericRequest(apiName, reqObj, function (err, rsp) { 150 | if (err) { 151 | callback(err, finalRsp); 152 | } else if (rsp.status !== 0) { 153 | callback(new Error('request unsuccess: ' + rsp.status), finalRsp); 154 | } else { 155 | finalRsp.status = rsp.status; 156 | finalRsp.ieeeaddr = finalRsp.ieeeaddr || rsp.ieeeaddr; 157 | finalRsp.nwkaddr = finalRsp.nwkaddr || rsp.nwkaddr; 158 | finalRsp.numassocdev = finalRsp.numassocdev || rsp.numassocdev; 159 | finalRsp.assocdevlist = finalRsp.assocdevlist.concat(rsp.assocdevlist); 160 | 161 | totalToGet = totalToGet || (finalRsp.numassocdev - finalRsp.startindex); // compute at 1st rsp back 162 | accum = accum + rsp.assocdevlist.length; 163 | 164 | if (valObj.reqtype === 1 && accum < totalToGet) { // extended, include associated devices 165 | nextIndex = nextIndex + rsp.assocdevlist.length; 166 | reqObj.startindex = nextIndex; 167 | recursiveRequest(); 168 | } else { 169 | callback(null, finalRsp); 170 | } 171 | } 172 | }); 173 | }; 174 | 175 | recursiveRequest(); 176 | }; 177 | 178 | Zdo.prototype._concatListRequest = function (apiName, valObj, listKeys, callback) { 179 | // valObj = { dstaddr[, scanchannels, scanduration], startindex } 180 | // listKeys = { entries: 'networkcount', listcount: 'networklistcount', list: 'networklist' }; 181 | var self = this, 182 | totalToGet = null, 183 | accum = 0, 184 | nextIndex = valObj.startindex, 185 | reqObj = { 186 | dstaddr: valObj.dstaddr, 187 | scanchannels: valObj.scanchannels, 188 | scanduration: valObj.scanduration, 189 | startindex: valObj.startindex // starts from 0 190 | }, 191 | finalRsp = { 192 | srcaddr: null, 193 | status: null, 194 | startindex: valObj.startindex 195 | }; 196 | 197 | finalRsp[listKeys.entries] = null; // finalRsp.networkcount = null 198 | finalRsp[listKeys.listcount] = null; // finalRsp.networklistcount = null 199 | finalRsp[listKeys.list] = []; // finalRsp.networklist = [] 200 | 201 | if (apiName === 'mgmtNwkDiscReq') { 202 | reqObj.scanchannels = valObj.scanchannels; 203 | reqObj.scanduration = valObj.scanduration; 204 | } 205 | 206 | var recursiveRequest = function () { 207 | self._genericRequest(apiName, reqObj, function (err, rsp) { 208 | if (err) { 209 | callback(err, finalRsp); 210 | } else if (rsp.status !== 0) { 211 | callback(new Error('request unsuccess: ' + rsp.status), finalRsp); 212 | } else { 213 | finalRsp.status = rsp.status; 214 | finalRsp.srcaddr = finalRsp.srcaddr || rsp.srcaddr; 215 | finalRsp[listKeys.entries] = finalRsp[listKeys.entries] || rsp[listKeys.entries]; 216 | finalRsp[listKeys.listcount] = rsp[listKeys.listcount]; 217 | finalRsp[listKeys.list] = finalRsp[listKeys.list].concat(rsp[listKeys.list]); 218 | 219 | totalToGet = totalToGet || (finalRsp[listKeys.entries] - finalRsp.startindex); 220 | accum = accum + rsp[listKeys.list].length; 221 | 222 | if (accum < totalToGet) { 223 | nextIndex = nextIndex + rsp[listKeys.list].length; 224 | reqObj.startindex = nextIndex; 225 | recursiveRequest(); 226 | } else { 227 | callback(null, finalRsp); 228 | } 229 | } 230 | }); 231 | }; 232 | 233 | recursiveRequest(); 234 | }; 235 | 236 | module.exports = Zdo; 237 | -------------------------------------------------------------------------------- /lib/components/zdo_helper.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 'use strict'; 3 | 4 | var zdoHelper = {}, 5 | zdoReqRspMap, 6 | zdoIndSuffix; 7 | 8 | zdoReqRspMap = { 9 | nwkAddrReq: { ind: 'nwkAddrRsp', apiType: 'concat', suffix: [ 'ieeeaddr', 'startindex' ] }, 10 | ieeeAddrReq: { ind: 'ieeeAddrRsp', apiType: 'concat', suffix: [ 'shortaddr' ] }, // 'startindex' mismatch 11 | nodeDescReq: { ind: 'nodeDescRsp', apiType: 'generic', suffix: [ 'nwkaddrofinterest' ] }, 12 | powerDescReq: { ind: 'powerDescRsp', apiType: 'generic', suffix: [ 'nwkaddrofinterest' ] }, 13 | simpleDescReq: { ind: 'simpleDescRsp', apiType: 'generic', suffix: [ 'nwkaddrofinterest', 'endpoint' ] }, 14 | activeEpReq: { ind: 'activeEpRsp', apiType: 'generic', suffix: [ 'nwkaddrofinterest' ] }, 15 | matchDescReq: { ind: 'matchDescRsp', apiType: 'generic', suffix: [ 'nwkaddrofinterest' ] }, 16 | complexDescReq: { ind: 'complexDescRsp', apiType: 'generic', suffix: [ 'nwkaddrofinterest' ] }, 17 | userDescReq: { ind: 'userDescRsp', apiType: 'generic', suffix: [ 'nwkaddrofinterest' ] }, 18 | userDescSet: { ind: 'userDescConf', apiType: 'generic', suffix: [ 'nwkaddrofinterest' ] }, 19 | serverDiscReq: { ind: 'serverDiscRsp', apiType: 'special', suffix: [] }, 20 | endDeviceBindReq: { ind: 'endDeviceBindRsp', apiType: 'generic', suffix: [ 'dstaddr' ] }, // address 16bit mode unsupported 21 | bindReq: { ind: 'bindRsp', apiType: 'special', suffix: [ 'dstaddr' ] }, 22 | unbindReq: { ind: 'unbindRsp', apiType: 'generic', suffix: [ 'dstaddr' ] }, 23 | nwkDiscoveryReq: { ind: 'nwkDiscoveryCnf', apiType: 'generic', suffix: [] }, 24 | joinReq: { ind: 'joinCnf', apiType: 'generic', suffix: [] }, 25 | mgmtNwkDiscReq: { ind: 'mgmtNwkDiscRsp', apiType: 'concat', suffix: [ 'dstaddr', 'startindex' ] }, 26 | mgmtLqiReq: { ind: 'mgmtLqiRsp', apiType: 'concat', suffix: [ 'dstaddr', 'startindex' ] }, 27 | mgmtRtgReq: { ind: 'mgmtRtgRsp', apiType: 'concat', suffix: [ 'dstaddr', 'startindex' ] }, 28 | mgmtBindReq: { ind: 'mgmtBindRsp', apiType: 'concat', suffix: [ 'dstaddr', 'startindex' ] }, 29 | mgmtLeaveReq: { ind: 'mgmtLeaveRsp', apiType: 'generic', suffix: [ 'dstaddr' ] }, 30 | mgmtDirectJoinReq: { ind: 'mgmtDirectJoinRsp', apiType: 'generic', suffix: [ 'dstaddr' ] }, 31 | mgmtPermitJoinReq: { ind: 'mgmtPermitJoinRsp', apiType: 'special', suffix: [ 'dstaddr' ] }, 32 | mgmtNwkUpdateReq: null, 33 | endDeviceAnnce: null, 34 | msgCbRegister: null, 35 | msgCbRemove: null, 36 | startupFromApp: null, 37 | setLinkKey: null, 38 | removeLinkKey: null, 39 | getLinkKey: null, 40 | secAddLinkKey: null, 41 | secEntryLookupExt: null, 42 | extRouteDisc: null, 43 | extRouteCheck: null, 44 | extRemoveGroup: null, 45 | extRemoveAllGroup: null, 46 | extFindGroup: null, 47 | extAddGroup: null, 48 | extCountAllGroups: null, 49 | extRxIdle: null, 50 | extUpdateNwkKey: null, 51 | extSwitchNwkKey: null, 52 | extNwkInfo: null, 53 | extSecApsRemoveReq: null, 54 | extFindAllGroupsEndpoint: null, 55 | forceConcentratorChange: null, 56 | extSetParams: null, 57 | endDeviceTimeoutReq: null, 58 | sendData: null, 59 | nwkAddrOfInterestReq: null 60 | }; 61 | 62 | zdoIndSuffix = { 63 | nwkAddrRsp: [ 'ieeeaddr', 'startindex' ], 64 | ieeeAddrRsp: [ 'nwkaddr' ], // 'startindex' mismatch 65 | nodeDescRsp: [ 'nwkaddr' ], 66 | powerDescRsp: [ 'nwkaddr' ], 67 | simpleDescRsp: [ 'nwkaddr', 'endpoint' ], 68 | activeEpRsp: [ 'nwkaddr' ], 69 | matchDescRsp: [ 'nwkaddr' ], 70 | complexDescRsp: [ 'nwkaddr' ], 71 | userDescRsp: [ 'nwkaddr' ], 72 | userDescConf: [ 'nwkaddr' ], 73 | serverDiscRsp: null, // special, listen at controller.on('ZDO:serverDiscRsp') 74 | endDeviceBindRsp: [ 'srcaddr' ], 75 | bindRsp: [ 'srcaddr' ], 76 | unbindRsp: [ 'srcaddr' ], 77 | nwkDiscoveryCnf: null, 78 | joinCnf: null, 79 | mgmtNwkDiscRsp: [ 'srcaddr', 'startindex' ], 80 | mgmtLqiRsp: [ 'srcaddr', 'startindex' ], 81 | mgmtRtgRsp: [ 'srcaddr', 'startindex' ], 82 | mgmtBindRsp: [ 'srcaddr', 'startindex' ], 83 | mgmtLeaveRsp: [ 'srcaddr' ], 84 | mgmtDirectJoinRsp: [ 'srcaddr' ], 85 | mgmtPermitJoinRsp: [ 'srcaddr' ], 86 | stateChangeInd: null, // very special, tackled in event_bridge._zdoIndicationEventBridge() 87 | endDeviceAnnceInd: null, 88 | matchDescRspSent: null, 89 | statusErrorRsp: null, 90 | srcRtgInd: null, 91 | beacon_notify_ind: null, 92 | leaveInd: null, 93 | msgCbIncoming: null, 94 | tcDeviceInd: null, 95 | permitJoinInd: null 96 | }; 97 | 98 | /*************************************************************************************************/ 99 | /*** Public APIs ***/ 100 | /*************************************************************************************************/ 101 | zdoHelper.hasAreq = function (reqName) { 102 | var meta = zdoReqRspMap[reqName]; 103 | return meta ? (!!meta.ind) : false; 104 | }; 105 | 106 | zdoHelper.getRequestType = function (reqName) { 107 | var meta = zdoReqRspMap[reqName]; 108 | return meta ? meta.apiType : 'rspless'; 109 | }; 110 | 111 | zdoHelper.generateEventOfRequest = function (reqName, valObj) { 112 | var meta = zdoReqRspMap[reqName], 113 | evtName; 114 | 115 | if (!zdoHelper.hasAreq(reqName)) 116 | return; 117 | 118 | evtName = 'ZDO:' + meta.ind; 119 | 120 | if (meta.suffix.length === 0) 121 | return evtName; 122 | 123 | meta.suffix.forEach(function (key) { 124 | evtName = evtName + ':' + valObj[key].toString(); 125 | }); 126 | 127 | return evtName; 128 | }; 129 | 130 | zdoHelper.generateEventOfIndication = function (indName, msgData) { 131 | var meta = zdoIndSuffix[indName], 132 | evtName; 133 | 134 | evtName = 'ZDO:' + indName; 135 | 136 | if (!meta || (meta.length === 0)) 137 | return; 138 | 139 | meta.forEach(function (key) { 140 | evtName = evtName + ':' + msgData[key].toString(); 141 | }); 142 | 143 | return evtName; 144 | }; 145 | 146 | module.exports = zdoHelper; 147 | -------------------------------------------------------------------------------- /lib/components/zutils.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 'use strict'; 3 | 4 | var _ = require('busyman'), 5 | proving = require('proving'); 6 | 7 | var zutils = {}; 8 | 9 | zutils.toHexString = function (val, type) { 10 | var string, 11 | niplen = parseInt(type.slice(4)) / 4; 12 | 13 | string = val.toString(16); 14 | 15 | while (string.length !== niplen) { 16 | string = '0' + string; 17 | } 18 | 19 | return '0x' + string; 20 | }; 21 | 22 | zutils.toLongAddrString = function (addr) { 23 | var longAddr; 24 | 25 | if (_.isString(addr)) 26 | longAddr = (_.startsWith(addr, '0x') || _.startsWith(addr, '0X')) ? addr.slice(2, addr.length).toLowerCase() : addr.toLowerCase(); 27 | else if (_.isNumber(addr)) 28 | longAddr = addr.toString(16); 29 | else 30 | throw new TypeError('Address can only be a number or a string.'); 31 | 32 | for (var i = longAddr.length; i < 16; i++) { 33 | longAddr = '0' + longAddr; 34 | } 35 | 36 | return '0x' + longAddr; 37 | }; 38 | 39 | zutils.dotPath = function (path) { 40 | proving.string(path, 'Input path should be a string.'); 41 | 42 | path = path.replace(/\//g, '.'); // tranform slash notation into dot notation 43 | 44 | if (path[0] === '.') // if the first char of topic is '.', take it off 45 | path = path.slice(1); 46 | 47 | if (path[path.length-1] === '.') // if the last char of topic is '.', take it off 48 | path = path.slice(0, path.length - 1); 49 | 50 | return path; 51 | }; 52 | 53 | zutils.buildPathValuePairs = function (rootPath, obj) { 54 | var result = {}; 55 | rootPath = zutils.dotPath(rootPath); 56 | 57 | if (obj && typeof obj === 'object') { 58 | if (rootPath !== undefined && rootPath !== '' && rootPath !== '.' && rootPath !== '/') 59 | rootPath = rootPath + '.'; 60 | 61 | for (var key in obj) { 62 | if (obj.hasOwnProperty(key)) { 63 | var n = obj[key]; 64 | 65 | if (n && typeof n === 'object') 66 | result = Object.assign(result, zutils.buildPathValuePairs(rootPath + key, n)); 67 | else 68 | result[rootPath + key] = n; 69 | } 70 | } 71 | } else { 72 | result[rootPath] = obj; 73 | } 74 | 75 | return result; 76 | }; 77 | 78 | zutils.objectDiff = function (oldObj, newObj) { 79 | var pvp = zutils.buildPathValuePairs('/', newObj), 80 | diff = {}; 81 | 82 | _.forEach(pvp, function (val, path) { 83 | if (!_.has(oldObj, path) || _.get(oldObj, path) !== val) 84 | _.set(diff, path, val); 85 | }); 86 | 87 | return diff; 88 | }; 89 | 90 | module.exports = zutils; 91 | -------------------------------------------------------------------------------- /lib/config/nv_start_options.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 'use strict'; 3 | 4 | var ZSC = require('zstack-constants'), 5 | NVID8 = ZSC.SAPI.nvItemIdsUint8, 6 | NVID16 = ZSC.SYS.nvItemIds; 7 | 8 | var nvParams = { 9 | startupOption: { 10 | configid: NVID8.STARTUP_OPTION, // 0x03 11 | len: 0x01, 12 | value: [ 0x00 ] 13 | }, 14 | panId: { 15 | configid: NVID8.PANID, // 0x83 16 | len: 0x02, 17 | value: [ 0xFF, 0xFF ] 18 | }, 19 | extPanId: { 20 | configid: NVID8.EXTENDED_PAN_ID, // 0x2D 21 | len: 0x08, 22 | value: [ 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD ] 23 | }, 24 | channelList: { 25 | configid: NVID8.CHANLIST, // 0x84 26 | len: 0x04, 27 | value: [ 0x00, 0x08, 0x00, 0x00 ] // Little endian. Default is 0x00000800 for CH11; Ex: value: [ 0x00, 0x00, 0x00, 0x04 ] for CH26, [ 0x00, 0x00, 0x20, 0x00 ] for CH15. 28 | }, 29 | logicalType: { 30 | configid: NVID8.LOGICAL_TYPE, // 0x87 31 | len: 0x01, 32 | value: [ 0x00 ] 33 | }, 34 | precfgkey: { 35 | configid: NVID8.PRECFGKEY, // 0x62 36 | len: 0x10, 37 | value: [ 0x01, 0x03, 0x05, 0x07, 0x09, 0x0B, 0x0D, 0x0F, 0x00, 0x02, 0x04, 0x06, 0x08, 0x0A, 0x0C, 0x0D ] 38 | }, 39 | precfgkeysEnable: { 40 | configid: NVID8.PRECFGKEYS_ENABLE, // 0x63 41 | len: 0x01, 42 | value: [ 0x00 ] 43 | // value: 0 (FALSE) only coord defualtKey need to be set, and OTA to set other devices in the network. 44 | // value: 1 (TRUE) Not only coord, but also all devices need to set their defualtKey (the same key). Or they can't not join the network. 45 | }, 46 | zdoDirectCb: { 47 | configid: NVID8.ZDO_DIRECT_CB, // 0x8F 48 | len: 0x01, 49 | value: [ 0x01 ] 50 | }, 51 | securityMode: { 52 | id: NVID16.TCLK_TABLE_START, // 0x0101 53 | offset: 0x00, 54 | len: 0x20, 55 | // ZigBee Alliance Pre-configured TC Link Key - 'ZigBeeAlliance09' 56 | value: [ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x5a, 0x69, 0x67, 0x42, 0x65, 0x65, 0x41, 0x6c, 57 | 0x6c, 0x69, 0x61, 0x6e, 0x63, 0x65, 0x30, 0x39, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ] 58 | }, 59 | znpCfgItem: { 60 | id: NVID16.ZNP_HAS_CONFIGURED, // 0x0F00 61 | len: 0x01, 62 | initlen: 0x01, 63 | initvalue: [ 0x00 ] 64 | }, 65 | znpHasConfigured: { 66 | id: NVID16.ZNP_HAS_CONFIGURED, // 0x0F00 67 | offset: 0x00, 68 | len: 0x01, 69 | value: [ 0x55 ] 70 | }, 71 | afRegister: { 72 | endpoint: 0x08, 73 | appprofid: 0xBF0D, 74 | appdeviceid: 0x0501, 75 | appdevver: 0x01, 76 | latencyreq: 0x00, 77 | appnuminclusters: 0x04, 78 | appinclusterlist: [ 0x00, 0x00, 0x15, 0x00, 0x02, 0x07, 0x00, 0x04 ], 79 | appnumoutclusters: 0x00, 80 | appoutclusterlist: [] 81 | } 82 | }; 83 | 84 | module.exports = nvParams; 85 | -------------------------------------------------------------------------------- /lib/initializers/init_controller.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 'use strict'; 3 | 4 | var Q = require('q'), 5 | _ = require('busyman'), 6 | Ziee = require('ziee'), 7 | debug = require('debug')('zigbee-shepherd:init'); 8 | 9 | var Coordinator = require('../model/coord'), 10 | Coordpoint = require('../model/coordpoint'); 11 | 12 | var init = {}; 13 | 14 | /*************************************************************************************************/ 15 | /*** Public APIs ***/ 16 | /*************************************************************************************************/ 17 | init.setupCoord = function (controller, callback) { 18 | return controller.checkNvParams().then(function () { 19 | return init._bootCoordFromApp(controller); 20 | }).then(function (netInfo) { 21 | return init._registerDelegators(controller, netInfo); 22 | }).nodeify(callback); 23 | }; 24 | 25 | /*************************************************************************************************/ 26 | /*** Private APIs ***/ 27 | /*************************************************************************************************/ 28 | init._bootCoordFromApp = function (controller) { 29 | return controller.querie.coordState().then(function (state) { 30 | if (state !== 'ZB_COORD' && state !== 0x09) { 31 | debug('Start the ZNP as a coordinator...'); 32 | return init._startupCoord(controller); 33 | } 34 | }).then(function () { 35 | debug('Now the ZNP is a coordinator.'); 36 | return controller.querie.network(); 37 | }).then(function (netInfo) { 38 | // netInfo: { state, channel, panId, extPanId, ieeeAddr, nwkAddr } 39 | controller.setNetInfo(netInfo); 40 | return netInfo; 41 | }); 42 | }; 43 | 44 | init._startupCoord = function (controller) { 45 | var deferred = Q.defer(), 46 | stateChangeHdlr; 47 | 48 | stateChangeHdlr = function (data) { 49 | if (data.state === 9) { 50 | deferred.resolve(); 51 | controller.removeListener('ZDO:stateChangeInd', stateChangeHdlr); 52 | } 53 | }; 54 | 55 | controller.on('ZDO:stateChangeInd', stateChangeHdlr); 56 | controller.request('ZDO', 'startupFromApp', { startdelay: 100 }); 57 | 58 | return deferred.promise; 59 | }; 60 | 61 | init._registerDelegators = function (controller, netInfo) { 62 | var coord = controller.getCoord(), 63 | dlgInfos = [ 64 | { profId: 0x0104, epId: 1 }, { profId: 0x0101, epId: 2 }, { profId: 0x0105, epId: 3 }, 65 | { profId: 0x0107, epId: 4 }, { profId: 0x0108, epId: 5 }, { profId: 0x0109, epId: 6 } 66 | ]; 67 | 68 | return controller.simpleDescReq(0, netInfo.ieeeAddr).then(function (devInfo) { 69 | var deregisterEps = []; 70 | 71 | _.forEach(devInfo.epList, function (epId) { 72 | if (epId > 10) { 73 | deregisterEps.push(function () { 74 | return controller.request('AF', 'delete', { endpoint: epId }).delay(10).then(function () { 75 | debug('Deregister endpoint, epId: %s', epId); 76 | }); 77 | }); 78 | } 79 | }); 80 | 81 | if (!deregisterEps.length) { 82 | return devInfo; 83 | } else { 84 | return deregisterEps.reduce(function (soFar, fn) { 85 | return soFar.then(fn); 86 | }, Q(0)).then(function () { 87 | return devInfo; 88 | }); 89 | } 90 | }).then(function (devInfo) { 91 | var registerDlgs = []; 92 | 93 | if (!coord) 94 | coord = controller._coord = new Coordinator(devInfo); 95 | else 96 | coord.endpoints = {}; 97 | 98 | _.forEach(dlgInfos, function (dlgInfo) { 99 | var dlgDesc = { profId: dlgInfo.profId, epId: dlgInfo.epId, devId: 0x0005, inClusterList: [], outClusterList: [] }, 100 | dlgEp = new Coordpoint(coord, dlgDesc, true), 101 | simpleDesc; 102 | 103 | dlgEp.clusters = new Ziee(); 104 | coord.endpoints[dlgEp.getEpId()] = dlgEp; 105 | 106 | simpleDesc = _.find(devInfo.endpoints, function (ep) { 107 | return ep.epId === dlgInfo.epId; 108 | }); 109 | 110 | if (!_.isEqual(dlgDesc, simpleDesc)) { 111 | registerDlgs.push(function () { 112 | return controller.registerEp(dlgEp).delay(10).then(function () { 113 | debug('Register delegator, epId: %s, profId: %s ', dlgEp.getEpId(), dlgEp.getProfId()); 114 | }); 115 | }); 116 | } 117 | }); 118 | 119 | return registerDlgs.reduce(function (soFar, fn) { 120 | return soFar.then(fn); 121 | }, Q(0)); 122 | }).then(function () { 123 | return controller.querie.coordInfo().then(function (coordInfo) { 124 | coord.update(coordInfo); 125 | }); 126 | }); 127 | }; 128 | 129 | module.exports = init; 130 | -------------------------------------------------------------------------------- /lib/initializers/init_shepherd.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 'use strict'; 3 | 4 | var Q = require('q'), 5 | debug = require('debug')('zigbee-shepherd:init'); 6 | 7 | var af = require('../components/af'), 8 | loader = require('../components/loader'); 9 | 10 | var init = {}; 11 | 12 | init.setupShepherd = function (shepherd, callback) { 13 | var deferred = Q.defer(), 14 | controller = shepherd.controller, 15 | netInfo; 16 | 17 | debug('zigbee-shepherd booting...'); 18 | 19 | controller.start().then(function () { 20 | shepherd.af = af(controller); 21 | return controller.request('ZDO', 'mgmtPermitJoinReq', { addrmode: 0x02, dstaddr: 0 , duration: 0, tcsignificance: 0 }); 22 | }).then(function () { 23 | return shepherd._registerDev(controller.getCoord()); 24 | }).then(function () { 25 | return loader.reload(shepherd); // reload all devices from database 26 | }).then(function() { 27 | netInfo = controller.getNetInfo(); 28 | 29 | debug('Loading devices from database done.'); 30 | debug('zigbee-shepherd is up and ready.'); 31 | debug('Network information:'); 32 | debug(' >> State: %s', netInfo.state); 33 | debug(' >> Channel: %s', netInfo.channel); 34 | debug(' >> PanId: %s', netInfo.panId); 35 | debug(' >> Nwk Addr: %s', netInfo.nwkAddr); 36 | debug(' >> Ieee Addr: %s', netInfo.ieeeAddr); 37 | debug(' >> Ext. PanId: %s', netInfo.extPanId); 38 | }).then(function () { 39 | var devs = shepherd._devbox.exportAllObjs(); 40 | 41 | devs.forEach(function(dev) { 42 | if (dev.getNwkAddr() !== 0) 43 | return controller.checkOnline(dev); 44 | }); 45 | }).done(deferred.resolve, deferred.reject); 46 | 47 | return deferred.promise.nodeify(callback); 48 | }; 49 | 50 | module.exports = init; 51 | -------------------------------------------------------------------------------- /lib/model/coord.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 'use strict'; 3 | 4 | var _ = require('busyman'), 5 | util = require('util'), 6 | Device = require('./device'); 7 | 8 | function Coordinator(devInfo) { 9 | // devInfo = { type, ieeeAddr, nwkAddr, manufId, epList } 10 | 11 | Device.call(this, devInfo); 12 | 13 | this.status = 'online'; 14 | } 15 | 16 | util.inherits(Coordinator, Device); 17 | 18 | Coordinator.prototype.getDelegator = function (profId) { 19 | return _.find(this.endpoints, function (ep) { 20 | return ep.isDelegator() && (ep.getProfId() === profId); 21 | }); 22 | }; 23 | 24 | module.exports = Coordinator; 25 | -------------------------------------------------------------------------------- /lib/model/coordpoint.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 'use strict'; 3 | 4 | var util = require('util'), 5 | Endpoint = require('./endpoint'); 6 | 7 | function Coordpoint(coord, simpleDesc, isDelegator) { 8 | // simpleDesc = { profId, epId, devId, inClusterList, outClusterList } 9 | 10 | // coordpoint is a endpoint, but a 'LOCAL' endpoint 11 | // This class is used to create delegators, local applications 12 | 13 | Endpoint.call(this, coord, simpleDesc); 14 | 15 | this.isLocal = function () { 16 | return true; // this is a local endpoint, always return true 17 | }; 18 | 19 | this.isDelegator = function () { 20 | return !!(isDelegator || false); // this local endpoint maybe a delegator 21 | }; 22 | } 23 | 24 | util.inherits(Coordpoint, Endpoint); 25 | 26 | module.exports = Coordpoint; 27 | -------------------------------------------------------------------------------- /lib/model/device.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 'use strict'; 3 | 4 | var _ = require('busyman'); 5 | 6 | function Device(devInfo) { 7 | // devInfo = { type, ieeeAddr, nwkAddr, manufId, manufName, powerSource, modelId, epList } 8 | 9 | this._id = null; 10 | 11 | this.type = devInfo.type; 12 | this.ieeeAddr = devInfo.ieeeAddr; 13 | this.nwkAddr = devInfo.nwkAddr; 14 | this.manufId = devInfo.manufId; 15 | this.manufName = devInfo.manufName; 16 | this.powerSource = devInfo.powerSource; 17 | this.modelId = devInfo.modelId; 18 | this.epList = devInfo.epList; 19 | 20 | this.status = 'offline'; // 'online', 'offline' 21 | this.joinTime = null; 22 | this.endpoints = {}; // key is epId in number, { epId: epInst, epId: epInst, ... } 23 | } 24 | 25 | Device.prototype.dump = function () { 26 | var dumpOfEps = {}; 27 | 28 | _.forEach(this.endpoints, function (ep, epId) { 29 | dumpOfEps[epId] = ep.dump(); 30 | }); 31 | 32 | return { 33 | id: this._id, 34 | type: this.type, 35 | ieeeAddr: this.ieeeAddr, 36 | nwkAddr: this.nwkAddr, 37 | manufId: this.manufId, 38 | manufName: this.manufName, 39 | powerSource: this.powerSource, 40 | modelId: this.modelId, 41 | epList: _.cloneDeep(this.epList), 42 | status: this.status, 43 | joinTime: this.joinTime, 44 | endpoints: dumpOfEps 45 | }; 46 | }; 47 | 48 | Device.prototype.getEndpoint = function (epId) { 49 | return this.endpoints[epId]; 50 | }; 51 | 52 | Device.prototype.getIeeeAddr = function () { 53 | return this.ieeeAddr; 54 | }; 55 | 56 | Device.prototype.getNwkAddr = function () { 57 | return this.nwkAddr; 58 | }; 59 | 60 | Device.prototype.getManufId = function () { 61 | return this.manufId; 62 | }; 63 | 64 | Device.prototype.update = function (info) { 65 | var self = this, 66 | infoKeys = [ 'type', 'ieeeAddr', 'nwkAddr','manufId', 'epList', 'status', 'joinTime', 'manufName', 'modelId', 'powerSource' ]; 67 | 68 | _.forEach(info, function (val, key) { 69 | if (_.includes(infoKeys, key)) 70 | self[key] = val; 71 | }); 72 | }; 73 | 74 | Device.prototype._recoverFromRecord = function (rec) { 75 | this._recovered = true; 76 | this.status = 'offline'; 77 | this._setId(rec.id); 78 | 79 | return this; 80 | }; 81 | 82 | Device.prototype._setId = function (id) { 83 | this._id = id; 84 | }; 85 | 86 | Device.prototype._getId = function () { 87 | return this._id; 88 | }; 89 | 90 | module.exports = Device; 91 | -------------------------------------------------------------------------------- /lib/model/endpoint.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 'use strict'; 3 | 4 | var _ = require('busyman'); 5 | 6 | function Endpoint(device, simpleDesc) { 7 | // simpleDesc = { profId, epId, devId, inClusterList, outClusterList } 8 | 9 | this.isLocal = function () { 10 | return false; // this is a remote endpoint, always return false 11 | }; 12 | 13 | this.device = device; // bind to device 14 | this.profId = simpleDesc.profId; 15 | this.epId = simpleDesc.epId; 16 | this.devId = simpleDesc.devId; 17 | this.inClusterList = simpleDesc.inClusterList; // numbered cluster ids 18 | this.outClusterList = simpleDesc.outClusterList; // numbered cluster ids 19 | 20 | this.clusters = null; // instance of ziee 21 | 22 | // this.clusters.dumpSync() 23 | // { 24 | // genBasic: { 25 | // dir: { value: 1 }, // 0: 'unknown', 1: 'in', 2: 'out', 3: 'in' and 'out' 26 | // attrs: { 27 | // hwVersion: 0, 28 | // zclVersion: 1 29 | // } 30 | // } 31 | // } 32 | 33 | this.onAfDataConfirm = null; 34 | this.onAfReflectError = null; 35 | this.onAfIncomingMsg = null; 36 | this.onAfIncomingMsgExt = null; 37 | this.onZclFoundation = null; 38 | this.onZclFunctional = null; 39 | } 40 | 41 | /*************************************************************************************************/ 42 | /*** Public Methods ***/ 43 | /*************************************************************************************************/ 44 | Endpoint.prototype.getSimpleDesc = function () { 45 | return { 46 | profId: this.profId, 47 | epId: this.epId, 48 | devId: this.devId, 49 | inClusterList: _.cloneDeep(this.inClusterList), 50 | outClusterList: _.cloneDeep(this.outClusterList), 51 | }; 52 | }; 53 | 54 | Endpoint.prototype.getIeeeAddr = function () { 55 | return this.getDevice().getIeeeAddr(); 56 | }; 57 | 58 | Endpoint.prototype.getNwkAddr = function () { 59 | return this.getDevice().getNwkAddr(); 60 | }; 61 | 62 | Endpoint.prototype.dump = function () { 63 | var dumped = this.getSimpleDesc(); 64 | 65 | dumped.clusters = this.clusters.dumpSync(); 66 | 67 | return dumped; 68 | }; 69 | 70 | // zcl and binding methods will be attached in shepherd 71 | // endpoint.foundation = function (cId, cmd, zclData[, cfg], callback) {}; 72 | // endpoint.functional = function (cId, cmd, zclData[, cfg], callback) {}; 73 | // endpoint.read = function (cId, attrId, callback) {}; 74 | // endpoint.bind = function (cId, dstEpOrGrpId[, callback]) {}; 75 | // endpoint.unbind = function (cId, dstEpOrGrpId[, callback]) {}; 76 | 77 | /*************************************************************************************************/ 78 | /*** Protected Methods ***/ 79 | /*************************************************************************************************/ 80 | Endpoint.prototype.isZclSupported = function () { 81 | var zclSupport = false; 82 | 83 | if (this.profId < 0x8000 && this.devId < 0xc000) 84 | zclSupport = true; 85 | 86 | this.isZclSupported = function () { 87 | return zclSupport; 88 | }; 89 | 90 | return zclSupport; 91 | }; 92 | 93 | Endpoint.prototype.getDevice = function () { 94 | return this.device; 95 | }; 96 | 97 | Endpoint.prototype.getProfId = function () { 98 | return this.profId; 99 | }; 100 | 101 | Endpoint.prototype.getEpId = function () { 102 | return this.epId; 103 | }; 104 | 105 | Endpoint.prototype.getDevId = function () { 106 | return this.devId; 107 | }; 108 | 109 | Endpoint.prototype.getInClusterList = function () { 110 | return _.cloneDeep(this.inClusterList); 111 | }; 112 | 113 | Endpoint.prototype.getOutClusterList = function () { 114 | return _.cloneDeep(this.outClusterList); 115 | }; 116 | 117 | Endpoint.prototype.getClusterList = function () { 118 | var clusterList = this.getInClusterList(); 119 | 120 | this.getOutClusterList().forEach(function (cId) { 121 | if (!_.includes(clusterList, cId)) 122 | clusterList.push(cId); 123 | }); 124 | 125 | return clusterList.sort(function (a, b) { return a - b; }); 126 | }; 127 | 128 | Endpoint.prototype.getClusters = function () { 129 | return this.clusters; 130 | }; 131 | 132 | Endpoint.prototype.getManufId = function () { 133 | return this.getDevice().getManufId(); 134 | }; 135 | 136 | Endpoint.prototype.update = function (simpleDesc) { 137 | var self = this, 138 | descKeys = [ 'profId', 'epId', 'devId','inClusterList', 'outClusterList' ]; 139 | 140 | _.forEach(simpleDesc, function (val, key) { 141 | if (_.includes(descKeys, key)) 142 | self[key] = val; 143 | }); 144 | }; 145 | 146 | module.exports = Endpoint; 147 | -------------------------------------------------------------------------------- /lib/shepherd.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 'use strict'; 3 | 4 | var fs = require('fs'), 5 | util = require('util'), 6 | EventEmitter = require('events'); 7 | 8 | var Q = require('q'), 9 | _ = require('busyman'), 10 | Zive = require('zive'), 11 | zclId = require('zcl-id'), 12 | proving = require('proving'), 13 | Objectbox = require('objectbox'), 14 | debug = { shepherd: require('debug')('zigbee-shepherd') }; 15 | 16 | var init = require('./initializers/init_shepherd'), 17 | zutils = require('./components/zutils'), 18 | loader = require('./components/loader'), 19 | Controller = require('./components/controller'), 20 | eventHandlers = require('./components/event_handlers'); 21 | 22 | var Device = require('./model/device'), 23 | Coordinator = require('./model/coord'), 24 | Coordpoint = require('./model/coordpoint'); 25 | 26 | /*************************************************************************************************/ 27 | /*** ZShepherd Class ***/ 28 | /*************************************************************************************************/ 29 | function ZShepherd(path, opts) { 30 | // opts: { sp: {}, net: {}, dbPath: 'xxx' } 31 | var self = this, 32 | spCfg = {}; 33 | 34 | EventEmitter.call(this); 35 | 36 | opts = opts || {}; 37 | 38 | proving.string(path, 'path should be a string.'); 39 | proving.object(opts, 'opts should be an object if gieven.'); 40 | 41 | spCfg.path = path; 42 | spCfg.options = opts.hasOwnProperty('sp') ? opts.sp : { baudrate: 115200, rtscts: true }; 43 | 44 | /***************************************************/ 45 | /*** Protected Members ***/ 46 | /***************************************************/ 47 | this._startTime = 0; 48 | this._enabled = false; 49 | this._zApp = []; 50 | this._mounting = false; 51 | this._mountQueue = []; 52 | this.controller = new Controller(this, spCfg); // controller is the main actor 53 | this.controller.setNvParams(opts.net); 54 | this.af = null; 55 | 56 | this._dbPath = opts.dbPath; 57 | 58 | if (!this._dbPath) { // use default 59 | this._dbPath = __dirname + '/database/dev.db'; 60 | // create default db folder if not there 61 | try { 62 | fs.statSync(__dirname + '/database'); 63 | } catch (e) { 64 | fs.mkdirSync(__dirname + '/database'); 65 | } 66 | } 67 | 68 | this._devbox = new Objectbox(this._dbPath); 69 | 70 | this.acceptDevIncoming = function (devInfo, callback) { // Override at will. 71 | setImmediate(function () { 72 | var accepted = true; 73 | callback(null, accepted); 74 | }); 75 | }; 76 | 77 | /***************************************************/ 78 | /*** Event Handlers (Ind Event Bridges) ***/ 79 | /***************************************************/ 80 | eventHandlers.attachEventHandlers(this); 81 | 82 | this.controller.on('permitJoining', function (time) { 83 | self.emit('permitJoining', time); 84 | }); 85 | 86 | this.on('_ready', function () { 87 | self._startTime = Math.floor(Date.now()/1000); 88 | setImmediate(function () { 89 | self.emit('ready'); 90 | }); 91 | }); 92 | 93 | this.on('ind:incoming', function (dev) { 94 | var endpoints = []; 95 | 96 | _.forEach(dev.epList, function (epId) { 97 | endpoints.push(dev.getEndpoint(epId)); 98 | }); 99 | 100 | self.emit('ind', { type: 'devIncoming', endpoints: endpoints, data: dev.getIeeeAddr() }); 101 | }); 102 | 103 | this.on('ind:interview', function (dev, status) { 104 | self.emit('ind', { type: 'devInterview', status: status, data: dev }); 105 | }); 106 | 107 | this.on('ind:leaving', function (epList, ieeeAddr) { 108 | self.emit('ind', { type: 'devLeaving', endpoints: epList, data: ieeeAddr }); 109 | }); 110 | 111 | this.on('ind:changed', function (ep, notifData) { 112 | self.emit('ind', { type: 'devChange', endpoints: [ ep ], data: notifData }); 113 | }); 114 | 115 | this.on('ind:reported', function (ep, cId, attrs) { 116 | var cIdString = zclId.cluster(cId), 117 | notifData = { 118 | cid: '', 119 | data: {} 120 | }; 121 | 122 | self._updateFinalizer(ep, cId, attrs, true); 123 | 124 | cIdString = cIdString ? cIdString.key : cId; 125 | notifData.cid = cIdString; 126 | 127 | _.forEach(attrs, function (rec) { // { attrId, dataType, attrData } 128 | var attrIdString = zclId.attr(cIdString, rec.attrId); 129 | attrIdString = attrIdString ? attrIdString.key : rec.attrId; 130 | 131 | notifData.data[attrIdString] = rec.attrData; 132 | }); 133 | 134 | self.emit('ind', { type: 'attReport', endpoints: [ ep ], data: notifData }); 135 | }); 136 | 137 | this.on('ind:status', function (dev, status) { 138 | var endpoints = []; 139 | 140 | _.forEach(dev.epList, function (epId) { 141 | endpoints.push(dev.getEndpoint(epId)); 142 | }); 143 | 144 | self.emit('ind', { type: 'devStatus', endpoints: endpoints, data: status }); 145 | }); 146 | } 147 | 148 | util.inherits(ZShepherd, EventEmitter); 149 | 150 | /*************************************************************************************************/ 151 | /*** Public Methods ***/ 152 | /*************************************************************************************************/ 153 | ZShepherd.prototype.start = function (callback) { 154 | var self = this; 155 | 156 | return init.setupShepherd(this).then(function () { 157 | self._enabled = true; // shepherd is enabled 158 | self.emit('_ready'); // if all done, shepherd fires '_ready' event for inner use 159 | }).nodeify(callback); 160 | }; 161 | 162 | ZShepherd.prototype.stop = function (callback) { 163 | var self = this, 164 | devbox = this._devbox; 165 | 166 | return Q.fcall(function () { 167 | if (self._enabled) { 168 | self.permitJoin(0x00, 'all'); 169 | _.forEach(devbox.exportAllIds(), function (id) { 170 | devbox.removeElement(id); 171 | }); 172 | return self.controller.close(); 173 | } 174 | }).then(function () { 175 | self._enabled = false; 176 | self._zApp = null; 177 | self._zApp = []; 178 | }).nodeify(callback); 179 | }; 180 | 181 | ZShepherd.prototype.reset = function (mode, callback) { 182 | var self = this, 183 | devbox = this._devbox, 184 | removeDevs = []; 185 | 186 | proving.stringOrNumber(mode, 'mode should be a number or a string.'); 187 | 188 | if (mode === 'hard' || mode === 0) { 189 | // clear database 190 | if (self._devbox) { 191 | _.forEach(devbox.exportAllIds(), function (id) { 192 | removeDevs.push(Q.ninvoke(devbox, 'remove', id)); 193 | }); 194 | 195 | Q.all(removeDevs).then(function () { 196 | if (devbox.isEmpty()) 197 | debug.shepherd('Database cleared.'); 198 | else 199 | debug.shepherd('Database not cleared.'); 200 | }).fail(function (err) { 201 | debug.shepherd(err); 202 | }).done(); 203 | } else { 204 | devbox = new Objectbox(this._dbPath); 205 | } 206 | } 207 | 208 | return this.controller.reset(mode, callback); 209 | }; 210 | 211 | ZShepherd.prototype.permitJoin = function (time, type, callback) { 212 | if (_.isFunction(type) && !_.isFunction(callback)) { 213 | callback = type; 214 | type = 'all'; 215 | } else { 216 | type = type || 'all'; 217 | } 218 | 219 | if (!this._enabled) 220 | return Q.reject(new Error('Shepherd is not enabled.')).nodeify(callback); 221 | else 222 | return this.controller.permitJoin(time, type, callback); 223 | }; 224 | 225 | ZShepherd.prototype.info = function () { 226 | var net = this.controller.getNetInfo(); 227 | 228 | return { 229 | enabled: this._enabled, 230 | net: { 231 | state: net.state, 232 | channel: net.channel, 233 | panId: net.panId, 234 | extPanId: net.extPanId, 235 | ieeeAddr: net.ieeeAddr, 236 | nwkAddr: net.nwkAddr, 237 | }, 238 | startTime: this._startTime, 239 | joinTimeLeft: net.joinTimeLeft 240 | }; 241 | }; 242 | 243 | ZShepherd.prototype.mount = function (zApp, callback) { 244 | var self = this, 245 | deferred = (callback && Q.isPromise(callback.promise)) ? callback : Q.defer(), 246 | coord = this.controller.getCoord(), 247 | mountId, 248 | loEp; 249 | 250 | if (zApp.constructor.name !== 'Zive') 251 | throw new TypeError('zApp should be an instance of Zive class.'); 252 | 253 | if (this._mounting) { 254 | this._mountQueue.push(function () { 255 | self.mount(zApp, deferred); 256 | }); 257 | return deferred.promise.nodeify(callback); 258 | } 259 | 260 | this._mounting = true; 261 | 262 | Q.fcall(function () { 263 | _.forEach(self._zApp, function (app) { 264 | if (app === zApp) 265 | throw new Error('zApp already exists.'); 266 | }); 267 | self._zApp.push(zApp); 268 | }).then(function () { 269 | if (coord) { 270 | mountId = Math.max.apply(null, coord.epList); 271 | zApp._simpleDesc.epId = mountId > 10 ? mountId + 1 : 11; // epId 1-10 are reserved for delegator 272 | loEp = new Coordpoint(coord, zApp._simpleDesc); 273 | loEp.clusters = zApp.clusters; 274 | coord.endpoints[loEp.getEpId()] = loEp; 275 | zApp._endpoint = loEp; 276 | } else { 277 | throw new Error('Coordinator has not been initialized yet.'); 278 | } 279 | }).then(function () { 280 | return self.controller.registerEp(loEp).then(function () { 281 | debug.shepherd('Register zApp, epId: %s, profId: %s ', loEp.getEpId(), loEp.getProfId()); 282 | }); 283 | }).then(function () { 284 | return self.controller.querie.coordInfo().then(function (coordInfo) { 285 | coord.update(coordInfo); 286 | return Q.ninvoke(self._devbox, 'sync', coord._getId()); 287 | }); 288 | }).then(function () { 289 | self._attachZclMethods(loEp); 290 | self._attachZclMethods(zApp); 291 | 292 | loEp.onZclFoundation = function (msg, remoteEp) { 293 | setImmediate(function () { 294 | return zApp.foundationHandler(msg, remoteEp); 295 | }); 296 | }; 297 | loEp.onZclFunctional = function (msg, remoteEp) { 298 | setImmediate(function () { 299 | return zApp.functionalHandler(msg, remoteEp); 300 | }); 301 | }; 302 | 303 | deferred.resolve(loEp.getEpId()); 304 | }).fail(function (err) { 305 | deferred.reject(err); 306 | }).done(function () { 307 | self._mounting = false; 308 | if (self._mountQueue.length) 309 | process.nextTick(function () { 310 | self._mountQueue.shift()(); 311 | }); 312 | }); 313 | 314 | if (!(callback && Q.isPromise(callback.promise))) 315 | return deferred.promise.nodeify(callback); 316 | }; 317 | 318 | ZShepherd.prototype.list = function (ieeeAddrs) { 319 | var self = this, 320 | foundDevs; 321 | 322 | if (_.isString(ieeeAddrs)) 323 | ieeeAddrs = [ ieeeAddrs ]; 324 | else if (!_.isUndefined(ieeeAddrs) && !_.isArray(ieeeAddrs)) 325 | throw new TypeError('ieeeAddrs should be a string or an array of strings if given.'); 326 | else if (!ieeeAddrs) 327 | ieeeAddrs = _.map(this._devbox.exportAllObjs(), function (dev) { 328 | return dev.getIeeeAddr(); // list all 329 | }); 330 | 331 | foundDevs = _.map(ieeeAddrs, function (ieeeAddr) { 332 | proving.string(ieeeAddr, 'ieeeAddr should be a string.'); 333 | 334 | var devInfo, 335 | found = self._findDevByAddr(ieeeAddr); 336 | 337 | if (found) 338 | devInfo = _.omit(found.dump(), [ 'id', 'endpoints' ]); 339 | 340 | return devInfo; // will push undefined to foundDevs array if not found 341 | }); 342 | 343 | return foundDevs; 344 | }; 345 | 346 | ZShepherd.prototype.find = function (addr, epId) { 347 | proving.number(epId, 'epId should be a number.'); 348 | 349 | var dev = this._findDevByAddr(addr); 350 | return dev ? dev.getEndpoint(epId) : undefined; 351 | }; 352 | 353 | ZShepherd.prototype.lqi = function (ieeeAddr, callback) { 354 | proving.string(ieeeAddr, 'ieeeAddr should be a string.'); 355 | 356 | var self = this, 357 | dev = this._findDevByAddr(ieeeAddr); 358 | 359 | return Q.fcall(function () { 360 | if (dev) 361 | return self.controller.request('ZDO', 'mgmtLqiReq', { dstaddr: dev.getNwkAddr(), startindex: 0 }); 362 | else 363 | return Q.reject(new Error('device is not found.')); 364 | }).then(function (rsp) { // { srcaddr, status, neighbortableentries, startindex, neighborlqilistcount, neighborlqilist } 365 | if (rsp.status === 0) // success 366 | return _.map(rsp.neighborlqilist, function (neighbor) { 367 | return { ieeeAddr: neighbor.extAddr, lqi: neighbor.lqi }; 368 | }); 369 | }).nodeify(callback); 370 | }; 371 | 372 | ZShepherd.prototype.remove = function (ieeeAddr, cfg, callback) { 373 | proving.string(ieeeAddr, 'ieeeAddr should be a string.'); 374 | 375 | var dev = this._findDevByAddr(ieeeAddr); 376 | 377 | if (_.isFunction(cfg) && !_.isFunction(callback)) { 378 | callback = cfg; 379 | cfg = {}; 380 | } else { 381 | cfg = cfg || {}; 382 | } 383 | 384 | if (!dev) 385 | return Q.reject(new Error('device is not found.')).nodeify(callback); 386 | else 387 | return this.controller.remove(dev, cfg, callback); 388 | }; 389 | 390 | /*************************************************************************************************/ 391 | /*** Protected Methods ***/ 392 | /*************************************************************************************************/ 393 | ZShepherd.prototype._findDevByAddr = function (addr) { 394 | // addr: ieeeAddr(String) or nwkAddr(Number) 395 | proving.stringOrNumber(addr, 'addr should be a number or a string.'); 396 | 397 | return this._devbox.find(function (dev) { 398 | return _.isString(addr) ? dev.getIeeeAddr() === addr : dev.getNwkAddr() === addr; 399 | }); 400 | }; 401 | 402 | ZShepherd.prototype._registerDev = function (dev, callback) { 403 | var devbox = this._devbox, 404 | oldDev; 405 | 406 | if (!(dev instanceof Device) && !(dev instanceof Coordinator)) 407 | throw new TypeError('dev should be an instance of Device class.'); 408 | 409 | oldDev = _.isNil(dev._getId()) ? undefined : devbox.get(dev._getId()); 410 | 411 | return Q.fcall(function () { 412 | if (oldDev) { 413 | throw new Error('dev exists, unregister it first.'); 414 | } else if (dev._recovered) { 415 | return Q.ninvoke(devbox, 'set', dev._getId(), dev).then(function (id) { 416 | dev._recovered = false; 417 | delete dev._recovered; 418 | return id; 419 | }); 420 | } else { 421 | dev.update({ joinTime: Math.floor(Date.now()/1000) }); 422 | return Q.ninvoke(devbox, 'add', dev).then(function (id) { 423 | dev._setId(id); 424 | return id; 425 | }); 426 | } 427 | }).nodeify(callback); 428 | }; 429 | 430 | ZShepherd.prototype._unregisterDev = function (dev, callback) { 431 | return Q.ninvoke(this._devbox, 'remove', dev._getId()).nodeify(callback); 432 | }; 433 | 434 | ZShepherd.prototype._attachZclMethods = function (ep) { 435 | var self = this; 436 | 437 | if (ep.constructor.name === 'Zive') { 438 | var zApp = ep; 439 | zApp.foundation = function (dstAddr, dstEpId, cId, cmd, zclData, cfg, callback) { 440 | var dstEp = self.find(dstAddr, dstEpId); 441 | 442 | if (typeof cfg === 'function') { 443 | callback = cfg; 444 | cfg = {}; 445 | } 446 | 447 | if (!dstEp) 448 | return Q.reject(new Error('dstEp is not found.')).nodeify(callback); 449 | else 450 | return self._foundation(zApp._endpoint, dstEp, cId, cmd, zclData, cfg, callback); 451 | }; 452 | 453 | zApp.functional = function (dstAddr, dstEpId, cId, cmd, zclData, cfg, callback) { 454 | var dstEp = self.find(dstAddr, dstEpId); 455 | 456 | if (typeof cfg === 'function') { 457 | callback = cfg; 458 | cfg = {}; 459 | } 460 | 461 | if (!dstEp) 462 | return Q.reject(new Error('dstEp is not found.')).nodeify(callback); 463 | else 464 | return self._functional(zApp._endpoint, dstEp, cId, cmd, zclData, cfg, callback); 465 | }; 466 | } else { 467 | ep.foundation = function (cId, cmd, zclData, cfg, callback) { 468 | return self._foundation(ep, ep, cId, cmd, zclData, cfg, callback); 469 | }; 470 | ep.functional = function (cId, cmd, zclData, cfg, callback) { 471 | return self._functional(ep, ep, cId, cmd, zclData, cfg, callback); 472 | }; 473 | ep.bind = function (cId, dstEpOrGrpId, callback) { 474 | return self.controller.bind(ep, cId, dstEpOrGrpId, callback); 475 | }; 476 | ep.unbind = function (cId, dstEpOrGrpId, callback) { 477 | return self.controller.unbind(ep, cId, dstEpOrGrpId, callback); 478 | }; 479 | ep.read = function (cId, attrId, callback) { 480 | var deferred = Q.defer(), 481 | attr = zclId.attr(cId, attrId); 482 | 483 | attr = attr ? attr.value : attrId; 484 | 485 | self._foundation(ep, ep, cId, 'read', [{ attrId: attr }]).then(function (readStatusRecsRsp) { 486 | var rec = readStatusRecsRsp[0]; 487 | 488 | if (rec.status === 0) 489 | deferred.resolve(rec.attrData); 490 | else 491 | deferred.reject(new Error('request unsuccess: ' + rec.status)); 492 | }).catch(function(err) { 493 | deferred.reject(err); 494 | }); 495 | 496 | return deferred.promise.nodeify(callback); 497 | }; 498 | ep.write = function (cId, attrId, data, callback) { 499 | var deferred = Q.defer(), 500 | attr = zclId.attr(cId, attrId), 501 | attrType = zclId.attrType(cId, attrId).value; 502 | 503 | self._foundation(ep, ep, cId, 'write', [{ attrId: attr.value, dataType: attrType, attrData: data }]).then(function (writeStatusRecsRsp) { 504 | var rec = writeStatusRecsRsp[0]; 505 | 506 | if (rec.status === 0) 507 | deferred.resolve(data); 508 | else 509 | deferred.reject(new Error('request unsuccess: ' + rec.status)); 510 | }).catch(function(err) { 511 | deferred.reject(err); 512 | }); 513 | 514 | return deferred.promise.nodeify(callback); 515 | }; 516 | ep.report = function (cId, attrId, minInt, maxInt, repChange, callback) { 517 | var deferred = Q.defer(), 518 | coord = self.controller.getCoord(), 519 | dlgEp = coord.getDelegator(ep.getProfId()), 520 | cfgRpt = true, 521 | cfgRptRec, 522 | attrIdVal, 523 | attrTypeVal; 524 | 525 | if (arguments.length === 1) { 526 | cfgRpt = false; 527 | } else if (arguments.length === 2) { 528 | callback = attrId; 529 | cfgRpt = false; 530 | } else if (arguments.length === 5 && _.isFunction(repChange)) { 531 | callback = repChange; 532 | } 533 | 534 | if (cfgRpt) { 535 | attrIdVal = zclId.attr(cId, attrId); 536 | cfgRptRec = { 537 | direction : 0, 538 | attrId: attrIdVal ? attrIdVal.value : attrId, 539 | dataType : zclId.attrType(cId, attrId).value, 540 | minRepIntval : minInt, 541 | maxRepIntval : maxInt, 542 | repChange: repChange 543 | }; 544 | } 545 | 546 | Q.fcall(function () { 547 | if (dlgEp) { 548 | return ep.bind(cId, dlgEp).then(function () { 549 | if (cfgRpt) 550 | return ep.foundation(cId, 'configReport', [ cfgRptRec ]).then(function (rsp) { 551 | var status = rsp[0].status; 552 | if (status !== 0) 553 | deferred.reject(zclId.status(status).key); 554 | }); 555 | }); 556 | } else { 557 | return Q.reject(new Error('Profile: ' + ep.getProfId() + ' is not supported.')); 558 | } 559 | }).then(function () { 560 | deferred.resolve(); 561 | }).fail(function (err) { 562 | deferred.reject(err); 563 | }).done(); 564 | 565 | return deferred.promise.nodeify(callback); 566 | }; 567 | } 568 | }; 569 | 570 | ZShepherd.prototype._foundation = function (srcEp, dstEp, cId, cmd, zclData, cfg, callback) { 571 | var self = this; 572 | 573 | if (_.isFunction(cfg) && !_.isFunction(callback)) { 574 | callback = cfg; 575 | cfg = {}; 576 | } else { 577 | cfg = cfg || {}; 578 | } 579 | 580 | return this.af.zclFoundation(srcEp, dstEp, cId, cmd, zclData, cfg).then(function (msg) { 581 | var cmdString = zclId.foundation(cmd); 582 | cmdString = cmdString ? cmdString.key : cmd; 583 | 584 | if (cmdString === 'read') 585 | self._updateFinalizer(dstEp, cId, msg.payload); 586 | else if (cmdString === 'write' || cmdString === 'writeUndiv' || cmdString === 'writeNoRsp') 587 | self._updateFinalizer(dstEp, cId); 588 | 589 | return msg.payload; 590 | }).nodeify(callback); 591 | }; 592 | 593 | ZShepherd.prototype._functional = function (srcEp, dstEp, cId, cmd, zclData, cfg, callback) { 594 | var self = this; 595 | 596 | if (_.isFunction(cfg) && !_.isFunction(callback)) { 597 | callback = cfg; 598 | cfg = {}; 599 | } else { 600 | cfg = cfg || {}; 601 | } 602 | 603 | return this.af.zclFunctional(srcEp, dstEp, cId, cmd, zclData, cfg).then(function (msg) { 604 | self._updateFinalizer(dstEp, cId); 605 | return msg.payload; 606 | }).nodeify(callback); 607 | }; 608 | 609 | ZShepherd.prototype._updateFinalizer = function (ep, cId, attrs, reported) { 610 | var self = this, 611 | cIdString = zclId.cluster(cId), 612 | clusters = ep.getClusters().dumpSync(); 613 | 614 | cIdString = cIdString ? cIdString.key : cId; 615 | 616 | Q.fcall(function () { 617 | if (attrs) { 618 | var newAttrs = {}; 619 | 620 | _.forEach(attrs, function (rec) { // { attrId, status, dataType, attrData } 621 | var attrIdString = zclId.attr(cId, rec.attrId); 622 | attrIdString = attrIdString ? attrIdString.key : rec.attrId; 623 | 624 | if (reported) 625 | newAttrs[attrIdString] = rec.attrData; 626 | else 627 | newAttrs[attrIdString] = (rec.status === 0) ? rec.attrData : null; 628 | }); 629 | 630 | return newAttrs; 631 | } else { 632 | return self.af.zclClusterAttrsReq(ep, cId); 633 | } 634 | }).then(function (newAttrs) { 635 | var oldAttrs = clusters[cIdString].attrs, 636 | diff = zutils.objectDiff(oldAttrs, newAttrs); 637 | 638 | if (!_.isEmpty(diff)) { 639 | _.forEach(diff, function (val, attrId) { 640 | ep.getClusters().set(cIdString, 'attrs', attrId, val); 641 | }); 642 | 643 | self.emit('ind:changed', ep, { cid: cIdString, data: diff }); 644 | } 645 | }).fail(function () { 646 | return; 647 | }).done(); 648 | }; 649 | 650 | module.exports = ZShepherd; 651 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zigbee-shepherd", 3 | "version": "0.3.0", 4 | "description": "An open source ZigBee gateway solution with node.js.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "make test-all" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/zigbeer/zigbee-shepherd.git" 12 | }, 13 | "keywords": [ 14 | "zigbee", 15 | "cc253x" 16 | ], 17 | "author": "Jack Wu ", 18 | "contributors": [ 19 | { 20 | "name": "Hedy Wang", 21 | "email": "hedywings@gmail.com" 22 | }, 23 | { 24 | "name": "Simen Li", 25 | "email": "simenkid@gmail.com" 26 | } 27 | ], 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/zigbeer/zigbee-shepherd/issues" 31 | }, 32 | "homepage": "https://github.com/zigbeer/zigbee-shepherd#readme", 33 | "dependencies": { 34 | "areq": "^0.2.0", 35 | "busyman": "^0.3.0", 36 | "cc-znp": "^0.4.2", 37 | "debug": "^2.6.3", 38 | "objectbox": "^0.3.0", 39 | "proving": "^0.1.0", 40 | "q": "^1.4.1", 41 | "zcl-id": "^0.3.2", 42 | "zcl-packet": "^0.2.0", 43 | "ziee": "^0.3.0", 44 | "zive": "^0.2.2", 45 | "zstack-constants": "^0.2.0" 46 | }, 47 | "devDependencies": { 48 | "chai": "^3.5.0", 49 | "mocha": "^3.2.0", 50 | "sinon": "^1.17.4", 51 | "sinon-chai": "^2.8.0" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /test/database/dev.db: -------------------------------------------------------------------------------- 1 | {"$$indexCreated":{"fieldName":"id","unique":true,"sparse":false}} 2 | -------------------------------------------------------------------------------- /test/database/dev1.db: -------------------------------------------------------------------------------- 1 | {"$$indexCreated":{"fieldName":"id","unique":true,"sparse":false}} 2 | -------------------------------------------------------------------------------- /test/shepherd.test.js: -------------------------------------------------------------------------------- 1 | var Q = require('q'), 2 | fs = require('fs'), 3 | path = require('path'), 4 | Zive = require('zive'), 5 | Ziee = require('ziee'), 6 | chai = require('chai'), 7 | sinon = require('sinon'), 8 | sinonChai = require('sinon-chai'), 9 | expect = chai.expect; 10 | 11 | var Shepherd = require('../index.js'), 12 | Coord = require('../lib/model/coord'), 13 | Device = require('../lib/model/device'), 14 | Endpoint = require('../lib/model/endpoint'); 15 | 16 | chai.use(sinonChai); 17 | 18 | var coordinator = new Coord({ 19 | type: 0, 20 | ieeeAddr: '0x00124b00019c2ee9', 21 | nwkAddr: 0, 22 | manufId: 10, 23 | epList: [ 1, 2] 24 | }); 25 | 26 | var dev1 = new Device({ 27 | type: 1, 28 | ieeeAddr: '0x00137a00000161f2', 29 | nwkAddr: 100, 30 | manufId: 10, 31 | epList: [ 1 ] 32 | }); 33 | 34 | var zApp = new Zive({ profId: 0x0104, devId: 6 }, new Ziee()); 35 | 36 | describe('Top Level of Tests', function () { 37 | before(function (done) { 38 | var unlink1 = false, 39 | unlink2 = false; 40 | 41 | fs.stat('./test/database/dev.db', function (err, stats) { 42 | if (err) { 43 | fs.stat('./test/database', function (err, stats) { 44 | if (err) { 45 | fs.mkdir('./test/database', function () { 46 | unlink1 = true; 47 | if (unlink1 && unlink2) 48 | done(); 49 | }); 50 | } else { 51 | unlink1 = true; 52 | if (unlink1 && unlink2) 53 | done(); 54 | } 55 | }); 56 | } else if (stats.isFile()) { 57 | fs.unlink(path.resolve('./test/database/dev.db'), function () { 58 | unlink1 = true; 59 | if (unlink1 && unlink2) 60 | done(); 61 | }); 62 | } 63 | }); 64 | 65 | fs.stat('./test/database/dev1.db', function (err, stats) { 66 | if (err) { 67 | fs.stat('./test/database', function (err, stats) { 68 | if (err) { 69 | fs.mkdir('./test/database', function () { 70 | unlink2 = true; 71 | if (unlink1 && unlink2) 72 | done(); 73 | }); 74 | } else { 75 | unlink2 = true; 76 | if (unlink1 && unlink2) 77 | done(); 78 | } 79 | }); 80 | } else if (stats.isFile()) { 81 | fs.unlink(path.resolve('./test/database/dev1.db'), function () { 82 | unlink2 = true; 83 | if (unlink1 && unlink2) 84 | done(); 85 | }); 86 | } 87 | }); 88 | }); 89 | 90 | describe('Constructor Check', function () { 91 | var shepherd; 92 | before(function () { 93 | shepherd = new Shepherd('/dev/ttyUSB0', { dbPath: __dirname + '/database/dev.db' }); 94 | }); 95 | 96 | it('should has all correct members after new', function () { 97 | expect(shepherd._startTime).to.be.equal(0); 98 | expect(shepherd._enabled).to.be.false; 99 | expect(shepherd._zApp).to.be.an('array'); 100 | expect(shepherd.controller).to.be.an('object'); 101 | expect(shepherd.af).to.be.null; 102 | expect(shepherd._dbPath).to.be.equal(__dirname + '/database/dev.db'); 103 | expect(shepherd._devbox).to.be.an('object'); 104 | }); 105 | 106 | it('should throw if path is not a string', function () { 107 | expect(function () { return new Shepherd({}, {}); }).to.throw(TypeError); 108 | expect(function () { return new Shepherd([], {}); }).to.throw(TypeError); 109 | expect(function () { return new Shepherd(1, {}); }).to.throw(TypeError); 110 | expect(function () { return new Shepherd(true, {}); }).to.throw(TypeError); 111 | expect(function () { return new Shepherd(NaN, {}); }).to.throw(TypeError); 112 | 113 | expect(function () { return new Shepherd('xxx'); }).not.to.throw(Error); 114 | }); 115 | 116 | it('should throw if opts is given but not an object', function () { 117 | expect(function () { return new Shepherd('xxx', []); }).to.throw(TypeError); 118 | expect(function () { return new Shepherd('xxx', 1); }).to.throw(TypeError); 119 | expect(function () { return new Shepherd('xxx', true); }).to.throw(TypeError); 120 | 121 | expect(function () { return new Shepherd('xxx', {}); }).not.to.throw(Error); 122 | }); 123 | }); 124 | 125 | describe('Signature Check', function () { 126 | var shepherd; 127 | before(function () { 128 | shepherd = new Shepherd('/dev/ttyUSB0', { dbPath: __dirname + '/database/dev.db' }); 129 | shepherd._enabled = true; 130 | }); 131 | 132 | describe('#.reset', function () { 133 | it('should throw if mode is not a number and not a string', function () { 134 | expect(function () { shepherd.reset({}); }).to.throw(TypeError); 135 | expect(function () { shepherd.reset(true); }).to.throw(TypeError); 136 | }); 137 | }); 138 | 139 | describe('#.permitJoin', function () { 140 | it('should throw if time is not a number', function () { 141 | expect(function () { shepherd.permitJoin({}); }).to.throw(TypeError); 142 | expect(function () { shepherd.permitJoin(true); }).to.throw(TypeError); 143 | }); 144 | 145 | it('should throw if type is given but not a number and not a string', function () { 146 | expect(function () { shepherd.permitJoin({}); }).to.throw(TypeError); 147 | expect(function () { shepherd.permitJoin(true); }).to.throw(TypeError); 148 | }); 149 | }); 150 | 151 | describe('#.mount', function () { 152 | it('should throw if zApp is not an object', function () { 153 | expect(function () { shepherd.mount(true); }).to.throw(TypeError); 154 | expect(function () { shepherd.mount('ceed'); }).to.throw(TypeError); 155 | }); 156 | }); 157 | 158 | describe('#.list', function () { 159 | it('should throw if ieeeAddrs is not an array of strings', function () { 160 | expect(function () { shepherd.list({}); }).to.throw(TypeError); 161 | expect(function () { shepherd.list(true); }).to.throw(TypeError); 162 | expect(function () { shepherd.list([ 'ceed', {} ]); }).to.throw(TypeError); 163 | 164 | expect(function () { shepherd.list('ceed'); }).not.to.throw(Error); 165 | expect(function () { shepherd.list([ 'ceed', 'xxx' ]); }).not.to.throw(Error); 166 | }); 167 | }); 168 | 169 | describe('#.find', function () { 170 | it('should throw if addr is not a number and not a string', function () { 171 | expect(function () { shepherd.find({}, 1); }).to.throw(TypeError); 172 | expect(function () { shepherd.find(true, 1); }).to.throw(TypeError); 173 | }); 174 | 175 | it('should throw if epId is not a number', function () { 176 | expect(function () { shepherd.find(1, {}); }).to.throw(TypeError); 177 | expect(function () { shepherd.find(1, true); }).to.throw(TypeError); 178 | }); 179 | }); 180 | 181 | describe('#.lqi', function () { 182 | it('should throw if ieeeAddr is not a string', function () { 183 | expect(function () { shepherd.lqi({}); }).to.throw(TypeError); 184 | expect(function () { shepherd.lqi(true); }).to.throw(TypeError); 185 | expect(function () { shepherd.lqi('ceed'); }).not.to.throw(TypeError); 186 | }); 187 | }); 188 | 189 | describe('#.remove', function () { 190 | it('should throw if ieeeAddr is not a string', function () { 191 | expect(function () { shepherd.remove({}); }).to.throw(TypeError); 192 | expect(function () { shepherd.remove(true); }).to.throw(TypeError); 193 | expect(function () { shepherd.remove('ceed'); }).not.to.throw(TypeError); 194 | }); 195 | }); 196 | }); 197 | 198 | describe('Functional Check', function () { 199 | var shepherd; 200 | before(function () { 201 | shepherd = new Shepherd('/dev/ttyUSB0', { dbPath: __dirname + '/database/dev1.db' }); 202 | 203 | shepherd.controller.request = function (subsys, cmdId, valObj, callback) { 204 | var deferred = Q.defer(); 205 | 206 | process.nextTick(function () { 207 | deferred.resolve({ status: 0 }); 208 | }); 209 | 210 | return deferred.promise.nodeify(callback); 211 | }; 212 | }); 213 | 214 | describe('#.permitJoin', function () { 215 | it('should not throw if shepherd is not enabled when permitJoin invoked - shepherd is disabled.', function (done) { 216 | shepherd.permitJoin(3).fail(function (err) { 217 | if (err.message === 'Shepherd is not enabled.') 218 | done(); 219 | }).done(); 220 | }); 221 | 222 | it('should trigger permitJoin counter and event when permitJoin invoked - shepherd is enabled.', function (done) { 223 | shepherd._enabled = true; 224 | shepherd.once('permitJoining', function (joinTime) { 225 | shepherd._enabled = false; 226 | if (joinTime === 3) 227 | done(); 228 | }); 229 | shepherd.permitJoin(3); 230 | }); 231 | }); 232 | 233 | describe('#.start', function () { 234 | this.timeout(6000); 235 | 236 | it('should start ok, _ready and reday should be fired, _enabled,', function (done) { 237 | var _readyCbCalled = false, 238 | readyCbCalled = false, 239 | startCbCalled = false, 240 | startStub = sinon.stub(shepherd, 'start', function (callback) { 241 | var deferred = Q.defer(); 242 | 243 | shepherd._enabled = true; 244 | shepherd.controller._coord = coordinator; 245 | deferred.resolve(); 246 | 247 | setTimeout(function () { 248 | shepherd.emit('_ready'); 249 | }, 50); 250 | 251 | return deferred.promise.nodeify(callback); 252 | }); 253 | 254 | shepherd.once('_ready', function () { 255 | _readyCbCalled = true; 256 | if (_readyCbCalled && readyCbCalled && startCbCalled && shepherd._enabled) 257 | setTimeout(function () { 258 | startStub.restore(); 259 | done(); 260 | }, 200); 261 | }); 262 | 263 | shepherd.once('ready', function () { 264 | readyCbCalled = true; 265 | if (_readyCbCalled && readyCbCalled && startCbCalled && shepherd._enabled) 266 | setTimeout(function () { 267 | startStub.restore(); 268 | done(); 269 | }, 200); 270 | }); 271 | 272 | shepherd.start(function (err) { 273 | startCbCalled = true; 274 | if (_readyCbCalled && readyCbCalled && startCbCalled && shepherd._enabled) 275 | setTimeout(function () { 276 | startStub.restore(); 277 | done(); 278 | }, 200); 279 | }); 280 | }); 281 | }); 282 | 283 | describe('#.info', function () { 284 | it('should get correct info about the shepherd', function () { 285 | var getNwkInfoStub = sinon.stub(shepherd.controller, 'getNetInfo').returns({ 286 | state: 'Coordinator', 287 | channel: 11, 288 | panId: '0x7c71', 289 | extPanId: '0xdddddddddddddddd', 290 | ieeeAddr: '0x00124b0001709887', 291 | nwkAddr: 0, 292 | joinTimeLeft: 49 293 | }), 294 | shpInfo = shepherd.info(); 295 | 296 | expect(shpInfo.enabled).to.be.true; 297 | expect(shpInfo.net).to.be.deep.equal({ state: 'Coordinator', channel: 11, panId: '0x7c71', extPanId: '0xdddddddddddddddd', ieeeAddr: '0x00124b0001709887', nwkAddr: 0 }); 298 | expect(shpInfo.joinTimeLeft).to.be.equal(49); 299 | getNwkInfoStub.restore(); 300 | }); 301 | }); 302 | 303 | describe('#.mount', function () { 304 | it('should mount zApp', function (done) { 305 | var coordStub = sinon.stub(shepherd.controller.querie, 'coordInfo', function (callback) { 306 | return Q({}).nodeify(callback); 307 | }), 308 | syncStub = sinon.stub(shepherd._devbox, 'sync', function (id, callback) { 309 | return Q({}).nodeify(callback); 310 | }); 311 | 312 | shepherd.mount(zApp, function (err, epId) { 313 | if (!err) { 314 | coordStub.restore(); 315 | syncStub.restore(); 316 | done(); 317 | } 318 | }); 319 | }); 320 | }); 321 | 322 | describe('#.list', function () { 323 | this.timeout(5000); 324 | 325 | it('should list one devices', function (done) { 326 | shepherd._registerDev(dev1).then(function () { 327 | var devList = shepherd.list(); 328 | expect(devList.length).to.be.equal(1); 329 | expect(devList[0].type).to.be.equal(1); 330 | expect(devList[0].ieeeAddr).to.be.equal('0x00137a00000161f2'); 331 | expect(devList[0].nwkAddr).to.be.equal(100); 332 | expect(devList[0].manufId).to.be.equal(10); 333 | expect(devList[0].epList).to.be.deep.equal([ 1 ]); 334 | expect(devList[0].status).to.be.equal('offline'); 335 | done(); 336 | }).fail(function (err) { 337 | console.log(err); 338 | }).done(); 339 | }); 340 | }); 341 | 342 | describe('#.find', function () { 343 | it('should find nothing', function () { 344 | expect(shepherd.find('nothing', 1)).to.be.undefined; 345 | }); 346 | }); 347 | 348 | describe('#.lqi', function () { 349 | it('should get lqi of the device', function (done) { 350 | var requestStub = sinon.stub(shepherd.controller, 'request', function (subsys, cmdId, valObj, callback) { 351 | var deferred = Q.defer(); 352 | 353 | process.nextTick(function () { 354 | deferred.resolve({ 355 | srcaddr: 100, 356 | status: 0, 357 | neighbortableentries: 1, 358 | startindex: 0, 359 | neighborlqilistcount: 1, 360 | neighborlqilist: [ 361 | { 362 | extPandId: '0xdddddddddddddddd', 363 | extAddr: '0x0123456789abcdef', 364 | nwkAddr: 200, 365 | deviceType: 1, 366 | rxOnWhenIdle: 0, 367 | relationship: 0, 368 | permitJoin: 0, 369 | depth: 1, 370 | lqi: 123 371 | } 372 | ] 373 | }); 374 | }); 375 | 376 | return deferred.promise.nodeify(callback); 377 | }); 378 | 379 | shepherd.lqi('0x00137a00000161f2', function (err, data) { 380 | if (!err) { 381 | expect(data[0].ieeeAddr).to.be.equal('0x0123456789abcdef'); 382 | expect(data[0].lqi).to.be.equal(123); 383 | requestStub.restore(); 384 | done(); 385 | } 386 | }); 387 | }); 388 | }); 389 | 390 | describe('#.remove', function () { 391 | it('should remove the device', function (done) { 392 | var requestStub = sinon.stub(shepherd.controller, 'request', function (subsys, cmdId, valObj, callback) { 393 | var deferred = Q.defer(); 394 | 395 | process.nextTick(function () { 396 | deferred.resolve({ srcaddr: 100, status: 0 }); 397 | }); 398 | 399 | return deferred.promise.nodeify(callback); 400 | }); 401 | 402 | shepherd.remove('0x00137a00000161f2', function (err) { 403 | if (!err) { 404 | requestStub.restore(); 405 | done(); 406 | } 407 | }); 408 | }); 409 | }); 410 | 411 | describe('#.acceptDevIncoming', function () { 412 | this.timeout(60000); 413 | 414 | it('should fire incoming message and get a new device', function (done) { 415 | var acceptDevIncomingStub = sinon.stub(shepherd, 'acceptDevIncoming', function (devInfo, cb) { 416 | setTimeout(function () { 417 | var accepted = true; 418 | cb(null, accepted); 419 | }, 6000); 420 | }); 421 | 422 | shepherd.once('ind:incoming', function (dev) { 423 | acceptDevIncomingStub.restore(); 424 | if (dev.getIeeeAddr() === '0x00124b000bb55881') 425 | done(); 426 | }); 427 | 428 | shepherd.controller.emit('ZDO:devIncoming', { 429 | type: 1, 430 | ieeeAddr: '0x00124b000bb55881', 431 | nwkAddr: 100, 432 | manufId: 10, 433 | epList: [], 434 | endpoints: [] 435 | }); 436 | }); 437 | }); 438 | 439 | describe('#.reset', function () { 440 | this.timeout(20000); 441 | it('should reset - soft', function (done) { 442 | var stopStub = sinon.stub(shepherd, 'stop', function (callback) { 443 | var deferred = Q.defer(); 444 | deferred.resolve(); 445 | return deferred.promise.nodeify(callback); 446 | }), 447 | startStub = sinon.stub(shepherd, 'start', function (callback) { 448 | var deferred = Q.defer(); 449 | deferred.resolve(); 450 | return deferred.promise.nodeify(callback); 451 | }); 452 | 453 | shepherd.controller.once('SYS:resetInd', function () { 454 | setTimeout(function () { 455 | stopStub.restore(); 456 | startStub.restore(); 457 | done(); 458 | }, 100); 459 | }); 460 | 461 | shepherd.reset('soft').done(); 462 | }); 463 | 464 | it('should reset - hard', function (done) { 465 | var stopStub = sinon.stub(shepherd, 'stop', function (callback) { 466 | var deferred = Q.defer(); 467 | deferred.resolve(); 468 | return deferred.promise.nodeify(callback); 469 | }), 470 | startStub = sinon.stub(shepherd, 'start', function (callback) { 471 | var deferred = Q.defer(); 472 | deferred.resolve(); 473 | return deferred.promise.nodeify(callback); 474 | }); 475 | 476 | shepherd.controller.once('SYS:resetInd', function () { 477 | setTimeout(function () { 478 | stopStub.restore(); 479 | startStub.restore(); 480 | done(); 481 | }, 100); 482 | }); 483 | 484 | shepherd.reset('hard').done(); 485 | }); 486 | }); 487 | 488 | describe('#.stop', function () { 489 | it('should stop ok, permitJoin 0 should be fired, _enabled should be false', function (done) { 490 | var joinFired = false, 491 | stopCalled = false, 492 | closeStub = sinon.stub(shepherd.controller, 'close', function (callback) { 493 | var deferred = Q.defer(); 494 | 495 | deferred.resolve(); 496 | 497 | return deferred.promise.nodeify(callback); 498 | }); 499 | 500 | shepherd.once('permitJoining', function (joinTime) { 501 | joinFired = true; 502 | if (joinTime === 0 && !shepherd._enabled && stopCalled && joinFired){ 503 | closeStub.restore(); 504 | done(); 505 | } 506 | }); 507 | 508 | shepherd.stop(function (err) { 509 | stopCalled = true; 510 | if (!err && !shepherd._enabled && stopCalled && joinFired) { 511 | closeStub.restore(); 512 | done(); 513 | } 514 | }); 515 | }); 516 | }); 517 | }); 518 | }); 519 | -------------------------------------------------------------------------------- /test/zcl.test.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect, 2 | zcl = require('../lib/components/zcl'); 3 | 4 | describe('APIs Arguments Check for Throwing Error', function() { 5 | describe('#.frame', function() { 6 | var frameCntl = {frameType:1, manufSpec: 0, direction: 0, disDefaultRsp: 1}; 7 | 8 | it('should be a function', function () { 9 | expect(zcl.frame).to.be.a('function'); 10 | }); 11 | 12 | it('should throw TypeError if input frameCntl is not an object', function () { 13 | expect(function () { return zcl.frame(undefined, 0, 0, 'toggle', {}, 'genOnOff'); }).to.throw(TypeError); 14 | expect(function () { return zcl.frame(null, 0, 0, 'toggle', {}, 'genOnOff'); }).to.throw(TypeError); 15 | expect(function () { return zcl.frame(NaN, 0, 0, 'toggle', {}, 'genOnOff'); }).to.throw(TypeError); 16 | expect(function () { return zcl.frame([], 0, 0, 'toggle', {}, 'genOnOff'); }).to.throw(TypeError); 17 | expect(function () { return zcl.frame(true, 0, 0, 'toggle', {}, 'genOnOff'); }).to.throw(TypeError); 18 | expect(function () { return zcl.frame(new Date(), 0, 0, 'toggle', {}, 'genOnOff'); }).to.throw(TypeError); 19 | expect(function () { return zcl.frame(function () {}, 0, 0, 'toggle', {}, 'genOnOff'); }).to.throw(TypeError); 20 | 21 | expect(function () { return zcl.frame(frameCntl, 0, 0, 'toggle', {}, 'genOnOff'); }).not.to.throw(TypeError); 22 | }); 23 | 24 | it('should throw TypeError if input manufCode is not a number', function () { 25 | expect(function () { return zcl.frame(frameCntl, undefined, 0, 'toggle', {}, 'genOnOff'); }).to.throw(TypeError); 26 | expect(function () { return zcl.frame(frameCntl, null, 0, 'toggle', {}, 'genOnOff'); }).to.throw(TypeError); 27 | expect(function () { return zcl.frame(frameCntl, NaN, 0, 'toggle', {}, 'genOnOff'); }).to.throw(TypeError); 28 | expect(function () { return zcl.frame(frameCntl, [], 0, 'toggle', {}, 'genOnOff'); }).to.throw(TypeError); 29 | expect(function () { return zcl.frame(frameCntl, true, 0, 'toggle', {}, 'genOnOff'); }).to.throw(TypeError); 30 | expect(function () { return zcl.frame(frameCntl, new Date(), 0, 'toggle', {}, 'genOnOff'); }).to.throw(TypeError); 31 | expect(function () { return zcl.frame(frameCntl, function () {}, 0, 'toggle', {}, 'genOnOff'); }).to.throw(TypeError); 32 | }); 33 | 34 | it('should throw TypeError if input seqNum is not a number', function () { 35 | expect(function () { return zcl.frame(frameCntl, 0, undefined, 'toggle', {}, 'genOnOff'); }).to.throw(TypeError); 36 | expect(function () { return zcl.frame(frameCntl, 0, null, 'toggle', {}, 'genOnOff'); }).to.throw(TypeError); 37 | expect(function () { return zcl.frame(frameCntl, 0, NaN, 'toggle', {}, 'genOnOff'); }).to.throw(TypeError); 38 | expect(function () { return zcl.frame(frameCntl, 0, [], 'toggle', {}, 'genOnOff'); }).to.throw(TypeError); 39 | expect(function () { return zcl.frame(frameCntl, 0, true, 'toggle', {}, 'genOnOff'); }).to.throw(TypeError); 40 | expect(function () { return zcl.frame(frameCntl, 0, new Date(), 'toggle', {}, 'genOnOff'); }).to.throw(TypeError); 41 | expect(function () { return zcl.frame(frameCntl, 0, function () {}, 'toggle', {}, 'genOnOff'); }).to.throw(TypeError); 42 | }); 43 | 44 | it('should throw TypeError if input cmd is not a number and not a string', function () { 45 | expect(function () { return zcl.frame(frameCntl, 0, 0, undefined, {}, 'genOnOff'); }).to.throw(TypeError); 46 | expect(function () { return zcl.frame(frameCntl, 0, 0, null, {}, 'genOnOff'); }).to.throw(TypeError); 47 | expect(function () { return zcl.frame(frameCntl, 0, 0, NaN, {}, 'genOnOff'); }).to.throw(TypeError); 48 | expect(function () { return zcl.frame(frameCntl, 0, 0, [], {}, 'genOnOff'); }).to.throw(TypeError); 49 | expect(function () { return zcl.frame(frameCntl, 0, 0, true, {}, 'genOnOff'); }).to.throw(TypeError); 50 | expect(function () { return zcl.frame(frameCntl, 0, 0, new Date(), {}, 'genOnOff'); }).to.throw(TypeError); 51 | expect(function () { return zcl.frame(frameCntl, 0, 0, function () {}, {}, 'genOnOff'); }).to.throw(TypeError); 52 | 53 | expect(function () { return zcl.frame(frameCntl, 0, 0, 'toggle', {}, 'genOnOff'); }).not.to.throw(TypeError); 54 | expect(function () { return zcl.frame(frameCntl, 0, 0, 2, {}, 'genOnOff'); }).not.to.throw(TypeError); 55 | }); 56 | 57 | it('should throw TypeError if input zclPayload is not an object and not an array', function () { 58 | expect(function () { return zcl.frame(frameCntl, 0, 0, 'toggle', undefined, 'genOnOff'); }).to.throw(TypeError); 59 | expect(function () { return zcl.frame(frameCntl, 0, 0, 'toggle', null, 'genOnOff'); }).to.throw(TypeError); 60 | expect(function () { return zcl.frame(frameCntl, 0, 0, 'toggle', NaN, 'genOnOff'); }).to.throw(TypeError); 61 | expect(function () { return zcl.frame(frameCntl, 0, 0, 'toggle', true, 'genOnOff'); }).to.throw(TypeError); 62 | 63 | expect(function () { return zcl.frame(frameCntl, 0, 0, 'toggle', {}, 'genOnOff'); }).not.to.throw(TypeError); 64 | expect(function () { return zcl.frame(frameCntl, 0, 0, 'toggle', [], 'genOnOff'); }).not.to.throw(TypeError); 65 | }); 66 | 67 | it('should throw TypeError if input clusterId is not a number and not a string', function () { 68 | expect(function () { return zcl.frame(frameCntl, 0, 0, 'toggle', {}, undefined); }).to.throw(TypeError); 69 | expect(function () { return zcl.frame(frameCntl, 0, 0, 'toggle', {}, null); }).to.throw(TypeError); 70 | expect(function () { return zcl.frame(frameCntl, 0, 0, 'toggle', {}, NaN); }).to.throw(TypeError); 71 | expect(function () { return zcl.frame(frameCntl, 0, 0, 'toggle', {}, []); }).to.throw(TypeError); 72 | expect(function () { return zcl.frame(frameCntl, 0, 0, 'toggle', {}, true); }).to.throw(TypeError); 73 | expect(function () { return zcl.frame(frameCntl, 0, 0, 'toggle', {}, new Date()); }).to.throw(TypeError); 74 | expect(function () { return zcl.frame(frameCntl, 0, 0, 'toggle', {}, function () {}); }).to.throw(TypeError); 75 | 76 | expect(function () { return zcl.frame(frameCntl, 0, 0, 'toggle', {}, 'genOnOff'); }).not.to.throw(TypeError); 77 | expect(function () { return zcl.frame(frameCntl, 0, 0, 'toggle', {}, 6); }).not.to.throw(TypeError); 78 | }); 79 | }); 80 | 81 | describe('#.parse', function() { 82 | var zclBuf = new Buffer([0x11, 0x00, 0x02]); 83 | 84 | it('should be a function', function () { 85 | expect(zcl.parse).to.be.a('function'); 86 | }); 87 | 88 | it('should throw TypeError if input zclBuf is not a buffer', function () { 89 | expect(function () { return zcl.parse(undefined, 0, function () {}); }).to.throw(TypeError); 90 | expect(function () { return zcl.parse(null, 0, function () {}); }).to.throw(TypeError); 91 | expect(function () { return zcl.parse(NaN, 0, function () {}); }).to.throw(TypeError); 92 | expect(function () { return zcl.parse([], 0, function () {}); }).to.throw(TypeError); 93 | expect(function () { return zcl.parse(true, 0, function () {}); }).to.throw(TypeError); 94 | expect(function () { return zcl.parse(new Date(), 0, function () {}); }).to.throw(TypeError); 95 | expect(function () { return zcl.parse(function () {}, 0, function () {}); }).to.throw(TypeError); 96 | }); 97 | 98 | it('should throw TypeError if input clusterId is not a number and not a string', function () { 99 | expect(function () { return zcl.parse(zclBuf, undefined, function () {}); }).to.throw(TypeError); 100 | expect(function () { return zcl.parse(zclBuf, null, function () {}); }).to.throw(TypeError); 101 | expect(function () { return zcl.parse(zclBuf, NaN, function () {}); }).to.throw(TypeError); 102 | expect(function () { return zcl.parse(zclBuf, [], function () {}); }).to.throw(TypeError); 103 | expect(function () { return zcl.parse(zclBuf, true, function () {}); }).to.throw(TypeError); 104 | expect(function () { return zcl.parse(zclBuf, new Date(), function () {}); }).to.throw(TypeError); 105 | expect(function () { return zcl.parse(zclBuf, function () {}, function () {}); }).to.throw(TypeError); 106 | 107 | expect(function () { return zcl.parse(zclBuf, 'genOnOff', function () {}); }).not.to.throw(TypeError); 108 | expect(function () { return zcl.parse(zclBuf, 6, function () {}); }).not.to.throw(TypeError); 109 | }); 110 | }); 111 | 112 | describe('#.header', function() { 113 | it('should be a function', function () { 114 | expect(zcl.header).to.be.a('function'); 115 | }); 116 | 117 | it('should throw TypeError if input buf is not a buffer', function () { 118 | expect(function () { return zcl.header(undefined); }).to.throw(TypeError); 119 | expect(function () { return zcl.header(null); }).to.throw(TypeError); 120 | expect(function () { return zcl.header(NaN); }).to.throw(TypeError); 121 | expect(function () { return zcl.header([]); }).to.throw(TypeError); 122 | expect(function () { return zcl.header(true); }).to.throw(TypeError); 123 | expect(function () { return zcl.header(new Date()); }).to.throw(TypeError); 124 | expect(function () { return zcl.header(function () {}); }).to.throw(TypeError); 125 | }); 126 | }); 127 | }); 128 | 129 | describe('Module Methods Check', function() { 130 | describe('zcl foundation #.frame and #.parse Check', function () { 131 | var zclFrames = [ 132 | { 133 | frameCntl: { 134 | frameType: 0, 135 | manufSpec: 0, 136 | direction: 0, 137 | disDefaultRsp: 1 138 | }, 139 | manufCode: 0, 140 | seqNum: 0, 141 | cmdId: 'writeUndiv', 142 | payload: [ 143 | {attrId: 0x1234, dataType: 0x41, attrData: 'hello'}, 144 | {attrId: 0xabcd, dataType: 0x24, attrData: [100, 2406]}, 145 | {attrId: 0x1234, dataType: 0x08, attrData: 60} 146 | ] 147 | }, 148 | { 149 | frameCntl: { 150 | frameType: 0, 151 | manufSpec: 1, 152 | direction: 0, 153 | disDefaultRsp: 1 154 | }, 155 | manufCode: 0xaaaa, 156 | seqNum: 1, 157 | cmdId: 'configReport', 158 | payload: [ 159 | {direction: 0, attrId: 0x0001, dataType: 0x20, minRepIntval: 500, maxRepIntval: 1000, repChange: 10}, 160 | {direction: 1, attrId: 0x0001, timeout: 999}, 161 | {direction: 0, attrId: 0x0001, dataType: 0x43, minRepIntval: 100, maxRepIntval: 200} 162 | ] 163 | }, 164 | { 165 | frameCntl: { 166 | frameType: 0, 167 | manufSpec: 0, 168 | direction: 1, 169 | disDefaultRsp: 1 170 | }, 171 | manufCode: 0, 172 | seqNum: 2, 173 | cmdId: 'writeStrcut', 174 | payload: [ 175 | {attrId: 0x0011, selector: {indicator: 3, indexes: [0x0101, 0x0202, 0x0303]}, dataType: 0x21, attrData: 60000}, 176 | {attrId: 0x0022, selector: {indicator: 0}, dataType: 0x50, attrData: {elmType: 0x20, numElms: 3, elmVals: [1, 2, 3]}}, 177 | {attrId: 0x0033, selector: {indicator: 1, indexes: [0x0101]}, dataType: 0x4c, attrData: {numElms: 0x01, structElms: [{elmType: 0x20, elmVal: 1}]}} 178 | ] 179 | } 180 | ]; 181 | 182 | zclFrames.forEach(function(zclFrame) { 183 | var zBuf; 184 | 185 | it('zcl foundation framer and parser Check', function () { 186 | zBuf = zcl.frame(zclFrame.frameCntl, zclFrame.manufCode, zclFrame.seqNum, zclFrame.cmdId, zclFrame.payload); 187 | zcl.parse(zBuf, function (err, result) { 188 | expect(result).to.eql(zclFrame); 189 | }); 190 | }); 191 | }); 192 | }); 193 | 194 | describe('zcl functional #.frame and #.parse Check', function () { 195 | var zclFrames = [ 196 | { 197 | frameCntl: { 198 | frameType: 1, 199 | manufSpec: 0, 200 | direction: 0, 201 | disDefaultRsp: 1 202 | }, 203 | manufCode: 0, 204 | seqNum: 0, 205 | cmdId: 'add', 206 | payload: { 207 | groupid: 0x1234, 208 | sceneid: 0x08, 209 | transtime: 0x2468, 210 | scenename: 'genscenes', 211 | extensionfieldsets: [ { clstId: 0x0006, len: 0x3, extField: [0x01, 0x02, 0x03]}, 212 | { clstId: 0x0009, len: 0x5, extField: [0x05, 0x04, 0x03, 0x02, 0x01]} ] 213 | } 214 | }, 215 | { 216 | frameCntl: { 217 | frameType: 1, 218 | manufSpec: 1, 219 | direction: 1, 220 | disDefaultRsp: 0 221 | }, 222 | manufCode: 0xaaaa, 223 | seqNum: 1, 224 | cmdId: 'addRsp', 225 | payload: { 226 | status: 0x26, 227 | groupId: 0xffff, 228 | sceneId: 0x06 229 | } 230 | }, 231 | { 232 | frameCntl: { 233 | frameType: 1, 234 | manufSpec: 0, 235 | direction: 1, 236 | disDefaultRsp: 1 237 | }, 238 | manufCode: 0, 239 | seqNum: 2, 240 | cmdId: 'getSceneMembershipRsp', 241 | payload: { 242 | status: 0x01, 243 | capacity: 0x02, 244 | groupid: 0x2468, 245 | scenecount: 3, 246 | scenelist: [0x22, 0x33, 0x56] 247 | } 248 | } 249 | ]; 250 | 251 | zclFrames.forEach(function(zclFrame) { 252 | var zBuf; 253 | 254 | it('zcl functional framer and parser Check', function () { 255 | zBuf = zcl.frame(zclFrame.frameCntl, zclFrame.manufCode, zclFrame.seqNum, zclFrame.cmdId, zclFrame.payload, 0x0005); 256 | zcl.parse(zBuf, 0x0005, function (err, result) { 257 | if (result.cmdId === 'add') 258 | result.frameCntl.direction = 0; 259 | else 260 | result.frameCntl.direction = 1; 261 | 262 | expect(result).to.eql(zclFrame); 263 | }); 264 | }); 265 | }); 266 | }); 267 | 268 | describe('zcl #.header Check', function () { 269 | var headers = [ 270 | { 271 | buf: new Buffer([ 0x00, 0x00, 0x00 ]), 272 | obj: { 273 | frameCntl: { frameType: 0, manufSpec: 0, direction: 0, disDefaultRsp: 0 }, 274 | manufCode: null, 275 | seqNum: 0, 276 | cmdId: 0 277 | } 278 | }, 279 | { 280 | buf: new Buffer([ 0x1d, 0x34, 0x12, 0xff, 0x01 ]), 281 | obj: { 282 | frameCntl: { frameType: 1, manufSpec: 1, direction: 1, disDefaultRsp: 1 }, 283 | manufCode: 0x1234, 284 | seqNum: 0xff, 285 | cmdId: 0x01 286 | } 287 | }, 288 | ]; 289 | 290 | headers.forEach(function (header) { 291 | var result = zcl.header(header.buf); 292 | 293 | it('zcl header Check', function () { 294 | expect(result).to.eql(header.obj); 295 | }); 296 | }); 297 | }); 298 | 299 | describe('zcl #.header Check - Bad command', function () { 300 | var headers = [ 301 | { 302 | buf: new Buffer([ 0x1e, 0x34, 0x12, 0xff, 0x01 ]) 303 | }, 304 | { 305 | buf: new Buffer([ 0x1f, 0x34, 0x12, 0xff, 0x01 ]) 306 | }, 307 | ]; 308 | 309 | headers.forEach(function (header) { 310 | var result = zcl.header(header.buf); 311 | it('zcl header Check', function () { 312 | expect(result).to.be.undefined; 313 | }); 314 | }); 315 | }); 316 | 317 | }); --------------------------------------------------------------------------------