UNPKG

zigbee-herdsman

Version:

An open source ZigBee gateway solution with node.js.

820 lines 38.6 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Driver = void 0; const ezsp_1 = require("./ezsp"); const types_1 = require("./types"); const events_1 = require("events"); const struct_1 = require("./types/struct"); const utils_1 = require("./utils"); const named_1 = require("./types/named"); const multicast_1 = require("./multicast"); const utils_2 = require("../../../utils"); const es6_1 = __importDefault(require("fast-deep-equal/es6")); const backup_1 = require("../adapter/backup"); const logger_1 = require("../../../utils/logger"); const NS = 'zh:ezsp:driv'; const IEEE_PREFIX_MFG_ID = [ { mfgId: 0x115F, prefix: [0x04, 0xcf, 0xfc] }, { mfgId: 0x115F, prefix: [0x54, 0xef, 0x44] }, ]; const DEFAULT_MFG_ID = 0x1049; // we make three attempts to send the request const REQUEST_ATTEMPT_DELAYS = [500, 1000, 1500]; class Driver extends events_1.EventEmitter { ezsp; nwkOpt; greenPowerGroup; networkParams; version; eui64ToNodeId = new Map(); eui64ToRelays = new Map(); ieee; multicast; waitress; transactionID = 1; serialOpt; backupMan; constructor(serialOpt, nwkOpt, greenPowerGroup, backupPath) { super(); this.nwkOpt = nwkOpt; this.serialOpt = serialOpt; this.greenPowerGroup = greenPowerGroup; this.waitress = new utils_2.Waitress(this.waitressValidator, this.waitressTimeoutFormatter); this.backupMan = new backup_1.EZSPAdapterBackup(this, backupPath); } /** * Requested by the EZSP watchdog after too many failures, or by UART layer after port closed unexpectedly. * Tries to stop the layers below and startup again. * @returns */ async reset() { logger_1.logger.debug(`Reset connection.`, NS); try { // don't emit 'close' on stop since we don't want this to bubble back up as 'disconnected' to the controller. await this.stop(false); } catch (err) { logger_1.logger.debug(`Stop error ${err.stack}`, NS); } try { await (0, utils_2.Wait)(1000); logger_1.logger.debug(`Startup again.`, NS); await this.startup(); } catch (err) { logger_1.logger.debug(`Reset error ${err.stack}`, NS); try { // here we let emit await this.stop(); } catch (stopErr) { logger_1.logger.debug(`Failed to stop after failed reset ${stopErr.stack}`, NS); } } } async onEzspReset() { logger_1.logger.debug('onEzspReset()', NS); await this.reset(); } onEzspClose() { logger_1.logger.debug('onEzspClose()', NS); this.emit('close'); } async stop(emitClose = true) { logger_1.logger.debug('Stopping driver', NS); if (this.ezsp) { return this.ezsp.close(emitClose); } } async startup() { let result = 'resumed'; this.transactionID = 1; this.ezsp = undefined; this.ezsp = new ezsp_1.Ezsp(); this.ezsp.on('close', this.onEzspClose.bind(this)); try { await this.ezsp.connect(this.serialOpt); } catch (error) { logger_1.logger.debug(`EZSP could not connect: ${error.cause ?? error}`, NS); throw error; } this.ezsp.on('reset', this.onEzspReset.bind(this)); await this.ezsp.version(); await this.ezsp.updateConfig(); await this.ezsp.updatePolicies(); //await this.ezsp.setValue(EzspValueId.VALUE_MAXIMUM_OUTGOING_TRANSFER_SIZE, 82); //await this.ezsp.setValue(EzspValueId.VALUE_MAXIMUM_INCOMING_TRANSFER_SIZE, 82); await this.ezsp.setValue(named_1.EzspValueId.VALUE_END_DEVICE_KEEP_ALIVE_SUPPORT_MODE, 3); await this.ezsp.setValue(named_1.EzspValueId.VALUE_CCA_THRESHOLD, 0); await this.ezsp.setSourceRouting(); //const count = await ezsp.getConfigurationValue(EzspConfigId.CONFIG_APS_UNICAST_MESSAGE_COUNT); //logger.info("APS_UNICAST_MESSAGE_COUNT is set to %s", count, NS); await this.addEndpoint({ inputClusters: [0x0000, 0x0003, 0x0006, 0x000A, 0x0019, 0x001A, 0x0300], outputClusters: [0x0000, 0x0003, 0x0004, 0x0005, 0x0006, 0x0008, 0x0020, 0x0300, 0x0400, 0x0402, 0x0405, 0x0406, 0x0500, 0x0B01, 0x0B03, 0x0B04, 0x0702, 0x1000, 0xFC01, 0xFC02] }); await this.addEndpoint({ endpoint: 242, profileId: 0xA1E0, deviceId: 0x61, outputClusters: [0x0021] }); // getting MFG_STRING token //const mfgName = await ezsp.execCommand('getMfgToken', EzspMfgTokenId.MFG_STRING); // getting MFG_BOARD_NAME token //const boardName = await ezsp.execCommand('getMfgToken', EzspMfgTokenId.MFG_BOARD_NAME); /* eslint-disable prefer-const */ let verInfo = await this.ezsp.getValue(named_1.EzspValueId.VALUE_VERSION_INFO); let build, major, minor, patch, special; [build, verInfo] = types_1.uint16_t.deserialize(types_1.uint16_t, verInfo); [major, verInfo] = types_1.uint8_t.deserialize(types_1.uint8_t, verInfo); [minor, verInfo] = types_1.uint8_t.deserialize(types_1.uint8_t, verInfo); [patch, verInfo] = types_1.uint8_t.deserialize(types_1.uint8_t, verInfo); [special, verInfo] = types_1.uint8_t.deserialize(types_1.uint8_t, verInfo); /* eslint-enable prefer-const */ const vers = `${major}.${minor}.${patch}.${special} build ${build}`; logger_1.logger.debug(`EmberZNet version: ${vers}`, NS); this.version = { product: this.ezsp.ezspV, majorrel: `${major}`, minorrel: `${minor}`, maintrel: `${patch} `, revision: vers }; if (await this.needsToBeInitialised(this.nwkOpt)) { // need to check the backup const restore = await this.needsToBeRestore(this.nwkOpt); const res = await this.ezsp.execCommand('networkState'); logger_1.logger.debug(`Network state ${res.status}`, NS); if (res.status == named_1.EmberNetworkStatus.JOINED_NETWORK) { logger_1.logger.info(`Leaving current network and forming new network`, NS); const st = await this.ezsp.leaveNetwork(); if (st != types_1.EmberStatus.NETWORK_DOWN) { logger_1.logger.error(`leaveNetwork returned unexpected status: ${st}`, NS); } } if (restore) { // restore logger_1.logger.info("Restore network from backup", NS); await this.formNetwork(true); result = 'restored'; } else { // reset logger_1.logger.info("Form network", NS); await this.formNetwork(false); result = 'reset'; } } const state = (await this.ezsp.execCommand('networkState')).status; logger_1.logger.debug(`Network state ${state}`, NS); const netParams = await this.ezsp.execCommand('getNetworkParameters'); if (netParams.status != types_1.EmberStatus.SUCCESS) { logger_1.logger.error(`Command (getNetworkParameters) returned unexpected state: ${netParams.status}`, NS); } this.networkParams = netParams.parameters; logger_1.logger.debug(`Node type: ${netParams.nodeType}, Network parameters: ${this.networkParams}`, NS); const nwk = (await this.ezsp.execCommand('getNodeId')).nodeId; const ieee = (await this.ezsp.execCommand('getEui64')).eui64; this.ieee = new named_1.EmberEUI64(ieee); logger_1.logger.debug('Network ready', NS); this.ezsp.on('frame', this.handleFrame.bind(this)); this.handleNodeJoined(nwk, this.ieee); logger_1.logger.debug(`EZSP nwk=${nwk}, IEEE=0x${this.ieee}`, NS); const linkResult = await this.getKey(named_1.EmberKeyType.TRUST_CENTER_LINK_KEY); logger_1.logger.debug(`TRUST_CENTER_LINK_KEY: ${JSON.stringify(linkResult)}`, NS); const netResult = await this.getKey(named_1.EmberKeyType.CURRENT_NETWORK_KEY); logger_1.logger.debug(`CURRENT_NETWORK_KEY: ${JSON.stringify(netResult)}`, NS); await (0, utils_2.Wait)(1000); await this.ezsp.execCommand('setManufacturerCode', { code: DEFAULT_MFG_ID }); this.multicast = new multicast_1.Multicast(this); await this.multicast.startup([]); await this.multicast.subscribe(this.greenPowerGroup, 242); // await this.multicast.subscribe(1, 901); return result; } async needsToBeInitialised(options) { let valid = true; valid = valid && (await this.ezsp.networkInit()); const netParams = await this.ezsp.execCommand('getNetworkParameters'); const networkParams = netParams.parameters; logger_1.logger.debug(`Current Node type: ${netParams.nodeType}, Network parameters: ${networkParams}`, NS); valid = valid && (netParams.status == types_1.EmberStatus.SUCCESS); valid = valid && (netParams.nodeType == types_1.EmberNodeType.COORDINATOR); valid = valid && (options.panID == networkParams.panId); valid = valid && (options.channelList.includes(networkParams.radioChannel)); valid = valid && ((0, es6_1.default)(options.extendedPanID, networkParams.extendedPanId)); return !valid; } async formNetwork(restore) { let backup; await this.ezsp.execCommand('clearTransientLinkKeys'); let initial_security_state; if (restore) { backup = await this.backupMan.getStoredBackup(); initial_security_state = (0, utils_1.ember_security)(backup.networkOptions.networkKey); initial_security_state.bitmask |= named_1.EmberInitialSecurityBitmask.NO_FRAME_COUNTER_RESET; initial_security_state.networkKeySequenceNumber = backup.networkKeyInfo.sequenceNumber; initial_security_state.preconfiguredKey.contents = backup.ezsp.hashed_tclk; } else { await this.ezsp.execCommand('clearKeyTable'); initial_security_state = (0, utils_1.ember_security)(Buffer.from(this.nwkOpt.networkKey)); } await this.ezsp.setInitialSecurityState(initial_security_state); const parameters = new struct_1.EmberNetworkParameters(); parameters.radioTxPower = 5; parameters.joinMethod = named_1.EmberJoinMethod.USE_MAC_ASSOCIATION; parameters.nwkManagerId = 0; parameters.nwkUpdateId = 0; parameters.channels = 0x07FFF800; // all channels if (restore) { parameters.panId = backup.networkOptions.panId; parameters.extendedPanId = backup.networkOptions.extendedPanId; parameters.radioChannel = backup.logicalChannel; parameters.nwkUpdateId = backup.networkUpdateId; } else { parameters.radioChannel = this.nwkOpt.channelList[0]; parameters.panId = this.nwkOpt.panID; parameters.extendedPanId = Buffer.from(this.nwkOpt.extendedPanID); } await this.ezsp.formNetwork(parameters); await this.ezsp.setValue(named_1.EzspValueId.VALUE_STACK_TOKEN_WRITING, 1); } handleFrame(frameName, frame) { switch (true) { case (frameName === 'incomingMessageHandler'): { const eui64 = this.eui64ToNodeId.get(frame.sender); const handled = this.waitress.resolve({ address: frame.sender, payload: frame.message, frame: frame.apsFrame }); if (!handled) { this.emit('incomingMessage', { messageType: frame.type, apsFrame: frame.apsFrame, lqi: frame.lastHopLqi, rssi: frame.lastHopRssi, sender: frame.sender, bindingIndex: frame.bindingIndex, addressIndex: frame.addressIndex, message: frame.message, senderEui64: eui64 }); } break; } case (frameName === 'trustCenterJoinHandler'): { if (frame.status === named_1.EmberDeviceUpdate.DEVICE_LEFT) { this.handleNodeLeft(frame.newNodeId, frame.newNodeEui64); } else { if (frame.policyDecision !== types_1.EmberJoinDecision.DENY_JOIN) { this.handleNodeJoined(frame.newNodeId, frame.newNodeEui64); } } break; } case (frameName === 'incomingRouteRecordHandler'): { this.handleRouteRecord(frame.source, frame.longId, frame.lastHopLqi, frame.lastHopRssi, frame.relay); break; } case (frameName === 'incomingRouteErrorHandler'): { this.handleRouteError(frame.status, frame.target); break; } case (frameName === 'incomingNetworkStatusHandler'): { this.handleNetworkStatus(frame.errorCode, frame.target); break; } case (frameName === 'messageSentHandler'): { // todo const status = frame.status; if (status != 0) { // send failure } else { // send success // If there was a message to the group and this group is not known, // then we will register the coordinator in this group // Applicable for IKEA remotes const msgType = frame.type; if (msgType == named_1.EmberOutgoingMessageType.OUTGOING_MULTICAST) { const apsFrame = frame.apsFrame; if (apsFrame.destinationEndpoint == 255) { // eslint-disable-next-line @typescript-eslint/no-floating-promises this.multicast.subscribe(apsFrame.groupId, 1); } } } break; } case (frameName === 'macFilterMatchMessageHandler'): { const [rawFrame, data] = struct_1.EmberIeeeRawFrame.deserialize(struct_1.EmberIeeeRawFrame, frame.message); logger_1.logger.debug(`macFilterMatchMessageHandler frame message: ${rawFrame}`, NS); this.emit('incomingMessage', { messageType: null, apsFrame: rawFrame, lqi: frame.lastHopLqi, rssi: frame.lastHopRssi, sender: null, bindingIndex: null, addressIndex: null, message: data, senderEui64: new named_1.EmberEUI64(rawFrame.sourceAddress) }); break; } case (frameName === 'stackStatusHandler'): { logger_1.logger.debug(`stackStatusHandler: ${types_1.EmberStatus.valueToName(types_1.EmberStatus, frame.status)}`, NS); break; } // case (frameName === 'childJoinHandler'): { // if (!frame.joining) { // this.handleNodeLeft(frame.childId, frame.childEui64); // } else { // this.handleNodeJoined(frame.childId, frame.childEui64); // } // break; // } case (frameName == 'gpepIncomingMessageHandler'): { this.handleGPMessage(frame); break; } default: // <=== Application frame 35 (childJoinHandler) received: 00013e9c2ebd08feff9ffd9004 +1ms // <=== Application frame 35 (childJoinHandler) parsed: 0,1,39998,144,253,159,255,254,8,189,46,4 +1ms // Unhandled frame childJoinHandler +2s // <=== Application frame 98 (incomingSenderEui64Handler) received: 2ebd08feff9ffd90 +2ms // <=== Application frame 98 (incomingSenderEui64Handler) parsed: 144,253,159,255,254,8,189,46 +1ms // Unhandled frame incomingSenderEui64Handler // <=== Application frame 155 (zigbeeKeyEstablishmentHandler) received: 2ebd08feff9ffd9006 +2ms // <=== Application frame 155 (zigbeeKeyEstablishmentHandler) parsed: 144,253,159,255,254,8,189,46,6 +2ms // Unhandled frame zigbeeKeyEstablishmentHandler logger_1.logger.debug(`Unhandled frame ${frameName}`, NS); } } handleRouteRecord(nwk, ieee, lqi, rssi, relays) { // todo logger_1.logger.debug(`handleRouteRecord: nwk=${nwk}, ieee=${ieee.toString()}, lqi=${lqi}, rssi=${rssi}, relays=${relays}`, NS); this.setNode(nwk, ieee); // if (ieee && !(ieee instanceof EmberEUI64)) { // ieee = new EmberEUI64(ieee); // } // this.eui64ToRelays.set(ieee.toString(), relays); } handleRouteError(status, nwk) { // todo logger_1.logger.debug(`handleRouteError: nwk=${nwk}, status=${status}`, NS); //this.waitress.reject({address: nwk, payload: null, frame: null}, 'Route error'); // const ieee = await this.networkIdToEUI64(nwk); // this.eui64ToRelays.set(ieee.toString(), null); } handleNetworkStatus(errorCode, nwk) { // todo // <== Frame: e19401c4000684c5 // <== 0xc4: { // "_cls_":"incomingNetworkStatusHandler", // "_id_":196, // "_isRequest_":false, // "errorCode":6, // "target":50564 // } // https://docs.silabs.com/d/zigbee-stack-api/7.4.0/message#ember-incoming-network-status-handler logger_1.logger.debug(`handleNetworkStatus: nwk=${nwk}, errorCode=${errorCode}`, NS); } handleNodeLeft(nwk, ieee) { if (ieee && !(ieee instanceof named_1.EmberEUI64)) { ieee = new named_1.EmberEUI64(ieee); } this.eui64ToNodeId.delete(ieee.toString()); this.emit('deviceLeft', [nwk, ieee]); } async resetMfgId(mfgId) { await this.ezsp.execCommand('setManufacturerCode', { code: mfgId }); // 60 sec for waiting await (0, utils_2.Wait)(60000); await this.ezsp.execCommand('setManufacturerCode', { code: DEFAULT_MFG_ID }); } handleNodeJoined(nwk, ieee) { if (ieee && !(ieee instanceof named_1.EmberEUI64)) { ieee = new named_1.EmberEUI64(ieee); } for (const rec of IEEE_PREFIX_MFG_ID) { if ((Buffer.from(ieee.value)).indexOf(Buffer.from(rec.prefix)) == 0) { // set ManufacturerCode logger_1.logger.debug(`handleNodeJoined: change ManufacturerCode for ieee ${ieee} to ${rec.mfgId}`, NS); // eslint-disable-next-line @typescript-eslint/no-floating-promises this.resetMfgId(rec.mfgId); break; } } this.eui64ToNodeId.set(ieee.toString(), nwk); this.emit('deviceJoined', [nwk, ieee]); } setNode(nwk, ieee) { if (ieee && !(ieee instanceof named_1.EmberEUI64)) { ieee = new named_1.EmberEUI64(ieee); } this.eui64ToNodeId.set(ieee.toString(), nwk); } async request(nwk, apsFrame, data, extendedTimeout = false) { let result = false; for (const delay of REQUEST_ATTEMPT_DELAYS) { try { const seq = (apsFrame.sequence + 1) & 0xFF; let eui64; if (typeof nwk !== 'number') { eui64 = nwk; const strEui64 = eui64.toString(); let nodeId = this.eui64ToNodeId.get(strEui64); if (nodeId === undefined) { nodeId = (await this.ezsp.execCommand('lookupNodeIdByEui64', { eui64: eui64 })).nodeId; if (nodeId && nodeId !== 0xFFFF) { this.eui64ToNodeId.set(strEui64, nodeId); } else { throw new Error('Unknown EUI64:' + strEui64); } } nwk = nodeId; } else { eui64 = await this.networkIdToEUI64(nwk); } if (this.ezsp.ezspV < 8) { // const route = this.eui64ToRelays.get(eui64.toString()); // if (route) { // const = await this.ezsp.execCommand('setSourceRoute', {eui64}); // // } } if (extendedTimeout) { await this.ezsp.execCommand('setExtendedTimeout', { remoteEui64: eui64, extendedTimeout: true }); } const sendResult = await this.ezsp.sendUnicast(named_1.EmberOutgoingMessageType.OUTGOING_DIRECT, nwk, apsFrame, seq, data); // repeat only for these statuses if ([types_1.EmberStatus.MAX_MESSAGE_LIMIT_REACHED, types_1.EmberStatus.NO_BUFFERS, types_1.EmberStatus.NETWORK_BUSY] .includes(sendResult.status)) { // need to repeat after pause logger_1.logger.error(`Request send status ${sendResult.status}. Attempt to repeat the request`, NS); await (0, utils_2.Wait)(delay); } else { result = (sendResult.status == types_1.EmberStatus.SUCCESS); break; } } catch (e) { logger_1.logger.debug(`Request error ${e}: ${e.stack}`, NS); break; } } return result; } /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ async mrequest(apsFrame, data, timeout = 30000) { try { const seq = (apsFrame.sequence + 1) & 0xFF; await this.ezsp.sendMulticast(apsFrame, seq, data); return true; } catch (e) { return false; } } /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ async rawrequest(rawFrame, data, timeout = 10000) { try { const msgData = Buffer.concat([struct_1.EmberRawFrame.serialize(struct_1.EmberRawFrame, rawFrame), data]); await this.ezsp.execCommand('sendRawMessage', { message: msgData }); return true; } catch (e) { logger_1.logger.debug(`Request error ${e}: ${e.stack}`, NS); return false; } } /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ async ieeerawrequest(rawFrame, data, timeout = 10000) { try { const msgData = Buffer.concat([struct_1.EmberIeeeRawFrame.serialize(struct_1.EmberIeeeRawFrame, rawFrame), data]); await this.ezsp.execCommand('sendRawMessage', { message: msgData }); return true; } catch (e) { logger_1.logger.debug(`Request error ${e}: ${e.stack}`, NS); return false; } } /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ async brequest(destination, apsFrame, data) { try { const seq = (apsFrame.sequence + 1) & 0xFF; await this.ezsp.sendBroadcast(destination, apsFrame, seq, data); return true; } catch (e) { return false; } } nextTransactionID() { this.transactionID = (this.transactionID + 1) & 0xFF; return this.transactionID; } makeApsFrame(clusterId, disableResponse) { const frame = new struct_1.EmberApsFrame(); frame.clusterId = clusterId; frame.profileId = 0; frame.sequence = this.nextTransactionID(); frame.sourceEndpoint = 0; frame.destinationEndpoint = 0; frame.groupId = 0; frame.options = (types_1.EmberApsOption.APS_OPTION_ENABLE_ROUTE_DISCOVERY || types_1.EmberApsOption.APS_OPTION_ENABLE_ADDRESS_DISCOVERY); if (!disableResponse) { frame.options ||= types_1.EmberApsOption.APS_OPTION_RETRY; } return frame; } makeEmberRawFrame() { const frame = new struct_1.EmberRawFrame(); frame.sequence = this.nextTransactionID(); return frame; } makeEmberIeeeRawFrame() { const frame = new struct_1.EmberIeeeRawFrame(); frame.sequence = this.nextTransactionID(); return frame; } async zdoRequest(networkAddress, requestCmd, responseCmd, params) { const requestName = types_1.EmberZDOCmd.valueName(types_1.EmberZDOCmd, requestCmd); const responseName = types_1.EmberZDOCmd.valueName(types_1.EmberZDOCmd, responseCmd); logger_1.logger.debug(`ZDO ${requestName} params: ${JSON.stringify(params)}`, NS); const frame = this.makeApsFrame(requestCmd, false); const payload = this.makeZDOframe(requestCmd, { transId: frame.sequence, ...params }); const waiter = this.waitFor(networkAddress, responseCmd, frame.sequence); try { const res = await this.request(networkAddress, frame, payload); if (!res) { throw Error('zdoRequest>request error'); } const response = await waiter.start().promise; logger_1.logger.debug(`${responseName} frame: ${JSON.stringify(response.payload)}`, NS); const result = new ezsp_1.EZSPZDOResponseFrameData(responseCmd, response.payload); logger_1.logger.debug(`${responseName} parsed: ${JSON.stringify(result)}`, NS); return result; } catch (e) { this.waitress.remove(waiter.ID); logger_1.logger.debug(`zdoRequest error: ${e} ${e.stack}`, NS); throw e; } } async networkIdToEUI64(nwk) { for (const [eUI64, value] of this.eui64ToNodeId) { if (value === nwk) return new named_1.EmberEUI64(eUI64); } const value = await this.ezsp.execCommand('lookupEui64ByNodeId', { nodeId: nwk }); if (value.status === types_1.EmberStatus.SUCCESS) { const eUI64 = new named_1.EmberEUI64(value.eui64); this.eui64ToNodeId.set(eUI64.toString(), nwk); return eUI64; } else { throw new Error('Unrecognized nodeId:' + nwk); } } async preJoining(seconds) { if (seconds) { const ieee = new named_1.EmberEUI64('0xFFFFFFFFFFFFFFFF'); const linkKey = new types_1.EmberKeyData(); linkKey.contents = Buffer.from("ZigBeeAlliance09"); const result = await this.addTransientLinkKey(ieee, linkKey); if (result.status !== types_1.EmberStatus.SUCCESS) { throw new Error(`Add Transient Link Key for '${ieee}' failed`); } if (this.ezsp.ezspV >= 8) { await this.ezsp.setPolicy(named_1.EzspPolicyId.TRUST_CENTER_POLICY, named_1.EzspDecisionBitmask.ALLOW_UNSECURED_REJOINS | named_1.EzspDecisionBitmask.ALLOW_JOINS); //| EzspDecisionBitmask.JOINS_USE_INSTALL_CODE_KEY } } else { await this.ezsp.execCommand('clearTransientLinkKeys'); } } async permitJoining(seconds) { return this.ezsp.execCommand('permitJoining', { duration: seconds }); } makeZDOframe(name, params) { return this.ezsp.makeZDOframe(name, params); } async addEndpoint({ endpoint = 1, profileId = 260, deviceId = 0xBEEF, appFlags = 0, inputClusters = [], outputClusters = [] }) { const res = await this.ezsp.execCommand('addEndpoint', { endpoint: endpoint, profileId: profileId, deviceId: deviceId, appFlags: appFlags, inputClusterCount: inputClusters.length, outputClusterCount: outputClusters.length, inputClusterList: inputClusters, outputClusterList: outputClusters, }); logger_1.logger.debug(`Ezsp adding endpoint: ${JSON.stringify(res)}`, NS); } waitFor(address, clusterId, sequence, timeout = 10000) { return this.waitress.waitFor({ address, clusterId, sequence }, timeout); } waitressTimeoutFormatter(matcher, timeout) { return `${JSON.stringify(matcher)} after ${timeout}ms`; } waitressValidator(payload, matcher) { return (!matcher.address || payload.address === matcher.address) && (!payload.frame || payload.frame.clusterId === matcher.clusterId) && (!payload.frame || payload.payload[0] === matcher.sequence); } setRadioPower(value) { return this.ezsp.execCommand('setRadioPower', { power: value }); } setChannel(channel) { return this.ezsp.execCommand('setLogicalAndRadioChannel', { radioChannel: channel }); } addTransientLinkKey(partner, transientKey) { if (this.ezsp.ezspV < 13) { return this.ezsp.execCommand('addTransientLinkKey', { partner, transientKey }); } else { return this.ezsp.execCommand('importTransientKey', { partner, transientKey, flags: 0 }); } } async addInstallCode(ieeeAddress, key) { // Key need to be converted to aes hash string const hc = new struct_1.EmberAesMmoHashContext(); hc.result = Buffer.from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); hc.length = 0; const hash = await this.ezsp.execCommand('aesMmoHash', { context: hc, finalize: true, data: key }); if (hash.status == types_1.EmberStatus.SUCCESS) { const ieee = new named_1.EmberEUI64(ieeeAddress); const linkKey = new types_1.EmberKeyData(); linkKey.contents = hash.returnContext.result; const result = await this.addTransientLinkKey(ieee, linkKey); if (result.status !== types_1.EmberStatus.SUCCESS) { throw new Error(`Add install code for '${ieeeAddress}' failed`); } } else { throw new Error(`Add install code for '${ieeeAddress}' failed`); } } handleGPMessage(frame) { // Commissioning if (frame.gpdCommandId == 0xE0) { let data = frame.payload.subarray(5); /* eslint-disable */ let st, deviceId, options, extOptions, key, mic, counter; [st, data] = types_1.uint8_t.deserialize(types_1.uint8_t, data); [deviceId, data] = types_1.uint8_t.deserialize(types_1.uint8_t, data); [options, data] = types_1.uint8_t.deserialize(types_1.uint8_t, data); [extOptions, data] = types_1.uint8_t.deserialize(types_1.uint8_t, data); [key, data] = types_1.EmberKeyData.deserialize(types_1.EmberKeyData, data); [mic, data] = types_1.uint32_t.deserialize(types_1.uint32_t, data); [counter, data] = types_1.uint32_t.deserialize(types_1.uint32_t, data); /* eslint-enable */ const gpdMessage = { messageType: frame.gpdCommandId, apsFrame: { profileId: 0xA1E0, sourceEndpoint: 242, clusterId: 0x0021, sequence: frame.sequenceNumber, }, lqi: frame.gpdLink, message: { commandID: frame.gpdCommandId, commandFrame: { options: options, securityKey: Buffer.from(key.contents), deviceID: deviceId, outgoingCounter: counter, }, srcID: frame.srcId, }, sender: frame.addr, }; this.emit('incomingMessage', gpdMessage); } else { const gpdMessage = { messageType: frame.gpdCommandId, apsFrame: { profileId: 0xA1E0, sourceEndpoint: 242, clusterId: 0x0021, sequence: frame.sequenceNumber, }, lqi: frame.gpdLink, message: { commandID: frame.gpdCommandId, frameCounter: frame.sequenceNumber, srcID: frame.srcId, }, sender: frame.addr, }; this.emit('incomingMessage', gpdMessage); } } async getKey(keyType) { if (this.ezsp.ezspV < 13) { return this.ezsp.execCommand('getKey', { keyType }); } else { // Mapping EmberKeyType to SecManKeyType (ezsp13) const SecManKeyType = { [named_1.EmberKeyType.TRUST_CENTER_LINK_KEY]: 2, [named_1.EmberKeyType.CURRENT_NETWORK_KEY]: 1, }; const smc = new struct_1.EmberSecurityManagerContext(); smc.type = SecManKeyType[keyType]; smc.index = 0; smc.derivedType = named_1.EmberDerivedKeyType.NONE; smc.eui64 = new named_1.EmberEUI64('0x0000000000000000'); smc.multiNetworkIndex = 0; smc.flags = 0; smc.psaKeyAlgPermission = 0; const keyInfo = await this.ezsp.execCommand('exportKey', { context: smc }); if (keyInfo.status !== named_1.SLStatus.SL_STATUS_OK) { logger_1.logger.error(`exportKey(${named_1.EmberKeyType.valueToName(named_1.EmberKeyType, keyType)}) ` + `returned unexpected SL status: ${keyInfo.status}`, NS); } return keyInfo; } } async getNetworkKeyInfo() { if (this.ezsp.ezspV < 13) { throw new Error(`getNetKeyInfo(): Invalid call on EZSP < 13.`); } else { const keyInfo = await this.ezsp.execCommand('getNetworkKeyInfo'); if (keyInfo.status !== named_1.SLStatus.SL_STATUS_OK) { logger_1.logger.error(`getNetworkKeyInfo() returned unexpected SL status: ${keyInfo.status}`, NS); } return keyInfo; } } async needsToBeRestore(options) { // if no backup and the settings have been changed, then need to start a new network const backup = await this.backupMan.getStoredBackup(); if (!backup) return false; let valid = true; //valid = valid && (await this.ezsp.networkInit()); const netParams = await this.ezsp.execCommand('getNetworkParameters'); const networkParams = netParams.parameters; logger_1.logger.debug(`Current Node type: ${netParams.nodeType}, Network parameters: ${networkParams}`, NS); logger_1.logger.debug(`Backuped network parameters: ${backup.networkOptions}`, NS); const networkKey = await this.getKey(named_1.EmberKeyType.CURRENT_NETWORK_KEY); let netKey = null; if (this.ezsp.ezspV < 13) { netKey = Buffer.from(networkKey.keyStruct.key.contents); } else { netKey = Buffer.from(networkKey.keyData.contents); } // if the settings in the backup match the chip, then need to warn to delete the backup file first valid = valid && (networkParams.panId == backup.networkOptions.panId); valid = valid && (networkParams.radioChannel == backup.logicalChannel); valid = valid && (Buffer.from(networkParams.extendedPanId).equals(backup.networkOptions.extendedPanId)); valid = valid && (Buffer.from(netKey).equals(backup.networkOptions.networkKey)); if (valid) { logger_1.logger.error(`Configuration is not consistent with adapter backup!`, NS); logger_1.logger.error(`- PAN ID: configured=${options.panID}, adapter=${networkParams.panId}, backup=${backup.networkOptions.panId}`, NS); logger_1.logger.error(`- Extended PAN ID: configured=${Buffer.from(options.extendedPanID).toString("hex")}, ` + `adapter=${Buffer.from(networkParams.extendedPanId).toString("hex")}, ` + `backup=${Buffer.from(networkParams.extendedPanId).toString("hex")}`, NS); logger_1.logger.error(`- Channel: configured=${options.channelList}, adapter=${networkParams.radioChannel}, ` + `backup=${backup.logicalChannel}`, NS); logger_1.logger.error(`- Network key: configured=${Buffer.from(options.networkKey).toString("hex")}, ` + `adapter=${Buffer.from(netKey).toString("hex")}, ` + `backup=${backup.networkOptions.networkKey.toString("hex")}`, NS); logger_1.logger.error(`Please update configuration to prevent further issues.`, NS); logger_1.logger.error(`If you wish to re-commission your network, please remove coordinator backup.`, NS); logger_1.logger.error(`Re-commissioning your network will require re-pairing of all devices!`, NS); throw new Error("startup failed - configuration-adapter mismatch - see logs above for more information"); } valid = true; // if the settings in the backup match the config, then the old network is in the chip and needs to be restored valid = valid && (options.panID == backup.networkOptions.panId); valid = valid && (options.channelList.includes(backup.logicalChannel)); valid = valid && (Buffer.from(options.extendedPanID).equals(backup.networkOptions.extendedPanId)); valid = valid && (Buffer.from(options.networkKey).equals(backup.networkOptions.networkKey)); return valid; } } exports.Driver = Driver; //# sourceMappingURL=driver.js.map