UNPKG

zigbee-shepherd

Version:

An open source ZigBee gateway solution with node.js.

709 lines (584 loc) 25.8 kB
/* jshint node: true */ 'use strict'; const EventEmitter = require('events'); const Q = require('q'); const proving = require('proving'); const Objectbox = require('objectbox'); const debug = { shepherd: require('debug')('zigbee-shepherd') }; const init = require('./initializers/init_shepherd'); const zutils = require('./components/zutils'); const Controller = require('./components/controller'); const eventHandlers = require('./components/event_handlers'); const Device = require('./model/device'); const Group = require('./model/group'); const Coordpoint = require('./model/coordpoint'); /*************************************************************************************************/ /*** ZShepherd Class ***/ /*************************************************************************************************/ class ZShepherd extends EventEmitter { constructor(path, opts) { super(); // opts: { sp: {}, net: {}, dbPath: 'xxx', zclId: ZclID } const self = this; const spCfg = {}; opts = opts || {}; proving.string(path, 'path should be a string.'); proving.object(opts, 'opts should be an object if gieven.'); spCfg.path = path; spCfg.options = opts.hasOwnProperty('sp') ? opts.sp : { baudrate: 115200, rtscts: true }; /***************************************************/ /*** Protected Members ***/ /***************************************************/ this._startTime = 0; this._enabled = false; this._zApp = []; this._mounting = false; this._mountQueue = []; this.controller = new Controller(this, spCfg); // controller is the main actor this.controller.setNvParams(opts.net); this.af = null; this.zclId = opts.zclId; this._dbPath = opts.dbPath; if (!this._dbPath) { // use default throw new Error('dbPath must be specified') } this._devbox = new Objectbox(this._dbPath); this.acceptDevIncoming = function (devInfo, callback) { // Override at will. setImmediate(() => { const accepted = true; callback(null, accepted); }); }; /***************************************************/ /*** Event Handlers (Ind Event Bridges) ***/ /***************************************************/ eventHandlers.attachEventHandlers(this); this.controller.on('permitJoining', time => { self.emit('permitJoining', time); }); this.on('_ready', () => { self._startTime = Math.floor(Date.now()/1000); setImmediate(() => { self.emit('ready'); }); }); this.on('ind:incoming', dev => { const endpoints = []; dev.epList.forEach(epId => { endpoints.push(dev.getEndpoint(epId)); }); self.emit('ind', { type: 'devIncoming', endpoints, data: dev.getIeeeAddr() }); }); this.on('ind:interview', (dev, status) => { self.emit('ind', { type: 'devInterview', status, data: dev }); }); this.on('ind:leaving', (epList, ieeeAddr) => { self.emit('ind', { type: 'devLeaving', endpoints: epList, data: ieeeAddr }); }); this.on('ind:changed', (ep, notifData) => { self.emit('ind', { type: 'devChange', endpoints: [ ep ], data: notifData }); }); this.on('ind:cmd', (ep, cId, payload, cmdId, msg) => { const cIdString = self.zclId.cluster(cId); const type = `cmd${cmdId.charAt(0).toUpperCase() + cmdId.substr(1)}`; const notifData = {}; notifData.cid = cIdString ? cIdString.key : cId; notifData.data = payload; self.emit('ind', { type, endpoints: [ ep ], data: notifData, linkquality: msg.linkquality }); }); this.on('ind:statusChange', (ep, cId, payload, msg) => { let cIdString = self.zclId.cluster(cId); const notifData = { cid: '', zoneStatus: null }; cIdString = cIdString ? cIdString.key : cId; notifData.cid = cIdString; notifData.zoneStatus = payload.zonestatus; self.emit('ind', { type: 'statusChange', endpoints: [ ep ], data: notifData, linkquality: msg.linkquality }); }); this.on('ind:reported', (ep, cId, attrs, msg) => { let cIdString = self.zclId.cluster(cId); const notifData = { cid: '', data: {} }; self._updateFinalizer(ep, cId, attrs, true); cIdString = cIdString ? cIdString.key : cId; notifData.cid = cIdString; attrs.forEach(rec => { // { attrId, dataType, attrData } let attrIdString = self.zclId.attr(cIdString, rec.attrId); attrIdString = attrIdString ? attrIdString.key : rec.attrId; notifData.data[attrIdString] = rec.attrData; }); self.emit('ind', { type: 'attReport', endpoints: [ ep ], data: notifData, linkquality: msg.linkquality }); }); this.on('ind:status', (dev, status) => { const endpoints = []; dev.epList.forEach(epId => { endpoints.push(dev.getEndpoint(epId)); }); self.emit('ind', { type: 'devStatus', endpoints, data: status }); }); } /*************************************************************************************************/ /*** Public Methods ***/ /*************************************************************************************************/ start(callback) { const self = this; return init.setupShepherd(this).then(() => { self._enabled = true; // shepherd is enabled self.emit('_ready'); // if all done, shepherd fires '_ready' event for inner use debug.shepherd('zigbee-shepherd is _ready and _enabled'); }).nodeify(callback); }; stop(callback) { const self = this; const devbox = this._devbox; debug.shepherd('zigbee-shepherd is stopping.'); return Q.fcall(() => { if (self._enabled) { self.permitJoin(0x00, 'all'); devbox.exportAllIds().forEach(id => { devbox.removeElement(id); }); return self.controller.close(); } }).then(() => { self._enabled = false; self._zApp = null; self._zApp = []; debug.shepherd('zigbee-shepherd is stopped.'); }).nodeify(callback); }; reset(mode, callback) { const self = this; const devbox = this._devbox; const removeDevs = []; proving.stringOrNumber(mode, 'mode should be a number or a string.'); if (mode === 'hard' || mode === 0) { // clear database if (devbox) { devbox.exportAllIds().forEach(id => { removeDevs.push(Q.ninvoke(devbox, 'remove', id)); }); Q.all(removeDevs).then(() => { if (devbox.isEmpty()) debug.shepherd('Database cleared.'); else debug.shepherd('Database not cleared.'); }).fail(err => { debug.shepherd(err); }).done(); } else { self._devbox = new Objectbox(this._dbPath); } } return this.controller.reset(mode, callback); }; permitJoin(time, type, callback) { if (typeof type === "function" && typeof callback !== "function") { callback = type; type = 'all'; } else { type = type || 'all'; } if (!this._enabled) return Q.reject(new Error('Shepherd is not enabled.')).nodeify(callback); else return this.controller.permitJoin(time, type, callback); }; info() { const net = this.controller.getNetInfo(); const firmware = this.controller.getFirmwareInfo(); return { enabled: this._enabled, net: { state: net.state, channel: net.channel, panId: net.panId, extPanId: net.extPanId, ieeeAddr: net.ieeeAddr, nwkAddr: net.nwkAddr, }, firmware, startTime: this._startTime, joinTimeLeft: net.joinTimeLeft }; }; mount(zApp, callback) { const self = this; const deferred = (callback && Q.isPromise(callback.promise)) ? callback : Q.defer(); const coord = this.controller._coord; let mountId; let loEp; if (zApp.constructor.name !== 'Zive') throw new TypeError('zApp should be an instance of Zive class.'); if (this._mounting) { this._mountQueue.push(() => { self.mount(zApp, deferred); }); return deferred.promise.nodeify(callback); } this._mounting = true; Q.fcall(() => { self._zApp.forEach(app => { if (app === zApp) throw new Error('zApp already exists.'); }); self._zApp.push(zApp); }).then(() => { if (coord) { mountId = Math.max.apply(null, coord.epList); zApp._simpleDesc.epId = mountId > 10 ? mountId + 1 : 11; // epId 1-10 are reserved for delegator loEp = new Coordpoint(coord, zApp._simpleDesc); loEp.clusters = zApp.clusters; coord.endpoints[loEp.getEpId()] = loEp; zApp._endpoint = loEp; } else { throw new Error('Coordinator has not been initialized yet.'); } }).then(() => self.controller.registerEp(loEp).then(() => { debug.shepherd('Register zApp, epId: %s, profId: %s ', loEp.getEpId(), loEp.getProfId()); })).then(() => self.controller.query.coordInfo().then(coordInfo => { coord.update(coordInfo); return Q.ninvoke(self._devbox, 'sync', coord._getId()); })).then(() => { self._attachZclMethods(loEp); self._attachZclMethods(zApp); loEp.onZclFoundation = function (msg, remoteEp) { setImmediate(() => zApp.foundationHandler(msg, remoteEp)); }; loEp.onZclFunctional = function (msg, remoteEp) { setImmediate(() => zApp.functionalHandler(msg, remoteEp)); }; deferred.resolve(loEp.getEpId()); }).fail(err => { deferred.reject(err); }).done(() => { self._mounting = false; if (self._mountQueue.length) process.nextTick(() => { self._mountQueue.shift()(); }); }); if (!(callback && Q.isPromise(callback.promise))) return deferred.promise.nodeify(callback); }; list(ieeeAddrs) { const self = this; let foundDevs; if (typeof ieeeAddrs === "string") ieeeAddrs = [ ieeeAddrs ]; else if (typeof ieeeAddrs !== "undefined" && !Array.isArray(ieeeAddrs)) throw new TypeError('ieeeAddrs should be a string or an array of strings if given.'); else if (!ieeeAddrs) ieeeAddrs = this._devbox.exportAllObjs().map(dev => // list all dev.getIeeeAddr()); foundDevs = ieeeAddrs.map(ieeeAddr => { proving.string(ieeeAddr, 'ieeeAddr should be a string.'); const found = self._findDevByAddr(ieeeAddr); if (!found) return const {id, endpoints, ...devInfo} = found.dump(); return devInfo; // will push undefined to foundDevs array if not found }); return foundDevs; }; getGroup(groupID) { proving.number(groupID, 'groupID should be a number.'); const group = new Group(groupID); this._attachZclMethods(group); return group; }; find(addr, epId) { proving.number(epId, 'epId should be a number.'); const dev = this._findDevByAddr(addr); return dev ? dev.getEndpoint(epId) : undefined; }; lqi(ieeeAddr, callback) { proving.string(ieeeAddr, 'ieeeAddr should be a string.'); const self = this; const dev = this._findDevByAddr(ieeeAddr); return Q.fcall(() => { if (dev) return self.controller.request('ZDO', 'mgmtLqiReq', { dstaddr: dev.getNwkAddr(), startindex: 0 }); else return Q.reject(new Error('device is not found.')); }).then(rsp => { // { srcaddr, status, neighbortableentries, startindex, neighborlqilistcount, neighborlqilist } if (rsp.status === 0) // success return rsp.neighborlqilist.map(neighbor => ({ ieeeAddr: neighbor.extAddr, nwkAddr: neighbor.nwkAddr, lqi: neighbor.lqi })); }).nodeify(callback); }; remove(ieeeAddr, cfg, callback) { proving.string(ieeeAddr, 'ieeeAddr should be a string.'); const dev = this._findDevByAddr(ieeeAddr); if (typeof cfg === "function" && typeof callback !== "function") { callback = cfg; cfg = {}; } else { cfg = cfg || {}; } if (!dev) return Q.reject(new Error('device is not found.')).nodeify(callback); else return this.controller.remove(dev, cfg, callback); }; lqiScan(ieeeAddr) { const info = this.info(); const self = this; const noDuplicate = {}; const processResponse = function(parent){ return function(data){ let chain = Q(); data.forEach(devinfo => { const ieeeAddr = devinfo.ieeeAddr; if (ieeeAddr == "0x0000000000000000") return; let dev = self._findDevByAddr(ieeeAddr); devinfo.parent = parent; devinfo.status = dev ? dev.status : "offline"; const dedupKey = `${parent}|${ieeeAddr}`; if (dev && dev.type == "Router" && !noDuplicate[dedupKey]) { chain = chain.then(() => self.lqi(ieeeAddr).then(processResponse(ieeeAddr))); } noDuplicate[dedupKey] = devinfo; }); return chain; }; } if(!ieeeAddr){ ieeeAddr = info.net.ieeeAddr; } return self.lqi(ieeeAddr) .timeout(1000) .then(processResponse(ieeeAddr)) .then(() => Object.values(noDuplicate)) .catch(() => Object.values(noDuplicate)); }; /*************************************************************************************************/ /*** Protected Methods ***/ /*************************************************************************************************/ _findDevByAddr(addr) { // addr: ieeeAddr(String) or nwkAddr(Number) proving.stringOrNumber(addr, 'addr should be a number or a string.'); return this._devbox.find(dev => typeof addr === "string" ? dev.getIeeeAddr() === addr : dev.getNwkAddr() === addr); }; _registerDev(dev, callback) { const devbox = this._devbox; let oldDev; if (!(dev instanceof Device)) throw new TypeError('dev should be an instance of Device class.'); oldDev = dev._getId() == null ? undefined : devbox.get(dev._getId()); return Q.fcall(() => { if (oldDev) { throw new Error('dev exists, unregister it first.'); } else if (dev._recovered) { return Q.ninvoke(devbox, 'set', dev._getId(), dev).then(id => { dev._recovered = false; delete dev._recovered; return id; }); } else { dev.update({ joinTime: Math.floor(Date.now()/1000) }); return Q.ninvoke(devbox, 'add', dev).then(id => { dev._setId(id); return id; }); } }).nodeify(callback); }; _unregisterDev(dev, callback) { return Q.ninvoke(this._devbox, 'remove', dev._getId()).nodeify(callback); }; _attachZclMethods(ep) { const self = this; if (ep.constructor.name === 'Zive') { const zApp = ep; zApp.foundation = function (dstAddr, dstEpId, cId, cmd, zclData, cfg, callback) { const dstEp = self.find(dstAddr, dstEpId); if (typeof cfg === 'function') { callback = cfg; cfg = {}; } if (!dstEp) return Q.reject(new Error('dstEp is not found.')).nodeify(callback); else return self._foundation(zApp._endpoint, dstEp, cId, cmd, zclData, cfg, callback); }; zApp.functional = function (dstAddr, dstEpId, cId, cmd, zclData, cfg, callback) { const dstEp = self.find(dstAddr, dstEpId); if (typeof cfg === 'function') { callback = cfg; cfg = {}; } if (!dstEp) return Q.reject(new Error('dstEp is not found.')).nodeify(callback); else return self._functional(zApp._endpoint, dstEp, cId, cmd, zclData, cfg, callback); }; } else if (ep instanceof Group) { ep.functional = function (cId, cmd, zclData, cfg, callback) { return self._functional(ep, ep, cId, cmd, zclData, cfg, callback); }; } else { ep.foundation = function (cId, cmd, zclData, cfg, callback) { return self._foundation(ep, ep, cId, cmd, zclData, cfg, callback); }; ep.functional = function (cId, cmd, zclData, cfg, callback) { return self._functional(ep, ep, cId, cmd, zclData, cfg, callback); }; ep.bind = function (cId, dstEpOrGrpId, callback) { return self.controller.bind(ep, cId, dstEpOrGrpId, callback); }; ep.unbind = function (cId, dstEpOrGrpId, callback) { return self.controller.unbind(ep, cId, dstEpOrGrpId, callback); }; ep.read = function (cId, attrId, callback) { const deferred = Q.defer(); let attr = self.zclId.attr(cId, attrId); attr = attr ? attr.value : attrId; self._foundation(ep, ep, cId, 'read', [{ attrId: attr }]).then(readStatusRecsRsp => { const rec = readStatusRecsRsp[0]; if (rec.status === 0) deferred.resolve(rec.attrData); else deferred.reject(new Error(`request unsuccess: ${rec.status}`)); }).catch(err => { deferred.reject(err); }); return deferred.promise.nodeify(callback); }; ep.write = function (cId, attrId, data, callback) { const deferred = Q.defer(); const attr = self.zclId.attr(cId, attrId); const attrType = self.zclId.attrType(cId, attrId).value; self._foundation(ep, ep, cId, 'write', [{ attrId: attr.value, dataType: attrType, attrData: data }]).then(writeStatusRecsRsp => { const rec = writeStatusRecsRsp[0]; if (rec.status === 0) deferred.resolve(data); else deferred.reject(new Error(`request unsuccess: ${rec.status}`)); }).catch(err => { deferred.reject(err); }); return deferred.promise.nodeify(callback); }; ep.report = function (cId, attrId, minInt, maxInt, repChange, callback) { const deferred = Q.defer(); const coord = self.controller._coord; const dlgEp = coord.getDelegator(ep.getProfId()); let cfgRpt = true; let cfgRptRec; let attrIdVal; if (arguments.length === 1) { cfgRpt = false; } else if (arguments.length === 2) { callback = attrId; cfgRpt = false; } else if (arguments.length === 5 && typeof repChange === "function") { callback = repChange; } if (cfgRpt) { attrIdVal = self.zclId.attr(cId, attrId); cfgRptRec = { direction : 0, attrId: attrIdVal ? attrIdVal.value : attrId, dataType : self.zclId.attrType(cId, attrId).value, minRepIntval : minInt, maxRepIntval : maxInt, repChange }; } Q.fcall(() => { if (dlgEp) { return ep.bind(cId, dlgEp).then(() => { if (cfgRpt) return ep.foundation(cId, 'configReport', [ cfgRptRec ]).then(rsp => { const status = rsp[0].status; if (status !== 0) deferred.reject(self.zclId.status(status).key); }); }); } else { return Q.reject(new Error(`Profile: ${ep.getProfId()} is not supported.`)); } }).then(() => { deferred.resolve(); }).fail(err => { deferred.reject(err); }).done(); return deferred.promise.nodeify(callback); }; } }; _foundation(srcEp, dstEp, cId, cmd, zclData, cfg, callback) { const self = this; if (typeof cfg === "function" && typeof callback !== "function") { callback = cfg; cfg = {}; } else { cfg = cfg || {}; } return this.af.zclFoundation(srcEp, dstEp, cId, cmd, zclData, cfg).then(msg => { let cmdString = self.zclId.foundation(cmd); cmdString = cmdString ? cmdString.key : cmd; if (cmdString === 'read') self._updateFinalizer(dstEp, cId, msg.payload); else if (cmdString === 'write' || cmdString === 'writeUndiv' || cmdString === 'writeNoRsp') self._updateFinalizer(dstEp, cId); return msg.payload; }).nodeify(callback); }; _functional(srcEp, dstEp, cId, cmd, zclData, cfg, callback) { const self = this; if (typeof cfg === "function" && typeof callback !== "function") { callback = cfg; cfg = {}; } else { cfg = cfg || {}; } return this.af.zclFunctional(srcEp, dstEp, cId, cmd, zclData, cfg).then(msg => { self._updateFinalizer(dstEp, cId); return msg.payload; }).nodeify(callback); }; _updateFinalizer(ep, cId, attrs, reported) { // Some eps don't have clusters, e.g. a Group if (!ep.getClusters) { return; } const self = this; let cIdString = self.zclId.cluster(cId); const clusters = ep.getClusters().dumpSync(); cIdString = cIdString ? cIdString.key : cId; Q.fcall(() => { if (attrs) { const newAttrs = {}; attrs.forEach(rec => { // { attrId, status, dataType, attrData } let attrIdString = self.zclId.attr(cId, rec.attrId); attrIdString = attrIdString ? attrIdString.key : rec.attrId; if (reported) newAttrs[attrIdString] = rec.attrData; else newAttrs[attrIdString] = (rec.status === 0) ? rec.attrData : null; }); return newAttrs; } else { return self.af.zclClusterAttrsReq(ep, cId); } }).then(newAttrs => { const oldAttrs = clusters[cIdString].attrs; const diff = zutils.objectDiff(oldAttrs, newAttrs); let trigger = false; Object.entries(diff).forEach(([attrId, val]) => { trigger = true; ep.getClusters().set(cIdString, 'attrs', attrId, val); }); if (trigger) self.emit('ind:changed', ep, { cid: cIdString, data: diff }); }).fail(() => {}).done(); }; } module.exports = ZShepherd;