From d84725bc22abe15d5ea3215b3a961fd6d65d22ff Mon Sep 17 00:00:00 2001 From: Nerivec <62446222+Nerivec@users.noreply.github.com> Date: Sat, 21 Sep 2024 21:01:35 +0200 Subject: [PATCH] feat: Prepare to move specific ZDO requests out of Adapter (#1187) * Prepare to move specific ZDO requests out of Adapter * Fix prettier. * Fix zstack tests. * Fix ember tests. * More coverage. * Fix prettier. * Add tests for zstack ZDO request payload alteration logic. * Cleanup zstack ZDO response handling & more robust tests. * Extra zstack edge-case coverage. * Update zstack to use `sendZdo`. * zstack: Match AREQ/SREQ logic, cleanup and optimize. More coverage. * Better condition for IEEE vs nwk matching (avoid possible edge-cases). * deconz * ezsp * Fix prettier. * ezsp: fix not compatible with 8.x.x. * zboss * zboss: fix. * deconz: fix `deviceJoined` event * zigate * zigate: better zdo buffalo patch * zboss: fix ignore response status in driver (handled upstream) * zboss: fix non-standard ZDO response payloads * zboss: fix leftover * zboss: fix permit join. * zboss: fix INDICATION payload * zboss: fix ZDO_DEV_UPDATE_IND. * zstack: remove `networkAddress` event in favor of `zdoResponse`. * zboss: revert unsupported join changes. * Fix prettier. * Feedback. * ZStack: only discover route when node descriptor request fails --------- Co-authored-by: Koen Kanters --- src/adapter/adapter.ts | 25 + src/adapter/deconz/adapter/deconzAdapter.ts | 981 ++++++--------- src/adapter/deconz/driver/constants.ts | 119 +- src/adapter/deconz/driver/driver.ts | 158 +-- src/adapter/deconz/driver/frameParser.ts | 284 +++-- src/adapter/ember/adapter/emberAdapter.ts | 734 +++++------ src/adapter/ember/adapter/oneWaitress.ts | 45 +- src/adapter/ezsp/adapter/ezspAdapter.ts | 610 ++++++---- src/adapter/ezsp/driver/driver.ts | 129 +- src/adapter/ezsp/driver/ezsp.ts | 4 + src/adapter/z-stack/adapter/manager.ts | 4 +- src/adapter/z-stack/adapter/zStackAdapter.ts | 566 +++++---- src/adapter/z-stack/znp/definition.ts | 571 ++++----- src/adapter/z-stack/znp/tstype.ts | 14 +- src/adapter/z-stack/znp/utils.ts | 11 +- src/adapter/z-stack/znp/znp.ts | 65 +- src/adapter/z-stack/znp/zpiObject.ts | 70 +- src/adapter/zboss/adapter/zbossAdapter.ts | 534 +++++--- src/adapter/zboss/commands.ts | 454 +++---- src/adapter/zboss/driver.ts | 171 +-- src/adapter/zboss/enums.ts | 36 +- src/adapter/zboss/frame.ts | 83 +- src/adapter/zboss/uart.ts | 7 +- .../zigate/adapter/patchZdoBuffaloBE.ts | 47 + src/adapter/zigate/adapter/zigateAdapter.ts | 602 ++++----- src/adapter/zigate/driver/buffaloZiGate.ts | 21 +- src/adapter/zigate/driver/commandType.ts | 436 +++---- src/adapter/zigate/driver/constants.ts | 47 +- src/adapter/zigate/driver/messageType.ts | 84 +- src/adapter/zigate/driver/zigate.ts | 135 ++- src/controller/controller.ts | 33 + src/zspec/zdo/buffaloZdo.ts | 142 +-- src/zspec/zdo/definition/tstypes.ts | 174 +++ src/zspec/zdo/utils.ts | 9 +- test/adapter/ember/emberAdapter.test.ts | 327 ++--- test/adapter/z-stack/adapter.test.ts | 1071 +++++++++++------ test/adapter/z-stack/znp.test.ts | 247 +++- test/adapter/zboss/fixZdoResponse.test.ts | 178 +++ test/adapter/zigate/patchZdoBuffaloBE.test.ts | 86 ++ test/controller.test.ts | 139 ++- test/zspec/zdo/utils.test.ts | 10 +- 41 files changed, 5403 insertions(+), 4060 deletions(-) create mode 100644 src/adapter/zigate/adapter/patchZdoBuffaloBE.ts create mode 100644 test/adapter/zboss/fixZdoResponse.test.ts create mode 100644 test/adapter/zigate/patchZdoBuffaloBE.test.ts diff --git a/src/adapter/adapter.ts b/src/adapter/adapter.ts index 753127c025..d98797eb0e 100644 --- a/src/adapter/adapter.ts +++ b/src/adapter/adapter.ts @@ -6,6 +6,8 @@ import * as Models from '../models'; import {logger} from '../utils/logger'; import {BroadcastAddress} from '../zspec/enums'; import * as Zcl from '../zspec/zcl'; +import * as Zdo from '../zspec/zdo'; +import * as ZdoTypes from '../zspec/zdo/definition/tstypes'; import * as AdapterEvents from './events'; import * as TsType from './tstype'; @@ -14,6 +16,7 @@ const NS = 'zh:adapter'; interface AdapterEventMap { deviceJoined: [payload: AdapterEvents.DeviceJoinedPayload]; zclPayload: [payload: AdapterEvents.ZclPayload]; + zdoResponse: [clusterId: Zdo.ClusterId, response: ZdoTypes.GenericZdoResponse]; disconnected: []; deviceAnnounce: [payload: AdapterEvents.DeviceAnnouncePayload]; deviceLeave: [payload: AdapterEvents.DeviceLeavePayload]; @@ -219,6 +222,28 @@ abstract class Adapter extends events.EventEmitter { * ZDO */ + public abstract sendZdo( + ieeeAddress: string, + networkAddress: number, + clusterId: Zdo.ClusterId, + payload: Buffer, + disableResponse: true, + ): Promise; + public abstract sendZdo( + ieeeAddress: string, + networkAddress: number, + clusterId: K, + payload: Buffer, + disableResponse: false, + ): Promise; + public abstract sendZdo( + ieeeAddress: string, + networkAddress: number, + clusterId: K, + payload: Buffer, + disableResponse: boolean, + ): Promise; + public abstract permitJoin(seconds: number, networkAddress?: number): Promise; public abstract lqi(networkAddress: number): Promise; diff --git a/src/adapter/deconz/adapter/deconzAdapter.ts b/src/adapter/deconz/adapter/deconzAdapter.ts index 2bb2ffc98b..3f497d4967 100644 --- a/src/adapter/deconz/adapter/deconzAdapter.ts +++ b/src/adapter/deconz/adapter/deconzAdapter.ts @@ -5,10 +5,13 @@ import assert from 'assert'; import {ZSpec} from '../../..'; import Device from '../../../controller/model/device'; import * as Models from '../../../models'; -import {Queue, Waitress} from '../../../utils'; +import {Wait, Waitress} from '../../../utils'; import {logger} from '../../../utils/logger'; import {BroadcastAddress} from '../../../zspec/enums'; +import {EUI64} from '../../../zspec/tstypes'; import * as Zcl from '../../../zspec/zcl'; +import * as Zdo from '../../../zspec/zdo'; +import * as ZdoTypes from '../../../zspec/zdo/definition/tstypes'; import Adapter from '../../adapter'; import * as Events from '../../events'; import { @@ -46,20 +49,19 @@ interface WaitressMatcher { class DeconzAdapter extends Adapter { private driver: Driver; - private queue: Queue; private openRequestsQueue: WaitForDataRequest[]; private transactionID: number; private frameParserEvent = frameParserEvents; - private joinPermitted: boolean; private fwVersion?: CoordinatorVersion; private waitress: Waitress; private TX_OPTIONS = 0x00; // No APS ACKS + private joinPermitted: boolean = false; public constructor(networkOptions: NetworkOptions, serialPortOptions: SerialPortOptions, backupPath: string, adapterOptions: AdapterOptions) { super(networkOptions, serialPortOptions, backupPath, adapterOptions); this.hasZdoMessageOverhead = true; - const concurrent = this.adapterOptions && this.adapterOptions.concurrent ? this.adapterOptions.concurrent : 2; + // const concurrent = this.adapterOptions && this.adapterOptions.concurrent ? this.adapterOptions.concurrent : 2; // TODO: https://github.com/Koenkk/zigbee2mqtt/issues/4884#issuecomment-728903121 const delay = this.adapterOptions && typeof this.adapterOptions.delay === 'number' ? this.adapterOptions.delay : 0; @@ -76,10 +78,8 @@ class DeconzAdapter extends Adapter { this.driver.on('rxFrame', (frame) => { processFrame(frame); }); - this.queue = new Queue(concurrent); this.transactionID = 0; this.openRequestsQueue = []; - this.joinPermitted = false; this.fwVersion = undefined; this.frameParserEvent.on('receivedDataPayload', (data) => { @@ -185,7 +185,7 @@ class DeconzAdapter extends Adapter { try { await this.driver.writeParameterRequest(PARAM.PARAM.Network.CHANNEL_MASK, setChannelMask); - await this.sleep(500); + await Wait(500); changed = true; } catch (error) { logger.debug('Could not set channel: ' + error, NS); @@ -201,7 +201,7 @@ class DeconzAdapter extends Adapter { try { await this.driver.writeParameterRequest(PARAM.PARAM.Network.PAN_ID, this.networkOptions.panID); - await this.sleep(500); + await Wait(500); changed = true; } catch (error) { logger.debug('Could not set panid: ' + error, NS); @@ -221,7 +221,7 @@ class DeconzAdapter extends Adapter { try { await this.driver.writeParameterRequest(PARAM.PARAM.Network.APS_EXT_PAN_ID, this.networkOptions.extendedPanID!); - await this.sleep(500); + await Wait(500); changed = true; } catch (error) { logger.debug('Could not set extended panid: ' + error, NS); @@ -237,7 +237,7 @@ class DeconzAdapter extends Adapter { try { await this.driver.writeParameterRequest(PARAM.PARAM.Network.NETWORK_KEY, this.networkOptions.networkKey!); - await this.sleep(500); + await Wait(500); changed = true; } catch (error) { logger.debug('Could not set network key: ' + error, NS); @@ -246,9 +246,9 @@ class DeconzAdapter extends Adapter { if (changed) { await this.driver.changeNetworkStateRequest(PARAM.PARAM.Network.NET_OFFLINE); - await this.sleep(2000); + await Wait(2000); await this.driver.changeNetworkStateRequest(PARAM.PARAM.Network.NET_CONNECTED); - await this.sleep(2000); + await Wait(2000); } return 'resumed'; @@ -288,40 +288,34 @@ class DeconzAdapter extends Adapter { } public async permitJoin(seconds: number, networkAddress?: number): Promise { - const transactionID = this.nextTransactionID(); - const request: ApsDataRequest = {}; - const zdpFrame = [transactionID, seconds, 0]; // tc_significance 1 or 0 ? - - request.requestId = transactionID; - request.destAddrMode = PARAM.PARAM.addressMode.NWK_ADDR; - request.destAddr16 = networkAddress || 0xfffc; - request.destEndpoint = 0; - request.profileId = 0; - request.clusterId = 0x36; // permit join - request.srcEndpoint = 0; - request.asduLength = 3; - request.asduPayload = zdpFrame; - request.txOptions = 0; - request.radius = PARAM.PARAM.txRadius.DEFAULT_RADIUS; - request.timeout = 5; + const clusterId = Zdo.ClusterId.PERMIT_JOINING_REQUEST; - try { - await this.driver.enqueueSendDataRequest(request); - if (seconds === 0) { - this.joinPermitted = false; - } else { - this.joinPermitted = true; + if (networkAddress) { + // `authentication`: TC significance always 1 (zb specs) + const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, seconds, 1, []); + + const result = await this.sendZdo(ZSpec.BLANK_EUI64, networkAddress, clusterId, zdoPayload, false); + + /* istanbul ignore next */ + if (!Zdo.Buffalo.checkStatus(result)) { + // TODO: will disappear once moved upstream + throw new Zdo.StatusError(result[0]); } + } else { await this.driver.writeParameterRequest(PARAM.PARAM.Network.PERMIT_JOIN, seconds); - logger.debug('PERMIT_JOIN - ' + seconds + ' seconds', NS); - } catch (error) { - const msg = 'PERMIT_JOIN FAILED - ' + error; - logger.debug(msg, NS); - // try again - await this.permitJoin(seconds, networkAddress); - //return Promise.reject(new Error(msg)); // do not reject + logger.debug(`Permit joining on coordinator for ${seconds} sec.`, NS); + + // broadcast permit joining ZDO + if (networkAddress === undefined) { + // `authentication`: TC significance always 1 (zb specs) + const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, seconds, 1, []); + + await this.sendZdo(ZSpec.BLANK_EUI64, ZSpec.BroadcastAddress.DEFAULT, clusterId, zdoPayload, true); + } } + + this.joinPermitted = seconds !== 0; } public async getCoordinatorVersion(): Promise { @@ -361,396 +355,151 @@ class DeconzAdapter extends Adapter { } public async lqi(networkAddress: number): Promise { + const clusterId = Zdo.ClusterId.LQI_TABLE_REQUEST; const neighbors: LQINeighbor[] = []; - - const add = (list: Buffer[]): void => { - for (const entry of list) { - const relationByte = entry.readUInt8(18); - const extAddr: number[] = []; - for (let i = 8; i < 16; i++) { - extAddr.push(entry[i]); + const request = async (startIndex: number): Promise<[tableEntries: number, entryCount: number]> => { + const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, startIndex); + const result = await this.sendZdo(ZSpec.BLANK_EUI64, networkAddress, clusterId, zdoPayload, false); + + /* istanbul ignore else */ + if (Zdo.Buffalo.checkStatus(result)) { + const payload = result[1]; + + for (const entry of payload.entryList) { + neighbors.push({ + ieeeAddr: entry.eui64, + networkAddress: entry.nwkAddress, + linkquality: entry.lqi, + relationship: entry.relationship, + depth: entry.depth, + }); } - neighbors.push({ - linkquality: entry.readUInt8(21), - networkAddress: entry.readUInt16LE(16), - ieeeAddr: this.driver.macAddrArrayToString(extAddr), - relationship: (relationByte >> 1) & ((1 << 3) - 1), - depth: entry.readUInt8(20), - }); + return [payload.neighborTableEntries, payload.entryList.length]; + } else { + // TODO: will disappear once moved upstream + throw new Zdo.StatusError(result[0]); } }; - const request = async ( - startIndex: number, - ): Promise<{ - status: number; - tableEntrys: number; - startIndex: number; - tableListCount: number; - tableList: Buffer[]; - }> => { - const transactionID = this.nextTransactionID(); - const req: ApsDataRequest = {}; - req.requestId = transactionID; - req.destAddrMode = PARAM.PARAM.addressMode.NWK_ADDR; - req.destAddr16 = networkAddress; - req.destEndpoint = 0; - req.profileId = 0; - req.clusterId = 0x31; // mgmt_lqi_request - req.srcEndpoint = 0; - req.asduLength = 2; - req.asduPayload = [transactionID, startIndex]; - req.txOptions = 0; - req.radius = PARAM.PARAM.txRadius.DEFAULT_RADIUS; - - this.driver - .enqueueSendDataRequest(req) - .then(() => {}) - .catch(() => {}); - - try { - const d = await this.waitForData(networkAddress, 0, 0x8031); - const data = d.asduPayload!; - - if (data[1] !== 0) { - // status - throw new Error(`LQI for '${networkAddress}' failed`); - } - const tableList: Buffer[] = []; - const response = { - status: data[1], - tableEntrys: data[2], - startIndex: data[3], - tableListCount: data[4], - tableList: tableList, - }; - - let tableEntry: number[] = []; - let counter = 0; - for (let i = 5; i < response.tableListCount * 22 + 5; i++) { - // one tableentry = 22 bytes - tableEntry.push(data[i]); - counter++; - if (counter === 22) { - response.tableList.push(Buffer.from(tableEntry)); - tableEntry = []; - counter = 0; - } - } + let [tableEntries, entryCount] = await request(0); - logger.debug( - 'LQI RESPONSE - addr: 0x' + - networkAddress.toString(16) + - ' status: ' + - response.status + - ' read ' + - (response.tableListCount + response.startIndex) + - '/' + - response.tableEntrys + - ' entrys', - NS, - ); - return response; - } catch (error) { - const msg = 'LQI REQUEST FAILED - addr: 0x' + networkAddress.toString(16) + ' ' + error; - logger.debug(msg, NS); - return await Promise.reject(new Error(msg)); - } - }; + const size = tableEntries; + let nextStartIndex = entryCount; - let response = await request(0); - add(response.tableList); - let nextStartIndex = response.tableListCount; + while (neighbors.length < size) { + [tableEntries, entryCount] = await request(nextStartIndex); - while (neighbors.length < response.tableEntrys) { - response = await request(nextStartIndex); - add(response.tableList); - nextStartIndex += response.tableListCount; + nextStartIndex += entryCount; } return {neighbors}; } public async routingTable(networkAddress: number): Promise { + const clusterId = Zdo.ClusterId.ROUTING_TABLE_REQUEST; const table: RoutingTableEntry[] = []; - const statusLookup: {[n: number]: string} = { - 0: 'ACTIVE', - 1: 'DISCOVERY_UNDERWAY', - 2: 'DISCOVERY_FAILED', - 3: 'INACTIVE', - }; - const add = (list: Buffer[]): void => { - for (const entry of list) { - const statusByte = entry.readUInt8(2); - const extAddr: number[] = []; - for (let i = 8; i < 16; i++) { - extAddr.push(entry[i]); + const request = async (startIndex: number): Promise<[tableEntries: number, entryCount: number]> => { + const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, startIndex); + const result = await this.sendZdo(ZSpec.BLANK_EUI64, networkAddress, clusterId, zdoPayload, false); + + /* istanbul ignore else */ + if (Zdo.Buffalo.checkStatus(result)) { + const payload = result[1]; + + for (const entry of payload.entryList) { + table.push({ + destinationAddress: entry.destinationAddress, + status: entry.status, + nextHop: entry.nextHopAddress, + }); } - table.push({ - destinationAddress: entry.readUInt16LE(0), - status: statusLookup[(statusByte >> 5) & ((1 << 3) - 1)], - nextHop: entry.readUInt16LE(3), - }); + return [payload.routingTableEntries, payload.entryList.length]; + } else { + // TODO: will disappear once moved upstream + throw new Zdo.StatusError(result[0]); } }; - const request = async ( - startIndex: number, - ): Promise<{ - status: number; - tableEntrys: number; - startIndex: number; - tableListCount: number; - tableList: Buffer[]; - }> => { - const transactionID = this.nextTransactionID(); - const req: ApsDataRequest = {}; - req.requestId = transactionID; - req.destAddrMode = PARAM.PARAM.addressMode.NWK_ADDR; - req.destAddr16 = networkAddress; - req.destEndpoint = 0; - req.profileId = 0; - req.clusterId = 0x32; // mgmt_rtg_request - req.srcEndpoint = 0; - req.asduLength = 2; - req.asduPayload = [transactionID, startIndex]; - req.txOptions = 0; - req.radius = PARAM.PARAM.txRadius.DEFAULT_RADIUS; - req.timeout = 30; - - this.driver - .enqueueSendDataRequest(req) - .then(() => {}) - .catch(() => {}); - - try { - const d = await this.waitForData(networkAddress, 0, 0x8032); - const data = d.asduPayload!; + let [tableEntries, entryCount] = await request(0); - if (data[1] !== 0) { - // status - throw new Error(`Routingtables for '${networkAddress}' failed`); - } - const tableList: Buffer[] = []; - const response = { - status: data[1], - tableEntrys: data[2], - startIndex: data[3], - tableListCount: data[4], - tableList: tableList, - }; + const size = tableEntries; + let nextStartIndex = entryCount; - let tableEntry: number[] = []; - let counter = 0; - for (let i = 5; i < response.tableListCount * 5 + 5; i++) { - // one tableentry = 5 bytes - tableEntry.push(data[i]); - counter++; - if (counter === 5) { - response.tableList.push(Buffer.from(tableEntry)); - tableEntry = []; - counter = 0; - } - } + while (table.length < size) { + [tableEntries, entryCount] = await request(nextStartIndex); - logger.debug( - 'ROUTING_TABLE RESPONSE - addr: 0x' + - networkAddress.toString(16) + - ' status: ' + - response.status + - ' read ' + - (response.tableListCount + response.startIndex) + - '/' + - response.tableEntrys + - ' entrys', - NS, - ); - return response; - } catch (error) { - const msg = 'ROUTING_TABLE REQUEST FAILED - addr: 0x' + networkAddress.toString(16) + ' ' + error; - logger.debug(msg, NS); - return await Promise.reject(new Error(msg)); - } - }; - - let response = await request(0); - add(response.tableList); - let nextStartIndex = response.tableListCount; - - while (table.length < response.tableEntrys) { - response = await request(nextStartIndex); - add(response.tableList); - nextStartIndex += response.tableListCount; + nextStartIndex += entryCount; } return {table}; } public async nodeDescriptor(networkAddress: number): Promise { - const transactionID = this.nextTransactionID(); - const nwk1 = networkAddress & 0xff; - const nwk2 = (networkAddress >> 8) & 0xff; - const request: ApsDataRequest = {}; - const zdpFrame = [transactionID, nwk1, nwk2]; - - request.requestId = transactionID; - request.destAddrMode = PARAM.PARAM.addressMode.NWK_ADDR; - request.destAddr16 = networkAddress; - request.destEndpoint = 0; - request.profileId = 0; - request.clusterId = 0x02; // node descriptor - request.srcEndpoint = 0; - request.asduLength = 3; - request.asduPayload = zdpFrame; - request.txOptions = 0; - request.radius = PARAM.PARAM.txRadius.DEFAULT_RADIUS; - request.timeout = 30; - - this.driver - .enqueueSendDataRequest(request) - .then(() => {}) - .catch(() => {}); - - try { - const d = await this.waitForData(networkAddress, 0, 0x8002); - const data = d.asduPayload!; - - const buf = Buffer.from(data); - const logicaltype = data[4] & 7; - const type: DeviceType = logicaltype === 1 ? 'Router' : logicaltype === 2 ? 'EndDevice' : logicaltype === 0 ? 'Coordinator' : 'Unknown'; - const manufacturer = buf.readUInt16LE(7); + const clusterId = Zdo.ClusterId.NODE_DESCRIPTOR_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, networkAddress); + const result = await this.sendZdo(ZSpec.BLANK_EUI64, networkAddress, clusterId, zdoPayload, false); + + /* istanbul ignore else */ + if (Zdo.Buffalo.checkStatus(result)) { + const payload = result[1]; + let type: DeviceType = 'Unknown'; + + switch (payload.logicalType) { + case 0x0: + type = 'Coordinator'; + break; + case 0x1: + type = 'Router'; + break; + case 0x2: + type = 'EndDevice'; + break; + } - logger.debug( - 'RECEIVING NODE_DESCRIPTOR - addr: 0x' + - networkAddress.toString(16) + - ' type: ' + - type + - ' manufacturer: 0x' + - manufacturer.toString(16), - NS, - ); - return {manufacturerCode: manufacturer, type}; - } catch (error) { - const msg = 'RECEIVING NODE_DESCRIPTOR FAILED - addr: 0x' + networkAddress.toString(16) + ' ' + error; - logger.debug(msg, NS); - return await Promise.reject(new Error(msg)); + return {type, manufacturerCode: payload.manufacturerCode}; + } else { + // TODO: will disappear once moved upstream + throw new Zdo.StatusError(result[0]); } } public async activeEndpoints(networkAddress: number): Promise { - const transactionID = this.nextTransactionID(); - const nwk1 = networkAddress & 0xff; - const nwk2 = (networkAddress >> 8) & 0xff; - const request: ApsDataRequest = {}; - const zdpFrame = [transactionID, nwk1, nwk2]; - - request.requestId = transactionID; - request.destAddrMode = PARAM.PARAM.addressMode.NWK_ADDR; - request.destAddr16 = networkAddress; - request.destEndpoint = 0; - request.profileId = 0; - request.clusterId = 0x05; // active endpoints - request.srcEndpoint = 0; - request.asduLength = 3; - request.asduPayload = zdpFrame; - request.txOptions = 0; - request.radius = PARAM.PARAM.txRadius.DEFAULT_RADIUS; - request.timeout = 30; + const clusterId = Zdo.ClusterId.ACTIVE_ENDPOINTS_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, networkAddress); + const result = await this.sendZdo(ZSpec.BLANK_EUI64, networkAddress, clusterId, zdoPayload, false); - this.driver - .enqueueSendDataRequest(request) - .then(() => {}) - .catch(() => {}); + /* istanbul ignore else */ + if (Zdo.Buffalo.checkStatus(result)) { + const payload = result[1]; - try { - const d = await this.waitForData(networkAddress, 0, 0x8005); - const data = d.asduPayload; - - const buf = Buffer.from(data!); - const epCount = buf.readUInt8(4); - const epList = []; - for (let i = 5; i < epCount + 5; i++) { - epList.push(buf.readUInt8(i)); - } - logger.debug('ACTIVE_ENDPOINTS - addr: 0x' + networkAddress.toString(16) + ' EP list: ' + epList, NS); - return {endpoints: epList}; - } catch (error) { - const msg = 'READING ACTIVE_ENDPOINTS FAILED - addr: 0x' + networkAddress.toString(16) + ' ' + error; - logger.debug(msg, NS); - return await Promise.reject(new Error(msg)); + return {endpoints: payload.endpointList}; + } else { + // TODO: will disappear once moved upstream + throw new Zdo.StatusError(result[0]); } } public async simpleDescriptor(networkAddress: number, endpointID: number): Promise { - const transactionID = this.nextTransactionID(); - const nwk1 = networkAddress & 0xff; - const nwk2 = (networkAddress >> 8) & 0xff; - const request: ApsDataRequest = {}; - const zdpFrame = [transactionID, nwk1, nwk2, endpointID]; - - request.requestId = transactionID; - request.destAddrMode = PARAM.PARAM.addressMode.NWK_ADDR; - request.destAddr16 = networkAddress; - request.destEndpoint = 0; - request.profileId = 0; - request.clusterId = 0x04; // simple descriptor - request.srcEndpoint = 0; - request.asduLength = 4; - request.asduPayload = zdpFrame; - request.txOptions = 0; - request.radius = PARAM.PARAM.txRadius.DEFAULT_RADIUS; - request.timeout = 30; - - this.driver - .enqueueSendDataRequest(request) - .then(() => {}) - .catch(() => {}); + const clusterId = Zdo.ClusterId.SIMPLE_DESCRIPTOR_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, networkAddress, endpointID); + const result = await this.sendZdo(ZSpec.BLANK_EUI64, networkAddress, clusterId, zdoPayload, false); - try { - const d = await this.waitForData(networkAddress, 0, 0x8004); - const data = d.asduPayload!; - - const buf = Buffer.from(data); - const inCount = buf.readUInt8(11); - const inClusters = []; - let cIndex = 12; - for (let i = 0; i < inCount; i++) { - inClusters[i] = buf.readUInt16LE(cIndex); - cIndex += 2; - } - const outCount = buf.readUInt8(12 + inCount * 2); - const outClusters = []; - cIndex = 13 + inCount * 2; - for (let l = 0; l < outCount; l++) { - outClusters[l] = buf.readUInt16LE(cIndex); - cIndex += 2; - } + /* istanbul ignore else */ + if (Zdo.Buffalo.checkStatus(result)) { + const payload = result[1]; - const simpleDesc = { - profileID: buf.readUInt16LE(6), - endpointID: buf.readUInt8(5), - deviceID: buf.readUInt16LE(8), - inputClusters: inClusters, - outputClusters: outClusters, + return { + profileID: payload.profileId, + endpointID: payload.endpoint, + deviceID: payload.deviceId, + inputClusters: payload.inClusterList, + outputClusters: payload.outClusterList, }; - logger.debug( - 'RECEIVING SIMPLE_DESCRIPTOR - addr: 0x' + - networkAddress.toString(16) + - ' EP:' + - simpleDesc.endpointID + - ' inClusters: ' + - inClusters + - ' outClusters: ' + - outClusters, - NS, - ); - return simpleDesc; - } catch (error) { - const msg = 'RECEIVING SIMPLE_DESCRIPTOR FAILED - addr: 0x' + networkAddress.toString(16) + ' ' + error; - logger.debug(msg, NS); - return await Promise.reject(new Error(msg)); + } else { + // TODO: will disappear once moved upstream + throw new Zdo.StatusError(result[0]); } } @@ -844,6 +593,62 @@ class DeconzAdapter extends Adapter { return {promise: waiter.start().promise, cancel}; } + public async sendZdo( + ieeeAddress: string, + networkAddress: number, + clusterId: Zdo.ClusterId, + payload: Buffer, + disableResponse: true, + ): Promise; + public async sendZdo( + ieeeAddress: string, + networkAddress: number, + clusterId: K, + payload: Buffer, + disableResponse: false, + ): Promise; + public async sendZdo( + ieeeAddress: string, + networkAddress: number, + clusterId: K, + payload: Buffer, + disableResponse: boolean, + ): Promise { + const transactionID = this.nextTransactionID(); + payload[0] = transactionID; + const isNwkAddrRequest = clusterId === Zdo.ClusterId.NETWORK_ADDRESS_REQUEST; + const req: ApsDataRequest = { + requestId: transactionID, + destAddrMode: isNwkAddrRequest ? PARAM.PARAM.addressMode.IEEE_ADDR : PARAM.PARAM.addressMode.NWK_ADDR, + destAddr16: isNwkAddrRequest ? undefined : networkAddress, + destAddr64: isNwkAddrRequest ? ieeeAddress : undefined, + destEndpoint: Zdo.ZDO_ENDPOINT, + profileId: Zdo.ZDO_PROFILE_ID, + clusterId, + srcEndpoint: Zdo.ZDO_ENDPOINT, + asduLength: payload.length, + asduPayload: payload, + txOptions: 0, + radius: PARAM.PARAM.txRadius.DEFAULT_RADIUS, + timeout: 30, + }; + + this.driver + .enqueueSendDataRequest(req) + .then(() => {}) + .catch(() => {}); + + if (!disableResponse) { + const responseClusterId = Zdo.Utils.getResponseClusterId(clusterId); + + if (responseClusterId) { + const response = await this.waitForData(isNwkAddrRequest ? ieeeAddress : networkAddress, Zdo.ZDO_PROFILE_ID, responseClusterId); + + return response.zdo! as ZdoTypes.RequestToResponseMap[K]; + } + } + } + public async sendZclFrameToEndpoint( ieeeAddr: string, networkAddress: number, @@ -855,24 +660,21 @@ class DeconzAdapter extends Adapter { sourceEndpoint?: number, ): Promise { const transactionID = this.nextTransactionID(); - const request: ApsDataRequest = {}; - - const pay = zclFrame.toBuffer(); - //logger.info("zclFramte.toBuffer:", NS); - //logger.info(pay, NS); - - request.requestId = transactionID; - request.destAddrMode = PARAM.PARAM.addressMode.NWK_ADDR; - request.destAddr16 = networkAddress; - request.destEndpoint = endpoint; - request.profileId = sourceEndpoint === 242 && endpoint === 242 ? 0xa1e0 : 0x104; - request.clusterId = zclFrame.cluster.ID; - request.srcEndpoint = sourceEndpoint || 1; - request.asduLength = pay.length; - request.asduPayload = [...pay]; - request.txOptions = this.TX_OPTIONS; // 0x00 normal; 0x04 APS ACK - request.radius = PARAM.PARAM.txRadius.DEFAULT_RADIUS; - request.timeout = timeout; + const payload = zclFrame.toBuffer(); + const request: ApsDataRequest = { + requestId: transactionID, + destAddrMode: PARAM.PARAM.addressMode.NWK_ADDR, + destAddr16: networkAddress, + destEndpoint: endpoint, + profileId: sourceEndpoint === 242 && endpoint === 242 ? 0xa1e0 : 0x104, + clusterId: zclFrame.cluster.ID, + srcEndpoint: sourceEndpoint || 1, + asduLength: payload.length, + asduPayload: payload, + txOptions: this.TX_OPTIONS, // 0x00 normal, 0x04 APS ACK + radius: PARAM.PARAM.txRadius.DEFAULT_RADIUS, + timeout: timeout, + }; const command = zclFrame.command; @@ -905,23 +707,26 @@ class DeconzAdapter extends Adapter { try { let data = null; if ((command.response != undefined && !disableResponse) || !zclFrame.header.frameControl.disableDefaultResponse) { - data = await this.waitForData(networkAddress, 0x104, zclFrame.cluster.ID, zclFrame.header.transactionSequenceNumber, request.timeout); + data = await this.waitForData( + networkAddress, + ZSpec.HA_PROFILE_ID, + zclFrame.cluster.ID, + zclFrame.header.transactionSequenceNumber, + request.timeout, + ); } if (data !== null) { - const asdu = data.asduPayload!; - const buffer = Buffer.from(asdu); - const response: Events.ZclPayload = { address: data.srcAddr16 ?? `0x${data.srcAddr64!}`, - data: buffer, + data: data.asduPayload, clusterID: zclFrame.cluster.ID, - header: Zcl.Header.fromBuffer(buffer), - endpoint: data.srcEndpoint!, - linkquality: data.lqi!, + header: Zcl.Header.fromBuffer(data.asduPayload), + endpoint: data.srcEndpoint, + linkquality: data.lqi, groupID: data.srcAddrMode === 0x01 ? data.srcAddr16! : 0, wasBroadcast: data.srcAddrMode === 0x01 || data.srcAddrMode === 0xf, - destinationEndpoint: data.destEndpoint!, + destinationEndpoint: data.destEndpoint, }; logger.debug(`response received (${zclFrame.header.transactionSequenceNumber})`, NS); return response; @@ -935,24 +740,22 @@ class DeconzAdapter extends Adapter { public async sendZclFrameToGroup(groupID: number, zclFrame: Zcl.Frame): Promise { const transactionID = this.nextTransactionID(); - const request: ApsDataRequest = {}; - const pay = zclFrame.toBuffer(); - - logger.debug('zclFrame to group - zclFrame.payload:', NS); - logger.debug(zclFrame.payload, NS); - //logger.info("zclFramte.toBuffer:", NS); - //logger.info(pay, NS); - - request.requestId = transactionID; - request.destAddrMode = PARAM.PARAM.addressMode.GROUP_ADDR; - request.destAddr16 = groupID; - request.profileId = 0x104; - request.clusterId = zclFrame.cluster.ID; - request.srcEndpoint = 1; - request.asduLength = pay.length; - request.asduPayload = [...pay]; - request.txOptions = 0; - request.radius = PARAM.PARAM.txRadius.UNLIMITED; + const payload = zclFrame.toBuffer(); + + logger.debug(`zclFrame to group - ${groupID}`, NS); + + const request: ApsDataRequest = { + requestId: transactionID, + destAddrMode: PARAM.PARAM.addressMode.GROUP_ADDR, + destAddr16: groupID, + profileId: 0x104, + clusterId: zclFrame.cluster.ID, + srcEndpoint: 1, + asduLength: payload.length, + asduPayload: payload, + txOptions: 0, + radius: PARAM.PARAM.txRadius.UNLIMITED, + }; logger.debug(`sendZclFrameToGroup - message send`, NS); return await (this.driver.enqueueSendDataRequest(request) as Promise); @@ -960,23 +763,23 @@ class DeconzAdapter extends Adapter { public async sendZclFrameToAll(endpoint: number, zclFrame: Zcl.Frame, sourceEndpoint: number, destination: BroadcastAddress): Promise { const transactionID = this.nextTransactionID(); - const request: ApsDataRequest = {}; - const pay = zclFrame.toBuffer(); - - logger.debug('zclFrame to all - zclFrame.payload:', NS); - logger.debug(zclFrame.payload, NS); - - request.requestId = transactionID; - request.destAddrMode = PARAM.PARAM.addressMode.NWK_ADDR; - request.destAddr16 = destination; - request.destEndpoint = endpoint; - request.profileId = sourceEndpoint === 242 && endpoint === 242 ? 0xa1e0 : 0x104; - request.clusterId = zclFrame.cluster.ID; - request.srcEndpoint = sourceEndpoint; - request.asduLength = pay.length; - request.asduPayload = [...pay]; - request.txOptions = 0; - request.radius = PARAM.PARAM.txRadius.UNLIMITED; + const payload = zclFrame.toBuffer(); + + logger.debug(`zclFrame to all - ${endpoint}`, NS); + + const request: ApsDataRequest = { + requestId: transactionID, + destAddrMode: PARAM.PARAM.addressMode.NWK_ADDR, + destAddr16: destination, + destEndpoint: endpoint, + profileId: sourceEndpoint === 242 && endpoint === 242 ? 0xa1e0 : 0x104, + clusterId: zclFrame.cluster.ID, + srcEndpoint: sourceEndpoint, + asduLength: payload.length, + asduPayload: payload, + txOptions: 0, + radius: PARAM.PARAM.txRadius.UNLIMITED, + }; logger.debug(`sendZclFrameToAll - message send`, NS); return await (this.driver.enqueueSendDataRequest(request) as Promise); @@ -991,53 +794,24 @@ class DeconzAdapter extends Adapter { type: 'endpoint' | 'group', destinationEndpoint?: number, ): Promise { - const transactionID = this.nextTransactionID(); - const clid1 = clusterID & 0xff; - const clid2 = (clusterID >> 8) & 0xff; - const destAddrMode = type === 'group' ? PARAM.PARAM.addressMode.GROUP_ADDR : PARAM.PARAM.addressMode.IEEE_ADDR; - let destArray: number[]; - - if (type === 'endpoint') { - assert(destinationEndpoint, 'Destination endpoint must be defined when `type === endpoint`'); - destArray = this.driver.macAddrStringToArray(destinationAddressOrGroup as string); - destArray = destArray.concat([destinationEndpoint]); - } else { - destArray = [destinationAddressOrGroup as number & 0xff, ((destinationAddressOrGroup as number) >> 8) & 0xff]; - } - const request: ApsDataRequest = {}; - const zdpFrame = [transactionID] - .concat(this.driver.macAddrStringToArray(sourceIeeeAddress)) - .concat([sourceEndpoint, clid1, clid2, destAddrMode]) - .concat(destArray); - - request.requestId = transactionID; - request.destAddrMode = PARAM.PARAM.addressMode.NWK_ADDR; - request.destAddr16 = destinationNetworkAddress; - request.destEndpoint = 0; - request.profileId = 0; - request.clusterId = 0x21; // bind_request - request.srcEndpoint = 0; - request.asduLength = zdpFrame.length; - request.asduPayload = zdpFrame; - request.txOptions = 0x04; // 0x04 use APS ACKS - request.radius = PARAM.PARAM.txRadius.DEFAULT_RADIUS; - request.timeout = 30; - - this.driver - .enqueueSendDataRequest(request) - .then(() => {}) - .catch(() => {}); + const clusterId = Zdo.ClusterId.BIND_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest( + this.hasZdoMessageOverhead, + clusterId, + sourceIeeeAddress as EUI64, + sourceEndpoint, + clusterID, + type === 'group' ? Zdo.MULTICAST_BINDING : Zdo.UNICAST_BINDING, + destinationAddressOrGroup as EUI64, // not used with MULTICAST_BINDING + destinationAddressOrGroup as number, // not used with UNICAST_BINDING + destinationEndpoint ?? 0, // not used with MULTICAST_BINDING + ); + const result = await this.sendZdo(ZSpec.BLANK_EUI64, destinationNetworkAddress, clusterId, zdoPayload, false); - try { - const d = await this.waitForData(destinationNetworkAddress, 0, 0x8021); - const data = d.asduPayload!; - logger.debug('BIND RESPONSE - addr: 0x' + destinationNetworkAddress.toString(16) + ' status: ' + data[1], NS); - if (data[1] !== 0) { - throw new Error('status: ' + data[1]); - } - } catch (error) { - logger.debug('BIND FAILED - addr: 0x' + destinationNetworkAddress.toString(16) + ' ' + error, NS); - throw error; + /* istanbul ignore next */ + if (!Zdo.Buffalo.checkStatus(result)) { + // TODO: will disappear once moved upstream + throw new Zdo.StatusError(result[0]); } } @@ -1050,97 +824,36 @@ class DeconzAdapter extends Adapter { type: 'endpoint' | 'group', destinationEndpoint?: number, ): Promise { - const transactionID = this.nextTransactionID(); - const clid1 = clusterID & 0xff; - const clid2 = (clusterID >> 8) & 0xff; - const destAddrMode = type === 'group' ? PARAM.PARAM.addressMode.GROUP_ADDR : PARAM.PARAM.addressMode.IEEE_ADDR; - let destArray: number[]; - - if (type === 'endpoint') { - assert(destinationEndpoint, 'Destination endpoint must be defined when `type === endpoint`'); - destArray = this.driver.macAddrStringToArray(destinationAddressOrGroup as string); - destArray = destArray.concat([destinationEndpoint]); - } else { - destArray = [destinationAddressOrGroup as number & 0xff, ((destinationAddressOrGroup as number) >> 8) & 0xff]; - } - - const request: ApsDataRequest = {}; - const zdpFrame = [transactionID] - .concat(this.driver.macAddrStringToArray(sourceIeeeAddress)) - .concat([sourceEndpoint, clid1, clid2, destAddrMode]) - .concat(destArray); - - request.requestId = transactionID; - request.destAddrMode = PARAM.PARAM.addressMode.NWK_ADDR; - request.destAddr16 = destinationNetworkAddress; - request.destEndpoint = 0; - request.profileId = 0; - request.clusterId = 0x22; // unbind_request - request.srcEndpoint = 0; - request.asduLength = zdpFrame.length; - request.asduPayload = zdpFrame; - request.txOptions = 0x04; // 0x04 use APS ACKS - request.radius = PARAM.PARAM.txRadius.DEFAULT_RADIUS; - request.timeout = 30; - - this.driver - .enqueueSendDataRequest(request) - .then(() => {}) - .catch(() => {}); + const clusterId = Zdo.ClusterId.UNBIND_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest( + this.hasZdoMessageOverhead, + clusterId, + sourceIeeeAddress as EUI64, + sourceEndpoint, + clusterID, + type === 'group' ? Zdo.MULTICAST_BINDING : Zdo.UNICAST_BINDING, + destinationAddressOrGroup as EUI64, // not used with MULTICAST_BINDING + destinationAddressOrGroup as number, // not used with UNICAST_BINDING + destinationEndpoint ?? 0, // not used with MULTICAST_BINDING + ); + const result = await this.sendZdo(ZSpec.BLANK_EUI64, destinationNetworkAddress, clusterId, zdoPayload, false); - try { - const d = await this.waitForData(destinationNetworkAddress, 0, 0x8022); - const data = d.asduPayload!; - logger.debug('UNBIND RESPONSE - addr: 0x' + destinationNetworkAddress.toString(16) + ' status: ' + data[1], NS); - if (data[1] !== 0) { - throw new Error('status: ' + data[1]); - } - } catch (error) { - logger.debug('UNBIND FAILED - addr: 0x' + destinationNetworkAddress.toString(16) + ' ' + error, NS); - throw error; + /* istanbul ignore next */ + if (!Zdo.Buffalo.checkStatus(result)) { + // TODO: will disappear once moved upstream + throw new Zdo.StatusError(result[0]); } } public async removeDevice(networkAddress: number, ieeeAddr: string): Promise { - const transactionID = this.nextTransactionID(); - // const nwk1 = networkAddress & 0xff; - // const nwk2 = (networkAddress >> 8) & 0xff; - const request: ApsDataRequest = {}; - //const zdpFrame = [transactionID].concat(this.driver.macAddrStringToArray(ieeeAddr)).concat([0]); - const zdpFrame = [transactionID].concat([0, 0, 0, 0, 0, 0, 0, 0]).concat([0]); - - request.requestId = transactionID; - request.destAddrMode = PARAM.PARAM.addressMode.NWK_ADDR; - request.destAddr16 = networkAddress; - request.destEndpoint = 0; - request.profileId = 0; - request.clusterId = 0x34; // mgmt_leave_request - request.srcEndpoint = 0; - request.asduLength = 10; - request.asduPayload = zdpFrame; - request.txOptions = 0; - request.radius = PARAM.PARAM.txRadius.DEFAULT_RADIUS; - - this.driver - .enqueueSendDataRequest(request) - .then(() => {}) - .catch(() => {}); - - try { - const d = await this.waitForData(networkAddress, 0, 0x8034); - const data = d.asduPayload!; - logger.debug('REMOVE_DEVICE - addr: 0x' + networkAddress.toString(16) + ' status: ' + data[1], NS); - const payload: Events.DeviceLeavePayload = { - networkAddress: networkAddress, - ieeeAddr: ieeeAddr, - }; - if (data[1] !== 0) { - throw new Error('status: ' + data[1]); - } - this.emit('deviceLeave', payload); - } catch (error) { - logger.debug('REMOVE_DEVICE FAILED - addr: 0x' + networkAddress.toString(16) + ' ' + error, NS); - throw error; + const clusterId = Zdo.ClusterId.LEAVE_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, ieeeAddr as EUI64, Zdo.LeaveRequestFlags.WITHOUT_REJOIN); + const result = await this.sendZdo(ZSpec.BLANK_EUI64, networkAddress, clusterId, zdoPayload, false); + + /* istanbul ignore next */ + if (!Zdo.Buffalo.checkStatus(result)) { + // TODO: will disappear once moved upstream + throw new Zdo.StatusError(result[0]); } } @@ -1193,9 +906,12 @@ class DeconzAdapter extends Adapter { throw new Error('not supported'); } - // eslint-disable-next-line @typescript-eslint/no-unused-vars public async changeChannel(newChannel: number): Promise { - throw new Error(`Channel change is not supported for 'deconz'`); + const clusterId = Zdo.ClusterId.NWK_UPDATE_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, [newChannel], 0xfe, undefined, undefined, undefined); + + await this.sendZdo(ZSpec.BLANK_EUI64, ZSpec.BroadcastAddress.SLEEPY, clusterId, zdoPayload, true /* handled below */); + await Wait(12000); } // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -1211,12 +927,9 @@ class DeconzAdapter extends Adapter { /** * Private methods */ - private sleep(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); - } private waitForData( - addr: number, + addr: number | string, profileId: number, clusterId: number, transactionSequenceNumber?: number, @@ -1233,22 +946,22 @@ class DeconzAdapter extends Adapter { private checkReceivedGreenPowerIndication(ind: gpDataInd): void { const gpdHeader = Buffer.alloc(15); // applicationId === IEEE_ADDRESS ? 20 : 15 gpdHeader.writeUInt8(0b00000001, 0); // frameControl: FrameType.SPECIFIC + Direction.CLIENT_TO_SERVER + disableDefaultResponse=false - gpdHeader.writeUInt8(ind.seqNr!, 1); - gpdHeader.writeUInt8(ind.id!, 2); // commandIdentifier + gpdHeader.writeUInt8(ind.seqNr, 1); + gpdHeader.writeUInt8(ind.id, 2); // commandIdentifier gpdHeader.writeUInt16LE(0, 3); // options, only srcID present - gpdHeader.writeUInt32LE(ind.srcId!, 5); + gpdHeader.writeUInt32LE(ind.srcId, 5); // omitted: gpdIEEEAddr (ieeeAddr) // omitted: gpdEndpoint (uint8) - gpdHeader.writeUInt32LE(ind.frameCounter!, 9); - gpdHeader.writeUInt8(ind.commandId!, 13); - gpdHeader.writeUInt8(ind.commandFrameSize!, 14); + gpdHeader.writeUInt32LE(ind.frameCounter, 9); + gpdHeader.writeUInt8(ind.commandId, 13); + gpdHeader.writeUInt8(ind.commandFrameSize, 14); - const payBuf = Buffer.concat([gpdHeader, ind.commandFrame!]); + const payBuf = Buffer.concat([gpdHeader, ind.commandFrame]); const payload: Events.ZclPayload = { header: Zcl.Header.fromBuffer(payBuf), data: payBuf, clusterID: Zcl.Clusters.greenPower.ID, - address: ind.srcId! & 0xffff, + address: ind.srcId & 0xffff, endpoint: ZSpec.GP_ENDPOINT, linkquality: 0xff, // bogus groupID: ZSpec.GP_GROUP_ID, @@ -1261,11 +974,11 @@ class DeconzAdapter extends Adapter { } private checkReceivedDataPayload(resp: ReceivedDataResponse | null): void { - let srcAddr: number | undefined = undefined; + let srcAddr: number | undefined; + let srcEUI64: string | undefined; let header: Zcl.Header | undefined; - const payBuf = resp != null ? Buffer.from(resp.asduPayload!) : undefined; - if (resp != null) { + if (resp) { if (resp.srcAddr16 != null) { srcAddr = resp.srcAddr16; } else { @@ -1284,8 +997,24 @@ class DeconzAdapter extends Adapter { // so let's make sure they get the network address resp.srcAddr16 = srcAddr; // TODO: can't be undefined } - if (resp.profileId != 0x00) { - header = Zcl.Header.fromBuffer(payBuf!); // valid from check + + if (resp.profileId === Zdo.ZDO_PROFILE_ID) { + if (resp.clusterId === Zdo.ClusterId.NETWORK_ADDRESS_RESPONSE) { + if (Zdo.Buffalo.checkStatus(resp.zdo!)) { + srcEUI64 = resp.zdo![1].eui64; + } + } else if (resp.clusterId === Zdo.ClusterId.END_DEVICE_ANNOUNCE) { + // XXX: using same response for announce (handled by controller) or joined depending on permit join status? + if (this.joinPermitted === true && Zdo.Buffalo.checkStatus(resp.zdo!)) { + const payload = resp.zdo[1]; + + this.emit('deviceJoined', {networkAddress: payload.nwkAddress, ieeeAddr: payload.eui64}); + } + } + + this.emit('zdoResponse', resp.clusterId, resp.zdo!); + } else { + header = Zcl.Header.fromBuffer(resp.asduPayload); } } @@ -1294,18 +1023,18 @@ class DeconzAdapter extends Adapter { while (i--) { const req: WaitForDataRequest = this.openRequestsQueue[i]; - if (srcAddr != null && req.addr === srcAddr && req.clusterId === resp?.clusterId && req.profileId === resp?.profileId) { - if (header !== undefined && req.transactionSequenceNumber != undefined) { - if (req.transactionSequenceNumber === header.transactionSequenceNumber) { - logger.debug('resolve data request with transSeq Nr.: ' + req.transactionSequenceNumber, NS); - this.openRequestsQueue.splice(i, 1); - req.resolve?.(resp); - } - } else { - logger.debug('resolve data request without a transSeq Nr.', NS); - this.openRequestsQueue.splice(i, 1); - req.resolve?.(resp); - } + if ( + resp && + (req.addr === undefined || + (typeof req.addr === 'number' ? srcAddr !== undefined && req.addr === srcAddr : srcEUI64 && req.addr === srcEUI64)) && + req.clusterId === resp.clusterId && + req.profileId === resp.profileId && + (header === undefined || + req.transactionSequenceNumber === undefined || + req.transactionSequenceNumber === header.transactionSequenceNumber) + ) { + this.openRequestsQueue.splice(i, 1); + req.resolve(resp); } const now = Date.now(); @@ -1316,35 +1045,21 @@ class DeconzAdapter extends Adapter { //logger.debug("Timeout for request in openRequestsQueue addr: " + req.addr.toString(16) + " clusterId: " + req.clusterId.toString(16) + " profileId: " + req.profileId.toString(16), NS); //remove from busyQueue this.openRequestsQueue.splice(i, 1); - req.reject?.('waiting for response TIMEOUT'); - } - } - - // check unattended incomming messages - if (resp != null && resp.profileId === 0x00 && resp.clusterId === 0x13) { - // device Annce - const payload: Events.DeviceJoinedPayload = { - networkAddress: payBuf!.readUInt16LE(1), // valid from check - ieeeAddr: this.driver.macAddrArrayToString(resp.asduPayload!.slice(3, 11)), - }; - if (this.joinPermitted === true) { - this.emit('deviceJoined', payload); - } else { - this.emit('deviceAnnounce', payload); + req.reject(new Error('waiting for response TIMEOUT')); } } - if (resp != null && resp.profileId != 0x00) { + if (resp && resp.profileId != Zdo.ZDO_PROFILE_ID) { const payload: Events.ZclPayload = { - clusterID: resp.clusterId!, + clusterID: resp.clusterId, header, - data: payBuf!, // valid from check + data: resp.asduPayload, address: resp.destAddrMode === 0x03 ? `0x${resp.srcAddr64!}` : resp.srcAddr16!, - endpoint: resp.srcEndpoint!, - linkquality: resp.lqi!, + endpoint: resp.srcEndpoint, + linkquality: resp.lqi, groupID: resp.destAddrMode === 0x01 ? resp.destAddr16! : 0, wasBroadcast: resp.destAddrMode === 0x01 || resp.destAddrMode === 0xf, - destinationEndpoint: resp.destEndpoint!, + destinationEndpoint: resp.destEndpoint, }; this.waitress.resolve(payload); diff --git a/src/adapter/deconz/driver/constants.ts b/src/adapter/deconz/driver/constants.ts index 796babffdf..0d7d2c2b60 100644 --- a/src/adapter/deconz/driver/constants.ts +++ b/src/adapter/deconz/driver/constants.ts @@ -1,10 +1,8 @@ /* istanbul ignore file */ -/* eslint-disable */ -const PARAM: { - [s: string]: { - [s: string]: number; - }; -} = { + +import {GenericZdoResponse} from '../../../zspec/zdo/definition/tstypes'; + +const PARAM = { Network: { NET_OFFLINE: 0x00, NET_JOINING: 0x01, @@ -56,93 +54,94 @@ const PARAM: { }; interface Request { - commandId?: number; + commandId: number; networkState?: number; parameterId?: number; parameter?: parameterT; request?: ApsDataRequest; - seqNumber?: number; - resolve?: Function; - reject?: Function; + seqNumber: number; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + resolve: (value: any) => void; + reject: (value: Error) => void; ts?: number; } interface WaitForDataRequest { - addr?: number; - profileId?: number; - clusterId?: number; + addr: number | string; + profileId: number; + clusterId: number; transactionSequenceNumber?: number; - resolve?: Function; - reject?: Function; + resolve: (value: ReceivedDataResponse | PromiseLike) => void; + reject: (value: Error) => void; ts?: number; timeout?: number; } interface ReceivedDataResponse { - commandId?: number; - seqNr?: number; - status?: number; - frameLength?: number; - payloadLength?: number; - deviceState?: number; - destAddrMode?: number; + commandId: number; + seqNr: number; + status: number; + frameLength: number; + payloadLength: number; + deviceState: number; + destAddrMode: number; destAddr16?: number; destAddr64?: string; - destEndpoint?: number; - srcAddrMode?: number; + destEndpoint: number; + srcAddrMode: number; srcAddr16?: number; srcAddr64?: string; - srcEndpoint?: number; - profileId?: number; - clusterId?: number; - asduLength?: number; - asduPayload?: number[]; - lqi?: number; - rssi?: number; + srcEndpoint: number; + profileId: number; + clusterId: number; + asduLength: number; + asduPayload: Buffer; + lqi: number; + rssi: number; + zdo?: GenericZdoResponse; } interface gpDataInd { - rspId?: number; - seqNr?: number; - id?: number; - clusterId?: number; - options?: number; - srcId?: number; - frameCounter?: number; - commandId?: number; - commandFrameSize?: number; - commandFrame?: Buffer; + rspId: number; + seqNr: number; + id: number; + options: number; + srcId: number; + frameCounter: number; + commandId: number; + commandFrameSize: number; + commandFrame: Buffer; } interface DataStateResponse { - commandId?: number; - seqNr?: number; - status?: number; - frameLength?: number; - payloadLength?: number; - deviceState?: number; - requestId?: number; - destAddrMode?: number; + commandId: number; + seqNr: number; + status: number; + frameLength: number; + payloadLength: number; + deviceState: number; + requestId: number; + destAddrMode: number; destAddr16?: number; destAddr64?: string; destEndpoint?: number; - srcEndpoint?: number; - confirmStatus?: number; + srcEndpoint: number; + confirmStatus: number; } interface ApsDataRequest { - requestId?: number; - destAddrMode?: number; + requestId: number; + destAddrMode: number; destAddr16?: number; destAddr64?: string; //number[]; destEndpoint?: number; - profileId?: number; - clusterId?: number; - srcEndpoint?: number; - asduLength?: number; - asduPayload?: number[]; - txOptions?: number; - radius?: number; + profileId: number; + clusterId: number; + srcEndpoint: number; + asduLength: number; + asduPayload: Buffer; + txOptions: number; + radius: number; timeout?: number; // seconds } diff --git a/src/adapter/deconz/driver/driver.ts b/src/adapter/deconz/driver/driver.ts index 2d4a70839d..f5df769d7f 100644 --- a/src/adapter/deconz/driver/driver.ts +++ b/src/adapter/deconz/driver/driver.ts @@ -331,12 +331,10 @@ class Driver extends events.EventEmitter { private sendReadParameterRequest(parameterId: number, seqNumber: number) { /* command id, sequence number, 0, framelength(U16), payloadlength(U16), parameter id */ - const requestFrame = [PARAM.PARAM.FrameType.ReadParameter, seqNumber, 0x00, 0x08, 0x00, 0x01, 0x00, parameterId]; if (parameterId === PARAM.PARAM.Network.NETWORK_KEY) { - const requestFrame2 = [PARAM.PARAM.FrameType.ReadParameter, seqNumber, 0x00, 0x09, 0x00, 0x02, 0x00, parameterId, 0x00]; - this.sendRequest(requestFrame2); + this.sendRequest(Buffer.from([PARAM.PARAM.FrameType.ReadParameter, seqNumber, 0x00, 0x09, 0x00, 0x02, 0x00, parameterId, 0x00])); } else { - this.sendRequest(requestFrame); + this.sendRequest(Buffer.from([PARAM.PARAM.FrameType.ReadParameter, seqNumber, 0x00, 0x08, 0x00, 0x01, 0x00, parameterId])); } } @@ -361,13 +359,23 @@ class Driver extends events.EventEmitter { const pLength2 = payloadLength >> 8; if (parameterId === PARAM.PARAM.Network.NETWORK_KEY) { - const requestFrame2 = [PARAM.PARAM.FrameType.WriteParameter, seqNumber, 0x00, 0x19, 0x00, 0x12, 0x00, parameterId, 0x00].concat(value); - this.sendRequest(requestFrame2); + this.sendRequest( + Buffer.from([PARAM.PARAM.FrameType.WriteParameter, seqNumber, 0x00, 0x19, 0x00, 0x12, 0x00, parameterId, 0x00].concat(value)), + ); } else { - const requestframe = [PARAM.PARAM.FrameType.WriteParameter, seqNumber, 0x00, fLength1, fLength2, pLength1, pLength2, parameterId].concat( - this.parameterBuffer(value, parameterLength), + this.sendRequest( + Buffer.from([ + PARAM.PARAM.FrameType.WriteParameter, + seqNumber, + 0x00, + fLength1, + fLength2, + pLength1, + pLength2, + parameterId, + ...this.parameterBuffer(value, parameterLength), + ]), ); - this.sendRequest(requestframe); } } @@ -400,39 +408,32 @@ class Driver extends events.EventEmitter { } } - private parameterBuffer(parameter: parameterT, parameterLength: number): Array { - const paramArray = new Array(); - + private parameterBuffer(parameter: parameterT, parameterLength: number): Buffer { if (typeof parameter === 'number') { // for parameter <= 4 Byte if (parameterLength > 4) throw new Error('parameter to big for type number'); - for (let i = 0; i < parameterLength; i++) { - paramArray[i] = (parameter >> (8 * i)) & 0xff; - } + const buf = Buffer.alloc(parameterLength); + buf.writeUIntLE(parameter, 0, parameterLength); + + return buf; } else { - return parameter.reverse(); + return Buffer.from(parameter.reverse()); } - - return paramArray; } private sendReadFirmwareVersionRequest(seqNumber: number) { /* command id, sequence number, 0, framelength(U16) */ - const requestFrame = [PARAM.PARAM.FrameType.ReadFirmwareVersion, seqNumber, 0x00, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00]; - //logger.debug(requestFrame, NS); - this.sendRequest(requestFrame); + this.sendRequest(Buffer.from([PARAM.PARAM.FrameType.ReadFirmwareVersion, seqNumber, 0x00, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00])); } private sendReadDeviceStateRequest(seqNumber: number) { /* command id, sequence number, 0, framelength(U16) */ - const requestFrame = [PARAM.PARAM.FrameType.ReadDeviceState, seqNumber, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00]; - this.sendRequest(requestFrame); + this.sendRequest(Buffer.from([PARAM.PARAM.FrameType.ReadDeviceState, seqNumber, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00])); } - private sendRequest(buffer: number[]) { - const crc = this.calcCrc(Buffer.from(buffer)); - const frame = Buffer.from(buffer.concat([crc[0], crc[1]])); + private sendRequest(buffer: Buffer) { + const frame = Buffer.concat([buffer, this.calcCrc(buffer)]); const slipframe = slip.encode(frame); if (this.serialPort) { @@ -466,26 +467,26 @@ class Driver extends events.EventEmitter { switch (req.commandId) { case PARAM.PARAM.FrameType.ReadParameter: logger.debug(`send read parameter request from queue. seqNr: ${req.seqNumber} paramId: ${req.parameterId}`, NS); - this.sendReadParameterRequest(req.parameterId!, req.seqNumber!); + this.sendReadParameterRequest(req.parameterId!, req.seqNumber); break; case PARAM.PARAM.FrameType.WriteParameter: logger.debug( `send write parameter request from queue. seqNr: ${req.seqNumber} paramId: ${req.parameterId} param: ${req.parameter}`, NS, ); - this.sendWriteParameterRequest(req.parameterId!, req.parameter!, req.seqNumber!); + this.sendWriteParameterRequest(req.parameterId!, req.parameter!, req.seqNumber); break; case PARAM.PARAM.FrameType.ReadFirmwareVersion: logger.debug(`send read firmware version request from queue. seqNr: ${req.seqNumber}`, NS); - this.sendReadFirmwareVersionRequest(req.seqNumber!); + this.sendReadFirmwareVersionRequest(req.seqNumber); break; case PARAM.PARAM.FrameType.ReadDeviceState: logger.debug(`send read device state from queue. seqNr: ${req.seqNumber}`, NS); - this.sendReadDeviceStateRequest(req.seqNumber!); + this.sendReadDeviceStateRequest(req.seqNumber); break; case PARAM.PARAM.NetworkState.CHANGE_NETWORK_STATE: logger.debug(`send change network state request from queue. seqNr: ${req.seqNumber}`, NS); - this.sendChangeNetworkStateRequest(req.seqNumber!, req.networkState!); + this.sendChangeNetworkStateRequest(req.seqNumber, req.networkState!); break; default: throw new Error('process queue - unknown command id'); @@ -502,7 +503,7 @@ class Driver extends events.EventEmitter { const now = Date.now(); if (now - req.ts! > 10000) { - logger.debug(`Timeout for request - CMD: 0x${req.commandId!.toString(16)} seqNr: ${req.seqNumber}`, NS); + logger.debug(`Timeout for request - CMD: 0x${req.commandId.toString(16)} seqNr: ${req.seqNumber}`, NS); //remove from busyQueue busyQueue.splice(i, 1); this.timeoutCounter++; @@ -511,7 +512,7 @@ class Driver extends events.EventEmitter { clearTimeout(this.timeoutResetTimeout); this.timeoutResetTimeout = null; this.resetTimeoutCounterAfter1min(); - req.reject?.('TIMEOUT'); + req.reject(new Error('TIMEOUT')); if (this.timeoutCounter >= 2) { this.timeoutCounter = 0; logger.debug('too many timeouts - restart serial connecion', NS); @@ -539,8 +540,7 @@ class Driver extends events.EventEmitter { } private sendChangeNetworkStateRequest(seqNumber: number, networkState: number) { - const requestFrame = [PARAM.PARAM.NetworkState.CHANGE_NETWORK_STATE, seqNumber, 0x00, 0x06, 0x00, networkState]; - this.sendRequest(requestFrame); + this.sendRequest(Buffer.from([PARAM.PARAM.NetworkState.CHANGE_NETWORK_STATE, seqNumber, 0x00, 0x06, 0x00, networkState])); } private deviceStateRequest() { @@ -672,7 +672,7 @@ class Driver extends events.EventEmitter { enableRTS(); }, this.READY_TO_SEND_TIMEOUT); apsBusyQueue.push(req); - this.sendEnqueueSendDataRequest(req.request!, req.seqNumber!); + this.sendEnqueueSendDataRequest(req.request!, req.seqNumber); break; } default: @@ -697,17 +697,17 @@ class Driver extends events.EventEmitter { case PARAM.PARAM.APS.DATA_INDICATION: //logger.debug(`read received data request. seqNr: ${req.seqNumber}`, NS); if (this.DELAY === 0) { - this.sendReadReceivedDataRequest(req.seqNumber!); + this.sendReadReceivedDataRequest(req.seqNumber); } else { - await this.sendReadReceivedDataRequest(req.seqNumber!); + await this.sendReadReceivedDataRequest(req.seqNumber); } break; case PARAM.PARAM.APS.DATA_CONFIRM: //logger.debug(`query send data state request. seqNr: ${req.seqNumber}`, NS); if (this.DELAY === 0) { - this.sendQueryDataStateRequest(req.seqNumber!); + this.sendQueryDataStateRequest(req.seqNumber); } else { - await this.sendQueryDataStateRequest(req.seqNumber!); + await this.sendQueryDataStateRequest(req.seqNumber); } break; default: @@ -718,60 +718,70 @@ class Driver extends events.EventEmitter { private sendQueryDataStateRequest(seqNumber: number) { logger.debug(`DATA_CONFIRM - sending data state request - SeqNr. ${seqNumber}`, NS); - const requestFrame = [PARAM.PARAM.APS.DATA_CONFIRM, seqNumber, 0x00, 0x07, 0x00, 0x00, 0x00]; - this.sendRequest(requestFrame); + this.sendRequest(Buffer.from([PARAM.PARAM.APS.DATA_CONFIRM, seqNumber, 0x00, 0x07, 0x00, 0x00, 0x00])); } private sendReadReceivedDataRequest(seqNumber: number) { logger.debug(`DATA_INDICATION - sending read data request - SeqNr. ${seqNumber}`, NS); // payloadlength = 0, flag = none - const requestFrame = [PARAM.PARAM.APS.DATA_INDICATION, seqNumber, 0x00, 0x08, 0x00, 0x01, 0x00, 0x01]; - this.sendRequest(requestFrame); + this.sendRequest(Buffer.from([PARAM.PARAM.APS.DATA_INDICATION, seqNumber, 0x00, 0x08, 0x00, 0x01, 0x00, 0x01])); } private sendEnqueueSendDataRequest(request: ApsDataRequest, seqNumber: number) { - const payloadLength = 12 + (request.destAddrMode === 0x01 ? 2 : request.destAddrMode === 0x02 ? 3 : 9) + request.asduLength!; + const payloadLength = + 12 + + (request.destAddrMode === PARAM.PARAM.addressMode.GROUP_ADDR ? 2 : request.destAddrMode === PARAM.PARAM.addressMode.NWK_ADDR ? 3 : 9) + + request.asduLength; const frameLength = 7 + payloadLength; - const cid1 = request.clusterId! & 0xff; - const cid2 = (request.clusterId! >> 8) & 0xff; - const asdul1 = request.asduLength! & 0xff; - const asdul2 = (request.asduLength! >> 8) & 0xff; + const cid1 = request.clusterId & 0xff; + const cid2 = (request.clusterId >> 8) & 0xff; + const asdul1 = request.asduLength & 0xff; + const asdul2 = (request.asduLength >> 8) & 0xff; let destArray: Array = []; let dest = ''; - if (request.destAddr16 != null) { + + if (request.destAddr16 !== undefined) { destArray[0] = request.destAddr16 & 0xff; destArray[1] = (request.destAddr16 >> 8) & 0xff; dest = request.destAddr16.toString(16); } - if (request.destAddr64 != null) { + if (request.destAddr64 !== undefined) { dest = request.destAddr64; destArray = this.macAddrStringToArray(request.destAddr64); } - if (request.destEndpoint != null) { + if (request.destEndpoint !== undefined) { destArray.push(request.destEndpoint); dest += ' EP:'; dest += request.destEndpoint; } logger.debug(`DATA_REQUEST - destAddr: 0x${dest} SeqNr. ${seqNumber} request id: ${request.requestId}`, NS); - const requestFrame = [ - PARAM.PARAM.APS.DATA_REQUEST, - seqNumber, - 0x00, - frameLength & 0xff, - (frameLength >> 8) & 0xff, - payloadLength & 0xff, - (payloadLength >> 8) & 0xff, - request.requestId!, - 0x00, - request.destAddrMode!, - ] - .concat(destArray) - .concat([request.profileId! & 0xff, (request.profileId! >> 8) & 0xff, cid1, cid2, request.srcEndpoint!, asdul1, asdul2]) - .concat(request.asduPayload!) - .concat([request.txOptions!, request.radius!]); - - this.sendRequest(requestFrame); + + this.sendRequest( + Buffer.from([ + PARAM.PARAM.APS.DATA_REQUEST, + seqNumber, + 0x00, + frameLength & 0xff, + (frameLength >> 8) & 0xff, + payloadLength & 0xff, + (payloadLength >> 8) & 0xff, + request.requestId, + 0x00, + request.destAddrMode, + ...destArray, + request.profileId & 0xff, + (request.profileId >> 8) & 0xff, + cid1, + cid2, + request.srcEndpoint, + asdul1, + asdul2, + ...request.asduPayload, + request.txOptions, + request.radius, + ]), + ); } private processApsBusyQueue() { @@ -784,22 +794,22 @@ class Driver extends events.EventEmitter { timeout = req.request.timeout * 1000; // seconds * 1000 = milliseconds } if (now - req.ts! > timeout) { - logger.debug(`Timeout for aps request CMD: 0x${req.commandId!.toString(16)} seq: ${req.seqNumber}`, NS); + logger.debug(`Timeout for aps request CMD: 0x${req.commandId.toString(16)} seq: ${req.seqNumber}`, NS); //remove from busyQueue apsBusyQueue.splice(i, 1); - req.reject?.(new Error('APS TIMEOUT')); + req.reject(new Error('APS TIMEOUT')); } } } - private calcCrc(buffer: Uint8Array): Array { + private calcCrc(buffer: Uint8Array): Buffer { let crc = 0; for (let i = 0; i < buffer.length; i++) { crc += buffer[i]; } const crc0 = (~crc + 1) & 0xff; const crc1 = ((~crc + 1) >> 8) & 0xff; - return [crc0, crc1]; + return Buffer.from([crc0, crc1]); } public macAddrStringToArray(addr: string): Array { diff --git a/src/adapter/deconz/driver/frameParser.ts b/src/adapter/deconz/driver/frameParser.ts index 1d275ea2d7..ce9522653a 100644 --- a/src/adapter/deconz/driver/frameParser.ts +++ b/src/adapter/deconz/driver/frameParser.ts @@ -3,6 +3,7 @@ import {EventEmitter} from 'stream'; import {logger} from '../../../utils/logger'; +import * as Zdo from '../../../zspec/zdo'; import PARAM, { Command, DataStateResponse, @@ -137,59 +138,60 @@ function parseChangeNetworkStateResponse(view: DataView): number { return state; } -function parseQuerySendDataStateResponse(view: DataView): object | null { +function parseQuerySendDataStateResponse(view: DataView): DataStateResponse | null { try { - const response: DataStateResponse = {}; - - response.commandId = view.getUint8(0); - response.seqNr = view.getUint8(1); - response.status = view.getUint8(2); + const commandId = view.getUint8(0); + const seqNr = view.getUint8(1); + const status = view.getUint8(2); - if (response.status !== 0) { - if (response.status !== 5) { - logger.debug('DATA_CONFIRM RESPONSE - seqNr.: ' + response.seqNr + ' status: ' + response.status, NS); + if (status !== 0) { + if (status !== 5) { + logger.debug('DATA_CONFIRM RESPONSE - seqNr.: ' + seqNr + ' status: ' + status, NS); } return null; } - response.frameLength = 7; - response.payloadLength = view.getUint16(5, littleEndian); - response.deviceState = view.getUint8(7); - response.requestId = view.getUint8(8); - - response.destAddrMode = view.getUint8(9); + const frameLength = 7; + const payloadLength = view.getUint16(5, littleEndian); + const deviceState = view.getUint8(7); + const requestId = view.getUint8(8); + const destAddrMode = view.getUint8(9); + let destAddr64: string | undefined; + let destAddr16: number | undefined; + let destEndpoint: number | undefined; let destAddr = ''; - if (response.destAddrMode === 0x03) { + + if (destAddrMode === PARAM.PARAM.addressMode.IEEE_ADDR) { let res = view.getBigUint64(10, littleEndian).toString(16); while (res.length < 16) { res = '0' + res; } - response.destAddr64 = res; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const buf2 = view.buffer.slice(18, view.buffer.byteLength); - destAddr = response.destAddr64; + destAddr64 = res; + // const buf2 = view.buffer.slice(18, view.buffer.byteLength); + destAddr = destAddr64; } else { - response.destAddr16 = view.getUint16(10, littleEndian); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const buf2 = view.buffer.slice(12, view.buffer.byteLength); - destAddr = response.destAddr16.toString(16); + destAddr16 = view.getUint16(10, littleEndian); + // const buf2 = view.buffer.slice(12, view.buffer.byteLength); + destAddr = destAddr16.toString(16); } - if (response.destAddrMode === 0x02 || response.destAddrMode === 0x03) { - response.destEndpoint = view.getUint8(view.byteLength - 7); + + if (destAddrMode === PARAM.PARAM.addressMode.NWK_ADDR || destAddrMode === PARAM.PARAM.addressMode.IEEE_ADDR) { + destEndpoint = view.getUint8(view.byteLength - 7); } - response.srcEndpoint = view.getUint8(view.byteLength - 6); - response.confirmStatus = view.getInt8(view.byteLength - 5); + const srcEndpoint = view.getUint8(view.byteLength - 6); + const confirmStatus = view.getInt8(view.byteLength - 5); + + let newStatus = deviceState.toString(2); - let newStatus = response.deviceState.toString(2); for (let l = 0; l <= 8 - newStatus.length; l++) { newStatus = '0' + newStatus; } // resolve send data request promise - const i = apsBusyQueue.findIndex((r: Request) => r.request && r.request.requestId === response.requestId); + const i = apsBusyQueue.findIndex((r: Request) => r.request && r.request.requestId === requestId); if (i < 0) { return null; @@ -200,128 +202,163 @@ function parseQuerySendDataStateResponse(view: DataView): object | null { const req: Request = apsBusyQueue[i]; // TODO timeout (at driver.ts) - if (response.confirmStatus !== 0) { + if (confirmStatus !== 0) { // reject if status is not SUCCESS - //logger.debug("REJECT APS_REQUEST - request id: " + response.requestId + " confirm status: " + response.confirmStatus, NS); - req.reject?.(response.confirmStatus); + //logger.debug("REJECT APS_REQUEST - request id: " + requestId + " confirm status: " + confirmStatus, NS); + req.reject(new Error(`confirmStatus=${confirmStatus}`)); } else { - //logger.debug("RESOLVE APS_REQUEST - request id: " + response.requestId + " confirm status: " + response.confirmStatus, NS); - req.resolve?.(response.confirmStatus); + //logger.debug("RESOLVE APS_REQUEST - request id: " + requestId + " confirm status: " + confirmStatus, NS); + req.resolve(confirmStatus); } //remove from busyqueue apsBusyQueue.splice(i, 1); - logger.debug( - 'DATA_CONFIRM RESPONSE - destAddr: 0x' + destAddr + ' request id: ' + response.requestId + ' confirm status: ' + response.confirmStatus, - NS, - ); - frameParserEvents.emit('receivedDataNotification', response.deviceState); + logger.debug('DATA_CONFIRM RESPONSE - destAddr: 0x' + destAddr + ' request id: ' + requestId + ' confirm status: ' + confirmStatus, NS); + frameParserEvents.emit('receivedDataNotification', deviceState); - return response; + return { + commandId, + seqNr, + status, + frameLength, + payloadLength, + deviceState, + requestId, + destAddrMode, + destAddr16, + destAddr64, + destEndpoint, + srcEndpoint, + confirmStatus, + }; } catch (error) { logger.debug('DATA_CONFIRM RESPONSE - ' + error, NS); return null; } } -function parseReadReceivedDataResponse(view: DataView): object | null { +function parseReadReceivedDataResponse(view: DataView): ReceivedDataResponse | null { // min 28 bytelength try { - const response: ReceivedDataResponse = {}; let buf2, buf3; - response.commandId = view.getUint8(0); - response.seqNr = view.getUint8(1); - response.status = view.getUint8(2); + const commandId = view.getUint8(0); + const seqNr = view.getUint8(1); + const status = view.getUint8(2); - if (response.status != 0) { - if (response.status !== 5) { - logger.debug('DATA_INDICATION RESPONSE - seqNr.: ' + response.seqNr + ' status: ' + response.status, NS); + if (status != 0) { + if (status !== 5) { + logger.debug('DATA_INDICATION RESPONSE - seqNr.: ' + seqNr + ' status: ' + status, NS); } return null; } - response.frameLength = view.getUint16(3, littleEndian); - response.payloadLength = view.getUint16(5, littleEndian); - response.deviceState = view.getUint8(7); - response.destAddrMode = view.getUint8(8); + const frameLength = view.getUint16(3, littleEndian); + const payloadLength = view.getUint16(5, littleEndian); + const deviceState = view.getUint8(7); + const destAddrMode = view.getUint8(8); + let destAddr64: string | undefined; + let destAddr16: number | undefined; let destAddr = ''; - if (response.destAddrMode === 0x03) { + + if (destAddrMode === PARAM.PARAM.addressMode.IEEE_ADDR) { let res = view.getBigUint64(9, littleEndian).toString(16); while (res.length < 16) { res = '0' + res; } - response.destAddr64 = res; + destAddr64 = res; buf2 = view.buffer.slice(17, view.buffer.byteLength); - destAddr = response.destAddr64; + destAddr = destAddr64; } else { - response.destAddr16 = view.getUint16(9, littleEndian); + destAddr16 = view.getUint16(9, littleEndian); buf2 = view.buffer.slice(11, view.buffer.byteLength); - destAddr = response.destAddr16.toString(16); + destAddr = destAddr16.toString(16); } view = new DataView(buf2); - response.destEndpoint = view.getUint8(0); - response.srcAddrMode = view.getUint8(1); + const destEndpoint = view.getUint8(0); + const srcAddrMode = view.getUint8(1); + let srcAddr64: string | undefined; + let srcAddr16: number | undefined; let srcAddr = ''; - if (response.srcAddrMode === 0x02 || response.srcAddrMode === 0x04) { - response.srcAddr16 = view.getUint16(2, littleEndian); + + if (srcAddrMode === PARAM.PARAM.addressMode.NWK_ADDR || srcAddrMode === 0x04) { + srcAddr16 = view.getUint16(2, littleEndian); buf3 = view.buffer.slice(4, view.buffer.byteLength); - srcAddr = response.srcAddr16.toString(16); + srcAddr = srcAddr16.toString(16); } - if (response.srcAddrMode === 0x03 || response.srcAddrMode === 0x04) { + if (srcAddrMode === PARAM.PARAM.addressMode.IEEE_ADDR || srcAddrMode === 0x04) { let res = view.getBigUint64(2, littleEndian).toString(16); while (res.length < 16) { res = '0' + res; } - response.srcAddr64 = res; + srcAddr64 = res; buf3 = view.buffer.slice(10, view.buffer.byteLength); - srcAddr = response.srcAddr64; + srcAddr = srcAddr64; } view = new DataView(buf3!); // XXX: not validated? - response.srcEndpoint = view.getUint8(0); - response.profileId = view.getUint16(1, littleEndian); - response.clusterId = view.getUint16(3, littleEndian); - response.asduLength = view.getUint16(5, littleEndian); - - const payload = []; - let i = 0; - for (let u = 7; u < response.asduLength + 7; u++) { - payload[i] = view.getUint8(u); - i++; - } + const srcEndpoint = view.getUint8(0); + const profileId = view.getUint16(1, littleEndian); + const clusterId = view.getUint16(3, littleEndian); + const asduLength = view.getUint16(5, littleEndian); + const asduPayload = Buffer.from(view.buffer.slice(7, asduLength + 7)); + const lqi = view.getUint8(view.byteLength - 8); + const rssi = view.getInt8(view.byteLength - 3); - response.asduPayload = payload; - response.lqi = view.getUint8(view.byteLength - 8); - response.rssi = view.getInt8(view.byteLength - 3); + let newStatus = deviceState.toString(2); - let newStatus = response.deviceState.toString(2); for (let l = 0; l <= 8 - newStatus.length; l++) { newStatus = '0' + newStatus; } + logger.debug( 'DATA_INDICATION RESPONSE - seqNr. ' + - response.seqNr + + seqNr + ' srcAddr: 0x' + srcAddr + ' destAddr: 0x' + destAddr + ' profile id: 0x' + - response.profileId.toString(16) + + profileId.toString(16) + ' cluster id: 0x' + - response.clusterId.toString(16) + + clusterId.toString(16) + ' lqi: ' + - response.lqi, + lqi, NS, ); - logger.debug('response payload: ' + payload, NS); + logger.debug('response payload: ' + asduPayload.toString('hex'), NS); + frameParserEvents.emit('receivedDataNotification', deviceState); + + const response: ReceivedDataResponse = { + commandId, + seqNr, + status, + frameLength, + payloadLength, + deviceState, + destAddrMode, + destAddr16, + destAddr64, + destEndpoint, + srcAddrMode, + srcAddr16, + srcAddr64, + srcEndpoint, + profileId, + clusterId, + asduLength, + asduPayload, + lqi, + rssi, + zdo: profileId === Zdo.ZDO_PROFILE_ID ? Zdo.Buffalo.readResponse(true, clusterId, asduPayload) : undefined, + }; + frameParserEvents.emit('receivedDataPayload', response); - frameParserEvents.emit('receivedDataNotification', response.deviceState); return response; } catch (error) { logger.debug('DATA_INDICATION RESPONSE - ' + error, NS); @@ -366,51 +403,58 @@ function parseReceivedDataNotification(view: DataView): number | null { } } -function parseGreenPowerDataIndication(view: DataView): object | null { +function parseGreenPowerDataIndication(view: DataView): gpDataInd | null { try { - const ind: gpDataInd = {}; - ind.seqNr = view.getUint8(1); + let id, rspId, options, srcId, frameCounter, commandId, commandFrameSize, commandFrame; + const seqNr = view.getUint8(1); if (view.byteLength < 30) { logger.debug('GP data notification', NS); - ind.id = 0x00; // 0 = notification, 4 = commissioning - ind.rspId = 0x01; // 1 = pairing, 2 = commissioning - ind.options = 0; + id = 0x00; // 0 = notification, 4 = commissioning + rspId = 0x01; // 1 = pairing, 2 = commissioning + options = 0; view.getUint16(7, littleEndian); // frame ctrl field(7) ext.fcf(8) - ind.srcId = view.getUint32(9, littleEndian); - ind.frameCounter = view.getUint32(13, littleEndian); - ind.commandId = view.getUint8(17); - ind.commandFrameSize = view.byteLength - 18 - 6; // cut 18 from begin and 4 (sec mic) and 2 from end (cfc) - ind.commandFrame = Buffer.from(view.buffer.slice(18, ind.commandFrameSize + 18)); + srcId = view.getUint32(9, littleEndian); + frameCounter = view.getUint32(13, littleEndian); + commandId = view.getUint8(17); + commandFrameSize = view.byteLength - 18 - 6; // cut 18 from begin and 4 (sec mic) and 2 from end (cfc) + commandFrame = Buffer.from(view.buffer.slice(18, commandFrameSize + 18)); } else { logger.debug('GP commissioning notification', NS); - ind.id = 0x04; // 0 = notification, 4 = commissioning - ind.rspId = 0x01; // 1 = pairing, 2 = commissioning - ind.options = view.getUint16(14, littleEndian); // opt(14) ext.opt(15) - ind.srcId = view.getUint32(8, littleEndian); - ind.frameCounter = view.getUint32(36, littleEndian); - ind.commandId = view.getUint8(12); - ind.commandFrameSize = view.byteLength - 13 - 2; // cut 13 from begin and 2 from end (cfc) - ind.commandFrame = Buffer.from(view.buffer.slice(13, ind.commandFrameSize + 13)); + id = 0x04; // 0 = notification, 4 = commissioning + rspId = 0x01; // 1 = pairing, 2 = commissioning + options = view.getUint16(14, littleEndian); // opt(14) ext.opt(15) + srcId = view.getUint32(8, littleEndian); + frameCounter = view.getUint32(36, littleEndian); + commandId = view.getUint8(12); + commandFrameSize = view.byteLength - 13 - 2; // cut 13 from begin and 2 from end (cfc) + commandFrame = Buffer.from(view.buffer.slice(13, commandFrameSize + 13)); } - if ( - !( - lastReceivedGpInd.srcId === ind.srcId && - lastReceivedGpInd.commandId === ind.commandId && - lastReceivedGpInd.frameCounter === ind.frameCounter - ) - ) { - lastReceivedGpInd.srcId = ind.srcId; - lastReceivedGpInd.commandId = ind.commandId; - lastReceivedGpInd.frameCounter = ind.frameCounter; - //logger.debug(`GP_DATA_INDICATION - src id: ${ind.srcId} cmd id: ${ind.commandId} frameCounter: ${ind.frameCounter}`, NS); + const ind: gpDataInd = { + rspId, + seqNr, + id, + options, + srcId, + frameCounter, + commandId, + commandFrameSize, + commandFrame, + }; + + if (!(lastReceivedGpInd.srcId === srcId && lastReceivedGpInd.commandId === commandId && lastReceivedGpInd.frameCounter === frameCounter)) { + lastReceivedGpInd.srcId = srcId; + lastReceivedGpInd.commandId = commandId; + lastReceivedGpInd.frameCounter = frameCounter; + //logger.debug(`GP_DATA_INDICATION - src id: ${srcId} cmd id: ${commandId} frameCounter: ${frameCounter}`, NS); logger.debug( - `GP_DATA_INDICATION - src id: 0x${ind.srcId.toString(16)} cmd id: 0x${ind.commandId.toString(16)} frameCounter: 0x${ind.frameCounter.toString(16)}`, + `GP_DATA_INDICATION - src id: 0x${srcId.toString(16)} cmd id: 0x${commandId.toString(16)} frameCounter: 0x${frameCounter.toString(16)}`, NS, ); frameParserEvents.emit('receivedGreenPowerIndication', ind); } + return ind; } catch (error) { logger.debug('GREEN_POWER INDICATION - ' + error, NS); @@ -498,10 +542,10 @@ function processFrame(frame: Uint8Array): void { if (status !== 0) { // reject if status is not SUCCESS //logger.debug("REJECT REQUEST", NS); - req.reject?.({status}); + req.reject(new Error(`status=${status}`)); } else { //logger.debug("RESOLVE REQUEST", NS); - req.resolve?.(command); + req.resolve(command); } } diff --git a/src/adapter/ember/adapter/emberAdapter.ts b/src/adapter/ember/adapter/emberAdapter.ts index 33d32b450e..810a9e144d 100644 --- a/src/adapter/ember/adapter/emberAdapter.ts +++ b/src/adapter/ember/adapter/emberAdapter.ts @@ -13,7 +13,7 @@ import {EUI64, ExtendedPanId, NodeId, PanId} from '../../../zspec/tstypes'; import * as Zcl from '../../../zspec/zcl'; import * as Zdo from '../../../zspec/zdo'; import * as ZdoTypes from '../../../zspec/zdo/definition/tstypes'; -import {DeviceAnnouncePayload, DeviceJoinedPayload, DeviceLeavePayload, NetworkAddressPayload, ZclPayload} from '../../events'; +import {DeviceJoinedPayload, DeviceLeavePayload, ZclPayload} from '../../events'; import SerialPortUtils from '../../serialPortUtils'; import SocketPortUtils from '../../socketPortUtils'; import { @@ -449,6 +449,12 @@ export class EmberAdapter extends Adapter { ): Promise { switch (status) { case SLStatus.ZIGBEE_DELIVERY_FAILED: { + logger.debug( + () => + `~x~> DELIVERY_FAILED [indexOrDestination=${indexOrDestination} apsFrame=${JSON.stringify(apsFrame)} messageTag=${messageTag}]`, + NS, + ); + // no ACK was received from the destination switch (type) { case EmberOutgoingMessageType.BROADCAST: @@ -456,10 +462,7 @@ export class EmberAdapter extends Adapter { case EmberOutgoingMessageType.MULTICAST: case EmberOutgoingMessageType.MULTICAST_WITH_ALIAS: { // BC/MC not checking for message sent, avoid unnecessary waitress lookups - logger.error( - `Delivery of ${EmberOutgoingMessageType[type]} failed for '${indexOrDestination}' [apsFrame=${JSON.stringify(apsFrame)} messageTag=${messageTag}]`, - NS, - ); + logger.error(`Delivery of ${EmberOutgoingMessageType[type]} failed for '${indexOrDestination}'.`, NS); break; } default: { @@ -522,26 +525,22 @@ export class EmberAdapter extends Adapter { * @param messageContents The content of the response. */ private async onZDOResponse(apsFrame: EmberApsFrame, sender: NodeId, messageContents: Buffer): Promise { - const [status, payload] = Zdo.Buffalo.readResponse(this.hasZdoMessageOverhead, apsFrame.clusterId, messageContents); - - if (status === Zdo.Status.SUCCESS) { - logger.debug(() => `<~~~ [ZDO ${Zdo.ClusterId[apsFrame.clusterId]} from=${sender} ${payload ? JSON.stringify(payload) : 'OK'}]`, NS); - this.oneWaitress.resolveZDO(sender, apsFrame, payload); - - if (apsFrame.clusterId === Zdo.ClusterId.NETWORK_ADDRESS_RESPONSE) { - this.emit('networkAddress', { - networkAddress: (payload as ZdoTypes.NetworkAddressResponse).nwkAddress, - ieeeAddr: (payload as ZdoTypes.NetworkAddressResponse).eui64, - } as NetworkAddressPayload); - } else if (apsFrame.clusterId === Zdo.ClusterId.END_DEVICE_ANNOUNCE) { - this.emit('deviceAnnounce', { - networkAddress: (payload as ZdoTypes.EndDeviceAnnounce).nwkAddress, - ieeeAddr: (payload as ZdoTypes.EndDeviceAnnounce).eui64, - } as DeviceAnnouncePayload); + const result = Zdo.Buffalo.readResponse(this.hasZdoMessageOverhead, apsFrame.clusterId, messageContents); + + logger.debug(() => `<~~~ [ZDO ${Zdo.ClusterId[apsFrame.clusterId]} from=${sender} ${result[1] ? JSON.stringify(result[1]) : 'OK'}]`, NS); + + if (apsFrame.clusterId === Zdo.ClusterId.NETWORK_ADDRESS_RESPONSE) { + // special case to properly resolve a NETWORK_ADDRESS_RESPONSE following a NETWORK_ADDRESS_REQUEST (based on EUI64 from ZDO payload) + // NOTE: if response has invalid status (no EUI64 available), response waiter will eventually time out + /* istanbul ignore else */ + if (Zdo.Buffalo.checkStatus(result)) { + this.oneWaitress.resolveZDO(result[1].eui64, apsFrame, result); } } else { - this.oneWaitress.resolveZDO(sender, apsFrame, new Zdo.StatusError(status)); + this.oneWaitress.resolveZDO(sender, apsFrame, result); } + + this.emit('zdoResponse', apsFrame.clusterId, result); } /** @@ -1556,77 +1555,6 @@ export class EmberAdapter extends Adapter { return (this.zdoRequestSequence = ++this.zdoRequestSequence & APPLICATION_ZDO_SEQUENCE_MASK); } - /** - * ZDO - * - * @param destination - * @param clusterId uint16_t - * @param messageContents Content of the ZDO request (sequence to be assigned at index zero) - * @param options - * @returns status Indicates success or failure (with reason) of send - * @returns apsFrame The APS Frame resulting of the request being built and sent (`sequence` set from stack-given value). - * @returns messageTag The tag passed to ezspSend${x} function. - */ - private async sendZDORequest( - destination: NodeId, - clusterId: number, - messageContents: Buffer, - options: EmberApsOption, - ): Promise<[SLStatus, apsFrame: EmberApsFrame, messageTag: number]> { - const messageTag = this.nextZDORequestSequence(); - messageContents[0] = messageTag; - - const apsFrame: EmberApsFrame = { - profileId: Zdo.ZDO_PROFILE_ID, - clusterId, - sourceEndpoint: Zdo.ZDO_ENDPOINT, - destinationEndpoint: Zdo.ZDO_ENDPOINT, - options, - groupId: 0, - sequence: 0, // set by stack - }; - - if ( - destination === ZSpec.BroadcastAddress.DEFAULT || - destination === ZSpec.BroadcastAddress.RX_ON_WHEN_IDLE || - destination === ZSpec.BroadcastAddress.SLEEPY - ) { - logger.debug( - `~~~> [ZDO ${Zdo.ClusterId[clusterId]} BROADCAST to=${destination} messageTag=${messageTag} messageContents=${messageContents.toString('hex')}]`, - NS, - ); - const [status, apsSequence] = await this.ezsp.ezspSendBroadcast( - ZSpec.NULL_NODE_ID, // alias - destination, - 0, // nwkSequence - apsFrame, - ZDO_REQUEST_RADIUS, - messageTag, - messageContents, - ); - apsFrame.sequence = apsSequence; - - logger.debug(`~~~> [SENT ZDO type=BROADCAST apsSequence=${apsSequence} messageTag=${messageTag} status=${SLStatus[status]}`, NS); - return [status, apsFrame, messageTag]; - } else { - logger.debug( - `~~~> [ZDO ${Zdo.ClusterId[clusterId]} UNICAST to=${destination} messageTag=${messageTag} messageContents=${messageContents.toString('hex')}]`, - NS, - ); - const [status, apsSequence] = await this.ezsp.ezspSendUnicast( - EmberOutgoingMessageType.DIRECT, - destination, - apsFrame, - messageTag, - messageContents, - ); - apsFrame.sequence = apsSequence; - - logger.debug(`~~~> [SENT ZDO type=DIRECT apsSequence=${apsSequence} messageTag=${messageTag} status=${SLStatus[status]}`, NS); - return [status, apsFrame, messageTag]; - } - } - //---- END Ember ZDO //-- START Adapter implementation @@ -1813,35 +1741,15 @@ export class EmberAdapter extends Adapter { // queued public async changeChannel(newChannel: number): Promise { - return await this.queue.execute(async () => { - this.checkInterpanLock(); - - const zdoPayload = Zdo.Buffalo.buildRequest( - this.hasZdoMessageOverhead, - Zdo.ClusterId.NWK_UPDATE_REQUEST, - [newChannel], - 0xfe, - undefined, - undefined, - undefined, - ); - const [status] = await this.sendZDORequest( - ZSpec.BroadcastAddress.SLEEPY, - Zdo.ClusterId.NWK_UPDATE_REQUEST, - zdoPayload, - DEFAULT_APS_OPTIONS, - ); + const clusterId = Zdo.ClusterId.NWK_UPDATE_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, [newChannel], 0xfe, undefined, undefined, undefined); - if (status !== SLStatus.OK) { - throw new Error(`[ZDO] Failed broadcast channel change to '${newChannel}' with status=${SLStatus[status]}.`); - } - - await this.oneWaitress.startWaitingForEvent( - {eventName: OneWaitressEvents.STACK_STATUS_CHANNEL_CHANGED}, - DEFAULT_NETWORK_REQUEST_TIMEOUT * 2, // observed to ~9sec - '[ZDO] Change Channel', - ); - }); + await this.sendZdo(ZSpec.BLANK_EUI64, ZSpec.BroadcastAddress.SLEEPY, clusterId, zdoPayload, true /* handled below */); + await this.oneWaitress.startWaitingForEvent( + {eventName: OneWaitressEvents.STACK_STATUS_CHANNEL_CHANGED}, + DEFAULT_NETWORK_REQUEST_TIMEOUT * 2, // observed to ~9sec + '[ZDO] Change Channel', + ); } // queued @@ -1941,8 +1849,117 @@ export class EmberAdapter extends Adapter { //---- ZDO + // queued, non-InterPAN + public async sendZdo( + ieeeAddress: string, + networkAddress: number, + clusterId: Zdo.ClusterId, + payload: Buffer, + disableResponse: true, + ): Promise; + public async sendZdo( + ieeeAddress: string, + networkAddress: number, + clusterId: K, + payload: Buffer, + disableResponse: false, + ): Promise; + public async sendZdo( + ieeeAddress: string, + networkAddress: number, + clusterId: K, + payload: Buffer, + disableResponse: boolean, + ): Promise { + return await this.queue.execute(async () => { + this.checkInterpanLock(); + + const clusterName = Zdo.ClusterId[clusterId]; + const messageTag = this.nextZDORequestSequence(); + payload[0] = messageTag; + const apsFrame: EmberApsFrame = { + profileId: Zdo.ZDO_PROFILE_ID, + clusterId, + sourceEndpoint: Zdo.ZDO_ENDPOINT, + destinationEndpoint: Zdo.ZDO_ENDPOINT, + options: DEFAULT_APS_OPTIONS, + groupId: 0, + sequence: 0, // set by stack + }; + let status: SLStatus | undefined; + let apsSequence: number | undefined; + + if (ZSpec.Utils.isBroadcastAddress(networkAddress)) { + logger.debug( + () => `~~~> [ZDO ${clusterName} BROADCAST to=${networkAddress} messageTag=${messageTag} payload=${payload.toString('hex')}]`, + NS, + ); + + [status, apsSequence] = await this.ezsp.ezspSendBroadcast( + ZSpec.NULL_NODE_ID, // alias + networkAddress, + 0, // nwkSequence + apsFrame, + ZDO_REQUEST_RADIUS, + messageTag, + payload, + ); + + apsFrame.sequence = apsSequence; + + logger.debug(`~~~> [SENT ZDO BROADCAST messageTag=${messageTag} apsSequence=${apsSequence} status=${SLStatus[status]}]`, NS); + + if (status !== SLStatus.OK) { + throw new Error( + `~x~> [ZDO ${clusterName} BROADCAST to=${networkAddress} messageTag=${messageTag}] Failed to send request with status=${SLStatus[status]}.`, + ); + } + } else { + logger.debug( + () => + `~~~> [ZDO ${clusterName} UNICAST to=${ieeeAddress}:${networkAddress} messageTag=${messageTag} payload=${payload.toString('hex')}]`, + NS, + ); + + [status, apsSequence] = await this.ezsp.ezspSendUnicast( + EmberOutgoingMessageType.DIRECT, + networkAddress, + apsFrame, + messageTag, + payload, + ); + apsFrame.sequence = apsSequence; + + logger.debug(`~~~> [SENT ZDO UNICAST messageTag=${messageTag} apsSequence=${apsSequence} status=${SLStatus[status]}]`, NS); + + if (status !== SLStatus.OK) { + throw new Error( + `~x~> [ZDO ${clusterName} UNICAST to=${ieeeAddress}:${networkAddress} messageTag=${messageTag}] Failed to send request with status=${SLStatus[status]}.`, + ); + } + } + + if (!disableResponse) { + const responseClusterId = Zdo.Utils.getResponseClusterId(clusterId); + + /* istanbul ignore else */ + if (responseClusterId) { + return await this.oneWaitress.startWaitingFor( + { + target: responseClusterId === Zdo.ClusterId.NETWORK_ADDRESS_RESPONSE ? (ieeeAddress as EUI64) : networkAddress, + apsFrame, + zdoResponseClusterId: responseClusterId, + }, + DEFAULT_REQUEST_TIMEOUT, + ); + } + } + }, networkAddress /* TODO: replace with ieeeAddress once zdo moved upstream */); + } + // queued, non-InterPAN public async permitJoin(seconds: number, networkAddress?: number): Promise { + const clusterId = Zdo.ClusterId.PERMIT_JOINING_REQUEST; const preJoining = async (): Promise => { if (seconds) { const plaintextKey: SecManKey = {contents: Buffer.from(ZIGBEE_PROFILE_INTEROPERABILITY_LINK_KEY)}; @@ -1977,96 +1994,59 @@ export class EmberAdapter extends Adapter { if (networkAddress) { // specific device that is not `Coordinator` - return await this.queue.execute(async () => { + await this.queue.execute(async () => { this.checkInterpanLock(); await preJoining(); + }); - // `authentication`: TC significance always 1 (zb specs) - const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, Zdo.ClusterId.PERMIT_JOINING_REQUEST, seconds, 1, []); - const [status, apsFrame] = await this.sendZDORequest( - networkAddress, - Zdo.ClusterId.PERMIT_JOINING_REQUEST, - zdoPayload, - DEFAULT_APS_OPTIONS, // XXX: SDK has 0 here? - ); + // `authentication`: TC significance always 1 (zb specs) + const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, seconds, 1, []); - if (status !== SLStatus.OK) { - throw new Error(`[ZDO] Failed permit joining request for '${networkAddress}' with status=${SLStatus[status]}.`); - } + const result = await this.sendZdo(ZSpec.BLANK_EUI64, networkAddress, clusterId, zdoPayload, false); - await this.oneWaitress.startWaitingFor( - { - target: networkAddress, - apsFrame, - responseClusterId: Zdo.ClusterId.PERMIT_JOINING_RESPONSE, - }, - DEFAULT_REQUEST_TIMEOUT, - ); - }); + /* istanbul ignore next */ + if (!Zdo.Buffalo.checkStatus(result)) { + // TODO: will disappear once moved upstream + throw new Zdo.StatusError(result[0]); + } } else { // coordinator-only (0), or all - return await this.queue.execute(async () => { + await this.queue.execute(async () => { this.checkInterpanLock(); await preJoining(); + }); - const status = await this.ezsp.ezspPermitJoining(seconds); - - if (status !== SLStatus.OK) { - throw new Error(`[ZDO] Failed coordinator permit joining request with status=${SLStatus[status]}.`); - } + const status = await this.ezsp.ezspPermitJoining(seconds); - logger.debug(`Permit joining on coordinator for ${seconds} sec.`, NS); + if (status !== SLStatus.OK) { + throw new Error(`[ZDO] Failed coordinator permit joining request with status=${SLStatus[status]}.`); + } - // broadcast permit joining ZDO - if (networkAddress === undefined) { - // `authentication`: TC significance always 1 (zb specs) - const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, Zdo.ClusterId.PERMIT_JOINING_REQUEST, seconds, 1, []); + logger.debug(`Permit joining on coordinator for ${seconds} sec.`, NS); - const [bcStatus] = await this.sendZDORequest( - ZSpec.BroadcastAddress.DEFAULT, - Zdo.ClusterId.PERMIT_JOINING_REQUEST, - zdoPayload, - DEFAULT_APS_OPTIONS, - ); + // broadcast permit joining ZDO + if (networkAddress === undefined) { + // `authentication`: TC significance always 1 (zb specs) + const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, seconds, 1, []); - if (bcStatus !== SLStatus.OK) { - // don't throw, coordinator succeeded at least - logger.error(`[ZDO] Failed broadcast permit joining request with status=${SLStatus[bcStatus]}.`, NS); - } - } - }); + await this.sendZdo(ZSpec.BLANK_EUI64, ZSpec.BroadcastAddress.DEFAULT, clusterId, zdoPayload, true); + } } } // queued, non-InterPAN public async lqi(networkAddress: number): Promise { - return await this.queue.execute(async () => { - this.checkInterpanLock(); - - const neighbors: TsType.LQINeighbor[] = []; - const request = async (startIndex: number): Promise<[tableEntries: number, entryCount: number]> => { - const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, Zdo.ClusterId.LQI_TABLE_REQUEST, startIndex); - const [status, apsFrame] = await this.sendZDORequest( - networkAddress, - Zdo.ClusterId.LQI_TABLE_REQUEST, - zdoPayload, - DEFAULT_APS_OPTIONS, - ); - - if (status !== SLStatus.OK) { - throw new Error(`[ZDO] Failed LQI request for '${networkAddress}' (index '${startIndex}') with status=${SLStatus[status]}.`); - } + const clusterId = Zdo.ClusterId.LQI_TABLE_REQUEST; + const neighbors: TsType.LQINeighbor[] = []; + const request = async (startIndex: number): Promise<[tableEntries: number, entryCount: number]> => { + const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, startIndex); + const result = await this.sendZdo(ZSpec.BLANK_EUI64, networkAddress, clusterId, zdoPayload, false); - const result = await this.oneWaitress.startWaitingFor( - { - target: networkAddress, - apsFrame, - responseClusterId: Zdo.ClusterId.LQI_TABLE_RESPONSE, - }, - DEFAULT_REQUEST_TIMEOUT, - ); + /* istanbul ignore else */ + if (Zdo.Buffalo.checkStatus(result)) { + const payload = result[1]; - for (const entry of result.entryList) { + for (const entry of payload.entryList) { neighbors.push({ ieeeAddr: entry.eui64, networkAddress: entry.nwkAddress, @@ -2076,55 +2056,40 @@ export class EmberAdapter extends Adapter { }); } - return [result.neighborTableEntries, result.entryList.length]; - }; + return [payload.neighborTableEntries, payload.entryList.length]; + } else { + // TODO: will disappear once moved upstream + throw new Zdo.StatusError(result[0]); + } + }; - let [tableEntries, entryCount] = await request(0); + let [tableEntries, entryCount] = await request(0); - const size = tableEntries; - let nextStartIndex = entryCount; + const size = tableEntries; + let nextStartIndex = entryCount; - while (neighbors.length < size) { - [tableEntries, entryCount] = await request(nextStartIndex); + while (neighbors.length < size) { + [tableEntries, entryCount] = await request(nextStartIndex); - nextStartIndex += entryCount; - } + nextStartIndex += entryCount; + } - return {neighbors}; - }, networkAddress); + return {neighbors}; } // queued, non-InterPAN public async routingTable(networkAddress: number): Promise { - return await this.queue.execute(async () => { - this.checkInterpanLock(); - - const table: TsType.RoutingTableEntry[] = []; - const request = async (startIndex: number): Promise<[tableEntries: number, entryCount: number]> => { - const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, Zdo.ClusterId.ROUTING_TABLE_REQUEST, startIndex); - const [status, apsFrame] = await this.sendZDORequest( - networkAddress, - Zdo.ClusterId.ROUTING_TABLE_REQUEST, - zdoPayload, - DEFAULT_APS_OPTIONS, - ); - - if (status !== SLStatus.OK) { - throw new Error( - `[ZDO] Failed routing table request for '${networkAddress}' (index '${startIndex}') with status=${SLStatus[status]}.`, - ); - } + const clusterId = Zdo.ClusterId.ROUTING_TABLE_REQUEST; + const table: TsType.RoutingTableEntry[] = []; + const request = async (startIndex: number): Promise<[tableEntries: number, entryCount: number]> => { + const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, startIndex); + const result = await this.sendZdo(ZSpec.BLANK_EUI64, networkAddress, clusterId, zdoPayload, false); - const result = await this.oneWaitress.startWaitingFor( - { - target: networkAddress, - apsFrame, - responseClusterId: Zdo.ClusterId.ROUTING_TABLE_RESPONSE, - }, - DEFAULT_REQUEST_TIMEOUT, - ); + /* istanbul ignore else */ + if (Zdo.Buffalo.checkStatus(result)) { + const payload = result[1]; - for (const entry of result.entryList) { + for (const entry of payload.entryList) { table.push({ destinationAddress: entry.destinationAddress, status: entry.status, @@ -2132,53 +2097,39 @@ export class EmberAdapter extends Adapter { }); } - return [result.routingTableEntries, result.entryList.length]; - }; + return [payload.routingTableEntries, payload.entryList.length]; + } else { + // TODO: will disappear once moved upstream + throw new Zdo.StatusError(result[0]); + } + }; - let [tableEntries, entryCount] = await request(0); + let [tableEntries, entryCount] = await request(0); - const size = tableEntries; - let nextStartIndex = entryCount; + const size = tableEntries; + let nextStartIndex = entryCount; - while (table.length < size) { - [tableEntries, entryCount] = await request(nextStartIndex); + while (table.length < size) { + [tableEntries, entryCount] = await request(nextStartIndex); - nextStartIndex += entryCount; - } + nextStartIndex += entryCount; + } - return {table}; - }, networkAddress); + return {table}; } // queued, non-InterPAN public async nodeDescriptor(networkAddress: number): Promise { - return await this.queue.execute(async () => { - this.checkInterpanLock(); - - const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, Zdo.ClusterId.NODE_DESCRIPTOR_REQUEST, networkAddress); - const [status, apsFrame] = await this.sendZDORequest( - networkAddress, - Zdo.ClusterId.NODE_DESCRIPTOR_REQUEST, - zdoPayload, - DEFAULT_APS_OPTIONS, - ); - - if (status !== SLStatus.OK) { - throw new Error(`[ZDO] Failed node descriptor request for '${networkAddress}' with status=${SLStatus[status]}.`); - } - - const result = await this.oneWaitress.startWaitingFor( - { - target: networkAddress, - apsFrame, - responseClusterId: Zdo.ClusterId.NODE_DESCRIPTOR_RESPONSE, - }, - DEFAULT_REQUEST_TIMEOUT, - ); + const clusterId = Zdo.ClusterId.NODE_DESCRIPTOR_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, networkAddress); + const result = await this.sendZdo(ZSpec.BLANK_EUI64, networkAddress, clusterId, zdoPayload, false); + /* istanbul ignore else */ + if (Zdo.Buffalo.checkStatus(result)) { + const payload = result[1]; let type: TsType.DeviceType = 'Unknown'; - switch (result.logicalType) { + switch (payload.logicalType) { case 0x0: type = 'Coordinator'; break; @@ -2191,9 +2142,9 @@ export class EmberAdapter extends Adapter { } /* istanbul ignore else */ - if (result.serverMask.stackComplianceRevision < CURRENT_ZIGBEE_SPEC_REVISION) { + if (payload.serverMask.stackComplianceRevision < CURRENT_ZIGBEE_SPEC_REVISION) { // always 0 before rev. 21 where field was added - const rev = result.serverMask.stackComplianceRevision < 21 ? 'pre-21' : result.serverMask.stackComplianceRevision; + const rev = payload.serverMask.stackComplianceRevision < 21 ? 'pre-21' : payload.serverMask.stackComplianceRevision; logger.warning( `[ZDO] Device '${networkAddress}' is only compliant to revision '${rev}' of the ZigBee specification (current revision: ${CURRENT_ZIGBEE_SPEC_REVISION}).`, @@ -2201,81 +2152,51 @@ export class EmberAdapter extends Adapter { ); } - return {type, manufacturerCode: result.manufacturerCode}; - }, networkAddress); + return {type, manufacturerCode: payload.manufacturerCode}; + } else { + // TODO: will disappear once moved upstream + throw new Zdo.StatusError(result[0]); + } } // queued, non-InterPAN public async activeEndpoints(networkAddress: number): Promise { - return await this.queue.execute(async () => { - this.checkInterpanLock(); - - const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, Zdo.ClusterId.ACTIVE_ENDPOINTS_REQUEST, networkAddress); - const [status, apsFrame] = await this.sendZDORequest( - networkAddress, - Zdo.ClusterId.ACTIVE_ENDPOINTS_REQUEST, - zdoPayload, - DEFAULT_APS_OPTIONS, - ); - - if (status !== SLStatus.OK) { - throw new Error(`[ZDO] Failed active endpoints request for '${networkAddress}' with status=${SLStatus[status]}.`); - } + const clusterId = Zdo.ClusterId.ACTIVE_ENDPOINTS_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, networkAddress); + const result = await this.sendZdo(ZSpec.BLANK_EUI64, networkAddress, clusterId, zdoPayload, false); - const result = await this.oneWaitress.startWaitingFor( - { - target: networkAddress, - apsFrame, - responseClusterId: Zdo.ClusterId.ACTIVE_ENDPOINTS_RESPONSE, - }, - DEFAULT_REQUEST_TIMEOUT, - ); + /* istanbul ignore else */ + if (Zdo.Buffalo.checkStatus(result)) { + const payload = result[1]; - return {endpoints: result.endpointList}; - }, networkAddress); + return {endpoints: payload.endpointList}; + } else { + // TODO: will disappear once moved upstream + throw new Zdo.StatusError(result[0]); + } } // queued, non-InterPAN public async simpleDescriptor(networkAddress: number, endpointID: number): Promise { - return await this.queue.execute(async () => { - this.checkInterpanLock(); - - const zdoPayload = Zdo.Buffalo.buildRequest( - this.hasZdoMessageOverhead, - Zdo.ClusterId.SIMPLE_DESCRIPTOR_REQUEST, - networkAddress, - endpointID, - ); - const [status, apsFrame] = await this.sendZDORequest( - networkAddress, - Zdo.ClusterId.SIMPLE_DESCRIPTOR_REQUEST, - zdoPayload, - DEFAULT_APS_OPTIONS, - ); + const clusterId = Zdo.ClusterId.SIMPLE_DESCRIPTOR_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, networkAddress, endpointID); + const result = await this.sendZdo(ZSpec.BLANK_EUI64, networkAddress, clusterId, zdoPayload, false); - if (status !== SLStatus.OK) { - throw new Error( - `[ZDO] Failed simple descriptor request for '${networkAddress}' endpoint '${endpointID}' with status=${SLStatus[status]}.`, - ); - } - - const result = await this.oneWaitress.startWaitingFor( - { - target: networkAddress, - apsFrame, - responseClusterId: Zdo.ClusterId.SIMPLE_DESCRIPTOR_RESPONSE, - }, - DEFAULT_REQUEST_TIMEOUT, - ); + /* istanbul ignore else */ + if (Zdo.Buffalo.checkStatus(result)) { + const payload = result[1]; return { - profileID: result.profileId, - endpointID: result.endpoint, - deviceID: result.deviceId, - inputClusters: result.inClusterList, - outputClusters: result.outClusterList, + profileID: payload.profileId, + endpointID: payload.endpoint, + deviceID: payload.deviceId, + inputClusters: payload.inClusterList, + outputClusters: payload.outClusterList, }; - }, networkAddress); + } else { + // TODO: will disappear once moved upstream + throw new Zdo.StatusError(result[0]); + } } // queued, non-InterPAN @@ -2288,42 +2209,25 @@ export class EmberAdapter extends Adapter { type: 'endpoint' | 'group', destinationEndpoint?: number, ): Promise { - return await this.queue.execute(async () => { - this.checkInterpanLock(); - - const zdoPayload = Zdo.Buffalo.buildRequest( - this.hasZdoMessageOverhead, - Zdo.ClusterId.BIND_REQUEST, - sourceIeeeAddress as EUI64, - sourceEndpoint, - clusterID, - type === 'group' ? Zdo.MULTICAST_BINDING : Zdo.UNICAST_BINDING, - destinationAddressOrGroup as EUI64, // not used with MULTICAST_BINDING - destinationAddressOrGroup as number, // not used with UNICAST_BINDING - destinationEndpoint ?? 0, // not used with MULTICAST_BINDING - ); - const [status, apsFrame] = await this.sendZDORequest( - destinationNetworkAddress, - Zdo.ClusterId.BIND_REQUEST, - zdoPayload, - DEFAULT_APS_OPTIONS, - ); - - if (status !== SLStatus.OK) { - throw new Error( - `[ZDO] Failed bind request for '${destinationNetworkAddress}' destination '${destinationAddressOrGroup}' endpoint '${destinationEndpoint}' with status=${SLStatus[status]}.`, - ); - } + const clusterId = Zdo.ClusterId.BIND_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest( + this.hasZdoMessageOverhead, + clusterId, + sourceIeeeAddress as EUI64, + sourceEndpoint, + clusterID, + type === 'group' ? Zdo.MULTICAST_BINDING : Zdo.UNICAST_BINDING, + destinationAddressOrGroup as EUI64, // not used with MULTICAST_BINDING + destinationAddressOrGroup as number, // not used with UNICAST_BINDING + destinationEndpoint ?? 0, // not used with MULTICAST_BINDING + ); + const result = await this.sendZdo(ZSpec.BLANK_EUI64, destinationNetworkAddress, clusterId, zdoPayload, false); - await this.oneWaitress.startWaitingFor( - { - target: destinationNetworkAddress, - apsFrame, - responseClusterId: Zdo.ClusterId.BIND_RESPONSE, - }, - DEFAULT_REQUEST_TIMEOUT, - ); - }, destinationNetworkAddress); + /* istanbul ignore next */ + if (!Zdo.Buffalo.checkStatus(result)) { + // TODO: will disappear once moved upstream + throw new Zdo.StatusError(result[0]); + } } // queued, non-InterPAN @@ -2336,70 +2240,38 @@ export class EmberAdapter extends Adapter { type: 'endpoint' | 'group', destinationEndpoint?: number, ): Promise { - return await this.queue.execute(async () => { - this.checkInterpanLock(); - - const zdoPayload = Zdo.Buffalo.buildRequest( - this.hasZdoMessageOverhead, - Zdo.ClusterId.UNBIND_REQUEST, - sourceIeeeAddress as EUI64, - sourceEndpoint, - clusterID, - type === 'group' ? Zdo.MULTICAST_BINDING : Zdo.UNICAST_BINDING, - destinationAddressOrGroup as EUI64, // not used with MULTICAST_BINDING - destinationAddressOrGroup as number, // not used with UNICAST_BINDING - destinationEndpoint ?? 0, // not used with MULTICAST_BINDING - ); - const [status, apsFrame] = await this.sendZDORequest( - destinationNetworkAddress, - Zdo.ClusterId.UNBIND_REQUEST, - zdoPayload, - DEFAULT_APS_OPTIONS, - ); - - if (status !== SLStatus.OK) { - throw new Error( - `[ZDO] Failed unbind request for '${destinationNetworkAddress}' destination '${destinationAddressOrGroup}' endpoint '${destinationEndpoint}' with status=${SLStatus[status]}.`, - ); - } + const clusterId = Zdo.ClusterId.UNBIND_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest( + this.hasZdoMessageOverhead, + clusterId, + sourceIeeeAddress as EUI64, + sourceEndpoint, + clusterID, + type === 'group' ? Zdo.MULTICAST_BINDING : Zdo.UNICAST_BINDING, + destinationAddressOrGroup as EUI64, // not used with MULTICAST_BINDING + destinationAddressOrGroup as number, // not used with UNICAST_BINDING + destinationEndpoint ?? 0, // not used with MULTICAST_BINDING + ); + const result = await this.sendZdo(ZSpec.BLANK_EUI64, destinationNetworkAddress, clusterId, zdoPayload, false); - await this.oneWaitress.startWaitingFor( - { - target: destinationNetworkAddress, - apsFrame, - responseClusterId: Zdo.ClusterId.UNBIND_RESPONSE, - }, - DEFAULT_REQUEST_TIMEOUT, - ); - }, destinationNetworkAddress); + /* istanbul ignore next */ + if (!Zdo.Buffalo.checkStatus(result)) { + // TODO: will disappear once moved upstream + throw new Zdo.StatusError(result[0]); + } } // queued, non-InterPAN public async removeDevice(networkAddress: number, ieeeAddr: string): Promise { - return await this.queue.execute(async () => { - this.checkInterpanLock(); - - const zdoPayload = Zdo.Buffalo.buildRequest( - this.hasZdoMessageOverhead, - Zdo.ClusterId.LEAVE_REQUEST, - ieeeAddr as EUI64, - Zdo.LeaveRequestFlags.WITHOUT_REJOIN, - ); - const [status, apsFrame] = await this.sendZDORequest(networkAddress, Zdo.ClusterId.LEAVE_REQUEST, zdoPayload, DEFAULT_APS_OPTIONS); - - if (status !== SLStatus.OK) { - throw new Error(`[ZDO] Failed remove device request for '${networkAddress}' target '${ieeeAddr}' with status=${SLStatus[status]}.`); - } - - await this.oneWaitress.startWaitingFor( - { - target: networkAddress, - apsFrame, - responseClusterId: Zdo.ClusterId.LEAVE_RESPONSE, - }, - DEFAULT_REQUEST_TIMEOUT, - ); - }, networkAddress); + const clusterId = Zdo.ClusterId.LEAVE_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, ieeeAddr as EUI64, Zdo.LeaveRequestFlags.WITHOUT_REJOIN); + const result = await this.sendZdo(ZSpec.BLANK_EUI64, networkAddress, clusterId, zdoPayload, false); + + /* istanbul ignore next */ + if (!Zdo.Buffalo.checkStatus(result)) { + // TODO: will disappear once moved upstream + throw new Zdo.StatusError(result[0]); + } } //---- ZCL @@ -2445,7 +2317,10 @@ export class EmberAdapter extends Adapter { return await this.queue.execute(async () => { this.checkInterpanLock(); - logger.debug(() => `~~~> [ZCL to=${networkAddress} apsFrame=${JSON.stringify(apsFrame)} header=${JSON.stringify(zclFrame.header)}]`, NS); + logger.debug( + () => `~~~> [ZCL to=${ieeeAddr}:${networkAddress} apsFrame=${JSON.stringify(apsFrame)} header=${JSON.stringify(zclFrame.header)}]`, + NS, + ); for (let i = 1; i <= QUEUE_MAX_SEND_ATTEMPTS; i++) { let status: SLStatus = SLStatus.FAIL; @@ -2479,17 +2354,21 @@ export class EmberAdapter extends Adapter { if (status === SLStatus.OK) { break; } else if (disableRecovery || i == QUEUE_MAX_SEND_ATTEMPTS) { - throw new Error(`~x~> [ZCL to=${networkAddress}] Failed to send request with status=${SLStatus[status]}.`); + throw new Error( + `~x~> [ZCL to=${ieeeAddr}:${networkAddress} apsFrame=${JSON.stringify(apsFrame)}] Failed to send request with status=${SLStatus[status]}.`, + ); } else if (status === SLStatus.ZIGBEE_MAX_MESSAGE_LIMIT_REACHED || status === SLStatus.BUSY) { await Wait(QUEUE_BUSY_DEFER_MSEC); } else if (status === SLStatus.NETWORK_DOWN) { await Wait(QUEUE_NETWORK_DOWN_DEFER_MSEC); } else { - throw new Error(`~x~> [ZCL to=${networkAddress}] Failed to send request with status=${SLStatus[status]}.`); + throw new Error( + `~x~> [ZCL to=${ieeeAddr}:${networkAddress} apsFrame=${JSON.stringify(apsFrame)}] Failed to send request with status=${SLStatus[status]}.`, + ); } logger.debug( - `~x~> [ZCL to=${networkAddress}] Failed to send request attempt ${i}/${QUEUE_MAX_SEND_ATTEMPTS} with status=${SLStatus[status]}.`, + `~x~> [ZCL to=${ieeeAddr}:${networkAddress}] Failed to send request attempt ${i}/${QUEUE_MAX_SEND_ATTEMPTS} with status=${SLStatus[status]}.`, NS, ); } @@ -2529,10 +2408,11 @@ export class EmberAdapter extends Adapter { this.checkInterpanLock(); logger.debug(() => `~~~> [ZCL GROUP apsFrame=${JSON.stringify(apsFrame)} header=${JSON.stringify(zclFrame.header)}]`, NS); + // eslint-disable-next-line @typescript-eslint/no-unused-vars const [status, messageTag] = await this.ezsp.send( EmberOutgoingMessageType.MULTICAST, - apsFrame.groupId, // not used with MULTICAST + groupID, // not used with MULTICAST apsFrame, data, 0, // alias @@ -2540,7 +2420,7 @@ export class EmberAdapter extends Adapter { ); if (status !== SLStatus.OK) { - throw new Error(`~x~> [ZCL GROUP] Failed to send with status=${SLStatus[status]}.`); + throw new Error(`~x~> [ZCL GROUP groupId=${groupID}] Failed to send with status=${SLStatus[status]}.`); } // NOTE: since ezspMessageSentHandler could take a while here, we don't block, it'll just be logged if the delivery failed @@ -2582,7 +2462,7 @@ export class EmberAdapter extends Adapter { ); if (status !== SLStatus.OK) { - throw new Error(`~x~> [ZCL BROADCAST] Failed to send with status=${SLStatus[status]}.`); + throw new Error(`~x~> [ZCL BROADCAST destination=${destination}] Failed to send with status=${SLStatus[status]}.`); } // NOTE: since ezspMessageSentHandler could take a while here, we don't block, it'll just be logged if the delivery failed diff --git a/src/adapter/ember/adapter/oneWaitress.ts b/src/adapter/ember/adapter/oneWaitress.ts index 2ae16c5233..5bf2dd1b2c 100644 --- a/src/adapter/ember/adapter/oneWaitress.ts +++ b/src/adapter/ember/adapter/oneWaitress.ts @@ -3,8 +3,7 @@ import equals from 'fast-deep-equal/es6'; import {TOUCHLINK_PROFILE_ID} from '../../../zspec/consts'; -import {NodeId} from '../../../zspec/tstypes'; -import * as Zdo from '../../../zspec/zdo/'; +import {EUI64, NodeId} from '../../../zspec/tstypes'; import {ZclPayload} from '../../events'; import {EmberApsFrame} from '../types'; @@ -20,12 +19,13 @@ export enum OneWaitressEvents { type OneWaitressMatcher = { /** * Matches `indexOrDestination` in `ezspMessageSentHandler` or `sender` in `ezspIncomingMessageHandler` + * EUI64 is currently only for NetworkAddress Request/Response * Except for InterPAN touchlink, it should always be present. */ - target?: NodeId; + target?: NodeId | EUI64; apsFrame: EmberApsFrame; - /** Cluster ID for when the response doesn't match the request. Takes priority over apsFrame.clusterId. Should be mostly for ZDO requests. */ - responseClusterId?: number; + /** Cluster ID for ZDO (because request !== response). */ + zdoResponseClusterId?: number; /** ZCL frame transaction sequence number */ zclSequence?: number; /** Expected command ID for ZCL commands */ @@ -98,7 +98,7 @@ export class EmberOneWaitress { waiter.resolved = true; this.waiters.delete(index); - waiter.reject(new Error(`Delivery failed for ${JSON.stringify(apsFrame)}`)); + waiter.reject(new Error(`Delivery failed for '${target}'.`)); return true; } @@ -108,37 +108,31 @@ export class EmberOneWaitress { } /** - * Resolve or reject ZDO response based on given status. - * @param status - * @param sender - * @param apsFrame - * @param payload - * @returns + * Resolve ZDO response payload. + * @param sender Node ID or EUI64 in the response + * @param apsFrame APS Frame in the response + * @param payload Payload to resolve + * @returns True if resolved a waiter */ - public resolveZDO(sender: NodeId, apsFrame: EmberApsFrame, payload: unknown | Zdo.StatusError): boolean { + public resolveZDO(sender: NodeId | EUI64, apsFrame: EmberApsFrame, payload: unknown): boolean { for (const [index, waiter] of this.waiters.entries()) { if (waiter.timedout) { this.waiters.delete(index); continue; } - // always a sender expected in ZDO, profileId is a bit redundant here, but... if ( - sender === waiter.matcher.target && - apsFrame.profileId === waiter.matcher.apsFrame.profileId && - apsFrame.clusterId === (waiter.matcher.responseClusterId ?? waiter.matcher.apsFrame.clusterId) + waiter.matcher.zdoResponseClusterId !== undefined && // skip if not a zdo waiter + sender === waiter.matcher.target && // always a sender expected in ZDO + apsFrame.profileId === waiter.matcher.apsFrame.profileId && // profileId is a bit redundant here, but... + apsFrame.clusterId === waiter.matcher.zdoResponseClusterId ) { clearTimeout(waiter.timer); waiter.resolved = true; this.waiters.delete(index); - - if (payload instanceof Zdo.StatusError || payload instanceof Error) { - waiter.reject(new Error(`[ZDO] Failed response for '${sender}' cluster '${apsFrame.clusterId}' ${payload.message}.`)); - } else { - waiter.resolve(payload); - } + waiter.resolve(payload); return true; } @@ -147,6 +141,11 @@ export class EmberOneWaitress { return false; } + /** + * Resolve ZCL response payload + * @param payload Payload to resolve + * @returns True if resolved a waiter + */ public resolveZCL(payload: ZclPayload): boolean { if (!payload.header) return false; diff --git a/src/adapter/ezsp/adapter/ezspAdapter.ts b/src/adapter/ezsp/adapter/ezspAdapter.ts index 09dabad997..835c6a87b6 100644 --- a/src/adapter/ezsp/adapter/ezspAdapter.ts +++ b/src/adapter/ezsp/adapter/ezspAdapter.ts @@ -5,10 +5,13 @@ import assert from 'assert'; import * as Models from '../../../models'; import {Queue, RealpathSync, Wait, Waitress} from '../../../utils'; import {logger} from '../../../utils/logger'; -import {BroadcastAddress} from '../../../zspec/enums'; +import * as ZSpec from '../../../zspec'; +import {EUI64} from '../../../zspec/tstypes'; import * as Zcl from '../../../zspec/zcl'; +import * as Zdo from '../../../zspec/zdo'; +import * as ZdoTypes from '../../../zspec/zdo/definition/tstypes'; import Adapter from '../../adapter'; -import * as Events from '../../events'; +import {ZclPayload} from '../../events'; import SerialPortUtils from '../../serialPortUtils'; import SocketPortUtils from '../../socketPortUtils'; import { @@ -16,6 +19,7 @@ import { AdapterOptions, Coordinator, CoordinatorVersion, + DeviceType, LQI, LQINeighbor, NetworkOptions, @@ -28,8 +32,7 @@ import { StartResult, } from '../../tstype'; import {Driver, EmberIncomingMessage} from '../driver'; -import {EZSPZDOResponseFrameData} from '../driver/ezsp'; -import {EmberEUI64, EmberStatus, EmberZDOCmd, uint16_t} from '../driver/types'; +import {EmberEUI64, EmberStatus} from '../driver/types'; const NS = 'zh:ezsp'; @@ -48,17 +51,16 @@ interface WaitressMatcher { class EZSPAdapter extends Adapter { private driver: Driver; - private waitress: Waitress; + private waitress: Waitress; private interpanLock: boolean; private queue: Queue; private closing: boolean; - private deprecatedTimer?: NodeJS.Timeout; public constructor(networkOptions: NetworkOptions, serialPortOptions: SerialPortOptions, backupPath: string, adapterOptions: AdapterOptions) { super(networkOptions, serialPortOptions, backupPath, adapterOptions); this.hasZdoMessageOverhead = true; - this.waitress = new Waitress(this.waitressValidator, this.waitressTimeoutFormatter); + this.waitress = new Waitress(this.waitressValidator, this.waitressTimeoutFormatter); this.interpanLock = false; this.closing = false; @@ -75,18 +77,13 @@ class EZSPAdapter extends Adapter { private async processMessage(frame: EmberIncomingMessage): Promise { logger.debug(() => `processMessage: ${JSON.stringify(frame)}`, NS); - if (frame.apsFrame.profileId == 0) { - if (frame.apsFrame.clusterId == EmberZDOCmd.Device_annce && frame.apsFrame.destinationEndpoint == 0) { - let nwk, rst, ieee; - // eslint-disable-next-line prefer-const - [nwk, rst] = uint16_t.deserialize(uint16_t, frame.message.subarray(1)); - [ieee, rst] = EmberEUI64.deserialize(EmberEUI64, rst as Buffer); - ieee = new EmberEUI64(ieee); - logger.debug(`ZDO Device announce: ${nwk}, ${ieee.toString()}`, NS); - this.driver.handleNodeJoined(nwk, ieee); + + if (frame.apsFrame.profileId == Zdo.ZDO_PROFILE_ID) { + if (frame.apsFrame.clusterId >= 0x8000 /* response only */) { + this.emit('zdoResponse', frame.apsFrame.clusterId, frame.zdoResponse!); } - } else if (frame.apsFrame.profileId == 260 || frame.apsFrame.profileId == 0xffff) { - const payload: Events.ZclPayload = { + } else if (frame.apsFrame.profileId == ZSpec.HA_PROFILE_ID || frame.apsFrame.profileId == 0xffff) { + const payload: ZclPayload = { clusterID: frame.apsFrame.clusterId, header: Zcl.Header.fromBuffer(frame.message), data: frame.message, @@ -100,9 +97,9 @@ class EZSPAdapter extends Adapter { this.waitress.resolve(payload); this.emit('zclPayload', payload); - } else if (frame.apsFrame.profileId == 0xc05e && frame.senderEui64) { + } else if (frame.apsFrame.profileId == ZSpec.TOUCHLINK_PROFILE_ID && frame.senderEui64) { // ZLL Frame - const payload: Events.ZclPayload = { + const payload: ZclPayload = { clusterID: frame.apsFrame.clusterId, header: Zcl.Header.fromBuffer(frame.message), data: frame.message, @@ -116,13 +113,13 @@ class EZSPAdapter extends Adapter { this.waitress.resolve(payload); this.emit('zclPayload', payload); - } else if (frame.apsFrame.profileId == 0xa1e0) { + } else if (frame.apsFrame.profileId == ZSpec.GP_PROFILE_ID) { // GP Frame // Only handle when clusterId == 33 (greenPower), some devices send messages with this profileId // while the cluster is not greenPower // https://github.com/Koenkk/zigbee2mqtt/issues/20838 - if (frame.apsFrame.clusterId === 33) { - const payload: Events.ZclPayload = { + if (frame.apsFrame.clusterId === Zcl.Clusters.greenPower.ID) { + const payload: ZclPayload = { header: Zcl.Header.fromBuffer(frame.message), clusterID: frame.apsFrame.clusterId, data: frame.message, @@ -142,52 +139,37 @@ class EZSPAdapter extends Adapter { } } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private async handleDeviceJoin(arr: any[]): Promise { - const [nwk, ieee] = arr; - logger.debug(`Device join request received: ${nwk} ${ieee.toString('hex')}`, NS); - const payload: Events.DeviceJoinedPayload = { - networkAddress: nwk, - ieeeAddr: `0x${ieee.toString('hex')}`, - }; + private async handleDeviceJoin(nwk: number, ieee: EmberEUI64): Promise { + logger.debug(() => `Device join request received: ${nwk} ${ieee.toString()}`, NS); - if (nwk == 0) { - await this.nodeDescriptor(nwk); - } else { - this.emit('deviceJoined', payload); - } + this.emit('deviceJoined', { + networkAddress: nwk, + ieeeAddr: `0x${ieee.toString()}`, + }); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private handleDeviceLeft(arr: any[]): void { - const [nwk, ieee] = arr; - logger.debug(`Device left network request received: ${nwk} ${ieee}`, NS); + private handleDeviceLeft(nwk: number, ieee: EmberEUI64): void { + logger.debug(() => `Device left network request received: ${nwk} ${ieee.toString()}`, NS); - const payload: Events.DeviceLeavePayload = { + this.emit('deviceLeave', { networkAddress: nwk, - ieeeAddr: `0x${ieee.toString('hex')}`, - }; - this.emit('deviceLeave', payload); + ieeeAddr: `0x${ieee.toString()}`, + }); } /** * Adapter methods */ public async start(): Promise { - const logEzspDeprecated = (): void => { - const message = - `Deprecated driver 'ezsp' currently in use, 'ember' will become the officially supported EmberZNet ` + - `driver in next release. If using Zigbee2MQTT see https://github.com/Koenkk/zigbee2mqtt/discussions/21462`; - logger.warning(message, NS); - }; - logEzspDeprecated(); - this.deprecatedTimer = setInterval(logEzspDeprecated, 60 * 60 * 1000); // Every 60 mins + logger.warning( + `'ezsp' driver is deprecated and will only remain to provide support for older firmware (pre 7.4.x). Migration to 'ember' is recommended. If using Zigbee2MQTT see https://github.com/Koenkk/zigbee2mqtt/discussions/21462`, + NS, + ); return await this.driver.startup(); } public async stop(): Promise { this.closing = true; - clearInterval(this.deprecatedTimer); await this.driver.stop(); } @@ -222,29 +204,23 @@ class EZSPAdapter extends Adapter { public async getCoordinator(): Promise { return await this.queue.execute(async () => { this.checkInterpanLock(); - const networkAddress = 0x0000; - const message = await this.driver.zdoRequest(networkAddress, EmberZDOCmd.Active_EP_req, EmberZDOCmd.Active_EP_rsp, { - dstaddr: networkAddress, - }); - const activeEndpoints = message.activeeplist; - + const message = await this.activeEndpoints(ZSpec.COORDINATOR_ADDRESS); const endpoints = []; - for (const endpoint of activeEndpoints) { - const descriptor = await this.driver.zdoRequest(networkAddress, EmberZDOCmd.Simple_Desc_req, EmberZDOCmd.Simple_Desc_rsp, { - dstaddr: networkAddress, - targetEp: endpoint, - }); + + for (const endpoint of message.endpoints) { + const descriptor = await this.simpleDescriptor(ZSpec.COORDINATOR_ADDRESS, endpoint); + endpoints.push({ - profileID: descriptor.descriptor.profileid, - ID: descriptor.descriptor.endpoint, - deviceID: descriptor.descriptor.deviceid, - inputClusters: descriptor.descriptor.inclusterlist, - outputClusters: descriptor.descriptor.outclusterlist, + profileID: descriptor.profileID, + ID: descriptor.endpointID, + deviceID: descriptor.deviceID, + inputClusters: descriptor.inputClusters, + outputClusters: descriptor.outputClusters, }); } return { - networkAddress: networkAddress, + networkAddress: ZSpec.COORDINATOR_ADDRESS, manufacturerID: 0, ieeeAddr: `0x${this.driver.ieee.toString()}`, endpoints, @@ -253,26 +229,51 @@ class EZSPAdapter extends Adapter { } public async permitJoin(seconds: number, networkAddress?: number): Promise { - if (this.driver.ezsp.isInitialized()) { - return await this.queue.execute(async () => { - this.checkInterpanLock(); + if (!this.driver.ezsp.isInitialized()) { + return; + } + + const clusterId = Zdo.ClusterId.PERMIT_JOINING_REQUEST; + if (networkAddress) { + // specific device that is not `Coordinator` + await this.queue.execute(async () => { + this.checkInterpanLock(); await this.driver.preJoining(seconds); + }); - if (networkAddress) { - const result = await this.driver.zdoRequest( - networkAddress, - EmberZDOCmd.Mgmt_Permit_Joining_req, - EmberZDOCmd.Mgmt_Permit_Joining_rsp, - {duration: seconds, tcSignificant: false}, - ); - if (result.status !== EmberStatus.SUCCESS) { - throw new Error(`permitJoin for '${networkAddress}' failed`); - } - } else { - await this.driver.permitJoining(seconds); - } + // `authentication`: TC significance always 1 (zb specs) + const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, seconds, 1, []); + + const result = await this.sendZdo(ZSpec.BLANK_EUI64, networkAddress, clusterId, zdoPayload, false); + + /* istanbul ignore next */ + if (!Zdo.Buffalo.checkStatus(result)) { + // TODO: will disappear once moved upstream + throw new Zdo.StatusError(result[0]); + } + } else { + // coordinator-only (0), or all + await this.queue.execute(async () => { + this.checkInterpanLock(); + await this.driver.preJoining(seconds); }); + + const result = await this.driver.permitJoining(seconds); + + if (result.status !== EmberStatus.SUCCESS) { + throw new Error(`[ZDO] Failed coordinator permit joining request with status=${result.status}.`); + } + + logger.debug(`Permit joining on coordinator for ${seconds} sec.`, NS); + + // broadcast permit joining ZDO + if (networkAddress === undefined) { + // `authentication`: TC significance always 1 (zb specs) + const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, seconds, 1, []); + + await this.sendZdo(ZSpec.BLANK_EUI64, ZSpec.BroadcastAddress.DEFAULT, clusterId, zdoPayload, true); + } } } @@ -293,143 +294,228 @@ class EZSPAdapter extends Adapter { } public async lqi(networkAddress: number): Promise { - return await this.queue.execute(async (): Promise => { - this.checkInterpanLock(); - const neighbors: LQINeighbor[] = []; + const clusterId = Zdo.ClusterId.LQI_TABLE_REQUEST; + const neighbors: LQINeighbor[] = []; + const request = async (startIndex: number): Promise<[tableEntries: number, entryCount: number]> => { + const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, startIndex); + const result = await this.sendZdo(ZSpec.BLANK_EUI64, networkAddress, clusterId, zdoPayload, false); - const request = async (startIndex: number): Promise => { - const result = await this.driver.zdoRequest(networkAddress, EmberZDOCmd.Mgmt_Lqi_req, EmberZDOCmd.Mgmt_Lqi_rsp, { - startindex: startIndex, - }); - if (result.status !== EmberStatus.SUCCESS) { - throw new Error(`LQI for '${networkAddress}' failed with with status code ${result.status}`); - } - - return result; - }; + /* istanbul ignore else */ + if (Zdo.Buffalo.checkStatus(result)) { + const payload = result[1]; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const add = (list: any): void => { - for (const entry of list) { - this.driver.setNode(entry.nodeid, entry.ieee); + for (const entry of payload.entryList) { neighbors.push({ + ieeeAddr: entry.eui64, + networkAddress: entry.nwkAddress, linkquality: entry.lqi, - networkAddress: entry.nodeid, - ieeeAddr: `0x${new EmberEUI64(entry.ieee).toString()}`, - relationship: (entry.packed >> 4) & 0x7, + relationship: entry.relationship, depth: entry.depth, }); } - }; - - let response = await request(0); - add(response.neighborlqilist.neighbors); - const size = response.neighborlqilist.entries; - let nextStartIndex = response.neighborlqilist.neighbors.length; - while (neighbors.length < size) { - response = await request(nextStartIndex); - add(response.neighborlqilist.neighbors); - nextStartIndex += response.neighborlqilist.neighbors.length; + return [payload.neighborTableEntries, payload.entryList.length]; + } else { + // TODO: will disappear once moved upstream + throw new Zdo.StatusError(result[0]); } + }; - return {neighbors}; - }, networkAddress); + let [tableEntries, entryCount] = await request(0); + + const size = tableEntries; + let nextStartIndex = entryCount; + + while (neighbors.length < size) { + [tableEntries, entryCount] = await request(nextStartIndex); + + nextStartIndex += entryCount; + } + + return {neighbors}; } public async routingTable(networkAddress: number): Promise { - return await this.queue.execute(async (): Promise => { - this.checkInterpanLock(); - const table: RoutingTableEntry[] = []; + const clusterId = Zdo.ClusterId.ROUTING_TABLE_REQUEST; + const table: RoutingTableEntry[] = []; + const request = async (startIndex: number): Promise<[tableEntries: number, entryCount: number]> => { + const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, startIndex); + const result = await this.sendZdo(ZSpec.BLANK_EUI64, networkAddress, clusterId, zdoPayload, false); - const request = async (startIndex: number): Promise => { - const result = await this.driver.zdoRequest(networkAddress, EmberZDOCmd.Mgmt_Rtg_req, EmberZDOCmd.Mgmt_Rtg_rsp, { - startindex: startIndex, - }); - if (result.status !== EmberStatus.SUCCESS) { - throw new Error(`Routing table for '${networkAddress}' failed with status code ${result.status}`); - } + /* istanbul ignore else */ + if (Zdo.Buffalo.checkStatus(result)) { + const payload = result[1]; - return result; - }; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const add = (list: any): void => { - for (const entry of list) { + for (const entry of payload.entryList) { table.push({ - destinationAddress: entry.destination, + destinationAddress: entry.destinationAddress, status: entry.status, - nextHop: entry.nexthop, + nextHop: entry.nextHopAddress, }); } - }; - let response = await request(0); - add(response.routingtablelist.table); - const size = response.routingtablelist.entries; - let nextStartIndex = response.routingtablelist.table.length; - - while (table.length < size) { - response = await request(nextStartIndex); - add(response.routingtablelist.table); - nextStartIndex += response.routingtablelist.table.length; + return [payload.routingTableEntries, payload.entryList.length]; + } else { + // TODO: will disappear once moved upstream + throw new Zdo.StatusError(result[0]); } + }; - return {table}; - }, networkAddress); + let [tableEntries, entryCount] = await request(0); + + const size = tableEntries; + let nextStartIndex = entryCount; + + while (table.length < size) { + [tableEntries, entryCount] = await request(nextStartIndex); + + nextStartIndex += entryCount; + } + + return {table}; } public async nodeDescriptor(networkAddress: number): Promise { - return await this.queue.execute(async () => { - this.checkInterpanLock(); - try { - logger.debug(`Requesting 'Node Descriptor' for '${networkAddress}'`, NS); - const result = await this.nodeDescriptorInternal(networkAddress); - return result; - } catch (error) { - logger.debug(`Node descriptor request for '${networkAddress}' failed (${error}), retry`, NS); - throw error; + const clusterId = Zdo.ClusterId.NODE_DESCRIPTOR_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, networkAddress); + const result = await this.sendZdo(ZSpec.BLANK_EUI64, networkAddress, clusterId, zdoPayload, false); + + /* istanbul ignore else */ + if (Zdo.Buffalo.checkStatus(result)) { + const payload = result[1]; + let type: DeviceType = 'Unknown'; + + switch (payload.logicalType) { + case 0x0: + type = 'Coordinator'; + break; + case 0x1: + type = 'Router'; + break; + case 0x2: + type = 'EndDevice'; + break; } - }); - } - private async nodeDescriptorInternal(networkAddress: number): Promise { - const descriptor = await this.driver.zdoRequest(networkAddress, EmberZDOCmd.Node_Desc_req, EmberZDOCmd.Node_Desc_rsp, { - dstaddr: networkAddress, - }); - const logicaltype = descriptor.descriptor.byte1 & 0x07; - return { - manufacturerCode: descriptor.descriptor.manufacturer_code, - type: logicaltype == 0 ? 'Coordinator' : logicaltype == 1 ? 'Router' : 'EndDevice', - }; + return {type, manufacturerCode: payload.manufacturerCode}; + } else { + // TODO: will disappear once moved upstream + throw new Zdo.StatusError(result[0]); + } } public async activeEndpoints(networkAddress: number): Promise { - logger.debug(`Requesting 'Active endpoints' for '${networkAddress}'`, NS); - return await this.queue.execute(async () => { - const endpoints = await this.driver.zdoRequest(networkAddress, EmberZDOCmd.Active_EP_req, EmberZDOCmd.Active_EP_rsp, { - dstaddr: networkAddress, - }); - return {endpoints: [...endpoints.activeeplist]}; - }, networkAddress); + const clusterId = Zdo.ClusterId.ACTIVE_ENDPOINTS_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, networkAddress); + const result = await this.sendZdo(ZSpec.BLANK_EUI64, networkAddress, clusterId, zdoPayload, false); + + /* istanbul ignore else */ + if (Zdo.Buffalo.checkStatus(result)) { + const payload = result[1]; + + return {endpoints: payload.endpointList}; + } else { + // TODO: will disappear once moved upstream + throw new Zdo.StatusError(result[0]); + } } public async simpleDescriptor(networkAddress: number, endpointID: number): Promise { - logger.debug(`Requesting 'Simple Descriptor' for '${networkAddress}' endpoint ${endpointID}`, NS); - return await this.queue.execute(async () => { - this.checkInterpanLock(); - const descriptor = await this.driver.zdoRequest(networkAddress, EmberZDOCmd.Simple_Desc_req, EmberZDOCmd.Simple_Desc_rsp, { - dstaddr: networkAddress, - targetEp: endpointID, - }); + const clusterId = Zdo.ClusterId.SIMPLE_DESCRIPTOR_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, networkAddress, endpointID); + const result = await this.sendZdo(ZSpec.BLANK_EUI64, networkAddress, clusterId, zdoPayload, false); + + /* istanbul ignore else */ + if (Zdo.Buffalo.checkStatus(result)) { + const payload = result[1]; + return { - profileID: descriptor.descriptor.profileid, - endpointID: descriptor.descriptor.endpoint, - deviceID: descriptor.descriptor.deviceid, - inputClusters: descriptor.descriptor.inclusterlist, - outputClusters: descriptor.descriptor.outclusterlist, + profileID: payload.profileId, + endpointID: payload.endpoint, + deviceID: payload.deviceId, + inputClusters: payload.inClusterList, + outputClusters: payload.outClusterList, }; - }, networkAddress); + } else { + // TODO: will disappear once moved upstream + throw new Zdo.StatusError(result[0]); + } + } + + public async sendZdo( + ieeeAddress: string, + networkAddress: number, + clusterId: Zdo.ClusterId, + payload: Buffer, + disableResponse: true, + ): Promise; + public async sendZdo( + ieeeAddress: string, + networkAddress: number, + clusterId: K, + payload: Buffer, + disableResponse: false, + ): Promise; + public async sendZdo( + ieeeAddress: string, + networkAddress: number, + clusterId: K, + payload: Buffer, + disableResponse: boolean, + ): Promise { + return await this.queue.execute(async () => { + this.checkInterpanLock(); + + const clusterName = Zdo.ClusterId[clusterId]; + const frame = this.driver.makeApsFrame(clusterId, disableResponse); + payload[0] = frame.sequence; + let waiter: ReturnType | undefined; + let responseClusterId: number | undefined; + + if (!disableResponse) { + responseClusterId = Zdo.Utils.getResponseClusterId(clusterId); + + if (responseClusterId) { + waiter = this.driver.waitFor( + responseClusterId === Zdo.ClusterId.NETWORK_ADDRESS_RESPONSE ? ieeeAddress : networkAddress, + responseClusterId, + frame.sequence, + ); + } + } + + if (ZSpec.Utils.isBroadcastAddress(networkAddress)) { + logger.debug(() => `~~~> [ZDO ${clusterName} BROADCAST to=${networkAddress} payload=${payload.toString('hex')}]`, NS); + + const req = await this.driver.brequest(networkAddress, frame, payload); + + logger.debug(`~~~> [SENT ZDO BROADCAST]`, NS); + + if (!req) { + waiter?.cancel(); + throw new Error(`~x~> [ZDO ${clusterName} BROADCAST to=${networkAddress}] Failed to send request.`); + } + } else { + logger.debug(() => `~~~> [ZDO ${clusterName} UNICAST to=${ieeeAddress}:${networkAddress} payload=${payload.toString('hex')}]`, NS); + + const req = await this.driver.request(networkAddress, frame, payload); + + logger.debug(`~~~> [SENT ZDO UNICAST]`, NS); + + if (!req) { + waiter?.cancel(); + throw new Error(`~x~> [ZDO ${clusterName} UNICAST to=${ieeeAddress}:${networkAddress}] Failed to send request.`); + } + } + + if (waiter && responseClusterId !== undefined) { + const response = await waiter.start().promise; + + logger.debug(() => `<~~ [ZDO ${Zdo.ClusterId[responseClusterId]} ${JSON.stringify(response.zdoResponse!)}]`, NS); + + return response.zdoResponse! as ZdoTypes.RequestToResponseMap[K]; + } + }, networkAddress /* TODO: replace with ieeeAddress once zdo moved upstream */); } public async sendZclFrameToEndpoint( @@ -441,8 +527,8 @@ class EZSPAdapter extends Adapter { disableResponse: boolean, disableRecovery: boolean, sourceEndpoint?: number, - ): Promise { - return await this.queue.execute(async () => { + ): Promise { + return await this.queue.execute(async () => { this.checkInterpanLock(); return await this.sendZclFrameToEndpointInternal( ieeeAddr, @@ -470,7 +556,7 @@ class EZSPAdapter extends Adapter { disableRecovery: boolean, responseAttempt: number, dataRequestAttempt: number, - ): Promise { + ): Promise { if (ieeeAddr == null) { ieeeAddr = `0x${this.driver.ieee.toString()}`; } @@ -502,7 +588,7 @@ class EZSPAdapter extends Adapter { } const frame = this.driver.makeApsFrame(zclFrame.cluster.ID, disableResponse || zclFrame.header.frameControl.disableDefaultResponse); - frame.profileId = 0x0104; + frame.profileId = ZSpec.HA_PROFILE_ID; frame.sourceEndpoint = sourceEndpoint || 0x01; frame.destinationEndpoint = endpoint; frame.groupId = 0; @@ -545,7 +631,7 @@ class EZSPAdapter extends Adapter { return await this.queue.execute(async () => { this.checkInterpanLock(); const frame = this.driver.makeApsFrame(zclFrame.cluster.ID, false); - frame.profileId = 0x0104; + frame.profileId = ZSpec.HA_PROFILE_ID; frame.sourceEndpoint = 0x01; frame.destinationEndpoint = 0x01; frame.groupId = groupID; @@ -560,15 +646,22 @@ class EZSPAdapter extends Adapter { }); } - public async sendZclFrameToAll(endpoint: number, zclFrame: Zcl.Frame, sourceEndpoint: number, destination: BroadcastAddress): Promise { + public async sendZclFrameToAll( + endpoint: number, + zclFrame: Zcl.Frame, + sourceEndpoint: number, + destination: ZSpec.BroadcastAddress, + ): Promise { return await this.queue.execute(async () => { this.checkInterpanLock(); const frame = this.driver.makeApsFrame(zclFrame.cluster.ID, false); - frame.profileId = sourceEndpoint === 242 && endpoint === 242 ? 0xa1e0 : 0x0104; + frame.profileId = sourceEndpoint === ZSpec.GP_ENDPOINT && endpoint === ZSpec.GP_ENDPOINT ? ZSpec.GP_PROFILE_ID : ZSpec.HA_PROFILE_ID; frame.sourceEndpoint = sourceEndpoint; frame.destinationEndpoint = endpoint; frame.groupId = destination; + // XXX: should be: + // await this.driver.brequest(destination, frame, zclFrame.toBuffer()) await this.driver.mrequest(frame, zclFrame.toBuffer()); /** @@ -589,32 +682,25 @@ class EZSPAdapter extends Adapter { type: 'endpoint' | 'group', destinationEndpoint?: number, ): Promise { - return await this.queue.execute(async () => { - this.checkInterpanLock(); - const ieee = new EmberEUI64(sourceIeeeAddress); - let destAddr; - if (type === 'group') { - // 0x01 = 16-bit group address for DstAddr and DstEndpoint not present - destAddr = { - addrmode: 0x01, - nwk: destinationAddressOrGroup, - }; - } else { - // 0x03 = 64-bit extended address for DstAddr and DstEndpoint present - destAddr = { - addrmode: 0x03, - ieee: new EmberEUI64(destinationAddressOrGroup as string), - endpoint: destinationEndpoint, - }; - this.driver.setNode(destinationNetworkAddress, destAddr.ieee); - } - await this.driver.zdoRequest(destinationNetworkAddress, EmberZDOCmd.Bind_req, EmberZDOCmd.Bind_rsp, { - sourceEui: ieee, - sourceEp: sourceEndpoint, - clusterId: clusterID, - destAddr: destAddr, - }); - }, destinationNetworkAddress); + const clusterId = Zdo.ClusterId.BIND_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest( + this.hasZdoMessageOverhead, + clusterId, + sourceIeeeAddress as EUI64, + sourceEndpoint, + clusterID, + type === 'group' ? Zdo.MULTICAST_BINDING : Zdo.UNICAST_BINDING, + destinationAddressOrGroup as EUI64, // not used with MULTICAST_BINDING + destinationAddressOrGroup as number, // not used with UNICAST_BINDING + destinationEndpoint ?? 0, // not used with MULTICAST_BINDING + ); + const result = await this.sendZdo(ZSpec.BLANK_EUI64, destinationNetworkAddress, clusterId, zdoPayload, false); + + /* istanbul ignore next */ + if (!Zdo.Buffalo.checkStatus(result)) { + // TODO: will disappear once moved upstream + throw new Zdo.StatusError(result[0]); + } } public async unbind( @@ -626,44 +712,37 @@ class EZSPAdapter extends Adapter { type: 'endpoint' | 'group', destinationEndpoint?: number, ): Promise { - return await this.queue.execute(async () => { - this.checkInterpanLock(); - const ieee = new EmberEUI64(sourceIeeeAddress); - let destAddr; - if (type === 'group') { - // 0x01 = 16-bit group address for DstAddr and DstEndpoint not present - destAddr = { - addrmode: 0x01, - nwk: destinationAddressOrGroup, - }; - } else { - // 0x03 = 64-bit extended address for DstAddr and DstEndpoint present - destAddr = { - addrmode: 0x03, - ieee: new EmberEUI64(destinationAddressOrGroup as string), - endpoint: destinationEndpoint, - }; - this.driver.setNode(destinationNetworkAddress, destAddr.ieee); - } - await this.driver.zdoRequest(destinationNetworkAddress, EmberZDOCmd.Unbind_req, EmberZDOCmd.Unbind_rsp, { - sourceEui: ieee, - sourceEp: sourceEndpoint, - clusterId: clusterID, - destAddr: destAddr, - }); - }, destinationNetworkAddress); + const clusterId = Zdo.ClusterId.UNBIND_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest( + this.hasZdoMessageOverhead, + clusterId, + sourceIeeeAddress as EUI64, + sourceEndpoint, + clusterID, + type === 'group' ? Zdo.MULTICAST_BINDING : Zdo.UNICAST_BINDING, + destinationAddressOrGroup as EUI64, // not used with MULTICAST_BINDING + destinationAddressOrGroup as number, // not used with UNICAST_BINDING + destinationEndpoint ?? 0, // not used with MULTICAST_BINDING + ); + const result = await this.sendZdo(ZSpec.BLANK_EUI64, destinationNetworkAddress, clusterId, zdoPayload, false); + + /* istanbul ignore next */ + if (!Zdo.Buffalo.checkStatus(result)) { + // TODO: will disappear once moved upstream + throw new Zdo.StatusError(result[0]); + } } - public removeDevice(networkAddress: number, ieeeAddr: string): Promise { - return this.queue.execute(async () => { - this.checkInterpanLock(); - const ieee = new EmberEUI64(ieeeAddr); - this.driver.setNode(networkAddress, ieee); - await this.driver.zdoRequest(networkAddress, EmberZDOCmd.Mgmt_Leave_req, EmberZDOCmd.Mgmt_Leave_rsp, { - destAddr: ieee, - removechildrenRejoin: 0x00, - }); - }, networkAddress); + public async removeDevice(networkAddress: number, ieeeAddr: string): Promise { + const clusterId = Zdo.ClusterId.LEAVE_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, ieeeAddr as EUI64, Zdo.LeaveRequestFlags.WITHOUT_REJOIN); + const result = await this.sendZdo(ZSpec.BLANK_EUI64, networkAddress, clusterId, zdoPayload, false); + + /* istanbul ignore next */ + if (!Zdo.Buffalo.checkStatus(result)) { + // TODO: will disappear once moved upstream + throw new Zdo.StatusError(result[0]); + } } public async getNetworkParameters(): Promise { @@ -717,8 +796,8 @@ class EZSPAdapter extends Adapter { }); } - public async sendZclFrameInterPANBroadcast(zclFrame: Zcl.Frame, timeout: number): Promise { - return await this.queue.execute(async () => { + public async sendZclFrameInterPANBroadcast(zclFrame: Zcl.Frame, timeout: number): Promise { + return await this.queue.execute(async () => { logger.debug(`sendZclFrameInterPANBroadcast`, NS); const command = zclFrame.command; @@ -750,9 +829,12 @@ class EZSPAdapter extends Adapter { }); } - // eslint-disable-next-line @typescript-eslint/no-unused-vars public async changeChannel(newChannel: number): Promise { - throw new Error(`Channel change is not supported for 'ezsp'`); + const clusterId = Zdo.ClusterId.NWK_UPDATE_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, [newChannel], 0xfe, undefined, undefined, undefined); + + await this.sendZdo(ZSpec.BLANK_EUI64, ZSpec.BroadcastAddress.SLEEPY, clusterId, zdoPayload, true /* handled below */); + await Wait(12000); } public async setTransmitPower(value: number): Promise { @@ -776,7 +858,7 @@ class EZSPAdapter extends Adapter { clusterID: number, commandIdentifier: number, timeout: number, - ): {start: () => {promise: Promise}; cancel: () => void} { + ): {start: () => {promise: Promise}; cancel: () => void} { const waiter = this.waitress.waitFor( { address: networkAddress, @@ -800,7 +882,7 @@ class EZSPAdapter extends Adapter { clusterID: number, commandIdentifier: number, timeout: number, - ): {promise: Promise; cancel: () => void} { + ): {promise: Promise; cancel: () => void} { const waiter = this.waitForInternal(networkAddress, endpoint, transactionSequenceNumber, clusterID, commandIdentifier, timeout); return {cancel: waiter.cancel, promise: waiter.start().promise}; @@ -814,7 +896,7 @@ class EZSPAdapter extends Adapter { ); } - private waitressValidator(payload: Events.ZclPayload, matcher: WaitressMatcher): boolean { + private waitressValidator(payload: ZclPayload, matcher: WaitressMatcher): boolean { return Boolean( payload.header && (!matcher.address || payload.address === matcher.address) && diff --git a/src/adapter/ezsp/driver/driver.ts b/src/adapter/ezsp/driver/driver.ts index 1c6505cd89..e6fb969dfe 100644 --- a/src/adapter/ezsp/driver/driver.ts +++ b/src/adapter/ezsp/driver/driver.ts @@ -8,12 +8,14 @@ import {Wait, Waitress} from '../../../utils'; import {logger} from '../../../utils/logger'; import * as ZSpec from '../../../zspec'; import {Clusters} from '../../../zspec/zcl/definition/cluster'; +import * as Zdo from '../../../zspec/zdo'; +import {GenericZdoResponse} from '../../../zspec/zdo/definition/tstypes'; import {EZSPAdapterBackup} from '../adapter/backup'; import * as TsType from './../../tstype'; import {ParamsDesc} from './commands'; -import {Ezsp, EZSPFrameData, EZSPZDOResponseFrameData} from './ezsp'; +import {Ezsp, EZSPFrameData} from './ezsp'; import {Multicast} from './multicast'; -import {EmberApsOption, EmberJoinDecision, EmberKeyData, EmberNodeType, EmberStatus, EmberZDOCmd, uint8_t, uint16_t} from './types'; +import {EmberApsOption, EmberJoinDecision, EmberKeyData, EmberNodeType, EmberStatus, uint8_t, uint16_t} from './types'; import { EmberDerivedKeyType, EmberDeviceUpdate, @@ -53,13 +55,14 @@ interface AddEndpointParameters { } type EmberFrame = { - address: number; + address: number | string; payload: Buffer; frame: EmberApsFrame; + zdoResponse?: GenericZdoResponse; }; type EmberWaitressMatcher = { - address: number; + address: number | string; clusterId: number; sequence: number; }; @@ -79,6 +82,7 @@ export interface EmberIncomingMessage { addressIndex: number; message: Buffer; senderEui64: EmberEUI64; + zdoResponse?: GenericZdoResponse; } const IEEE_PREFIX_MFG_ID: IeeeMfg[] = [ @@ -283,7 +287,6 @@ export class Driver extends EventEmitter { this.ieee = new EmberEUI64(ieee); logger.debug('Network ready', NS); this.ezsp.on('frame', this.handleFrame.bind(this)); - this.handleNodeJoined(nwk, this.ieee); logger.debug(`EZSP nwk=${nwk}, IEEE=0x${this.ieee}`, NS); const linkResult = await this.getKey(EmberKeyType.TRUST_CENTER_LINK_KEY); logger.debug(`TRUST_CENTER_LINK_KEY: ${JSON.stringify(linkResult)}`, NS); @@ -362,25 +365,70 @@ export class Driver extends EventEmitter { private handleFrame(frameName: string, frame: EZSPFrameData): void { 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, - }); + const apsFrame: EmberApsFrame = frame.apsFrame; + + if (apsFrame.profileId == Zdo.ZDO_PROFILE_ID && apsFrame.clusterId >= 0x8000 /* response only */) { + const zdoResponse = Zdo.Buffalo.readResponse(true, apsFrame.clusterId, frame.message); + + if (apsFrame.clusterId === Zdo.ClusterId.NETWORK_ADDRESS_RESPONSE) { + // special case to properly resolve a NETWORK_ADDRESS_RESPONSE following a NETWORK_ADDRESS_REQUEST (based on EUI64 from ZDO payload) + // NOTE: if response has invalid status (no EUI64 available), response waiter will eventually time out + /* istanbul ignore else */ + if (Zdo.Buffalo.checkStatus(zdoResponse)) { + const eui64 = zdoResponse[1].eui64; + + // update cache with new network address + this.eui64ToNodeId.set(eui64, frame.sender); + + this.waitress.resolve({ + address: eui64, + payload: frame.message, + frame: apsFrame, + zdoResponse, + }); + } + } else { + this.waitress.resolve({ + address: frame.sender, + payload: frame.message, + frame: apsFrame, + zdoResponse, + }); + } - if (!handled) { + // always pass ZDO to bubble up to controller this.emit('incomingMessage', { messageType: frame.type, - apsFrame: frame.apsFrame, + apsFrame, lqi: frame.lastHopLqi, rssi: frame.lastHopRssi, sender: frame.sender, bindingIndex: frame.bindingIndex, addressIndex: frame.addressIndex, message: frame.message, - senderEui64: eui64, + senderEui64: this.eui64ToNodeId.get(frame.sender), + zdoResponse, }); + } else { + const handled = this.waitress.resolve({ + address: frame.sender, + payload: frame.message, + frame: apsFrame, + }); + + if (!handled) { + this.emit('incomingMessage', { + messageType: frame.type, + apsFrame, + lqi: frame.lastHopLqi, + rssi: frame.lastHopRssi, + sender: frame.sender, + bindingIndex: frame.bindingIndex, + addressIndex: frame.addressIndex, + message: frame.message, + senderEui64: this.eui64ToNodeId.get(frame.sender), + }); + } } break; } @@ -549,7 +597,7 @@ export class Driver extends EventEmitter { } this.eui64ToNodeId.delete(ieee.toString()); - this.emit('deviceLeft', [nwk, ieee]); + this.emit('deviceLeft', nwk, ieee); } private async resetMfgId(mfgId: number): Promise { @@ -565,7 +613,7 @@ export class Driver extends EventEmitter { } for (const rec of IEEE_PREFIX_MFG_ID) { - if (Buffer.from((ieee as EmberEUI64).value).indexOf(Buffer.from(rec.prefix)) == 0) { + if (Buffer.from(ieee.value).indexOf(Buffer.from(rec.prefix)) == 0) { // set ManufacturerCode logger.debug(`handleNodeJoined: change ManufacturerCode for ieee ${ieee} to ${rec.mfgId}`, NS); // eslint-disable-next-line @typescript-eslint/no-floating-promises @@ -575,7 +623,7 @@ export class Driver extends EventEmitter { } this.eui64ToNodeId.set(ieee.toString(), nwk); - this.emit('deviceJoined', [nwk, ieee]); + this.emit('deviceJoined', nwk, ieee); } public setNode(nwk: number, ieee: EmberEUI64 | number[]): void { @@ -724,45 +772,6 @@ export class Driver extends EventEmitter { return frame; } - public async zdoRequest( - networkAddress: number, - requestCmd: EmberZDOCmd, - responseCmd: EmberZDOCmd, - params: ParamsDesc, - ): Promise { - const requestName = EmberZDOCmd.valueName(EmberZDOCmd, requestCmd); - const responseName = EmberZDOCmd.valueName(EmberZDOCmd, responseCmd); - - logger.debug(() => `ZDO ${requestName} params: ${JSON.stringify(params)}`, NS); - - const frame = this.makeApsFrame(requestCmd as number, false); - const payload = this.makeZDOframe(requestCmd as number, {transId: frame.sequence, ...params}); - const waiter = this.waitFor(networkAddress, responseCmd as number, 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.debug(() => `${responseName} frame: ${JSON.stringify(response.payload)}`, NS); - - const result = new EZSPZDOResponseFrameData(responseCmd as number, response.payload); - - logger.debug(`${responseName} parsed: ${JSON.stringify(result)}`, NS); - - return result; - } catch (e) { - this.waitress.remove(waiter.ID); - logger.debug(`zdoRequest error: ${e}`, NS); - - throw e; - } - } - public async networkIdToEUI64(nwk: number): Promise { for (const [eUI64, value] of this.eui64ToNodeId) { if (value === nwk) return new EmberEUI64(eUI64); @@ -833,12 +842,14 @@ export class Driver extends EventEmitter { } public waitFor( - address: number, + address: number | string, clusterId: number, sequence: number, timeout = 10000, - ): {start: () => {promise: Promise; ID: number}; ID: number} { - return this.waitress.waitFor({address, clusterId, sequence}, timeout); + ): ReturnType & {cancel: () => void} { + const waiter = this.waitress.waitFor({address, clusterId, sequence}, timeout); + + return {...waiter, cancel: () => this.waitress.remove(waiter.ID)}; } private waitressTimeoutFormatter(matcher: EmberWaitressMatcher, timeout: number): string { diff --git a/src/adapter/ezsp/driver/ezsp.ts b/src/adapter/ezsp/driver/ezsp.ts index 7394f704c7..cb5e0adea1 100644 --- a/src/adapter/ezsp/driver/ezsp.ts +++ b/src/adapter/ezsp/driver/ezsp.ts @@ -456,6 +456,10 @@ export class Ezsp extends EventEmitter { const version = this.ezspV; const result = await this.execCommand('version', {desiredProtocolVersion: version}); + if (result.protocolVersion >= 14) { + throw new Error(`'ezsp' driver is not compatible with firmware 8.x.x or above (EZSP v14+). Use 'ember' driver instead.`); + } + if (result.protocolVersion !== version) { logger.debug(`Switching to eszp version ${result.protocolVersion}`, NS); diff --git a/src/adapter/z-stack/adapter/manager.ts b/src/adapter/z-stack/adapter/manager.ts index a5b39a63d7..f436a1eba3 100644 --- a/src/adapter/z-stack/adapter/manager.ts +++ b/src/adapter/z-stack/adapter/manager.ts @@ -278,7 +278,7 @@ export class ZnpAdapterManager { const deviceInfo = await this.znp.requestWithReply(Subsystem.UTIL, 'getDeviceInfo', {}); if (deviceInfo.payload.devicestate !== DevStates.ZB_COORD) { logger.debug('starting adapter as coordinator', NS); - const started = this.znp.waitFor(UnpiConstants.Type.AREQ, Subsystem.ZDO, 'stateChangeInd', {state: 9}, 60000); + const started = this.znp.waitFor(UnpiConstants.Type.AREQ, Subsystem.ZDO, 'stateChangeInd', undefined, undefined, 9, 60000); await this.znp.request(Subsystem.ZDO, 'startupFromApp', {startdelay: 100}, undefined, undefined, [ ZnpCommandStatus.SUCCESS, ZnpCommandStatus.FAILURE, @@ -362,7 +362,7 @@ export class ZnpAdapterManager { await this.znp.request(Subsystem.APP_CNF, 'bdbSetChannel', {isPrimary: 0x0, channel: 0x0}); /* perform bdb commissioning */ - const started = this.znp.waitFor(UnpiConstants.Type.AREQ, Subsystem.ZDO, 'stateChangeInd', {state: 9}, 60000); + const started = this.znp.waitFor(UnpiConstants.Type.AREQ, Subsystem.ZDO, 'stateChangeInd', undefined, undefined, 9, 60000); await this.znp.request(Subsystem.APP_CNF, 'bdbStartCommissioning', {mode: 0x04}); try { await started.start().promise; diff --git a/src/adapter/z-stack/adapter/zStackAdapter.ts b/src/adapter/z-stack/adapter/zStackAdapter.ts index 95ce964801..861f20e0b4 100644 --- a/src/adapter/z-stack/adapter/zStackAdapter.ts +++ b/src/adapter/z-stack/adapter/zStackAdapter.ts @@ -7,18 +7,10 @@ import {Queue, Wait, Waitress} from '../../../utils'; import {logger} from '../../../utils/logger'; import * as ZSpec from '../../../zspec'; import {BroadcastAddress} from '../../../zspec/enums'; +import {EUI64} from '../../../zspec/tstypes'; import * as Zcl from '../../../zspec/zcl'; import * as Zdo from '../../../zspec/zdo'; -import { - ActiveEndpointsResponse, - LQITableEntry, - LQITableResponse, - NetworkAddressResponse, - NodeDescriptorResponse, - RoutingTableResponse, - SimpleDescriptorResponse, - RoutingTableEntry as ZdoRoutingTableEntry, -} from '../../../zspec/zdo/definition/tstypes'; +import * as ZdoTypes from '../../../zspec/zdo/definition/tstypes'; import Adapter from '../../adapter'; import * as Events from '../../events'; import { @@ -41,7 +33,8 @@ import { import * as Constants from '../constants'; import {Constants as UnpiConstants} from '../unpi'; import {Znp, ZpiObject} from '../znp'; -import {ZpiObjectPayload} from '../znp/tstype'; +import Definition from '../znp/definition'; +import {isMtCmdAreqZdo} from '../znp/utils'; import {ZnpAdapterManager} from './manager'; import {ZnpVersion} from './tstype'; @@ -221,14 +214,25 @@ class ZStackAdapter extends Adapter { } public async permitJoin(seconds: number, networkAddress?: number): Promise { - const addrmode = networkAddress === undefined ? 0x0f : 0x02; - const dstaddr = networkAddress || 0xfffc; + const clusterId = Zdo.ClusterId.PERMIT_JOINING_REQUEST; + // `authentication`: TC significance always 1 (zb specs) + const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, seconds, 1, []); + + if (networkAddress === undefined) { + await this.sendZdo(ZSpec.BLANK_EUI64, ZSpec.BroadcastAddress.DEFAULT, clusterId, zdoPayload, true); + } else { + // NOTE: `sendZdo` takes care of adjusting the payload as appropriate based on `networkAddress === 0` or not + const result = await this.sendZdo(ZSpec.BLANK_EUI64, networkAddress, clusterId, zdoPayload, false); + + /* istanbul ignore next */ + if (!Zdo.Buffalo.checkStatus(result)) { + // TODO: will disappear once moved upstream + throw new Zdo.StatusError(result[0]); + } + } + await this.queue.execute(async () => { this.checkInterpanLock(); - const payload = {addrmode, dstaddr, duration: seconds, tcsignificance: 0}; - const permitJoinRsp = this.waitForAreqZdo('mgmtPermitJoinRsp'); - await this.znp.request(Subsystem.ZDO, 'mgmtPermitJoinReq', payload); - await permitJoinRsp.start(); await this.setLED(seconds == 0 ? 'off' : 'on'); }); } @@ -283,10 +287,19 @@ class ZStackAdapter extends Adapter { * this is currently not handled, the first nwkAddrRsp is taken. */ logger.debug(`Request network address of '${ieeeAddr}'`, NS); - const response = this.waitForAreqZdo('nwkAddrRsp', {ieeeaddr: ieeeAddr}); - await this.znp.request(Subsystem.ZDO, 'nwkAddrReq', {ieeeaddr: ieeeAddr, reqtype: 0, startindex: 0}); - const result = await response.start(); - return result.nwkAddress; + + const clusterId = Zdo.ClusterId.NETWORK_ADDRESS_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, ieeeAddr as EUI64, false, 0); + + const result = await this.sendZdo(ieeeAddr, ZSpec.NULL_NODE_ID, clusterId, zdoPayload, false); + + /* istanbul ignore else */ + if (Zdo.Buffalo.checkStatus(result)) { + return result[1].nwkAddress; + } else { + // TODO: will disappear once moved upstream + throw new Zdo.StatusError(result[0]); + } } private supportsAssocRemove(): boolean { @@ -308,68 +321,188 @@ class ZStackAdapter extends Adapter { } public async nodeDescriptor(networkAddress: number): Promise { - return await this.queue.execute(async () => { - this.checkInterpanLock(); - try { - const result = await this.nodeDescriptorInternal(networkAddress); - return result; - } catch (error) { - logger.debug(`Node descriptor request for '${networkAddress}' failed (${error}), retry`, NS); - // Doing a route discovery after simple descriptor request fails makes it succeed sometimes. - // https://github.com/Koenkk/zigbee2mqtt/issues/3276 - await this.discoverRoute(networkAddress); - const result = await this.nodeDescriptorInternal(networkAddress); - return result; + const clusterId = Zdo.ClusterId.NODE_DESCRIPTOR_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, networkAddress); + const result = await this.sendZdo(ZSpec.BLANK_EUI64, networkAddress, clusterId, zdoPayload, false); + + /* istanbul ignore else */ + if (Zdo.Buffalo.checkStatus(result)) { + const payload = result[1]; + let type: DeviceType = 'Unknown'; + + switch (payload.logicalType) { + case 0x0: + type = 'Coordinator'; + break; + case 0x1: + type = 'Router'; + break; + case 0x2: + type = 'EndDevice'; + break; } - }, networkAddress); - } - private async nodeDescriptorInternal(networkAddress: number): Promise { - const response = this.waitForAreqZdo('nodeDescRsp', {srcaddr: networkAddress}); - const payload = {dstaddr: networkAddress, nwkaddrofinterest: networkAddress}; - await this.znp.request(Subsystem.ZDO, 'nodeDescReq', payload, response.ID); - const descriptor = await response.start(); - - let type: DeviceType = 'Unknown'; - const logicalType = descriptor.logicalType; - for (const [key, value] of Object.entries(Constants.ZDO.deviceLogicalType)) { - if (value === logicalType) { - if (key === 'COORDINATOR') type = 'Coordinator'; - else if (key === 'ROUTER') type = 'Router'; - else if (key === 'ENDDEVICE') type = 'EndDevice'; - break; - } + return {type, manufacturerCode: payload.manufacturerCode}; + } else { + // TODO: will disappear once moved upstream + throw new Zdo.StatusError(result[0]); } - - return {manufacturerCode: descriptor.manufacturerCode, type}; } public async activeEndpoints(networkAddress: number): Promise { - return await this.queue.execute(async () => { - this.checkInterpanLock(); - const response = this.waitForAreqZdo('activeEpRsp', {srcaddr: networkAddress}); - const payload = {dstaddr: networkAddress, nwkaddrofinterest: networkAddress}; - await this.znp.request(Subsystem.ZDO, 'activeEpReq', payload, response.ID); - const activeEp = await response.start(); - return {endpoints: activeEp.endpointList}; - }, networkAddress); + const clusterId = Zdo.ClusterId.ACTIVE_ENDPOINTS_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, networkAddress); + const result = await this.sendZdo(ZSpec.BLANK_EUI64, networkAddress, clusterId, zdoPayload, false); + + /* istanbul ignore else */ + if (Zdo.Buffalo.checkStatus(result)) { + return {endpoints: result[1].endpointList}; + } else { + // TODO: will disappear once moved upstream + throw new Zdo.StatusError(result[0]); + } } public async simpleDescriptor(networkAddress: number, endpointID: number): Promise { - return await this.queue.execute(async () => { - this.checkInterpanLock(); - const response = this.waitForAreqZdo('simpleDescRsp', {srcaddr: networkAddress}); - const payload = {dstaddr: networkAddress, nwkaddrofinterest: networkAddress, endpoint: endpointID}; - await this.znp.request(Subsystem.ZDO, 'simpleDescReq', payload, response.ID); - const descriptor = await response.start(); + const clusterId = Zdo.ClusterId.SIMPLE_DESCRIPTOR_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, networkAddress, endpointID); + const result = await this.sendZdo(ZSpec.BLANK_EUI64, networkAddress, clusterId, zdoPayload, false); + + /* istanbul ignore else */ + if (Zdo.Buffalo.checkStatus(result)) { + const payload = result[1]; + return { - profileID: descriptor.profileId, - endpointID: descriptor.endpoint, - deviceID: descriptor.deviceId, - inputClusters: descriptor.inClusterList, - outputClusters: descriptor.outClusterList, + profileID: payload.profileId, + endpointID: payload.endpoint, + deviceID: payload.deviceId, + inputClusters: payload.inClusterList, + outputClusters: payload.outClusterList, }; - }, networkAddress); + } else { + // TODO: will disappear once moved upstream + throw new Zdo.StatusError(result[0]); + } + } + + public async sendZdo( + ieeeAddress: string, + networkAddress: number, + clusterId: Zdo.ClusterId, + payload: Buffer, + disableResponse: true, + ): Promise; + public async sendZdo( + ieeeAddress: string, + networkAddress: number, + clusterId: K, + payload: Buffer, + disableResponse: false, + ): Promise; + public async sendZdo( + ieeeAddress: string, + networkAddress: number, + clusterId: K, + payload: Buffer, + disableResponse: boolean, + ): Promise { + return await this.queue.execute(async () => { + this.checkInterpanLock(); + + // stack-specific requirements + switch (clusterId) { + case Zdo.ClusterId.PERMIT_JOINING_REQUEST: { + const finalPayload = Buffer.alloc(payload.length + 3); + finalPayload.writeUInt8(ZSpec.BroadcastAddress[networkAddress] ? AddressMode.ADDR_BROADCAST : AddressMode.ADDR_16BIT, 0); + // zstack uses AddressMode.ADDR_16BIT + ZSpec.BroadcastAddress.DEFAULT to signal "coordinator-only" + finalPayload.writeUInt16LE(networkAddress === 0 ? ZSpec.BroadcastAddress.DEFAULT : networkAddress, 1); + finalPayload.set(payload, 3); + + payload = finalPayload; + break; + } + + case Zdo.ClusterId.NWK_UPDATE_REQUEST: { + // extra zeroes for empty nwkManagerAddr if necessary + const zeroes = 9 - payload.length - 1; /* zstack doesn't have nwkUpdateId */ + const finalPayload = Buffer.alloc(payload.length + 3 + zeroes); + finalPayload.writeUInt16LE(networkAddress, 0); + finalPayload.writeUInt8(ZSpec.BroadcastAddress[networkAddress] ? AddressMode.ADDR_BROADCAST : AddressMode.ADDR_16BIT, 2); + finalPayload.set(payload, 3); + + payload = finalPayload; + break; + } + + case Zdo.ClusterId.BIND_REQUEST: + case Zdo.ClusterId.UNBIND_REQUEST: { + // extra zeroes for uint16 (in place of ieee when MULTICAST) and endpoint (not used when MULTICAST) + const zeroes = 21 - payload.length; + const finalPayload = Buffer.alloc(payload.length + 2 + zeroes); + finalPayload.writeUInt16LE(networkAddress, 0); + finalPayload.set(payload, 2); + + payload = finalPayload; + break; + } + + case Zdo.ClusterId.NETWORK_ADDRESS_REQUEST: + case Zdo.ClusterId.IEEE_ADDRESS_REQUEST: { + // no modification necessary + break; + } + + default: { + const finalPayload = Buffer.alloc(payload.length + 2); + finalPayload.writeUInt16LE(networkAddress, 0); + finalPayload.set(payload, 2); + + payload = finalPayload; + break; + } + } + + let waiter: ReturnType | undefined; + + if (!disableResponse) { + const responseClusterId = Zdo.Utils.getResponseClusterId(clusterId); + + /* istanbul ignore else */ + if (responseClusterId) { + const cmd = Definition[Subsystem.ZDO].find((c) => isMtCmdAreqZdo(c) && c.zdoClusterId === responseClusterId); + assert(cmd, `Response for ZDO cluster ID '${responseClusterId}' not supported.`); + + waiter = this.znp.waitFor( + UnpiConstants.Type.AREQ, + Subsystem.ZDO, + cmd.name, + responseClusterId === Zdo.ClusterId.NETWORK_ADDRESS_RESPONSE ? ieeeAddress : networkAddress, + undefined, + undefined, + ); + } + } + + try { + await this.znp.requestZdo(clusterId, payload, waiter?.ID); + } catch (error) { + if (clusterId === Zdo.ClusterId.NODE_DESCRIPTOR_REQUEST) { + // Discover route when node descriptor request fails + // https://github.com/Koenkk/zigbee2mqtt/issues/3276 + logger.debug(`Discover route to '${networkAddress}' because node descriptor request failed`, NS); + await this.discoverRoute(networkAddress); + await this.znp.requestZdo(clusterId, payload, waiter?.ID); + } else { + throw error; + } + } + + if (waiter) { + const response = await waiter.start().promise; + + return response.payload.zdo; + } + }, networkAddress /* TODO: replace with ieeeAddress once zdo moved upstream */); } public async sendZclFrameToEndpoint( @@ -709,79 +842,85 @@ class ZStackAdapter extends Adapter { } public async lqi(networkAddress: number): Promise { - return await this.queue.execute(async (): Promise => { - this.checkInterpanLock(); - const neighbors: LQINeighbor[] = []; + const clusterId = Zdo.ClusterId.LQI_TABLE_REQUEST; + const neighbors: LQINeighbor[] = []; + const request = async (startIndex: number): Promise<[tableEntries: number, entryCount: number]> => { + const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, startIndex); + const result = await this.sendZdo(ZSpec.BLANK_EUI64, networkAddress, clusterId, zdoPayload, false); - const request = async (startIndex: number): Promise => { - const response = this.waitForAreqZdo('mgmtLqiRsp', {srcaddr: networkAddress}); - await this.znp.request(Subsystem.ZDO, 'mgmtLqiReq', {dstaddr: networkAddress, startindex: startIndex}, response.ID); - const result = await response.start(); - return result; - }; + /* istanbul ignore else */ + if (Zdo.Buffalo.checkStatus(result)) { + const payload = result[1]; - const add = (list: LQITableEntry[]): void => { - for (const entry of list) { + for (const entry of payload.entryList) { neighbors.push({ - linkquality: entry.lqi, - networkAddress: entry.nwkAddress, ieeeAddr: entry.eui64, + networkAddress: entry.nwkAddress, + linkquality: entry.lqi, relationship: entry.relationship, depth: entry.depth, }); } - }; - - let response = await request(0); - add(response.entryList); - const size = response.neighborTableEntries; - let nextStartIndex = response.entryList.length; - while (neighbors.length < size) { - response = await request(nextStartIndex); - add(response.entryList); - nextStartIndex += response.entryList.length; + return [payload.neighborTableEntries, payload.entryList.length]; + } else { + // TODO: will disappear once moved upstream + throw new Zdo.StatusError(result[0]); } + }; - return {neighbors}; - }, networkAddress); + let [tableEntries, entryCount] = await request(0); + + const size = tableEntries; + let nextStartIndex = entryCount; + + while (neighbors.length < size) { + [tableEntries, entryCount] = await request(nextStartIndex); + + nextStartIndex += entryCount; + } + + return {neighbors}; } public async routingTable(networkAddress: number): Promise { - return await this.queue.execute(async (): Promise => { - this.checkInterpanLock(); - const table: RoutingTableEntry[] = []; + const clusterId = Zdo.ClusterId.ROUTING_TABLE_REQUEST; + const table: RoutingTableEntry[] = []; + const request = async (startIndex: number): Promise<[tableEntries: number, entryCount: number]> => { + const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, startIndex); + const result = await this.sendZdo(ZSpec.BLANK_EUI64, networkAddress, clusterId, zdoPayload, false); - const request = async (startIndex: number): Promise => { - const response = this.waitForAreqZdo('mgmtRtgRsp', {srcaddr: networkAddress}); - await this.znp.request(Subsystem.ZDO, 'mgmtRtgReq', {dstaddr: networkAddress, startindex: startIndex}, response.ID); - const result = await response.start(); - return result; - }; + /* istanbul ignore else */ + if (Zdo.Buffalo.checkStatus(result)) { + const payload = result[1]; - const add = (list: ZdoRoutingTableEntry[]): void => { - for (const entry of list) { + for (const entry of payload.entryList) { table.push({ destinationAddress: entry.destinationAddress, status: entry.status, nextHop: entry.nextHopAddress, }); } - }; - - let response = await request(0); - add(response.entryList); - const size = response.routingTableEntries; - let nextStartIndex = response.entryList.length; - while (table.length < size) { - response = await request(nextStartIndex); - add(response.entryList); - nextStartIndex += response.entryList.length; + return [payload.routingTableEntries, payload.entryList.length]; + } else { + // TODO: will disappear once moved upstream + throw new Zdo.StatusError(result[0]); } + }; - return {table}; - }, networkAddress); + let [tableEntries, entryCount] = await request(0); + + const size = tableEntries; + let nextStartIndex = entryCount; + + while (table.length < size) { + [tableEntries, entryCount] = await request(nextStartIndex); + + nextStartIndex += entryCount; + } + + return {table}; } public async addInstallCode(ieeeAddress: string, key: Buffer): Promise { @@ -799,16 +938,25 @@ class ZStackAdapter extends Adapter { type: 'endpoint' | 'group', destinationEndpoint?: number, ): Promise { - await this.bindInternal( - 'bind', - destinationNetworkAddress, - sourceIeeeAddress, + const clusterId = Zdo.ClusterId.BIND_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest( + this.hasZdoMessageOverhead, + clusterId, + sourceIeeeAddress as EUI64, sourceEndpoint, clusterID, - destinationAddressOrGroup, - type, - destinationEndpoint, + type === 'group' ? Zdo.MULTICAST_BINDING : Zdo.UNICAST_BINDING, + destinationAddressOrGroup as EUI64, // not used with MULTICAST_BINDING + destinationAddressOrGroup as number, // not used with UNICAST_BINDING + destinationEndpoint ?? 0, // not used with MULTICAST_BINDING ); + const result = await this.sendZdo(ZSpec.BLANK_EUI64, destinationNetworkAddress, clusterId, zdoPayload, false); + + /* istanbul ignore next */ + if (!Zdo.Buffalo.checkStatus(result)) { + // TODO: will disappear once moved upstream + throw new Zdo.StatusError(result[0]); + } } public async unbind( @@ -820,61 +968,37 @@ class ZStackAdapter extends Adapter { type: 'endpoint' | 'group', destinationEndpoint?: number, ): Promise { - await this.bindInternal( - 'unbind', - destinationNetworkAddress, - sourceIeeeAddress, + const clusterId = Zdo.ClusterId.UNBIND_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest( + this.hasZdoMessageOverhead, + clusterId, + sourceIeeeAddress as EUI64, sourceEndpoint, clusterID, - destinationAddressOrGroup, - type, - destinationEndpoint, + type === 'group' ? Zdo.MULTICAST_BINDING : Zdo.UNICAST_BINDING, + destinationAddressOrGroup as EUI64, // not used with MULTICAST_BINDING + destinationAddressOrGroup as number, // not used with UNICAST_BINDING + destinationEndpoint ?? 0, // not used with MULTICAST_BINDING ); - } + const result = await this.sendZdo(ZSpec.BLANK_EUI64, destinationNetworkAddress, clusterId, zdoPayload, false); - private async bindInternal( - bindType: 'bind' | 'unbind', - destinationNetworkAddress: number, - sourceIeeeAddress: string, - sourceEndpoint: number, - clusterID: number, - destinationAddressOrGroup: string | number, - targetType: 'endpoint' | 'group', - destinationEndpoint?: number, - ): Promise { - return await this.queue.execute(async () => { - this.checkInterpanLock(); - const response = this.waitForAreqZdo(`${bindType}Rsp`, {srcaddr: destinationNetworkAddress}); - - const payload = { - dstaddr: destinationNetworkAddress, - srcaddr: sourceIeeeAddress, - srcendpoint: sourceEndpoint, - clusterid: clusterID, - dstaddrmode: targetType === 'group' ? AddressMode.ADDR_GROUP : AddressMode.ADDR_64BIT, - dstaddress: this.toAddressString(destinationAddressOrGroup), - dstendpoint: targetType === 'group' ? 0xff : destinationEndpoint, - }; - - await this.znp.request(Subsystem.ZDO, `${bindType}Req`, payload, response.ID); - await response.start(); - }, destinationNetworkAddress); + /* istanbul ignore next */ + if (!Zdo.Buffalo.checkStatus(result)) { + // TODO: will disappear once moved upstream + throw new Zdo.StatusError(result[0]); + } } - public removeDevice(networkAddress: number, ieeeAddr: string): Promise { - return this.queue.execute(async () => { - this.checkInterpanLock(); - const response = this.waitForAreqZdo('mgmtLeaveRsp', {srcaddr: networkAddress}); + public async removeDevice(networkAddress: number, ieeeAddr: string): Promise { + const clusterId = Zdo.ClusterId.LEAVE_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, ieeeAddr as EUI64, Zdo.LeaveRequestFlags.WITHOUT_REJOIN); + const result = await this.sendZdo(ZSpec.BLANK_EUI64, networkAddress, clusterId, zdoPayload, false); - const payload = { - dstaddr: networkAddress, - deviceaddress: ieeeAddr, - removechildrenRejoin: 0, - }; - - await this.znp.request(Subsystem.ZDO, 'mgmtLeaveReq', payload, response.ID); - await response.start(); - }, networkAddress); + /* istanbul ignore next */ + if (!Zdo.Buffalo.checkStatus(result)) { + // TODO: will disappear once moved upstream + throw new Zdo.StatusError(result[0]); + } } /** @@ -892,6 +1016,10 @@ class ZStackAdapter extends Adapter { } if (object.subsystem === Subsystem.ZDO) { + if (isMtCmdAreqZdo(object.command)) { + this.emit('zdoResponse', object.command.zdoClusterId, object.payload.zdo); + } + if (object.command.name === 'tcDeviceInd') { const payload: Events.DeviceJoinedPayload = { networkAddress: object.payload.nwkaddr, @@ -900,17 +1028,17 @@ class ZStackAdapter extends Adapter { this.emit('deviceJoined', payload); } else if (object.command.name === 'endDeviceAnnceInd') { - const zdoResult = object.parseZdoPayload(); - + // TODO: better way??? /* istanbul ignore else */ - if (Zdo.Buffalo.checkStatus(zdoResult)) { + if (Zdo.Buffalo.checkStatus(object.payload.zdo)) { + const zdoPayload = object.payload.zdo[1]; const payload: Events.DeviceAnnouncePayload = { - networkAddress: zdoResult[1].nwkAddress, - ieeeAddr: zdoResult[1].eui64, + networkAddress: zdoPayload.nwkAddress, + ieeeAddr: zdoPayload.eui64, }; // Only discover routes to end devices, if bit 1 of capabilities === 0 it's an end device. - const isEndDevice = zdoResult[1].capabilities.deviceType === 0; + const isEndDevice = zdoPayload.capabilities.deviceType === 0; if (isEndDevice) { if (!this.deviceAnnounceRouteDiscoveryDebouncers.has(payload.networkAddress)) { // If a device announces multiple times in a very short time, it makes no sense @@ -933,20 +1061,6 @@ class ZStackAdapter extends Adapter { assert(debouncer); debouncer(); } - - this.emit('deviceAnnounce', payload); - } - } else if (object.command.name === 'nwkAddrRsp') { - const zdoResult = object.parseZdoPayload(); - - /* istanbul ignore else */ - if (Zdo.Buffalo.checkStatus(zdoResult)) { - const payload: Events.NetworkAddressPayload = { - networkAddress: zdoResult[1].nwkAddress, - ieeeAddr: zdoResult[1].eui64, - }; - - this.emit('networkAddress', payload); } } else if (object.command.name === 'concentratorIndCb') { // Some routers may change short addresses and the announcement @@ -959,12 +1073,15 @@ class ZStackAdapter extends Adapter { // mappings. // https://e2e.ti.com/cfs-file/__key/communityserver-discussions-components-files/158/4403.zstacktask.c // https://github.com/Koenkk/zigbee-herdsman/issues/74 - const payload: Events.NetworkAddressPayload = { - networkAddress: object.payload.srcaddr, - ieeeAddr: object.payload.extaddr, - }; - - this.emit('networkAddress', payload); + this.emit('zdoResponse', Zdo.ClusterId.NETWORK_ADDRESS_RESPONSE, [ + Zdo.Status.SUCCESS, + { + eui64: object.payload.extaddr, + nwkAddress: object.payload.srcaddr, + startIndex: 0, + assocDevList: [], + } as ZdoTypes.NetworkAddressResponse, + ]); } else { /* istanbul ignore else */ if (object.command.name === 'leaveInd') { @@ -1101,22 +1218,12 @@ class ZStackAdapter extends Adapter { } public async changeChannel(newChannel: number): Promise { - return await this.queue.execute(async () => { - this.checkInterpanLock(); + const clusterId = Zdo.ClusterId.NWK_UPDATE_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, [newChannel], 0xfe, 0, undefined, 0); - const payload = { - dstaddr: 0xffff, // broadcast with sleepy - dstaddrmode: AddressMode.ADDR_BROADCAST, - channelmask: [newChannel].reduce((a, c) => a + (1 << c), 0), - scanduration: 0xfe, // change channel - scancount: 0, - nwkmanageraddr: 0, - }; - - await this.znp.request(Subsystem.ZDO, 'mgmtNwkUpdateReq', payload); - // wait for the broadcast to propagate and the adapter to actually change - await Wait(10000); - }); + await this.sendZdo(ZSpec.BLANK_EUI64, ZSpec.BroadcastAddress.SLEEPY, clusterId, zdoPayload, true /* handled below */); + // wait for the broadcast to propagate and the adapter to actually change + await Wait(10000); } public async setTransmitPower(value: number): Promise { @@ -1125,27 +1232,6 @@ class ZStackAdapter extends Adapter { }); } - private waitForAreqZdo(command: string, payload?: ZpiObjectPayload): {start: () => Promise; ID: number} { - const result = this.znp.waitFor(UnpiConstants.Type.AREQ, Subsystem.ZDO, command, payload); - const start = (): Promise => { - const startResult = result.start(); - return new Promise((resolve, reject) => { - startResult.promise - .then((response) => { - const [status, payload] = response.parseZdoPayload(); - - if (status === Zdo.Status.SUCCESS) { - resolve(payload as T); - } else { - reject(new Zdo.StatusError(status)); - } - }) - .catch(reject); - }); - }; - return {start, ID: result.ID}; - } - private waitForInternal( networkAddress: number | undefined, endpoint: number, @@ -1208,7 +1294,7 @@ class ZStackAdapter extends Adapter { timeout: number, ): Promise { const transactionID = this.nextTransactionID(); - const response = this.znp.waitFor(Type.AREQ, Subsystem.AF, 'dataConfirm', {transid: transactionID}, timeout); + const response = this.znp.waitFor(Type.AREQ, Subsystem.AF, 'dataConfirm', undefined, transactionID, undefined, timeout); await this.znp.request( Subsystem.AF, @@ -1252,7 +1338,9 @@ class ZStackAdapter extends Adapter { attemptsLeft = 5, ): Promise { const transactionID = this.nextTransactionID(); - const response = confirmation ? this.znp.waitFor(Type.AREQ, Subsystem.AF, 'dataConfirm', {transid: transactionID}, timeout) : undefined; + const response = confirmation + ? this.znp.waitFor(Type.AREQ, Subsystem.AF, 'dataConfirm', undefined, transactionID, undefined, timeout) + : undefined; await this.znp.request( Subsystem.AF, diff --git a/src/adapter/z-stack/znp/definition.ts b/src/adapter/z-stack/znp/definition.ts index ea7f367919..21b7bdf256 100644 --- a/src/adapter/z-stack/znp/definition.ts +++ b/src/adapter/z-stack/znp/definition.ts @@ -3,18 +3,6 @@ import {Type as CommandType, Subsystem} from '../unpi/constants'; import ParameterType from './parameterType'; import {MtCmd} from './tstype'; -const convertSkipSrcAddr = (buffer: Buffer): Buffer => buffer.subarray(2); -const convertSwapStartIndexNumAssocDev = (buffer: Buffer): Buffer => { - // ZStack swaps the `startindex` and `numassocdev` compared to ZDO. - // Swap them back here. - const startIndex = buffer[11]; - const assocDevCount = buffer[12]; - const newBuffer = Buffer.from(buffer); - newBuffer[11] = assocDevCount; - newBuffer[12] = startIndex; - return newBuffer; -}; - const Definition: { [Subsystem.SYS]: MtCmd[]; [Subsystem.MAC]: MtCmd[]; @@ -1054,79 +1042,86 @@ const Definition: { name: 'nwkAddrReq', ID: 0, type: CommandType.SREQ, - request: [ - {name: 'ieeeaddr', parameterType: ParameterType.IEEEADDR}, - {name: 'reqtype', parameterType: ParameterType.UINT8}, - {name: 'startindex', parameterType: ParameterType.UINT8}, - ], - response: [{name: 'status', parameterType: ParameterType.UINT8}], + zdoClusterId: ZdoClusterId.NETWORK_ADDRESS_REQUEST, + // request: [ + // {name: 'ieeeaddr', parameterType: ParameterType.IEEEADDR}, + // {name: 'reqtype', parameterType: ParameterType.UINT8}, + // {name: 'startindex', parameterType: ParameterType.UINT8}, + // ], + // response: [{name: 'status', parameterType: ParameterType.UINT8}], }, { name: 'ieeeAddrReq', ID: 1, type: CommandType.SREQ, - request: [ - {name: 'shortaddr', parameterType: ParameterType.UINT16}, - {name: 'reqtype', parameterType: ParameterType.UINT8}, - {name: 'startindex', parameterType: ParameterType.UINT8}, - ], - response: [{name: 'status', parameterType: ParameterType.UINT8}], + zdoClusterId: ZdoClusterId.IEEE_ADDRESS_REQUEST, + // request: [ + // {name: 'shortaddr', parameterType: ParameterType.UINT16}, + // {name: 'reqtype', parameterType: ParameterType.UINT8}, + // {name: 'startindex', parameterType: ParameterType.UINT8}, + // ], + // response: [{name: 'status', parameterType: ParameterType.UINT8}], }, { name: 'nodeDescReq', ID: 2, type: CommandType.SREQ, - request: [ - {name: 'dstaddr', parameterType: ParameterType.UINT16}, - {name: 'nwkaddrofinterest', parameterType: ParameterType.UINT16}, - ], - response: [{name: 'status', parameterType: ParameterType.UINT8}], + zdoClusterId: ZdoClusterId.NODE_DESCRIPTOR_REQUEST, + // request: [ + // {name: 'dstaddr', parameterType: ParameterType.UINT16}, + // {name: 'nwkaddrofinterest', parameterType: ParameterType.UINT16}, + // ], + // response: [{name: 'status', parameterType: ParameterType.UINT8}], }, { name: 'powerDescReq', ID: 3, type: CommandType.SREQ, - request: [ - {name: 'dstaddr', parameterType: ParameterType.UINT16}, - {name: 'nwkaddrofinterest', parameterType: ParameterType.UINT16}, - ], - response: [{name: 'status', parameterType: ParameterType.UINT8}], + zdoClusterId: ZdoClusterId.POWER_DESCRIPTOR_REQUEST, + // request: [ + // {name: 'dstaddr', parameterType: ParameterType.UINT16}, + // {name: 'nwkaddrofinterest', parameterType: ParameterType.UINT16}, + // ], + // response: [{name: 'status', parameterType: ParameterType.UINT8}], }, { name: 'simpleDescReq', ID: 4, type: CommandType.SREQ, - request: [ - {name: 'dstaddr', parameterType: ParameterType.UINT16}, - {name: 'nwkaddrofinterest', parameterType: ParameterType.UINT16}, - {name: 'endpoint', parameterType: ParameterType.UINT8}, - ], - response: [{name: 'status', parameterType: ParameterType.UINT8}], + zdoClusterId: ZdoClusterId.SIMPLE_DESCRIPTOR_REQUEST, + // request: [ + // {name: 'dstaddr', parameterType: ParameterType.UINT16}, + // {name: 'nwkaddrofinterest', parameterType: ParameterType.UINT16}, + // {name: 'endpoint', parameterType: ParameterType.UINT8}, + // ], + // response: [{name: 'status', parameterType: ParameterType.UINT8}], }, { name: 'activeEpReq', ID: 5, type: CommandType.SREQ, - request: [ - {name: 'dstaddr', parameterType: ParameterType.UINT16}, - {name: 'nwkaddrofinterest', parameterType: ParameterType.UINT16}, - ], - response: [{name: 'status', parameterType: ParameterType.UINT8}], + zdoClusterId: ZdoClusterId.ACTIVE_ENDPOINTS_REQUEST, + // request: [ + // {name: 'dstaddr', parameterType: ParameterType.UINT16}, + // {name: 'nwkaddrofinterest', parameterType: ParameterType.UINT16}, + // ], + // response: [{name: 'status', parameterType: ParameterType.UINT8}], }, { name: 'matchDescReq', ID: 6, type: CommandType.SREQ, - request: [ - {name: 'dstaddr', parameterType: ParameterType.UINT16}, - {name: 'nwkaddrofinterest', parameterType: ParameterType.UINT16}, - {name: 'profileid', parameterType: ParameterType.UINT16}, - {name: 'numinclusters', parameterType: ParameterType.UINT8}, - {name: 'inclusterlist', parameterType: ParameterType.LIST_UINT16}, - {name: 'numoutclusters', parameterType: ParameterType.UINT8}, - {name: 'outclusterlist', parameterType: ParameterType.LIST_UINT16}, - ], - response: [{name: 'status', parameterType: ParameterType.UINT8}], + zdoClusterId: ZdoClusterId.MATCH_DESCRIPTORS_REQUEST, + // request: [ + // {name: 'dstaddr', parameterType: ParameterType.UINT16}, + // {name: 'nwkaddrofinterest', parameterType: ParameterType.UINT16}, + // {name: 'profileid', parameterType: ParameterType.UINT16}, + // {name: 'numinclusters', parameterType: ParameterType.UINT8}, + // {name: 'inclusterlist', parameterType: ParameterType.LIST_UINT16}, + // {name: 'numoutclusters', parameterType: ParameterType.UINT8}, + // {name: 'outclusterlist', parameterType: ParameterType.LIST_UINT16}, + // ], + // response: [{name: 'status', parameterType: ParameterType.UINT8}], }, { name: 'complexDescReq', @@ -1175,8 +1170,9 @@ const Definition: { name: 'serverDiscReq', ID: 12, type: CommandType.SREQ, - request: [{name: 'servermask', parameterType: ParameterType.UINT16}], - response: [{name: 'status', parameterType: ParameterType.UINT8}], + zdoClusterId: ZdoClusterId.SYSTEM_SERVER_DISCOVERY_REQUEST, + // request: [{name: 'servermask', parameterType: ParameterType.UINT16}], + // response: [{name: 'status', parameterType: ParameterType.UINT8}], }, { name: 'endDeviceBindReq', @@ -1199,31 +1195,33 @@ const Definition: { name: 'bindReq', ID: 33, type: CommandType.SREQ, - request: [ - {name: 'dstaddr', parameterType: ParameterType.UINT16}, - {name: 'srcaddr', parameterType: ParameterType.IEEEADDR}, - {name: 'srcendpoint', parameterType: ParameterType.UINT8}, - {name: 'clusterid', parameterType: ParameterType.UINT16}, - {name: 'dstaddrmode', parameterType: ParameterType.UINT8}, - {name: 'dstaddress', parameterType: ParameterType.IEEEADDR}, - {name: 'dstendpoint', parameterType: ParameterType.UINT8}, - ], - response: [{name: 'status', parameterType: ParameterType.UINT8}], + zdoClusterId: ZdoClusterId.BIND_REQUEST, + // request: [ + // {name: 'dstaddr', parameterType: ParameterType.UINT16}, + // {name: 'srcaddr', parameterType: ParameterType.IEEEADDR}, + // {name: 'srcendpoint', parameterType: ParameterType.UINT8}, + // {name: 'clusterid', parameterType: ParameterType.UINT16}, + // {name: 'dstaddrmode', parameterType: ParameterType.UINT8}, + // {name: 'dstaddress', parameterType: ParameterType.IEEEADDR}, + // {name: 'dstendpoint', parameterType: ParameterType.UINT8}, + // ], + // response: [{name: 'status', parameterType: ParameterType.UINT8}], }, { name: 'unbindReq', ID: 34, type: CommandType.SREQ, - request: [ - {name: 'dstaddr', parameterType: ParameterType.UINT16}, - {name: 'srcaddr', parameterType: ParameterType.IEEEADDR}, - {name: 'srcendpoint', parameterType: ParameterType.UINT8}, - {name: 'clusterid', parameterType: ParameterType.UINT16}, - {name: 'dstaddrmode', parameterType: ParameterType.UINT8}, - {name: 'dstaddress', parameterType: ParameterType.IEEEADDR}, - {name: 'dstendpoint', parameterType: ParameterType.UINT8}, - ], - response: [{name: 'status', parameterType: ParameterType.UINT8}], + zdoClusterId: ZdoClusterId.UNBIND_REQUEST, + // request: [ + // {name: 'dstaddr', parameterType: ParameterType.UINT16}, + // {name: 'srcaddr', parameterType: ParameterType.IEEEADDR}, + // {name: 'srcendpoint', parameterType: ParameterType.UINT8}, + // {name: 'clusterid', parameterType: ParameterType.UINT16}, + // {name: 'dstaddrmode', parameterType: ParameterType.UINT8}, + // {name: 'dstaddress', parameterType: ParameterType.IEEEADDR}, + // {name: 'dstendpoint', parameterType: ParameterType.UINT8}, + // ], + // response: [{name: 'status', parameterType: ParameterType.UINT8}], }, { name: 'setLinkKey', @@ -1294,42 +1292,46 @@ const Definition: { name: 'mgmtLqiReq', ID: 49, type: CommandType.SREQ, - request: [ - {name: 'dstaddr', parameterType: ParameterType.UINT16}, - {name: 'startindex', parameterType: ParameterType.UINT8}, - ], - response: [{name: 'status', parameterType: ParameterType.UINT8}], + zdoClusterId: ZdoClusterId.LQI_TABLE_REQUEST, + // request: [ + // {name: 'dstaddr', parameterType: ParameterType.UINT16}, + // {name: 'startindex', parameterType: ParameterType.UINT8}, + // ], + // response: [{name: 'status', parameterType: ParameterType.UINT8}], }, { name: 'mgmtRtgReq', ID: 50, type: CommandType.SREQ, - request: [ - {name: 'dstaddr', parameterType: ParameterType.UINT16}, - {name: 'startindex', parameterType: ParameterType.UINT8}, - ], - response: [{name: 'status', parameterType: ParameterType.UINT8}], + zdoClusterId: ZdoClusterId.ROUTING_TABLE_REQUEST, + // request: [ + // {name: 'dstaddr', parameterType: ParameterType.UINT16}, + // {name: 'startindex', parameterType: ParameterType.UINT8}, + // ], + // response: [{name: 'status', parameterType: ParameterType.UINT8}], }, { name: 'mgmtBindReq', ID: 51, type: CommandType.SREQ, - request: [ - {name: 'dstaddr', parameterType: ParameterType.UINT16}, - {name: 'startindex', parameterType: ParameterType.UINT8}, - ], - response: [{name: 'status', parameterType: ParameterType.UINT8}], + zdoClusterId: ZdoClusterId.BINDING_TABLE_REQUEST, + // request: [ + // {name: 'dstaddr', parameterType: ParameterType.UINT16}, + // {name: 'startindex', parameterType: ParameterType.UINT8}, + // ], + // response: [{name: 'status', parameterType: ParameterType.UINT8}], }, { name: 'mgmtLeaveReq', ID: 52, type: CommandType.SREQ, - request: [ - {name: 'dstaddr', parameterType: ParameterType.UINT16}, - {name: 'deviceaddress', parameterType: ParameterType.IEEEADDR}, - {name: 'removechildrenRejoin', parameterType: ParameterType.UINT8}, - ], - response: [{name: 'status', parameterType: ParameterType.UINT8}], + zdoClusterId: ZdoClusterId.LEAVE_REQUEST, + // request: [ + // {name: 'dstaddr', parameterType: ParameterType.UINT16}, + // {name: 'deviceaddress', parameterType: ParameterType.IEEEADDR}, + // {name: 'removechildrenRejoin', parameterType: ParameterType.UINT8}, + // ], + // response: [{name: 'status', parameterType: ParameterType.UINT8}], }, { name: 'mgmtDirectJoinReq', @@ -1346,28 +1348,30 @@ const Definition: { name: 'mgmtPermitJoinReq', ID: 54, type: CommandType.SREQ, - request: [ - {name: 'addrmode', parameterType: ParameterType.UINT8}, - {name: 'dstaddr', parameterType: ParameterType.UINT16}, - {name: 'duration', parameterType: ParameterType.UINT8}, - {name: 'tcsignificance', parameterType: ParameterType.UINT8}, - ], - response: [{name: 'status', parameterType: ParameterType.UINT8}], + zdoClusterId: ZdoClusterId.PERMIT_JOINING_REQUEST, + // request: [ + // {name: 'addrmode', parameterType: ParameterType.UINT8}, + // {name: 'dstaddr', parameterType: ParameterType.UINT16}, + // {name: 'duration', parameterType: ParameterType.UINT8}, + // {name: 'tcsignificance', parameterType: ParameterType.UINT8}, + // ], + // response: [{name: 'status', parameterType: ParameterType.UINT8}], }, { name: 'mgmtNwkUpdateReq', ID: 55, // TODO: 0x0038 => 56?? type: CommandType.SREQ, - request: [ - {name: 'dstaddr', parameterType: ParameterType.UINT16}, - {name: 'dstaddrmode', parameterType: ParameterType.UINT8}, - {name: 'channelmask', parameterType: ParameterType.UINT32}, - {name: 'scanduration', parameterType: ParameterType.UINT8}, - // TODO: below two have various combinations of present/not present depending on scanduration - {name: 'scancount', parameterType: ParameterType.UINT8}, - {name: 'nwkmanageraddr', parameterType: ParameterType.UINT16}, - ], - response: [{name: 'status', parameterType: ParameterType.UINT8}], + zdoClusterId: ZdoClusterId.NWK_UPDATE_REQUEST, + // request: [ + // {name: 'dstaddr', parameterType: ParameterType.UINT16}, + // {name: 'dstaddrmode', parameterType: ParameterType.UINT8}, + // {name: 'channelmask', parameterType: ParameterType.UINT32}, + // {name: 'scanduration', parameterType: ParameterType.UINT8}, + // // TODO: below two have various combinations of present/not present depending on scanduration + // {name: 'scancount', parameterType: ParameterType.UINT8}, + // {name: 'nwkmanageraddr', parameterType: ParameterType.UINT16}, + // ], + // response: [{name: 'status', parameterType: ParameterType.UINT8}], }, { name: 'msgCbRegister', @@ -1400,130 +1404,109 @@ const Definition: { name: 'nwkAddrRsp', ID: 128, type: CommandType.AREQ, - zdo: { - cluterId: ZdoClusterId.NETWORK_ADDRESS_RESPONSE, - convert: convertSwapStartIndexNumAssocDev, - }, - request: [ - {name: 'status', parameterType: ParameterType.UINT8}, - // Parse the ieeeaddr as it is needed for ZNP waitFor (see zStackAdapter.requestNetworkAddress()) - {name: 'ieeeaddr', parameterType: ParameterType.IEEEADDR}, - // {name: 'nwkaddr', parameterType: ParameterType.UINT16}, - // {name: 'startindex', parameterType: ParameterType.UINT8}, - // {name: 'numassocdev', parameterType: ParameterType.UINT8}, - // {name: 'assocdevlist', parameterType: ParameterType.LIST_ASSOC_DEV}, - ], + zdoClusterId: ZdoClusterId.NETWORK_ADDRESS_RESPONSE, + // request: [ + // {name: 'status', parameterType: ParameterType.UINT8}, + // Parse the ieeeaddr as it is needed for ZNP waitFor (see zStackAdapter.requestNetworkAddress()) + // {name: 'ieeeaddr', parameterType: ParameterType.IEEEADDR}, + // {name: 'nwkaddr', parameterType: ParameterType.UINT16}, + // {name: 'startindex', parameterType: ParameterType.UINT8}, + // {name: 'numassocdev', parameterType: ParameterType.UINT8}, + // {name: 'assocdevlist', parameterType: ParameterType.LIST_ASSOC_DEV}, + // ], }, { name: 'ieeeAddrRsp', ID: 129, type: CommandType.AREQ, - zdo: { - cluterId: ZdoClusterId.IEEE_ADDRESS_RESPONSE, - convert: convertSwapStartIndexNumAssocDev, - }, - request: [ - // {name: 'status', parameterType: ParameterType.UINT8}, - // {name: 'ieeeaddr', parameterType: ParameterType.IEEEADDR}, - // {name: 'nwkaddr', parameterType: ParameterType.UINT16}, - // {name: 'startindex', parameterType: ParameterType.UINT8}, - // {name: 'numassocdev', parameterType: ParameterType.UINT8}, - // {name: 'assocdevlist', parameterType: ParameterType.LIST_ASSOC_DEV}, - ], + zdoClusterId: ZdoClusterId.IEEE_ADDRESS_RESPONSE, + // request: [ + // {name: 'status', parameterType: ParameterType.UINT8}, + // {name: 'ieeeaddr', parameterType: ParameterType.IEEEADDR}, + // {name: 'nwkaddr', parameterType: ParameterType.UINT16}, + // {name: 'startindex', parameterType: ParameterType.UINT8}, + // {name: 'numassocdev', parameterType: ParameterType.UINT8}, + // {name: 'assocdevlist', parameterType: ParameterType.LIST_ASSOC_DEV}, + // ], }, { name: 'nodeDescRsp', ID: 130, type: CommandType.AREQ, - zdo: { - cluterId: ZdoClusterId.NODE_DESCRIPTOR_RESPONSE, - convert: convertSkipSrcAddr, - }, - request: [ - {name: 'srcaddr', parameterType: ParameterType.UINT16}, - // {name: 'status', parameterType: ParameterType.UINT8}, - // {name: 'nwkaddr', parameterType: ParameterType.UINT16}, - // {name: 'logicaltype_cmplxdescavai_userdescavai', parameterType: ParameterType.UINT8}, - // {name: 'apsflags_freqband', parameterType: ParameterType.UINT8}, - // {name: 'maccapflags', parameterType: ParameterType.UINT8}, - // {name: 'manufacturercode', parameterType: ParameterType.UINT16}, - // {name: 'maxbuffersize', parameterType: ParameterType.UINT8}, - // {name: 'maxintransfersize', parameterType: ParameterType.UINT16}, - // {name: 'servermask', parameterType: ParameterType.UINT16}, - // {name: 'maxouttransfersize', parameterType: ParameterType.UINT16}, - // {name: 'descriptorcap', parameterType: ParameterType.UINT8}, - ], + zdoClusterId: ZdoClusterId.NODE_DESCRIPTOR_RESPONSE, + // request: [ + // {name: 'srcaddr', parameterType: ParameterType.UINT16}, + // {name: 'status', parameterType: ParameterType.UINT8}, + // {name: 'nwkaddr', parameterType: ParameterType.UINT16}, + // {name: 'logicaltype_cmplxdescavai_userdescavai', parameterType: ParameterType.UINT8}, + // {name: 'apsflags_freqband', parameterType: ParameterType.UINT8}, + // {name: 'maccapflags', parameterType: ParameterType.UINT8}, + // {name: 'manufacturercode', parameterType: ParameterType.UINT16}, + // {name: 'maxbuffersize', parameterType: ParameterType.UINT8}, + // {name: 'maxintransfersize', parameterType: ParameterType.UINT16}, + // {name: 'servermask', parameterType: ParameterType.UINT16}, + // {name: 'maxouttransfersize', parameterType: ParameterType.UINT16}, + // {name: 'descriptorcap', parameterType: ParameterType.UINT8}, + // ], }, { name: 'powerDescRsp', ID: 131, type: CommandType.AREQ, - zdo: { - cluterId: ZdoClusterId.POWER_DESCRIPTOR_RESPONSE, - convert: convertSkipSrcAddr, - }, - request: [ - {name: 'srcaddr', parameterType: ParameterType.UINT16}, - // {name: 'status', parameterType: ParameterType.UINT8}, - // {name: 'nwkaddr', parameterType: ParameterType.UINT16}, - // {name: 'currentpowermode_avaipowersrc', parameterType: ParameterType.UINT8}, - // {name: 'currentpowersrc_currentpowersrclevel', parameterType: ParameterType.UINT8}, - ], + zdoClusterId: ZdoClusterId.POWER_DESCRIPTOR_RESPONSE, + // request: [ + // {name: 'srcaddr', parameterType: ParameterType.UINT16}, + // {name: 'status', parameterType: ParameterType.UINT8}, + // {name: 'nwkaddr', parameterType: ParameterType.UINT16}, + // {name: 'currentpowermode_avaipowersrc', parameterType: ParameterType.UINT8}, + // {name: 'currentpowersrc_currentpowersrclevel', parameterType: ParameterType.UINT8}, + // ], }, { name: 'simpleDescRsp', ID: 132, type: CommandType.AREQ, - zdo: { - cluterId: ZdoClusterId.SIMPLE_DESCRIPTOR_RESPONSE, - convert: convertSkipSrcAddr, - }, - request: [ - {name: 'srcaddr', parameterType: ParameterType.UINT16}, - // {name: 'status', parameterType: ParameterType.UINT8}, - // {name: 'nwkaddr', parameterType: ParameterType.UINT16}, - // {name: 'len', parameterType: ParameterType.UINT8}, - // {name: 'endpoint', parameterType: ParameterType.UINT8}, - // {name: 'profileid', parameterType: ParameterType.UINT16}, - // {name: 'deviceid', parameterType: ParameterType.UINT16}, - // {name: 'deviceversion', parameterType: ParameterType.UINT8}, - // {name: 'numinclusters', parameterType: ParameterType.UINT8}, - // {name: 'inclusterlist', parameterType: ParameterType.LIST_UINT16}, - // {name: 'numoutclusters', parameterType: ParameterType.UINT8}, - // {name: 'outclusterlist', parameterType: ParameterType.LIST_UINT16}, - ], + zdoClusterId: ZdoClusterId.SIMPLE_DESCRIPTOR_RESPONSE, + // request: [ + // {name: 'srcaddr', parameterType: ParameterType.UINT16}, + // {name: 'status', parameterType: ParameterType.UINT8}, + // {name: 'nwkaddr', parameterType: ParameterType.UINT16}, + // {name: 'len', parameterType: ParameterType.UINT8}, + // {name: 'endpoint', parameterType: ParameterType.UINT8}, + // {name: 'profileid', parameterType: ParameterType.UINT16}, + // {name: 'deviceid', parameterType: ParameterType.UINT16}, + // {name: 'deviceversion', parameterType: ParameterType.UINT8}, + // {name: 'numinclusters', parameterType: ParameterType.UINT8}, + // {name: 'inclusterlist', parameterType: ParameterType.LIST_UINT16}, + // {name: 'numoutclusters', parameterType: ParameterType.UINT8}, + // {name: 'outclusterlist', parameterType: ParameterType.LIST_UINT16}, + // ], }, { name: 'activeEpRsp', ID: 133, type: CommandType.AREQ, - zdo: { - cluterId: ZdoClusterId.ACTIVE_ENDPOINTS_RESPONSE, - convert: convertSkipSrcAddr, - }, - request: [ - {name: 'srcaddr', parameterType: ParameterType.UINT16}, - // {name: 'status', parameterType: ParameterType.UINT8}, - // {name: 'nwkaddr', parameterType: ParameterType.UINT16}, - // {name: 'activeepcount', parameterType: ParameterType.UINT8}, - // {name: 'activeeplist', parameterType: ParameterType.LIST_UINT8}, - ], + zdoClusterId: ZdoClusterId.ACTIVE_ENDPOINTS_RESPONSE, + // request: [ + // {name: 'srcaddr', parameterType: ParameterType.UINT16}, + // {name: 'status', parameterType: ParameterType.UINT8}, + // {name: 'nwkaddr', parameterType: ParameterType.UINT16}, + // {name: 'activeepcount', parameterType: ParameterType.UINT8}, + // {name: 'activeeplist', parameterType: ParameterType.LIST_UINT8}, + // ], }, { name: 'matchDescRsp', ID: 134, type: CommandType.AREQ, - zdo: { - cluterId: ZdoClusterId.MATCH_DESCRIPTORS_RESPONSE, - convert: convertSkipSrcAddr, - }, - request: [ - {name: 'srcaddr', parameterType: ParameterType.UINT16}, - // {name: 'status', parameterType: ParameterType.UINT8}, - // {name: 'nwkaddr', parameterType: ParameterType.UINT16}, - // {name: 'matchlength', parameterType: ParameterType.UINT8}, - // {name: 'matchlist', parameterType: ParameterType.BUFFER}, - ], + zdoClusterId: ZdoClusterId.MATCH_DESCRIPTORS_RESPONSE, + // request: [ + // {name: 'srcaddr', parameterType: ParameterType.UINT16}, + // {name: 'status', parameterType: ParameterType.UINT8}, + // {name: 'nwkaddr', parameterType: ParameterType.UINT16}, + // {name: 'matchlength', parameterType: ParameterType.UINT8}, + // {name: 'matchlist', parameterType: ParameterType.BUFFER}, + // ], }, { name: 'complexDescRsp', @@ -1563,15 +1546,12 @@ const Definition: { name: 'serverDiscRsp', ID: 138, type: CommandType.AREQ, - zdo: { - cluterId: ZdoClusterId.SYSTEM_SERVER_DISCOVERY_RESPONSE, - convert: convertSkipSrcAddr, - }, - request: [ - {name: 'srcaddr', parameterType: ParameterType.UINT16}, - // {name: 'status', parameterType: ParameterType.UINT8}, - // {name: 'servermask', parameterType: ParameterType.UINT16}, - ], + zdoClusterId: ZdoClusterId.SYSTEM_SERVER_DISCOVERY_RESPONSE, + // request: [ + // {name: 'srcaddr', parameterType: ParameterType.UINT16}, + // {name: 'status', parameterType: ParameterType.UINT8}, + // {name: 'servermask', parameterType: ParameterType.UINT16}, + // ], }, { // https://github.com/Koenkk/zigbee2mqtt/issues/3363 @@ -1593,27 +1573,21 @@ const Definition: { name: 'bindRsp', ID: 161, type: CommandType.AREQ, - zdo: { - cluterId: ZdoClusterId.BIND_RESPONSE, - convert: convertSkipSrcAddr, - }, - request: [ - {name: 'srcaddr', parameterType: ParameterType.UINT16}, - // {name: 'status', parameterType: ParameterType.UINT8}, - ], + zdoClusterId: ZdoClusterId.BIND_RESPONSE, + // request: [ + // {name: 'srcaddr', parameterType: ParameterType.UINT16}, + // {name: 'status', parameterType: ParameterType.UINT8}, + // ], }, { name: 'unbindRsp', ID: 162, type: CommandType.AREQ, - zdo: { - cluterId: ZdoClusterId.UNBIND_RESPONSE, - convert: convertSkipSrcAddr, - }, - request: [ - {name: 'srcaddr', parameterType: ParameterType.UINT16}, - // {name: 'status', parameterType: ParameterType.UINT8}, - ], + zdoClusterId: ZdoClusterId.UNBIND_RESPONSE, + // request: [ + // {name: 'srcaddr', parameterType: ParameterType.UINT16}, + // {name: 'status', parameterType: ParameterType.UINT8}, + // ], }, { name: 'mgmtNwkDiscRsp', @@ -1632,65 +1606,53 @@ const Definition: { name: 'mgmtLqiRsp', ID: 177, type: CommandType.AREQ, - zdo: { - cluterId: ZdoClusterId.LQI_TABLE_RESPONSE, - convert: convertSkipSrcAddr, - }, - request: [ - {name: 'srcaddr', parameterType: ParameterType.UINT16}, - // {name: 'status', parameterType: ParameterType.UINT8}, - // {name: 'neighbortableentries', parameterType: ParameterType.UINT8}, - // {name: 'startindex', parameterType: ParameterType.UINT8}, - // {name: 'neighborlqilistcount', parameterType: ParameterType.UINT8}, - // {name: 'neighborlqilist', parameterType: ParameterType.LIST_NEIGHBOR_LQI}, - ], + zdoClusterId: ZdoClusterId.LQI_TABLE_RESPONSE, + // request: [ + // {name: 'srcaddr', parameterType: ParameterType.UINT16}, + // {name: 'status', parameterType: ParameterType.UINT8}, + // {name: 'neighbortableentries', parameterType: ParameterType.UINT8}, + // {name: 'startindex', parameterType: ParameterType.UINT8}, + // {name: 'neighborlqilistcount', parameterType: ParameterType.UINT8}, + // {name: 'neighborlqilist', parameterType: ParameterType.LIST_NEIGHBOR_LQI}, + // ], }, { name: 'mgmtRtgRsp', ID: 178, type: CommandType.AREQ, - zdo: { - cluterId: ZdoClusterId.ROUTING_TABLE_RESPONSE, - convert: convertSkipSrcAddr, - }, - request: [ - {name: 'srcaddr', parameterType: ParameterType.UINT16}, - // {name: 'status', parameterType: ParameterType.UINT8}, - // {name: 'routingtableentries', parameterType: ParameterType.UINT8}, - // {name: 'startindex', parameterType: ParameterType.UINT8}, - // {name: 'routingtablelistcount', parameterType: ParameterType.UINT8}, - // {name: 'routingtablelist', parameterType: ParameterType.LIST_ROUTING_TABLE}, - ], + zdoClusterId: ZdoClusterId.ROUTING_TABLE_RESPONSE, + // request: [ + // {name: 'srcaddr', parameterType: ParameterType.UINT16}, + // {name: 'status', parameterType: ParameterType.UINT8}, + // {name: 'routingtableentries', parameterType: ParameterType.UINT8}, + // {name: 'startindex', parameterType: ParameterType.UINT8}, + // {name: 'routingtablelistcount', parameterType: ParameterType.UINT8}, + // {name: 'routingtablelist', parameterType: ParameterType.LIST_ROUTING_TABLE}, + // ], }, { name: 'mgmtBindRsp', ID: 179, type: CommandType.AREQ, - zdo: { - cluterId: ZdoClusterId.BINDING_TABLE_RESPONSE, - convert: convertSkipSrcAddr, - }, - request: [ - {name: 'srcaddr', parameterType: ParameterType.UINT16}, - // {name: 'status', parameterType: ParameterType.UINT8}, - // {name: 'bindingtableentries', parameterType: ParameterType.UINT8}, - // {name: 'startindex', parameterType: ParameterType.UINT8}, - // {name: 'bindingtablelistcount', parameterType: ParameterType.UINT8}, - // {name: 'bindingtablelist', parameterType: ParameterType.LIST_BIND_TABLE}, - ], + zdoClusterId: ZdoClusterId.BINDING_TABLE_RESPONSE, + // request: [ + // {name: 'srcaddr', parameterType: ParameterType.UINT16}, + // {name: 'status', parameterType: ParameterType.UINT8}, + // {name: 'bindingtableentries', parameterType: ParameterType.UINT8}, + // {name: 'startindex', parameterType: ParameterType.UINT8}, + // {name: 'bindingtablelistcount', parameterType: ParameterType.UINT8}, + // {name: 'bindingtablelist', parameterType: ParameterType.LIST_BIND_TABLE}, + // ], }, { name: 'mgmtLeaveRsp', ID: 180, type: CommandType.AREQ, - zdo: { - cluterId: ZdoClusterId.LEAVE_RESPONSE, - convert: convertSkipSrcAddr, - }, - request: [ - {name: 'srcaddr', parameterType: ParameterType.UINT16}, - // {name: 'status', parameterType: ParameterType.UINT8}, - ], + zdoClusterId: ZdoClusterId.LEAVE_RESPONSE, + // request: [ + // {name: 'srcaddr', parameterType: ParameterType.UINT16}, + // {name: 'status', parameterType: ParameterType.UINT8}, + // ], }, { name: 'mgmtDirectJoinRsp', @@ -1705,32 +1667,26 @@ const Definition: { name: 'mgmtPermitJoinRsp', ID: 182, type: CommandType.AREQ, - zdo: { - cluterId: ZdoClusterId.PERMIT_JOINING_RESPONSE, - convert: convertSkipSrcAddr, - }, - request: [ - {name: 'srcaddr', parameterType: ParameterType.UINT16}, - // {name: 'status', parameterType: ParameterType.UINT8}, - ], + zdoClusterId: ZdoClusterId.PERMIT_JOINING_RESPONSE, + // request: [ + // {name: 'srcaddr', parameterType: ParameterType.UINT16}, + // {name: 'status', parameterType: ParameterType.UINT8}, + // ], }, { name: 'mgmtNwkUpdateNotify', ID: 184, type: CommandType.AREQ, - zdo: { - cluterId: ZdoClusterId.NWK_UPDATE_RESPONSE, - convert: convertSkipSrcAddr, - }, - request: [ - {name: 'srcaddr', parameterType: ParameterType.UINT16}, - // {name: 'status', parameterType: ParameterType.UINT8}, - // {name: 'scannedchannels', parameterType: ParameterType.UINT32}, - // {name: 'totaltrans', parameterType: ParameterType.UINT16}, - // {name: 'transfails', parameterType: ParameterType.UINT16}, - // {name: 'energylength', parameterType: ParameterType.UINT8}, - // {name: 'energyvalues', parameterType: ParameterType.LIST_UINT8}, - ], + zdoClusterId: ZdoClusterId.NWK_UPDATE_RESPONSE, + // request: [ + // {name: 'srcaddr', parameterType: ParameterType.UINT16}, + // {name: 'status', parameterType: ParameterType.UINT8}, + // {name: 'scannedchannels', parameterType: ParameterType.UINT32}, + // {name: 'totaltrans', parameterType: ParameterType.UINT16}, + // {name: 'transfails', parameterType: ParameterType.UINT16}, + // {name: 'energylength', parameterType: ParameterType.UINT8}, + // {name: 'energyvalues', parameterType: ParameterType.LIST_UINT8}, + // ], }, { name: 'stateChangeInd', @@ -1742,16 +1698,13 @@ const Definition: { name: 'endDeviceAnnceInd', ID: 193, type: CommandType.AREQ, - zdo: { - cluterId: ZdoClusterId.END_DEVICE_ANNOUNCE, - convert: convertSkipSrcAddr, - }, - request: [ - {name: 'srcaddr', parameterType: ParameterType.UINT16}, - // {name: 'nwkaddr', parameterType: ParameterType.UINT16}, - // {name: 'ieeeaddr', parameterType: ParameterType.IEEEADDR}, - // {name: 'capabilities', parameterType: ParameterType.UINT8}, - ], + zdoClusterId: ZdoClusterId.END_DEVICE_ANNOUNCE, + // request: [ + // {name: 'srcaddr', parameterType: ParameterType.UINT16}, + // {name: 'nwkaddr', parameterType: ParameterType.UINT16}, + // {name: 'ieeeaddr', parameterType: ParameterType.IEEEADDR}, + // {name: 'capabilities', parameterType: ParameterType.UINT8}, + // ], }, { name: 'matchDescRspSent', diff --git a/src/adapter/z-stack/znp/tstype.ts b/src/adapter/z-stack/znp/tstype.ts index 4ac1f8085c..8008f0edc6 100644 --- a/src/adapter/z-stack/znp/tstype.ts +++ b/src/adapter/z-stack/znp/tstype.ts @@ -15,22 +15,26 @@ interface MtCmdBase { type: number; request: MtParameter[]; response: MtParameter[]; - zdo: {cluterId: ZdoClusterId; convert: (buffer: Buffer) => Buffer}; + zdoClusterId: ZdoClusterId; } -interface MtCmdAreq extends Omit { +interface MtCmdAreq extends Omit { type: CommandType.AREQ; } -interface MtCmdSreq extends Omit { +interface MtCmdSreq extends Omit { type: CommandType.SREQ; } -export interface MtCmdAreqZdo extends Omit { +export interface MtCmdAreqZdo extends Omit { type: CommandType.AREQ; } -export type MtCmd = MtCmdAreq | MtCmdSreq | MtCmdAreqZdo; +export interface MtCmdSreqZdo extends Omit { + type: CommandType.SREQ; +} + +export type MtCmd = MtCmdAreq | MtCmdSreq | MtCmdAreqZdo | MtCmdSreqZdo; // eslint-disable-next-line @typescript-eslint/no-explicit-any export type ZpiObjectPayload = {[s: string]: any}; diff --git a/src/adapter/z-stack/znp/utils.ts b/src/adapter/z-stack/znp/utils.ts index 84f53ad519..e6d6df9076 100644 --- a/src/adapter/z-stack/znp/utils.ts +++ b/src/adapter/z-stack/znp/utils.ts @@ -1,7 +1,10 @@ -import assert from 'assert'; +import {Type} from '../unpi/constants'; +import {MtCmd, MtCmdAreqZdo, MtCmdSreqZdo} from './tstype'; -import {MtCmd, MtCmdAreqZdo} from './tstype'; +export function isMtCmdAreqZdo(cmd: MtCmd): cmd is MtCmdAreqZdo { + return cmd.type === Type.AREQ && 'zdoClusterId' in cmd; +} -export function assertIsMtCmdAreqZdo(cmd: MtCmd): asserts cmd is MtCmdAreqZdo { - assert('zdo' in cmd, `'${cmd.name}' is not a MtCmdAreqZdo`); +export function isMtCmdSreqZdo(cmd: MtCmd): cmd is MtCmdSreqZdo { + return cmd.type === Type.SREQ && 'zdoClusterId' in cmd; } diff --git a/src/adapter/z-stack/znp/znp.ts b/src/adapter/z-stack/znp/znp.ts index 714ab2267c..a43e9751c3 100755 --- a/src/adapter/z-stack/znp/znp.ts +++ b/src/adapter/z-stack/znp/znp.ts @@ -1,17 +1,19 @@ +import assert from 'assert'; import events from 'events'; import net from 'net'; -import Equals from 'fast-deep-equal/es6'; - import {Queue, RealpathSync, Wait, Waitress} from '../../../utils'; import {logger} from '../../../utils/logger'; +import {ClusterId as ZdoClusterId} from '../../../zspec/zdo'; import {SerialPort} from '../../serialPort'; import SerialPortUtils from '../../serialPortUtils'; import SocketPortUtils from '../../socketPortUtils'; import * as Constants from '../constants'; import {Frame as UnpiFrame, Parser as UnpiParser, Writer as UnpiWriter} from '../unpi'; import {Subsystem, Type} from '../unpi/constants'; +import Definition from './definition'; import {ZpiObjectPayload} from './tstype'; +import {isMtCmdSreqZdo} from './utils'; import ZpiObject from './zpiObject'; const { @@ -31,7 +33,9 @@ interface WaitressMatcher { type: Type; subsystem: Subsystem; command: string; - payload?: ZpiObjectPayload; + target?: number | string; + transid?: number; + state?: number; } const autoDetectDefinitions = [ @@ -308,6 +312,30 @@ class Znp extends events.EventEmitter { }); } + public requestZdo(clusterId: ZdoClusterId, payload: Buffer, waiterID?: number): Promise { + return this.queue.execute(async () => { + const cmd = Definition[Subsystem.ZDO].find((c) => isMtCmdSreqZdo(c) && c.zdoClusterId === clusterId); + assert(cmd, `Command for ZDO cluster ID '${clusterId}' not supported.`); + + const unpiFrame = new UnpiFrame(Type.SREQ, Subsystem.ZDO, cmd.ID, payload); + const waiter = this.waitress.waitFor({type: Type.SRSP, subsystem: Subsystem.ZDO, command: cmd.name}, timeouts.SREQ); + + this.unpiWriter.writeFrame(unpiFrame); + + const result = await waiter.start().promise; + + if (result?.payload.status !== undefined && result.payload.status !== ZnpCommandStatus.SUCCESS) { + if (waiterID !== undefined) { + this.waitress.remove(waiterID); + } + + throw new Error( + `--> 'SREQ: ZDO - ${ZdoClusterId[clusterId]} - ${payload.toString('hex')}' failed with status '${statusDescription(result.payload.status)}'`, + ); + } + }); + } + private waitressTimeoutFormatter(matcher: WaitressMatcher, timeout: number): string { return `${Type[matcher.type]} - ${Subsystem[matcher.subsystem]} - ${matcher.command} after ${timeout}ms`; } @@ -316,27 +344,26 @@ class Znp extends events.EventEmitter { type: Type, subsystem: Subsystem, command: string, - payload: ZpiObjectPayload = {}, + target: number | string | undefined, + transid: number | undefined, + state: number | undefined, timeout: number = timeouts.default, ): {start: () => {promise: Promise; ID: number}; ID: number} { - return this.waitress.waitFor({type, subsystem, command, payload}, timeout); + return this.waitress.waitFor({type, subsystem, command, target, transid, state}, timeout); } private waitressValidator(zpiObject: ZpiObject, matcher: WaitressMatcher): boolean { - const requiredMatch = - matcher.type === zpiObject.type && matcher.subsystem == zpiObject.subsystem && matcher.command === zpiObject.command.name; - let payloadMatch = true; - - if (matcher.payload) { - for (const key in matcher.payload) { - if (!Equals(zpiObject.payload[key], matcher.payload[key])) { - payloadMatch = false; - break; - } - } - } - - return requiredMatch && payloadMatch; + return ( + matcher.type === zpiObject.type && + matcher.subsystem == zpiObject.subsystem && + matcher.command === zpiObject.command.name && + (matcher.target === undefined || + (typeof matcher.target === 'number' + ? matcher.target === zpiObject.payload.srcaddr + : matcher.target === zpiObject.payload.zdo?.[1]?.eui64)) && + (matcher.transid === undefined || matcher.transid === zpiObject.payload.transid) && + (matcher.state === undefined || matcher.state === zpiObject.payload.state) + ); } } diff --git a/src/adapter/z-stack/znp/zpiObject.ts b/src/adapter/z-stack/znp/zpiObject.ts index b41b8c62e1..9298737737 100755 --- a/src/adapter/z-stack/znp/zpiObject.ts +++ b/src/adapter/z-stack/znp/zpiObject.ts @@ -1,5 +1,6 @@ import assert from 'assert'; +import {ClusterId as ZdoClusterId} from '../../../zspec/zdo'; import {BuffaloZdo} from '../../../zspec/zdo/buffaloZdo'; import {Frame as UnpiFrame} from '../unpi'; import {MaxDataSize, Subsystem, Type} from '../unpi/constants'; @@ -7,7 +8,7 @@ import BuffaloZnp from './buffaloZnp'; import Definition from './definition'; import ParameterType from './parameterType'; import {BuffaloZnpOptions, MtCmd, MtParameter, MtType, ZpiObjectPayload} from './tstype'; -import {assertIsMtCmdAreqZdo} from './utils'; +import {isMtCmdAreqZdo, isMtCmdSreqZdo} from './utils'; const BufferAndListTypes = [ ParameterType.BUFFER, @@ -22,14 +23,22 @@ const BufferAndListTypes = [ ParameterType.LIST_UINT8, ]; -class ZpiObject { +type ZpiObjectType = 'Request' | 'Response'; + +class ZpiObject { public readonly type: Type; public readonly subsystem: Subsystem; public readonly command: MtCmd; public readonly payload: ZpiObjectPayload; - public readonly unpiFrame: UnpiFrame; - - private constructor(type: Type, subsystem: Subsystem, command: MtCmd, payload: ZpiObjectPayload, unpiFrame: UnpiFrame) { + public readonly unpiFrame: T extends 'Request' ? UnpiFrame : undefined; + + private constructor( + type: Type, + subsystem: Subsystem, + command: MtCmd, + payload: ZpiObjectPayload, + unpiFrame: T extends 'Request' ? UnpiFrame : undefined, + ) { this.type = type; this.subsystem = subsystem; this.command = command; @@ -37,29 +46,32 @@ class ZpiObject { this.unpiFrame = unpiFrame; } - public static createRequest(subsystem: Subsystem, command: string, payload: ZpiObjectPayload): ZpiObject { + public static createRequest(subsystem: Subsystem, command: string, payload: ZpiObjectPayload): ZpiObject<'Request'> { if (!Definition[subsystem]) { throw new Error(`Subsystem '${subsystem}' does not exist`); } const cmd = Definition[subsystem].find((c) => c.name === command); - if (cmd === undefined) { + + if (cmd === undefined || isMtCmdAreqZdo(cmd) || isMtCmdSreqZdo(cmd) || cmd.request === undefined) { throw new Error(`Command request '${command}' from subsystem '${subsystem}' not found`); } // Create the UnpiFrame const buffalo = new BuffaloZnp(Buffer.alloc(MaxDataSize)); + for (const parameter of cmd.request) { const value = payload[parameter.name]; buffalo.write(parameter.parameterType, value, {}); } + const buffer = buffalo.getWritten(); const unpiFrame = new UnpiFrame(cmd.type, subsystem, cmd.ID, buffer); return new ZpiObject(cmd.type, subsystem, cmd, payload, unpiFrame); } - public static fromUnpiFrame(frame: UnpiFrame): ZpiObject { + public static fromUnpiFrame(frame: UnpiFrame): ZpiObject<'Response'> { const cmd = Definition[frame.subsystem].find((c) => c.ID === frame.commandID); if (!cmd) { @@ -67,14 +79,34 @@ class ZpiObject { } let payload: ZpiObjectPayload = {}; - const parameters = frame.type === Type.SRSP && cmd.type !== Type.AREQ ? cmd.response : cmd.request; - assert( - parameters, - `CommandID '${frame.commandID}' from subsystem '${frame.subsystem}' cannot be a ` + - `${frame.type === Type.SRSP ? 'response' : 'request'}`, - ); - payload = this.readParameters(frame.data, parameters); - return new ZpiObject(frame.type, frame.subsystem, cmd, payload, frame); + + // hotpath AREQ & SREQ ZDO since payload is identical (no need to instantiate BuffaloZnp or parse generically) + if (isMtCmdAreqZdo(cmd)) { + if (cmd.zdoClusterId === ZdoClusterId.NETWORK_ADDRESS_RESPONSE || cmd.zdoClusterId === ZdoClusterId.IEEE_ADDRESS_RESPONSE) { + // ZStack swaps the `startindex` and `numassocdev` compared to ZDO swap them back before handing to ZDO + const startIndex = frame.data[11]; + const assocDevCount = frame.data[12]; + frame.data[11] = assocDevCount; + frame.data[12] = startIndex; + payload.zdo = BuffaloZdo.readResponse(false, cmd.zdoClusterId, frame.data); + } else { + payload.srcaddr = frame.data.readUInt16LE(0); + payload.zdo = BuffaloZdo.readResponse(false, cmd.zdoClusterId, frame.data.subarray(2)); + } + } else if (isMtCmdSreqZdo(cmd)) { + payload.status = frame.data.readUInt8(0); + } else { + const parameters = frame.type === Type.SRSP && cmd.type !== Type.AREQ ? cmd.response : cmd.request; + assert( + parameters, + `CommandID '${frame.commandID}' from subsystem '${frame.subsystem}' cannot be a ` + + `${frame.type === Type.SRSP ? 'response' : 'request'}`, + ); + payload = this.readParameters(frame.data, parameters); + } + + // GC UnpiFrame as early as possible, no longer needed + return new ZpiObject(frame.type, frame.subsystem, cmd, payload, undefined); } private static readParameters(buffer: Buffer, parameters: MtParameter[]): ZpiObjectPayload { @@ -110,12 +142,6 @@ class ZpiObject { ); } - public parseZdoPayload(): ReturnType { - assertIsMtCmdAreqZdo(this.command); - const data = this.command.zdo.convert(this.unpiFrame.data); - return BuffaloZdo.readResponse(false, this.command.zdo.cluterId, data); - } - public toString(): string { return `${Type[this.type]}: ${Subsystem[this.subsystem]} - ${this.command.name} - ${JSON.stringify(this.payload)}`; } diff --git a/src/adapter/zboss/adapter/zbossAdapter.ts b/src/adapter/zboss/adapter/zbossAdapter.ts index 8a1885f7fd..4fa08082d5 100644 --- a/src/adapter/zboss/adapter/zbossAdapter.ts +++ b/src/adapter/zboss/adapter/zbossAdapter.ts @@ -1,15 +1,20 @@ /* istanbul ignore file */ +import assert from 'assert'; + import {Adapter, TsType} from '../..'; import {Backup} from '../../../models'; -import {Queue, RealpathSync, Waitress} from '../../../utils'; +import {Queue, RealpathSync, Wait, Waitress} from '../../../utils'; import {logger} from '../../../utils/logger'; -import {BroadcastAddress} from '../../../zspec/enums'; +import * as ZSpec from '../../../zspec'; +import {EUI64} from '../../../zspec/tstypes'; import * as Zcl from '../../../zspec/zcl'; -import {DeviceJoinedPayload, DeviceLeavePayload, ZclPayload} from '../../events'; +import * as Zdo from '../../../zspec/zdo'; +import * as ZdoTypes from '../../../zspec/zdo/definition/tstypes'; +import {ZclPayload} from '../../events'; import SerialPortUtils from '../../serialPortUtils'; import SocketPortUtils from '../../socketPortUtils'; -import {Coordinator, LQI, LQINeighbor} from '../../tstype'; +import {Coordinator} from '../../tstype'; import {ZBOSSDriver} from '../driver'; import {CommandId, DeviceUpdateStatus} from '../enums'; import {FrameType, ZBOSSFrame} from '../frame'; @@ -33,7 +38,6 @@ export class ZBOSSAdapter extends Adapter { private queue: Queue; private readonly driver: ZBOSSDriver; private waitress: Waitress; - public coordinator?: Coordinator; constructor( networkOptions: TsType.NetworkOptions, @@ -54,56 +58,56 @@ export class ZBOSSAdapter extends Adapter { private async processMessage(frame: ZBOSSFrame): Promise { logger.debug(() => `processMessage: ${JSON.stringify(frame)}`, NS); - if ( - frame.type == FrameType.INDICATION && - frame.commandId == CommandId.ZDO_DEV_UPDATE_IND && - frame.payload.status == DeviceUpdateStatus.LEFT - ) { - logger.debug(`Device left network request received: ${frame.payload.nwk} ${frame.payload.ieee}`, NS); - const payload: DeviceLeavePayload = { - networkAddress: frame.payload.nwk, - ieeeAddr: frame.payload.ieee, - }; - - this.emit('deviceLeave', payload); - } - if (frame.type == FrameType.INDICATION && frame.commandId == CommandId.NWK_LEAVE_IND) { - logger.debug(`Device left network request received from ${frame.payload.ieee}`, NS); - const payload: DeviceLeavePayload = { - networkAddress: frame.payload.nwk, - ieeeAddr: frame.payload.ieee, - }; - - this.emit('deviceLeave', payload); - } - if (frame.type == FrameType.INDICATION && frame.commandId == CommandId.ZDO_DEV_ANNCE_IND) { - logger.debug(`Device join request received: ${frame.payload.nwk} ${frame.payload.ieee}`, NS); - const payload: DeviceJoinedPayload = { - networkAddress: frame.payload.nwk, - ieeeAddr: frame.payload.ieee, - }; - this.emit('deviceJoined', payload); - } + if (frame.payload.zdoClusterId !== undefined) { + this.emit('zdoResponse', frame.payload.zdoClusterId, frame.payload.zdo!); + } else if (frame.type == FrameType.INDICATION) { + switch (frame.commandId) { + case CommandId.ZDO_DEV_UPDATE_IND: { + logger.debug(`Device ${frame.payload.ieee}:${frame.payload.nwk} ${DeviceUpdateStatus[frame.payload.status]}.`, NS); + + if (frame.payload.status === DeviceUpdateStatus.LEFT) { + this.emit('deviceLeave', { + networkAddress: frame.payload.nwk, + ieeeAddr: frame.payload.ieee, + }); + } else { + // SECURE_REJOIN, UNSECURE_JOIN, TC_REJOIN + this.emit('deviceJoined', { + networkAddress: frame.payload.nwk, + ieeeAddr: frame.payload.ieee, + }); + } + break; + } - if (frame.type == FrameType.INDICATION && frame.commandId == CommandId.APSDE_DATA_IND) { - logger.debug(`ZCL frame received from ${frame.payload.srcNwk} ${frame.payload.srcEndpoint}`, NS); - const payload: ZclPayload = { - clusterID: frame.payload.clusterID, - header: Zcl.Header.fromBuffer(frame.payload.data), - data: frame.payload.data, - address: frame.payload.srcNwk, - endpoint: frame.payload.srcEndpoint, - linkquality: frame.payload.lqi, - groupID: frame.payload.grpNwk, - wasBroadcast: false, - destinationEndpoint: frame.payload.dstEndpoint, - }; + case CommandId.NWK_LEAVE_IND: { + this.emit('deviceLeave', { + networkAddress: frame.payload.nwk, + ieeeAddr: frame.payload.ieee, + }); + break; + } - this.waitress.resolve(payload); - this.emit('zclPayload', payload); + case CommandId.APSDE_DATA_IND: { + const payload: ZclPayload = { + clusterID: frame.payload.clusterID, + header: Zcl.Header.fromBuffer(frame.payload.data), + data: frame.payload.data, + address: frame.payload.srcNwk, + endpoint: frame.payload.srcEndpoint, + linkquality: frame.payload.lqi, + groupID: frame.payload.grpNwk, + wasBroadcast: false, + destinationEndpoint: frame.payload.dstEndpoint, + }; + + this.waitress.resolve(payload); + this.emit('zclPayload', payload); + break; + } + } } - //this.emit('event', frame); } public static async isValidPath(path: string): Promise { @@ -142,26 +146,56 @@ export class ZBOSSAdapter extends Adapter { public async getCoordinator(): Promise { return await this.queue.execute(async () => { - const info = await this.driver.getCoordinator(); - logger.debug(() => `ZBOSS Adapter Coordinator description:\n${JSON.stringify(info)}`, NS); - this.coordinator = { - networkAddress: info.networkAddress, - manufacturerID: 0, - ieeeAddr: info.ieeeAddr, - endpoints: info.endpoints, - }; + const activeEndpoints = await this.activeEndpoints(0x0000); + const ap = []; + + for (const ep of activeEndpoints.endpoints) { + const sd = await this.simpleDescriptor(0x0000, ep); + + ap.push({ + ID: sd.endpointID, + profileID: sd.profileID, + deviceID: sd.deviceID, + inputClusters: sd.inputClusters, + outputClusters: sd.outputClusters, + }); + } - return this.coordinator; + return { + ieeeAddr: this.driver.netInfo.ieeeAddr, + networkAddress: ZSpec.COORDINATOR_ADDRESS, + manufacturerID: 0x0000, + endpoints: ap, + }; }); } public async getCoordinatorVersion(): Promise { - return await this.driver.getCoordinatorVersion(); + return await this.queue.execute(async () => { + const ver = await this.driver.execCommand(CommandId.GET_MODULE_VERSION, {}); + const cver = await this.driver.execCommand(CommandId.GET_COORDINATOR_VERSION, {}); + const ver2str = (version: number): string => { + const major = (version >> 24) & 0xff; + const minor = (version >> 16) & 0xff; + const revision = (version >> 8) & 0xff; + const commit = version & 0xff; + return `${major}.${minor}.${revision}.${commit}`; + }; + + return { + type: `zboss`, + meta: { + coordinator: cver.payload.version, + stack: ver2str(ver.payload.stackVersion), + protocol: ver2str(ver.payload.protocolVersion), + revision: ver2str(ver.payload.fwVersion), + }, + }; + }); } - // eslint-disable-next-line @typescript-eslint/no-unused-vars public async reset(type: 'soft' | 'hard'): Promise { - return await Promise.reject(new Error('Not supported')); + throw new Error(`This adapter does not reset '${type}'`); } public async supportsBackup(): Promise { @@ -187,124 +221,208 @@ export class ZBOSSAdapter extends Adapter { }); } - public async supportsChangeChannel(): Promise { - return false; - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars public async changeChannel(newChannel: number): Promise { - throw new Error(`Channel change is not supported for 'zboss' yet`); + const clusterId = Zdo.ClusterId.NWK_UPDATE_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, [newChannel], 0xfe, undefined, undefined, undefined); + + await this.sendZdo(ZSpec.BLANK_EUI64, ZSpec.BroadcastAddress.SLEEPY, clusterId, zdoPayload, true /* handled below */); + await Wait(12000); } public async setTransmitPower(value: number): Promise { if (this.driver.isInitialized()) { return await this.queue.execute(async () => { - await this.driver.setTXPower(value); + await this.driver.execCommand(CommandId.SET_TX_POWER, {txPower: value}); }); } } - // eslint-disable-next-line @typescript-eslint/no-unused-vars public async addInstallCode(ieeeAddress: string, key: Buffer): Promise { + logger.error(() => `NOT SUPPORTED: sendZclFrameToGroup(${ieeeAddress},${key.toString('hex')}`, NS); throw new Error(`Install code is not supported for 'zboss' yet`); } - public async permitJoin(seconds: number, networkAddress: number): Promise { + public async permitJoin(seconds: number, networkAddress?: number): Promise { if (this.driver.isInitialized()) { - return await this.queue.execute(async () => { - await this.driver.permitJoin(networkAddress, seconds); - if (!networkAddress) { - // send broadcast permit - await this.driver.permitJoin(0xfffc, seconds); + const clusterId = Zdo.ClusterId.PERMIT_JOINING_REQUEST; + // `authentication`: TC significance always 1 (zb specs) + const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, seconds, 1, []); + + if (networkAddress) { + // `device-only` + const result = await this.sendZdo(ZSpec.BLANK_EUI64, networkAddress, clusterId, zdoPayload, false); + + /* istanbul ignore next */ + if (!Zdo.Buffalo.checkStatus(result)) { + // TODO: will disappear once moved upstream + throw new Zdo.StatusError(result[0]); } - }); + } else { + // `coordinator-only` (for `all` too) + const result = await this.sendZdo(ZSpec.BLANK_EUI64, ZSpec.COORDINATOR_ADDRESS, clusterId, zdoPayload, false); + + /* istanbul ignore next */ + if (!Zdo.Buffalo.checkStatus(result)) { + // TODO: will disappear once moved upstream + throw new Zdo.StatusError(result[0]); + } + + if (networkAddress === undefined) { + // `all`: broadcast + await this.sendZdo(ZSpec.BLANK_EUI64, ZSpec.BroadcastAddress.DEFAULT, clusterId, zdoPayload, true); + } + } } } public async lqi(networkAddress: number): Promise { - return await this.queue.execute(async (): Promise => { - const neighbors: LQINeighbor[] = []; + const clusterId = Zdo.ClusterId.LQI_TABLE_REQUEST; + const neighbors: TsType.LQINeighbor[] = []; + const request = async (startIndex: number): Promise<[tableEntries: number, entryCount: number]> => { + const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, startIndex); + const result = await this.sendZdo(ZSpec.BLANK_EUI64, networkAddress, clusterId, zdoPayload, false); - const request = async (startIndex: number): Promise => { - try { - const result = await this.driver.lqi(networkAddress, startIndex); + /* istanbul ignore else */ + if (Zdo.Buffalo.checkStatus(result)) { + const payload = result[1]; - return result; - } catch (error) { - throw new Error(`LQI for '${networkAddress}' failed: ${error}`); - } - }; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const add = (list: any): void => { - for (const entry of list) { + for (const entry of payload.entryList) { neighbors.push({ + ieeeAddr: entry.eui64, + networkAddress: entry.nwkAddress, linkquality: entry.lqi, - networkAddress: entry.nwk, - ieeeAddr: entry.ieee, - relationship: (entry.relationship >> 4) & 0x7, + relationship: entry.relationship, depth: entry.depth, }); } - }; - let response = (await request(0)).payload; - add(response.neighbors); - const size = response.entries; - let nextStartIndex = response.neighbors.length; - - while (neighbors.length < size) { - response = await request(nextStartIndex); - add(response.neighbors); - nextStartIndex += response.neighbors.length; + return [payload.neighborTableEntries, payload.entryList.length]; + } else { + // TODO: will disappear once moved upstream + throw new Zdo.StatusError(result[0]); } + }; - return {neighbors}; - }, networkAddress); + let [tableEntries, entryCount] = await request(0); + + const size = tableEntries; + let nextStartIndex = entryCount; + + while (neighbors.length < size) { + [tableEntries, entryCount] = await request(nextStartIndex); + + nextStartIndex += entryCount; + } + + return {neighbors}; } - // eslint-disable-next-line @typescript-eslint/no-unused-vars public async routingTable(networkAddress: number): Promise { - throw new Error(`Routing table is not supported for 'zboss' yet`); + throw new Error(`Routing table is not supported for 'zboss' yet '${networkAddress}'`); + // const clusterId = Zdo.ClusterId.ROUTING_TABLE_REQUEST; + // const table: TsType.RoutingTableEntry[] = []; + // const request = async (startIndex: number): Promise<[tableEntries: number, entryCount: number]> => { + // const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, startIndex); + // const result = await this.sendZdo(ZSpec.BLANK_EUI64, networkAddress, clusterId, zdoPayload, false); + + // /* istanbul ignore else */ + // if (Zdo.Buffalo.checkStatus(result)) { + // const payload = result[1]; + + // for (const entry of payload.entryList) { + // table.push({ + // destinationAddress: entry.destinationAddress, + // status: entry.status, + // nextHop: entry.nextHopAddress, + // }); + // } + + // return [payload.routingTableEntries, payload.entryList.length]; + // } else { + // // TODO: will disappear once moved upstream + // throw new Zdo.StatusError(result[0]); + // } + // }; + + // let [tableEntries, entryCount] = await request(0); + + // const size = tableEntries; + // let nextStartIndex = entryCount; + + // while (table.length < size) { + // [tableEntries, entryCount] = await request(nextStartIndex); + + // nextStartIndex += entryCount; + // } + + // return {table}; } public async nodeDescriptor(networkAddress: number): Promise { - return await this.queue.execute(async () => { - try { - logger.debug(`Requesting 'Node Descriptor' for '${networkAddress}'`, NS); - const descriptor = await this.driver.nodeDescriptor(networkAddress); - const logicaltype = descriptor.payload.flags & 0x07; - return { - manufacturerCode: descriptor.payload.manufacturerCode, - type: logicaltype == 0 ? 'Coordinator' : logicaltype == 1 ? 'Router' : 'EndDevice', - }; - } catch (error) { - logger.debug(`Node descriptor request for '${networkAddress}' failed (${error}), retry`, NS); - throw error; + const clusterId = Zdo.ClusterId.NODE_DESCRIPTOR_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, networkAddress); + const result = await this.sendZdo(ZSpec.BLANK_EUI64, networkAddress, clusterId, zdoPayload, false); + + /* istanbul ignore else */ + if (Zdo.Buffalo.checkStatus(result)) { + const payload = result[1]; + let type: TsType.DeviceType = 'Unknown'; + + switch (payload.logicalType) { + case 0x0: + type = 'Coordinator'; + break; + case 0x1: + type = 'Router'; + break; + case 0x2: + type = 'EndDevice'; + break; } - }); + + return {type, manufacturerCode: payload.manufacturerCode}; + } else { + // TODO: will disappear once moved upstream + throw new Zdo.StatusError(result[0]); + } } public async activeEndpoints(networkAddress: number): Promise { - logger.debug(`Requesting 'Active endpoints' for '${networkAddress}'`, NS); - return await this.queue.execute(async () => { - const endpoints = await this.driver.activeEndpoints(networkAddress); - return {endpoints: [...endpoints.payload.endpoints]}; - }, networkAddress); + const clusterId = Zdo.ClusterId.ACTIVE_ENDPOINTS_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, networkAddress); + const result = await this.sendZdo(ZSpec.BLANK_EUI64, networkAddress, clusterId, zdoPayload, false); + + /* istanbul ignore else */ + if (Zdo.Buffalo.checkStatus(result)) { + const payload = result[1]; + + return {endpoints: payload.endpointList}; + } else { + // TODO: will disappear once moved upstream + throw new Zdo.StatusError(result[0]); + } } public async simpleDescriptor(networkAddress: number, endpointID: number): Promise { - logger.debug(`Requesting 'Simple Descriptor' for '${networkAddress}' endpoint ${endpointID}`, NS); - return await this.queue.execute(async () => { - const sd = await this.driver.simpleDescriptor(networkAddress, endpointID); + const clusterId = Zdo.ClusterId.SIMPLE_DESCRIPTOR_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, networkAddress, endpointID); + const result = await this.sendZdo(ZSpec.BLANK_EUI64, networkAddress, clusterId, zdoPayload, false); + + /* istanbul ignore else */ + if (Zdo.Buffalo.checkStatus(result)) { + const payload = result[1]; + return { - profileID: sd.payload.profileID, - endpointID: sd.payload.endpoint, - deviceID: sd.payload.deviceID, - inputClusters: sd.payload.inputClusters, - outputClusters: sd.payload.outputClusters, + profileID: payload.profileId, + endpointID: payload.endpoint, + deviceID: payload.deviceId, + inputClusters: payload.inClusterList, + outputClusters: payload.outClusterList, }; - }, networkAddress); + } else { + // TODO: will disappear once moved upstream + throw new Zdo.StatusError(result[0]); + } } public async bind( @@ -316,17 +434,25 @@ export class ZBOSSAdapter extends Adapter { type: 'endpoint' | 'group', destinationEndpoint?: number, ): Promise { - return await this.queue.execute(async () => { - await this.driver.bind( - destinationNetworkAddress, - sourceIeeeAddress, - sourceEndpoint, - clusterID, - destinationAddressOrGroup, - type, - destinationEndpoint, - ); - }, destinationNetworkAddress); + const clusterId = Zdo.ClusterId.BIND_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest( + this.hasZdoMessageOverhead, + clusterId, + sourceIeeeAddress as EUI64, + sourceEndpoint, + clusterID, + type === 'group' ? Zdo.MULTICAST_BINDING : Zdo.UNICAST_BINDING, + destinationAddressOrGroup as EUI64, // not used with MULTICAST_BINDING + destinationAddressOrGroup as number, // not used with UNICAST_BINDING + destinationEndpoint ?? 0, // not used with MULTICAST_BINDING + ); + const result = await this.sendZdo(ZSpec.BLANK_EUI64, destinationNetworkAddress, clusterId, zdoPayload, false); + + /* istanbul ignore next */ + if (!Zdo.Buffalo.checkStatus(result)) { + // TODO: will disappear once moved upstream + throw new Zdo.StatusError(result[0]); + } } public async unbind( @@ -338,23 +464,90 @@ export class ZBOSSAdapter extends Adapter { type: 'endpoint' | 'group', destinationEndpoint: number, ): Promise { - return await this.queue.execute(async () => { - await this.driver.unbind( - destinationNetworkAddress, - sourceIeeeAddress, - sourceEndpoint, - clusterID, - destinationAddressOrGroup, - type, - destinationEndpoint, - ); - }, destinationNetworkAddress); + const clusterId = Zdo.ClusterId.UNBIND_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest( + this.hasZdoMessageOverhead, + clusterId, + sourceIeeeAddress as EUI64, + sourceEndpoint, + clusterID, + type === 'group' ? Zdo.MULTICAST_BINDING : Zdo.UNICAST_BINDING, + destinationAddressOrGroup as EUI64, // not used with MULTICAST_BINDING + destinationAddressOrGroup as number, // not used with UNICAST_BINDING + destinationEndpoint ?? 0, // not used with MULTICAST_BINDING + ); + const result = await this.sendZdo(ZSpec.BLANK_EUI64, destinationNetworkAddress, clusterId, zdoPayload, false); + + /* istanbul ignore next */ + if (!Zdo.Buffalo.checkStatus(result)) { + // TODO: will disappear once moved upstream + throw new Zdo.StatusError(result[0]); + } } public async removeDevice(networkAddress: number, ieeeAddr: string): Promise { - return await this.queue.execute(async () => { - await this.driver.removeDevice(networkAddress, ieeeAddr); - }, networkAddress); + const clusterId = Zdo.ClusterId.LEAVE_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, ieeeAddr as EUI64, Zdo.LeaveRequestFlags.WITHOUT_REJOIN); + const result = await this.sendZdo(ZSpec.BLANK_EUI64, networkAddress, clusterId, zdoPayload, false); + + /* istanbul ignore next */ + if (!Zdo.Buffalo.checkStatus(result)) { + // TODO: will disappear once moved upstream + throw new Zdo.StatusError(result[0]); + } + } + + public async sendZdo( + ieeeAddress: string, + networkAddress: number, + clusterId: Zdo.ClusterId, + payload: Buffer, + disableResponse: true, + ): Promise; + public async sendZdo( + ieeeAddress: string, + networkAddress: number, + clusterId: K, + payload: Buffer, + disableResponse: false, + ): Promise; + public async sendZdo( + ieeeAddress: string, + networkAddress: number, + clusterId: K, + payload: Buffer, + disableResponse: boolean, + ): Promise { + return await this.queue.execute(async () => { + // stack-specific requirements + switch (clusterId) { + case Zdo.ClusterId.NETWORK_ADDRESS_REQUEST: + case Zdo.ClusterId.IEEE_ADDRESS_REQUEST: + case Zdo.ClusterId.BIND_REQUEST: // XXX: according to `FRAMES`, might not support group bind? + case Zdo.ClusterId.UNBIND_REQUEST: // XXX: according to `FRAMES`, might not support group unbind? + case Zdo.ClusterId.LQI_TABLE_REQUEST: + case Zdo.ClusterId.ROUTING_TABLE_REQUEST: + case Zdo.ClusterId.BINDING_TABLE_REQUEST: + case Zdo.ClusterId.LEAVE_REQUEST: + case Zdo.ClusterId.PERMIT_JOINING_REQUEST: { + const prefixedPayload = Buffer.alloc(payload.length + 2); + prefixedPayload.writeUInt16LE(networkAddress, 0); + prefixedPayload.set(payload, 2); + + payload = prefixedPayload; + break; + } + } + + const zdoResponseClusterId = Zdo.Utils.getResponseClusterId(clusterId); + const frame = await this.driver.requestZdo(clusterId, payload, disableResponse || zdoResponseClusterId === undefined); + + if (!disableResponse && zdoResponseClusterId !== undefined) { + assert(frame, `ZDO ${Zdo.ClusterId[clusterId]} expected response ${Zdo.ClusterId[zdoResponseClusterId]}.`); + + return frame.payload.zdo as ZdoTypes.RequestToResponseMap[K]; + } + }, networkAddress /* TODO: replace with ieeeAddress once zdo moved upstream */); } public async sendZclFrameToEndpoint( @@ -404,7 +597,7 @@ export class ZBOSSAdapter extends Adapter { assocRestore: {ieeeadr: string; nwkaddr: number; noderelation: number} | null, ): Promise { if (ieeeAddr == null) { - ieeeAddr = this.coordinator!.ieeeAddr; + ieeeAddr = this.driver.netInfo.ieeeAddr; } logger.debug( `sendZclFrameToEndpointInternal ${ieeeAddr}:${networkAddress}/${endpoint} ` + @@ -485,28 +678,33 @@ export class ZBOSSAdapter extends Adapter { } } - // eslint-disable-next-line @typescript-eslint/no-unused-vars public async sendZclFrameToGroup(groupID: number, zclFrame: Zcl.Frame, sourceEndpoint?: number): Promise { + logger.error(() => `NOT SUPPORTED: sendZclFrameToGroup(${groupID},${JSON.stringify(zclFrame)},${sourceEndpoint})`, NS); return; } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - public async sendZclFrameToAll(endpoint: number, zclFrame: Zcl.Frame, sourceEndpoint: number, destination: BroadcastAddress): Promise { + public async sendZclFrameToAll( + endpoint: number, + zclFrame: Zcl.Frame, + sourceEndpoint: number, + destination: ZSpec.BroadcastAddress, + ): Promise { + logger.error(() => `NOT SUPPORTED: sendZclFrameToAll(${endpoint},${JSON.stringify(zclFrame)},${sourceEndpoint},${destination})`, NS); return; } - // eslint-disable-next-line @typescript-eslint/no-unused-vars public async setChannelInterPAN(channel: number): Promise { + logger.error(`NOT SUPPORTED: setChannelInterPAN(${channel})`, NS); return; } - // eslint-disable-next-line @typescript-eslint/no-unused-vars public async sendZclFrameInterPANToIeeeAddr(zclFrame: Zcl.Frame, ieeeAddress: string): Promise { + logger.error(() => `NOT SUPPORTED: sendZclFrameInterPANToIeeeAddr(${JSON.stringify(zclFrame)},${ieeeAddress})`, NS); return; } - // eslint-disable-next-line @typescript-eslint/no-unused-vars public async sendZclFrameInterPANBroadcast(zclFrame: Zcl.Frame, timeout: number): Promise { + logger.error(() => `NOT SUPPORTED: sendZclFrameInterPANBroadcast(${JSON.stringify(zclFrame)},${timeout})`, NS); throw new Error(`Is not supported for 'zboss' yet`); } diff --git a/src/adapter/zboss/commands.ts b/src/adapter/zboss/commands.ts index 9c780b18c5..044575265c 100644 --- a/src/adapter/zboss/commands.ts +++ b/src/adapter/zboss/commands.ts @@ -1,9 +1,11 @@ /* istanbul ignore file */ import {BuffaloZclDataType, DataType} from '../../zspec/zcl/definition/enums'; +import {ClusterId as ZdoClusterId} from '../../zspec/zdo'; import { BuffaloZBOSSDataType, CommandId, + DeviceAuthorizedType, DeviceType, DeviceUpdateStatus, PolicyType, @@ -32,6 +34,43 @@ interface ZBOSSFrameDesc { indication?: ParamsDesc[]; } +export const ZDO_REQ_CLUSTER_ID_TO_ZBOSS_COMMAND_ID: Readonly>> = { + [ZdoClusterId.NETWORK_ADDRESS_REQUEST]: CommandId.ZDO_NWK_ADDR_REQ, + [ZdoClusterId.IEEE_ADDRESS_REQUEST]: CommandId.ZDO_IEEE_ADDR_REQ, + [ZdoClusterId.POWER_DESCRIPTOR_REQUEST]: CommandId.ZDO_POWER_DESC_REQ, + [ZdoClusterId.NODE_DESCRIPTOR_REQUEST]: CommandId.ZDO_NODE_DESC_REQ, + [ZdoClusterId.SIMPLE_DESCRIPTOR_REQUEST]: CommandId.ZDO_SIMPLE_DESC_REQ, + [ZdoClusterId.ACTIVE_ENDPOINTS_REQUEST]: CommandId.ZDO_ACTIVE_EP_REQ, + [ZdoClusterId.MATCH_DESCRIPTORS_REQUEST]: CommandId.ZDO_MATCH_DESC_REQ, + [ZdoClusterId.BIND_REQUEST]: CommandId.ZDO_BIND_REQ, + [ZdoClusterId.UNBIND_REQUEST]: CommandId.ZDO_UNBIND_REQ, + [ZdoClusterId.LEAVE_REQUEST]: CommandId.ZDO_MGMT_LEAVE_REQ, + [ZdoClusterId.PERMIT_JOINING_REQUEST]: CommandId.ZDO_PERMIT_JOINING_REQ, + [ZdoClusterId.BINDING_TABLE_REQUEST]: CommandId.ZDO_MGMT_BIND_REQ, + [ZdoClusterId.LQI_TABLE_REQUEST]: CommandId.ZDO_MGMT_LQI_REQ, + // [ZdoClusterId.ROUTING_TABLE_REQUEST]: CommandId.ZDO_MGMT_RTG_REQ, + [ZdoClusterId.NWK_UPDATE_REQUEST]: CommandId.ZDO_MGMT_NWK_UPDATE_REQ, +}; + +export const ZBOSS_COMMAND_ID_TO_ZDO_RSP_CLUSTER_ID: Readonly>> = { + [CommandId.ZDO_NWK_ADDR_REQ]: ZdoClusterId.NETWORK_ADDRESS_RESPONSE, + [CommandId.ZDO_IEEE_ADDR_REQ]: ZdoClusterId.IEEE_ADDRESS_RESPONSE, + [CommandId.ZDO_POWER_DESC_REQ]: ZdoClusterId.POWER_DESCRIPTOR_RESPONSE, + [CommandId.ZDO_NODE_DESC_REQ]: ZdoClusterId.NODE_DESCRIPTOR_RESPONSE, + [CommandId.ZDO_SIMPLE_DESC_REQ]: ZdoClusterId.SIMPLE_DESCRIPTOR_RESPONSE, + [CommandId.ZDO_ACTIVE_EP_REQ]: ZdoClusterId.ACTIVE_ENDPOINTS_RESPONSE, + [CommandId.ZDO_MATCH_DESC_REQ]: ZdoClusterId.MATCH_DESCRIPTORS_RESPONSE, + [CommandId.ZDO_BIND_REQ]: ZdoClusterId.BIND_RESPONSE, + [CommandId.ZDO_UNBIND_REQ]: ZdoClusterId.UNBIND_RESPONSE, + [CommandId.ZDO_MGMT_LEAVE_REQ]: ZdoClusterId.LEAVE_RESPONSE, + [CommandId.ZDO_PERMIT_JOINING_REQ]: ZdoClusterId.PERMIT_JOINING_RESPONSE, + [CommandId.ZDO_MGMT_BIND_REQ]: ZdoClusterId.BINDING_TABLE_RESPONSE, + [CommandId.ZDO_MGMT_LQI_REQ]: ZdoClusterId.LQI_TABLE_RESPONSE, + // [CommandId.ZDO_MGMT_RTG_REQ]: ZdoClusterId.ROUTING_TABLE_RESPONSE, + [CommandId.ZDO_MGMT_NWK_UPDATE_REQ]: ZdoClusterId.NWK_UPDATE_RESPONSE, + [CommandId.ZDO_DEV_ANNCE_IND]: ZdoClusterId.END_DEVICE_ANNOUNCE, +}; + const commonResponse = [ {name: 'category', type: DataType.UINT8, typed: StatusCategory}, {name: 'status', type: DataType.UINT8, typed: [StatusCodeGeneric, StatusCodeAPS, StatusCodeCBKE]}, @@ -326,176 +365,176 @@ export const FRAMES: {[key in CommandId]?: ZBOSSFrameDesc} = { // ------------------------------------------ // Request for a remote device NWK address - [CommandId.ZDO_NWK_ADDR_REQ]: { - request: [ - {name: 'nwk', type: DataType.UINT16}, - {name: 'ieee', type: DataType.IEEE_ADDR}, - {name: 'type', type: DataType.UINT8}, - {name: 'startIndex', type: DataType.UINT8}, - ], - response: [ - ...commonResponse, - {name: 'ieee', type: DataType.IEEE_ADDR}, - {name: 'nwk', type: DataType.UINT16}, - {name: 'num', type: DataType.UINT8, condition: (payload, buffalo) => buffalo && buffalo.isMore()}, - {name: 'startIndex', type: DataType.UINT8, condition: (payload, buffalo) => buffalo && buffalo.isMore()}, - {name: 'nwks', type: BuffaloZclDataType.LIST_UINT16, options: (payload, options) => (options.length = payload.num)}, - ], - }, + // [CommandId.ZDO_NWK_ADDR_REQ]: { + // request: [ + // {name: 'nwk', type: DataType.UINT16}, + // {name: 'ieee', type: DataType.IEEE_ADDR}, + // {name: 'type', type: DataType.UINT8}, + // {name: 'startIndex', type: DataType.UINT8}, + // ], + // response: [ + // ...commonResponse, + // {name: 'ieee', type: DataType.IEEE_ADDR}, + // {name: 'nwk', type: DataType.UINT16}, + // {name: 'num', type: DataType.UINT8, condition: (payload, buffalo) => buffalo && buffalo.isMore()}, + // {name: 'startIndex', type: DataType.UINT8, condition: (payload, buffalo) => buffalo && buffalo.isMore()}, + // {name: 'nwks', type: BuffaloZclDataType.LIST_UINT16, options: (payload, options) => (options.length = payload.num)}, + // ], + // }, // Request for a remote device IEEE address - [CommandId.ZDO_IEEE_ADDR_REQ]: { - request: [ - {name: 'destNwk', type: DataType.UINT16}, - {name: 'nwk', type: DataType.UINT16}, - {name: 'type', type: DataType.UINT8}, - {name: 'startIndex', type: DataType.UINT8}, - ], - response: [ - ...commonResponse, - {name: 'ieee', type: DataType.IEEE_ADDR}, - {name: 'nwk', type: DataType.UINT16}, - {name: 'num', type: DataType.UINT8, condition: (payload, buffalo) => buffalo && buffalo.isMore()}, - {name: 'startIndex', type: DataType.UINT8, condition: (payload, buffalo) => buffalo && buffalo.isMore()}, - {name: 'nwks', type: BuffaloZclDataType.LIST_UINT16, options: (payload, options) => (options.length = payload.num)}, - ], - }, + // [CommandId.ZDO_IEEE_ADDR_REQ]: { + // request: [ + // {name: 'destNwk', type: DataType.UINT16}, + // {name: 'nwk', type: DataType.UINT16}, + // {name: 'type', type: DataType.UINT8}, + // {name: 'startIndex', type: DataType.UINT8}, + // ], + // response: [ + // ...commonResponse, + // {name: 'ieee', type: DataType.IEEE_ADDR}, + // {name: 'nwk', type: DataType.UINT16}, + // {name: 'num', type: DataType.UINT8, condition: (payload, buffalo) => buffalo && buffalo.isMore()}, + // {name: 'startIndex', type: DataType.UINT8, condition: (payload, buffalo) => buffalo && buffalo.isMore()}, + // {name: 'nwks', type: BuffaloZclDataType.LIST_UINT16, options: (payload, options) => (options.length = payload.num)}, + // ], + // }, // Get the Power Descriptor from a remote device - [CommandId.ZDO_POWER_DESC_REQ]: { - request: [{name: 'nwk', type: DataType.UINT16}], - response: [...commonResponse, {name: 'powerDescriptor', type: DataType.UINT16}, {name: 'nwk', type: DataType.UINT16}], - }, + // [CommandId.ZDO_POWER_DESC_REQ]: { + // request: [{name: 'nwk', type: DataType.UINT16}], + // response: [...commonResponse, {name: 'powerDescriptor', type: DataType.UINT16}, {name: 'nwk', type: DataType.UINT16}], + // }, // Get the Node Descriptor from a remote device - [CommandId.ZDO_NODE_DESC_REQ]: { - request: [{name: 'nwk', type: DataType.UINT16}], - response: [ - ...commonResponse, - {name: 'flags', type: DataType.UINT16}, - {name: 'macCapabilities', type: DataType.UINT8}, - {name: 'manufacturerCode', type: DataType.UINT16}, - {name: 'bufferSize', type: DataType.UINT8}, - {name: 'incomingSize', type: DataType.UINT16}, - {name: 'serverMask', type: DataType.UINT16}, - {name: 'outgoingSize', type: DataType.UINT16}, - {name: 'descriptorCapabilities', type: DataType.UINT8}, - {name: 'nwk', type: DataType.UINT16}, - ], - }, + // [CommandId.ZDO_NODE_DESC_REQ]: { + // request: [{name: 'nwk', type: DataType.UINT16}], + // response: [ + // ...commonResponse, + // {name: 'flags', type: DataType.UINT16}, + // {name: 'macCapabilities', type: DataType.UINT8}, + // {name: 'manufacturerCode', type: DataType.UINT16}, + // {name: 'bufferSize', type: DataType.UINT8}, + // {name: 'incomingSize', type: DataType.UINT16}, + // {name: 'serverMask', type: DataType.UINT16}, + // {name: 'outgoingSize', type: DataType.UINT16}, + // {name: 'descriptorCapabilities', type: DataType.UINT8}, + // {name: 'nwk', type: DataType.UINT16}, + // ], + // }, // Get the Simple Descriptor from a remote device - [CommandId.ZDO_SIMPLE_DESC_REQ]: { - request: [ - {name: 'nwk', type: DataType.UINT16}, - {name: 'endpoint', type: DataType.UINT8}, - ], - response: [ - ...commonResponse, - {name: 'endpoint', type: DataType.UINT8}, - {name: 'profileID', type: DataType.UINT16}, - {name: 'deviceID', type: DataType.UINT16}, - {name: 'version', type: DataType.UINT8}, - {name: 'inputClusterCount', type: DataType.UINT8}, - {name: 'outputClusterCount', type: DataType.UINT8}, - { - name: 'inputClusters', - type: BuffaloZclDataType.LIST_UINT16, - options: (payload, options) => (options.length = payload.inputClusterCount), - }, - { - name: 'outputClusters', - type: BuffaloZclDataType.LIST_UINT16, - options: (payload, options) => (options.length = payload.outputClusterCount), - }, - {name: 'nwk', type: DataType.UINT16}, - ], - }, + // [CommandId.ZDO_SIMPLE_DESC_REQ]: { + // request: [ + // {name: 'nwk', type: DataType.UINT16}, + // {name: 'endpoint', type: DataType.UINT8}, + // ], + // response: [ + // ...commonResponse, + // {name: 'endpoint', type: DataType.UINT8}, + // {name: 'profileID', type: DataType.UINT16}, + // {name: 'deviceID', type: DataType.UINT16}, + // {name: 'version', type: DataType.UINT8}, + // {name: 'inputClusterCount', type: DataType.UINT8}, + // {name: 'outputClusterCount', type: DataType.UINT8}, + // { + // name: 'inputClusters', + // type: BuffaloZclDataType.LIST_UINT16, + // options: (payload, options) => (options.length = payload.inputClusterCount), + // }, + // { + // name: 'outputClusters', + // type: BuffaloZclDataType.LIST_UINT16, + // options: (payload, options) => (options.length = payload.outputClusterCount), + // }, + // {name: 'nwk', type: DataType.UINT16}, + // ], + // }, // Get a list of Active Endpoints from a remote device - [CommandId.ZDO_ACTIVE_EP_REQ]: { - request: [{name: 'nwk', type: DataType.UINT16}], - response: [ - ...commonResponse, - {name: 'len', type: DataType.UINT8}, - {name: 'endpoints', type: BuffaloZclDataType.LIST_UINT8, options: (payload, options) => (options.length = payload.len)}, - {name: 'nwk', type: DataType.UINT16}, - ], - }, + // [CommandId.ZDO_ACTIVE_EP_REQ]: { + // request: [{name: 'nwk', type: DataType.UINT16}], + // response: [ + // ...commonResponse, + // {name: 'len', type: DataType.UINT8}, + // {name: 'endpoints', type: BuffaloZclDataType.LIST_UINT8, options: (payload, options) => (options.length = payload.len)}, + // {name: 'nwk', type: DataType.UINT16}, + // ], + // }, // Send Match Descriptor request to a remote device - [CommandId.ZDO_MATCH_DESC_REQ]: { - request: [ - {name: 'nwk', type: DataType.UINT16}, - {name: 'profileID', type: DataType.UINT16}, - {name: 'inputClusterCount', type: DataType.UINT8}, - {name: 'outputClusterCount', type: DataType.UINT8}, - { - name: 'inputClusters', - type: BuffaloZclDataType.LIST_UINT16, - options: (payload, options) => (options.length = payload.inputClusterCount), - }, - { - name: 'outputClusters', - type: BuffaloZclDataType.LIST_UINT16, - options: (payload, options) => (options.length = payload.outputClusterCount), - }, - ], - response: [ - ...commonResponse, - {name: 'len', type: DataType.UINT8}, - {name: 'endpoints', type: BuffaloZclDataType.LIST_UINT8, options: (payload, options) => (options.length = payload.len)}, - {name: 'nwk', type: DataType.UINT16}, - ], - }, + // [CommandId.ZDO_MATCH_DESC_REQ]: { + // request: [ + // {name: 'nwk', type: DataType.UINT16}, + // {name: 'profileID', type: DataType.UINT16}, + // {name: 'inputClusterCount', type: DataType.UINT8}, + // {name: 'outputClusterCount', type: DataType.UINT8}, + // { + // name: 'inputClusters', + // type: BuffaloZclDataType.LIST_UINT16, + // options: (payload, options) => (options.length = payload.inputClusterCount), + // }, + // { + // name: 'outputClusters', + // type: BuffaloZclDataType.LIST_UINT16, + // options: (payload, options) => (options.length = payload.outputClusterCount), + // }, + // ], + // response: [ + // ...commonResponse, + // {name: 'len', type: DataType.UINT8}, + // {name: 'endpoints', type: BuffaloZclDataType.LIST_UINT8, options: (payload, options) => (options.length = payload.len)}, + // {name: 'nwk', type: DataType.UINT16}, + // ], + // }, // Send Bind request to a remote device - [CommandId.ZDO_BIND_REQ]: { - request: [ - {name: 'target', type: DataType.UINT16}, - {name: 'srcIeee', type: DataType.IEEE_ADDR}, - {name: 'srcEP', type: DataType.UINT8}, - {name: 'clusterID', type: DataType.UINT16}, - {name: 'addrMode', type: DataType.UINT8}, - {name: 'dstIeee', type: DataType.IEEE_ADDR}, - {name: 'dstEP', type: DataType.UINT8}, - ], - response: [...commonResponse], - }, + // [CommandId.ZDO_BIND_REQ]: { + // request: [ + // {name: 'target', type: DataType.UINT16}, + // {name: 'srcIeee', type: DataType.IEEE_ADDR}, + // {name: 'srcEP', type: DataType.UINT8}, + // {name: 'clusterID', type: DataType.UINT16}, + // {name: 'addrMode', type: DataType.UINT8}, + // {name: 'dstIeee', type: DataType.IEEE_ADDR}, + // {name: 'dstEP', type: DataType.UINT8}, + // ], + // response: [...commonResponse], + // }, // Send Unbind request to a remote device - [CommandId.ZDO_UNBIND_REQ]: { - request: [ - {name: 'target', type: DataType.UINT16}, - {name: 'srcIeee', type: DataType.IEEE_ADDR}, - {name: 'srcEP', type: DataType.UINT8}, - {name: 'clusterID', type: DataType.UINT16}, - {name: 'addrMode', type: DataType.UINT8}, - {name: 'dstIeee', type: DataType.IEEE_ADDR}, - {name: 'dstEP', type: DataType.UINT8}, - ], - response: [...commonResponse], - }, + // [CommandId.ZDO_UNBIND_REQ]: { + // request: [ + // {name: 'target', type: DataType.UINT16}, + // {name: 'srcIeee', type: DataType.IEEE_ADDR}, + // {name: 'srcEP', type: DataType.UINT8}, + // {name: 'clusterID', type: DataType.UINT16}, + // {name: 'addrMode', type: DataType.UINT8}, + // {name: 'dstIeee', type: DataType.IEEE_ADDR}, + // {name: 'dstEP', type: DataType.UINT8}, + // ], + // response: [...commonResponse], + // }, // Request that a Remote Device leave the network - [CommandId.ZDO_MGMT_LEAVE_REQ]: { - request: [ - {name: 'nwk', type: DataType.UINT16}, - {name: 'ieee', type: DataType.IEEE_ADDR}, - {name: 'flags', type: DataType.UINT8}, - ], - response: [...commonResponse], - }, + // [CommandId.ZDO_MGMT_LEAVE_REQ]: { + // request: [ + // {name: 'nwk', type: DataType.UINT16}, + // {name: 'ieee', type: DataType.IEEE_ADDR}, + // {name: 'flags', type: DataType.UINT8}, + // ], + // response: [...commonResponse], + // }, // Request a remote device or devices to allow or disallow association - [CommandId.ZDO_PERMIT_JOINING_REQ]: { - request: [ - {name: 'nwk', type: DataType.UINT16}, - {name: 'duration', type: DataType.UINT8}, - {name: 'tcSignificance', type: DataType.UINT8}, - ], - response: [...commonResponse], - }, + // [CommandId.ZDO_PERMIT_JOINING_REQ]: { + // request: [ + // {name: 'nwk', type: DataType.UINT16}, + // {name: 'duration', type: DataType.UINT8}, + // {name: 'tcSignificance', type: DataType.UINT8}, + // ], + // response: [...commonResponse], + // }, // Device announce indication - [CommandId.ZDO_DEV_ANNCE_IND]: { - request: [], - response: [], - indication: [ - {name: 'nwk', type: DataType.UINT16}, - {name: 'ieee', type: DataType.IEEE_ADDR}, - {name: 'macCapabilities', type: DataType.UINT8}, - ], - }, + // [CommandId.ZDO_DEV_ANNCE_IND]: { + // request: [], + // response: [], + // indication: [ + // {name: 'nwk', type: DataType.UINT16}, + // {name: 'ieee', type: DataType.IEEE_ADDR}, + // {name: 'macCapabilities', type: DataType.UINT8}, + // ], + // }, // Rejoin to remote network even if joined already. If joined, clear internal data structures prior to joining. That call is useful for rejoin after parent loss. [CommandId.ZDO_REJOIN]: { request: [ @@ -519,51 +558,51 @@ export const FRAMES: {[key in CommandId]?: ZBOSSFrameDesc} = { response: [...commonResponse], }, // Sends a ZDO Mgmt Bind request to a remote device - [CommandId.ZDO_MGMT_BIND_REQ]: { - request: [ - {name: 'nwk', type: DataType.UINT16}, - {name: 'startIndex', type: DataType.UINT8}, - ], - response: [...commonResponse], - }, + // [CommandId.ZDO_MGMT_BIND_REQ]: { + // request: [ + // {name: 'nwk', type: DataType.UINT16}, + // {name: 'startIndex', type: DataType.UINT8}, + // ], + // response: [...commonResponse], + // }, // Sends a ZDO Mgmt LQI request to a remote device - [CommandId.ZDO_MGMT_LQI_REQ]: { - request: [ - {name: 'nwk', type: DataType.UINT16}, - {name: 'startIndex', type: DataType.UINT8}, - ], - response: [ - ...commonResponse, - {name: 'entries', type: DataType.UINT8}, - {name: 'startIndex', type: DataType.UINT8}, - {name: 'len', type: DataType.UINT8}, - { - name: 'neighbors', - type: BuffaloZBOSSDataType.LIST_TYPED, - typed: [ - {name: 'extendedPanID', type: BuffaloZBOSSDataType.EXTENDED_PAN_ID}, - {name: 'ieee', type: DataType.IEEE_ADDR}, - {name: 'nwk', type: DataType.UINT16}, - {name: 'relationship', type: DataType.UINT8}, - {name: 'joining', type: DataType.UINT8}, - {name: 'depth', type: DataType.UINT8}, - {name: 'lqi', type: DataType.UINT8}, - ], - options: (payload, options) => (options.length = payload.len), - }, - ], - }, + // [CommandId.ZDO_MGMT_LQI_REQ]: { + // request: [ + // {name: 'nwk', type: DataType.UINT16}, + // {name: 'startIndex', type: DataType.UINT8}, + // ], + // response: [ + // ...commonResponse, + // {name: 'entries', type: DataType.UINT8}, + // {name: 'startIndex', type: DataType.UINT8}, + // {name: 'len', type: DataType.UINT8}, + // { + // name: 'neighbors', + // type: BuffaloZBOSSDataType.LIST_TYPED, + // typed: [ + // {name: 'extendedPanID', type: BuffaloZBOSSDataType.EXTENDED_PAN_ID}, + // {name: 'ieee', type: DataType.IEEE_ADDR}, + // {name: 'nwk', type: DataType.UINT16}, + // {name: 'relationship', type: DataType.UINT8}, + // {name: 'joining', type: DataType.UINT8}, + // {name: 'depth', type: DataType.UINT8}, + // {name: 'lqi', type: DataType.UINT8}, + // ], + // options: (payload, options) => (options.length = payload.len), + // }, + // ], + // }, // Sends a ZDO Mgmt NWK Update Request to a remote device - [CommandId.ZDO_MGMT_NWK_UPDATE_REQ]: { - request: [ - {name: 'channelMask', type: DataType.UINT32}, - {name: 'duration', type: DataType.UINT8}, - {name: 'count', type: DataType.UINT8}, - {name: 'managerNwk', type: DataType.UINT16}, - {name: 'nwk', type: DataType.UINT16}, - ], - response: [...commonResponse], - }, + // [CommandId.ZDO_MGMT_NWK_UPDATE_REQ]: { + // request: [ + // {name: 'channelMask', type: DataType.UINT32}, + // {name: 'duration', type: DataType.UINT8}, + // {name: 'count', type: DataType.UINT8}, + // {name: 'managerNwk', type: DataType.UINT16}, + // {name: 'nwk', type: DataType.UINT16}, + // ], + // response: [...commonResponse], + // }, // Require statistics (last message LQI\RSSI, counters, etc.) from the ZDO level [CommandId.ZDO_GET_STATS]: { request: [{name: 'cleanup', type: DataType.UINT8}], @@ -614,8 +653,12 @@ export const FRAMES: {[key in CommandId]?: ZBOSSFrameDesc} = { indication: [ {name: 'ieee', type: DataType.IEEE_ADDR}, {name: 'nwk', type: DataType.UINT16}, - {name: 'authType', type: DataType.UINT8}, - {name: 'authStatus', type: DataType.UINT8}, + {name: 'authType', type: DataType.UINT8, typed: DeviceAuthorizedType}, + { + name: 'authStatus', + type: DataType.UINT8, + /* typed: DeviceAuthorizedLegacyStatus | DeviceAuthorizedR21TCLKStatus | DeviceAuthorizedSECBKEStatus */ + }, ], }, // Indicates some device joined the network @@ -626,6 +669,9 @@ export const FRAMES: {[key in CommandId]?: ZBOSSFrameDesc} = { {name: 'ieee', type: DataType.IEEE_ADDR}, {name: 'nwk', type: DataType.UINT16}, {name: 'status', type: DataType.UINT8, typed: DeviceUpdateStatus}, + // not in dsr-corporation spec + // {name: 'tcAction', type: DataType.UINT8, typed: DeviceUpdateTCAction}, + // {name: 'parentNwk', type: DataType.UINT16}, ], }, // Sets manufacturer code field in the node descriptor diff --git a/src/adapter/zboss/driver.ts b/src/adapter/zboss/driver.ts index 6b53657264..7d081107b5 100644 --- a/src/adapter/zboss/driver.ts +++ b/src/adapter/zboss/driver.ts @@ -1,5 +1,6 @@ /* istanbul ignore file */ +import assert from 'assert'; import EventEmitter from 'events'; import equals from 'fast-deep-equal/es6'; @@ -8,6 +9,8 @@ import {TsType} from '..'; import {KeyValue} from '../../controller/tstype'; import {Queue, Waitress} from '../../utils'; import {logger} from '../../utils/logger'; +import * as Zdo from '../../zspec/zdo'; +import {ZDO_REQ_CLUSTER_ID_TO_ZBOSS_COMMAND_ID} from './commands'; import {CommandId, DeviceType, PolicyType, ResetOptions, StatusCodeGeneric} from './enums'; import {FrameType, makeFrame, ZBOSSFrame} from './frame'; import {ZBOSSUart} from './uart'; @@ -17,7 +20,7 @@ const NS = 'zh:zboss:driv'; const MAX_INIT_ATTEMPTS = 5; type ZBOSSWaitressMatcher = { - tsn: number | null; + tsn?: number; commandId: number; }; @@ -38,7 +41,7 @@ export class ZBOSSDriver extends EventEmitter { private queue: Queue; private tsn = 1; // command sequence private nwkOpt: TsType.NetworkOptions; - public netInfo?: ZBOSSNetworkInfo; + public netInfo!: ZBOSSNetworkInfo; // expected valid upon startup of driver constructor(options: TsType.SerialPortOptions, nwkOpt: TsType.NetworkOptions) { super(); @@ -89,7 +92,7 @@ export class ZBOSSDriver extends EventEmitter { // const restore = await this.needsToBeRestore(this.nwkOpt); const restore = false; - if (this.netInfo?.joined) { + if (this.netInfo.joined) { logger.info(`Leaving current network and forming new network`, NS); await this.reset(ResetOptions.FactoryReset); } @@ -261,7 +264,7 @@ export class ZBOSSDriver extends EventEmitter { return await this.queue.execute(async (): Promise => { const frame = makeFrame(FrameType.REQUEST, commandId, params); frame.tsn = this.tsn; - const waiter = this.waitFor(commandId, commandId == CommandId.NCP_RESET ? null : this.tsn, timeout); + const waiter = this.waitFor(commandId, commandId == CommandId.NCP_RESET ? undefined : this.tsn, timeout); this.tsn = (this.tsn + 1) & 255; try { @@ -282,7 +285,11 @@ export class ZBOSSDriver extends EventEmitter { }); } - public waitFor(commandId: number, tsn: number | null, timeout = 10000): {start: () => {promise: Promise; ID: number}; ID: number} { + public waitFor( + commandId: number, + tsn: number | undefined, + timeout = 10000, + ): {start: () => {promise: Promise; ID: number}; ID: number} { return this.waitress.waitFor({commandId, tsn}, timeout); } @@ -291,83 +298,7 @@ export class ZBOSSDriver extends EventEmitter { } private waitressValidator(payload: ZBOSSFrame, matcher: ZBOSSWaitressMatcher): boolean { - return (matcher.tsn == null || payload.tsn === matcher.tsn) && matcher.commandId == payload.commandId; - } - - public async getCoordinator(): Promise { - const message = await this.activeEndpoints(0x0000); - const activeEndpoints = message.payload.endpoints || []; - const ap = []; - for (const ep in activeEndpoints) { - const sd = await this.simpleDescriptor(0x0000, activeEndpoints[ep]); - ap.push({ - ID: sd.payload.endpoint, - profileID: sd.payload.profileID, - deviceID: sd.payload.deviceID, - inputClusters: sd.payload.inputClusters, - outputClusters: sd.payload.outputClusters, - }); - } - return { - ieeeAddr: this.netInfo?.ieeeAddr || '', - networkAddress: 0x0000, - manufacturerID: 0x0000, - endpoints: ap, - }; - } - - public async getCoordinatorVersion(): Promise { - const ver = await this.execCommand(CommandId.GET_MODULE_VERSION, {}); - const cver = await this.execCommand(CommandId.GET_COORDINATOR_VERSION, {}); - const ver2str = (version: number): string => { - const major = (version >> 24) & 0xff; - const minor = (version >> 16) & 0xff; - const revision = (version >> 8) & 0xff; - const commit = version & 0xff; - return `${major}.${minor}.${revision}.${commit}`; - }; - - return { - type: `zboss`, - meta: { - coordinator: cver.payload.version, - stack: ver2str(ver.payload.stackVersion), - protocol: ver2str(ver.payload.protocolVersion), - revision: ver2str(ver.payload.fwVersion), - }, - }; - } - - public async permitJoin(nwk: number, duration: number): Promise { - await this.execCommand(CommandId.ZDO_PERMIT_JOINING_REQ, {nwk: nwk, duration: duration, tcSignificance: 1}); - } - - public async setTXPower(value: number): Promise { - await this.execCommand(CommandId.SET_TX_POWER, {txPower: value}); - } - - public async lqi(nwk: number, index: number): Promise { - return await this.execCommand(CommandId.ZDO_MGMT_LQI_REQ, {nwk: nwk, startIndex: index}); - } - - public async neighbors(ieee: string): Promise { - return await this.execCommand(CommandId.NWK_GET_NEIGHBOR_BY_IEEE, {ieee: ieee}); - } - - public async nodeDescriptor(nwk: number): Promise { - return await this.execCommand(CommandId.ZDO_NODE_DESC_REQ, {nwk: nwk}); - } - - public async activeEndpoints(nwk: number): Promise { - return await this.execCommand(CommandId.ZDO_ACTIVE_EP_REQ, {nwk: nwk}); - } - - public async simpleDescriptor(nwk: number, ep: number): Promise { - return await this.execCommand(CommandId.ZDO_SIMPLE_DESC_REQ, {nwk: nwk, endpoint: ep}); - } - - public async removeDevice(nwk: number, ieee: string): Promise { - return await this.execCommand(CommandId.ZDO_MGMT_LEAVE_REQ, {nwk: nwk, ieee: ieee, flags: 0}); + return (matcher.tsn === undefined || matcher.tsn === payload.tsn) && matcher.commandId == payload.commandId; } public async request(ieee: string, profileID: number, clusterID: number, dstEp: number, srcEp: number, data: Buffer): Promise { @@ -390,43 +321,47 @@ export class ZBOSSDriver extends EventEmitter { return await this.execCommand(CommandId.APSDE_DATA_REQ, payload); } - public async bind( - destinationNetworkAddress: number, - sourceIeeeAddress: string, - sourceEndpoint: number, - clusterID: number, - destinationAddressOrGroup: string | number, - type: 'endpoint' | 'group', - destinationEndpoint?: number, - ): Promise { - return await this.execCommand(CommandId.ZDO_BIND_REQ, { - target: destinationNetworkAddress, - srcIeee: sourceIeeeAddress, - srcEP: sourceEndpoint, - clusterID: clusterID, - addrMode: type == 'endpoint' ? 3 /* ieee */ : 1 /* group */, - dstIeee: destinationAddressOrGroup, - dstEP: destinationEndpoint || 1, - }); - } + public async requestZdo(clusterId: Zdo.ClusterId, payload: Buffer, disableResponse: boolean): Promise { + if (!this.port.portOpen) { + throw new Error('Connection not initialized'); + } - public async unbind( - destinationNetworkAddress: number, - sourceIeeeAddress: string, - sourceEndpoint: number, - clusterID: number, - destinationAddressOrGroup: string | number, - type: 'endpoint' | 'group', - destinationEndpoint?: number, - ): Promise { - return await this.execCommand(CommandId.ZDO_UNBIND_REQ, { - target: destinationNetworkAddress, - srcIeee: sourceIeeeAddress, - srcEP: sourceEndpoint, - clusterID: clusterID, - addrMode: type == 'endpoint' ? 3 /* ieee */ : 1 /* group */, - dstIeee: destinationAddressOrGroup, - dstEP: destinationEndpoint || 1, + const commandId = ZDO_REQ_CLUSTER_ID_TO_ZBOSS_COMMAND_ID[clusterId]; + assert(commandId !== undefined, `ZDO cluster ID '${clusterId}' not supported.`); + + const cmdLog = `${Zdo.ClusterId[clusterId]}(cmd: ${commandId})`; + logger.debug(() => `===> ZDO ${cmdLog}: ${payload.toString('hex')}`, NS); + + return await this.queue.execute(async () => { + const buf = Buffer.alloc(5 + payload.length); + buf.writeInt8(0, 0); + buf.writeInt8(FrameType.REQUEST, 1); + buf.writeUInt16LE(commandId, 2); + buf.writeUInt8(this.tsn, 4); + buf.set(payload, 5); + + let waiter; + + if (!disableResponse) { + waiter = this.waitFor(commandId, this.tsn, 10000); + } + + this.tsn = (this.tsn + 1) & 255; + + try { + await this.port.sendBuffer(buf); + + if (waiter) { + return await waiter.start().promise; + } + } catch (error) { + if (waiter) { + this.waitress.remove(waiter.ID); + } + + logger.debug(`=x=> Failed to send ${cmdLog}: ${(error as Error).stack}`, NS); + throw new Error(`Failed to send ${cmdLog}.`); + } }); } diff --git a/src/adapter/zboss/enums.ts b/src/adapter/zboss/enums.ts index 8df83288f5..378f97d4e8 100644 --- a/src/adapter/zboss/enums.ts +++ b/src/adapter/zboss/enums.ts @@ -215,6 +215,7 @@ export enum CommandId { ZDO_SYSTEM_SRV_DISCOVERY_REQ = 0x020e, ZDO_MGMT_BIND_REQ = 0x020f, ZDO_MGMT_LQI_REQ = 0x0210, + // ZDO_MGMT_RTG_REQ = 0x0???, ZDO_MGMT_NWK_UPDATE_REQ = 0x0211, ZDO_GET_STATS = 0x0213, ZDO_DEV_AUTHORIZED_IND = 0x0214, @@ -320,9 +321,40 @@ export enum BuffaloZBOSSDataType { EXTENDED_PAN_ID = 3001, } +export enum DeviceAuthorizedType { + LEGACY = 0, + R21_TCLK = 1, + SE_CBKE = 2, +} + +export enum DeviceAuthorizedLegacyStatus { + SUCCESS = 0, + FAILED = 1, +} + +export enum DeviceAuthorizedR21TCLKStatus { + SUCCESS = 0, + TIMEOUT = 1, + FAILED = 2, +} + +export enum DeviceAuthorizedSECBKEStatus { + SUCCESS = 0, +} + export enum DeviceUpdateStatus { - SECURE_REJOIN = 0, - UNSECURE_REJOIN = 1, + SECURED_REJOIN = 0, + UNSECURED_JOIN = 1, LEFT = 2, TC_REJOIN = 3, + // 0x04 – 0x07 = Reserved +} + +export enum DeviceUpdateTCAction { + /* authorize device */ + AUTHORIZE = 0, + /* deby authorization - msend Remove device */ + DENY = 1, + /* ignore Update Device - that meay lead to authorization deny */ + IGNORE = 2, } diff --git a/src/adapter/zboss/frame.ts b/src/adapter/zboss/frame.ts index 16b9a6d49f..b44e5b7b76 100644 --- a/src/adapter/zboss/frame.ts +++ b/src/adapter/zboss/frame.ts @@ -5,7 +5,10 @@ import {DataType} from '../../zspec/zcl'; import {BuffaloZcl} from '../../zspec/zcl/buffaloZcl'; import {BuffaloZclDataType} from '../../zspec/zcl/definition/enums'; import {BuffaloZclOptions} from '../../zspec/zcl/definition/tstype'; -import {FRAMES, ParamsDesc} from './commands'; +import {ClusterId as ZdoClusterId} from '../../zspec/zdo'; +import {BuffaloZdo} from '../../zspec/zdo/buffaloZdo'; +import {GenericZdoResponse} from '../../zspec/zdo/definition/tstypes'; +import {FRAMES, ParamsDesc, ZBOSS_COMMAND_ID_TO_ZDO_RSP_CLUSTER_ID} from './commands'; import {BuffaloZBOSSDataType, CommandId} from './enums'; export class ZBOSSBuffaloZcl extends BuffaloZcl { @@ -99,24 +102,72 @@ function getFrameDesc(type: FrameType, key: CommandId): ParamsDesc[] { } } +function fixNonStandardZdoRspPayload(clusterId: ZdoClusterId, buffer: Buffer): Buffer { + switch (clusterId) { + case ZdoClusterId.NODE_DESCRIPTOR_RESPONSE: + case ZdoClusterId.POWER_DESCRIPTOR_RESPONSE: + case ZdoClusterId.ACTIVE_ENDPOINTS_RESPONSE: + case ZdoClusterId.MATCH_DESCRIPTORS_RESPONSE: { + // flip nwkAddress from end to start + return Buffer.concat([buffer.subarray(0, 1), buffer.subarray(-2), buffer.subarray(1, -2)]); + } + + case ZdoClusterId.SIMPLE_DESCRIPTOR_RESPONSE: { + // flip nwkAddress from end to start + // add length after nwkAddress + // move outClusterCount before inClusterList + const inClusterListSize = buffer[7] * 2; // uint16 + return Buffer.concat([ + buffer.subarray(0, 1), // status + buffer.subarray(-2), // nwkAddress + Buffer.from([buffer.length - 3 /* status + nwkAddress */]), + buffer.subarray(1, 8), // endpoint>inClusterCount + buffer.subarray(9, 9 + inClusterListSize), // inClusterList + buffer.subarray(8, 9), // outClusterCount + buffer.subarray(9 + inClusterListSize, -2), // outClusterList + ]); + } + } + + return buffer; +} + export function readZBOSSFrame(buffer: Buffer): ZBOSSFrame { const buf = new ZBOSSBuffaloZcl(buffer); const version = buf.readUInt8(); - const type = buf.readUInt8(); - const commandId = buf.readUInt16(); - let tsn = 0; - if ([FrameType.REQUEST, FrameType.RESPONSE].includes(type)) { - tsn = buf.readUInt8(); + const type: FrameType = buf.readUInt8(); + const commandId: CommandId = buf.readUInt16(); + const tsn = type === FrameType.REQUEST || type === FrameType.RESPONSE ? buf.readUInt8() : 0; + + const zdoResponseClusterId = + type === FrameType.RESPONSE || type === FrameType.INDICATION ? ZBOSS_COMMAND_ID_TO_ZDO_RSP_CLUSTER_ID[commandId] : undefined; + + if (zdoResponseClusterId !== undefined) { + // FrameType.INDICATION has no tsn (above), no category + const category = type === FrameType.RESPONSE ? buf.readUInt8() : undefined; + const zdoPayload = fixNonStandardZdoRspPayload(zdoResponseClusterId, buffer.subarray(type === FrameType.RESPONSE ? 6 : 4)); + const zdo = BuffaloZdo.readResponse(false, zdoResponseClusterId, zdoPayload); + + return { + version, + type, + commandId, + tsn, + payload: { + category, + zdoClusterId: zdoResponseClusterId, + zdo, + }, + }; + } else { + return { + version, + type, + commandId, + tsn, + payload: readPayload(type, commandId, buf), + }; } - const payload = readPayload(type, commandId, buf); - - return { - version, - type, - commandId, - tsn, - payload, - }; } export function writeZBOSSFrame(frame: ZBOSSFrame): Buffer { @@ -140,7 +191,7 @@ export interface ZBOSSFrame { type: FrameType; commandId: CommandId; tsn: number; - payload: KeyValue; + payload: KeyValue & {zdoCluster?: ZdoClusterId; zdo?: GenericZdoResponse}; } export function makeFrame(type: FrameType, commandId: CommandId, params: KeyValue): ZBOSSFrame { diff --git a/src/adapter/zboss/uart.ts b/src/adapter/zboss/uart.ts index 47196cc464..5d3859a5ce 100644 --- a/src/adapter/zboss/uart.ts +++ b/src/adapter/zboss/uart.ts @@ -271,9 +271,8 @@ export class ZBOSSUart extends EventEmitter { } } - public async sendFrame(frame: ZBOSSFrame): Promise { + public async sendBuffer(buf: Buffer): Promise { try { - const buf = writeZBOSSFrame(frame); logger.debug(`--> FRAME: ${buf.toString('hex')}`, NS); let flags = (this.sendSeq & 0x03) << 2; // sequence flags = flags | ZBOSS_FLAG_FIRST_FRAGMENT | ZBOSS_FLAG_LAST_FRAGMENT; @@ -297,6 +296,10 @@ export class ZBOSSUart extends EventEmitter { } } + public async sendFrame(frame: ZBOSSFrame): Promise { + return await this.sendBuffer(writeZBOSSFrame(frame)); + } + private async sendDATA(data: Buffer, isACK: boolean = false): Promise { const seq = this.sendSeq; const nextSeq = this.sendSeq; diff --git a/src/adapter/zigate/adapter/patchZdoBuffaloBE.ts b/src/adapter/zigate/adapter/patchZdoBuffaloBE.ts new file mode 100644 index 0000000000..80898951d9 --- /dev/null +++ b/src/adapter/zigate/adapter/patchZdoBuffaloBE.ts @@ -0,0 +1,47 @@ +import {EUI64} from '../../../zspec/tstypes'; +import {BuffaloZdo} from '../../../zspec/zdo/buffaloZdo'; + +class ZiGateZdoBuffalo extends BuffaloZdo { + public writeUInt16(value: number): void { + this.buffer.writeUInt16BE(value, this.position); + this.position += 2; + } + + public readUInt16(): number { + const value = this.buffer.readUInt16BE(this.position); + this.position += 2; + return value; + } + + public writeUInt32(value: number): void { + this.buffer.writeUInt32BE(value, this.position); + this.position += 4; + } + + public readUInt32(): number { + const value = this.buffer.readUInt32BE(this.position); + this.position += 4; + return value; + } + + public writeIeeeAddr(value: string /*TODO: EUI64*/): void { + this.writeUInt32(parseInt(value.slice(2, 10), 16)); + this.writeUInt32(parseInt(value.slice(10), 16)); + } + + public readIeeeAddr(): EUI64 { + return `0x${this.readBuffer(8).toString('hex')}`; + } +} + +/** + * Patch BuffaloZdo to use Big Endian variants. + */ +export const patchZdoBuffaloBE = (): void => { + BuffaloZdo.prototype.writeUInt16 = ZiGateZdoBuffalo.prototype.writeUInt16; + BuffaloZdo.prototype.readUInt16 = ZiGateZdoBuffalo.prototype.readUInt16; + BuffaloZdo.prototype.writeUInt32 = ZiGateZdoBuffalo.prototype.writeUInt32; + BuffaloZdo.prototype.readUInt32 = ZiGateZdoBuffalo.prototype.readUInt32; + BuffaloZdo.prototype.writeIeeeAddr = ZiGateZdoBuffalo.prototype.writeIeeeAddr; + BuffaloZdo.prototype.readIeeeAddr = ZiGateZdoBuffalo.prototype.readIeeeAddr; +}; diff --git a/src/adapter/zigate/adapter/zigateAdapter.ts b/src/adapter/zigate/adapter/zigateAdapter.ts index 504e4af930..b7fed30624 100644 --- a/src/adapter/zigate/adapter/zigateAdapter.ts +++ b/src/adapter/zigate/adapter/zigateAdapter.ts @@ -1,20 +1,22 @@ /* istanbul ignore file */ -import {Buffalo} from '../../../buffalo'; -import {KeyValue} from '../../../controller/tstype'; import * as Models from '../../../models'; import {Queue, Wait, Waitress} from '../../../utils'; import {logger} from '../../../utils/logger'; +import * as ZSpec from '../../../zspec'; import {BroadcastAddress} from '../../../zspec/enums'; +import {EUI64} from '../../../zspec/tstypes'; import * as Zcl from '../../../zspec/zcl'; +import * as Zdo from '../../../zspec/zdo'; +import * as ZdoTypes from '../../../zspec/zdo/definition/tstypes'; import Adapter from '../../adapter'; import * as Events from '../../events'; import * as TsType from '../../tstype'; -import {ActiveEndpoints, DeviceType, LQI, LQINeighbor, NodeDescriptor, SimpleDescriptor} from '../../tstype'; import {RawAPSDataRequestPayload} from '../driver/commandType'; import {ADDRESS_MODE, coordinatorEndpoints, DEVICE_TYPE, ZiGateCommandCode, ZiGateMessageCode, ZPSNwkKeyState} from '../driver/constants'; import Driver from '../driver/zigate'; import ZiGateObject from '../driver/ziGateObject'; +import {patchZdoBuffaloBE} from './patchZdoBuffaloBE'; const NS = 'zh:zigate'; const default_bind_group = 901; // https://github.com/Koenkk/zigbee-herdsman-converters/blob/master/lib/constants.js#L3 @@ -28,8 +30,6 @@ interface WaitressMatcher { direction: number; } -const channelsToMask = (channels: number[]): number => channels.map((x) => 2 ** x).reduce((acc, x) => acc + x, 0); - class ZiGateAdapter extends Adapter { private driver: Driver; private joinPermitted: boolean; @@ -43,8 +43,9 @@ class ZiGateAdapter extends Adapter { backupPath: string, adapterOptions: TsType.AdapterOptions, ) { + patchZdoBuffaloBE(); super(networkOptions, serialPortOptions, backupPath, adapterOptions); - this.hasZdoMessageOverhead = false; + this.hasZdoMessageOverhead = false; // false for requests, true for responses this.joinPermitted = false; this.closing = false; @@ -58,6 +59,7 @@ class ZiGateAdapter extends Adapter { this.driver.on('LeaveIndication', this.leaveIndicationListener.bind(this)); this.driver.on('DeviceAnnounce', this.deviceAnnounceListener.bind(this)); this.driver.on('close', this.onZiGateClose.bind(this)); + this.driver.on('zdoResponse', this.onZdoResponse.bind(this)); } /** @@ -84,9 +86,9 @@ class ZiGateAdapter extends Adapter { await this.driver.sendCommand(ZiGateCommandCode.AddGroup, { addressMode: ADDRESS_MODE.short, - shortAddress: 0x0000, - sourceEndpoint: 0x01, - destinationEndpoint: 0x01, + shortAddress: ZSpec.COORDINATOR_ADDRESS, + sourceEndpoint: ZSpec.HA_ENDPOINT, + destinationEndpoint: ZSpec.HA_ENDPOINT, groupAddress: default_bind_group, }); } catch (error) { @@ -107,7 +109,7 @@ class ZiGateAdapter extends Adapter { // @TODO deal hardcoded endpoints, made by analogy with deconz // polling the coordinator on some firmware went into a memory leak, so we don't ask this info const response: TsType.Coordinator = { - networkAddress: 0, + networkAddress: ZSpec.COORDINATOR_ADDRESS, manufacturerID: 0, ieeeAddr: networkResponse.payload.extendedAddress, endpoints: coordinatorEndpoints.slice(), // copy @@ -134,15 +136,29 @@ class ZiGateAdapter extends Adapter { } public async permitJoin(seconds: number, networkAddress?: number): Promise { - const result = await this.driver.sendCommand(ZiGateCommandCode.PermitJoin, { - targetShortAddress: networkAddress || 0xfffc, - interval: seconds, - TCsignificance: 0, - }); + const clusterId = Zdo.ClusterId.PERMIT_JOINING_REQUEST; + + if (networkAddress !== undefined) { + // specific device that is not `Coordinator` + // `authentication`: TC significance always 1 (zb specs) + const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, seconds, 1, []); + + const result = await this.sendZdo(ZSpec.BLANK_EUI64, networkAddress, clusterId, zdoPayload, false); + + /* istanbul ignore next */ + if (!Zdo.Buffalo.checkStatus(result)) { + // TODO: will disappear once moved upstream + throw new Zdo.StatusError(result[0]); + } + } else { + // broadcast permit joining ZDO + // `authentication`: TC significance always 1 (zb specs) + const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, seconds, 1, []); - // const result = await this.driver.sendCommand(ZiGateCommandCode.PermitJoinStatus, {}); - // Suitable only for the coordinator, not the entire network or point-to-point for routers - this.joinPermitted = result.payload.status === 0; + await this.sendZdo(ZSpec.BLANK_EUI64, ZSpec.BroadcastAddress.DEFAULT, clusterId, zdoPayload, true); + } + + this.joinPermitted = seconds !== 0; } // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -184,9 +200,12 @@ class ZiGateAdapter extends Adapter { throw new Error('This adapter does not support backup'); } - // eslint-disable-next-line @typescript-eslint/no-unused-vars public async changeChannel(newChannel: number): Promise { - throw new Error(`Channel change is not supported for 'zigate'`); + const clusterId = Zdo.ClusterId.NWK_UPDATE_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, [newChannel], 0xfe, undefined, undefined, undefined); + + await this.sendZdo(ZSpec.BLANK_EUI64, ZSpec.BroadcastAddress.SLEEPY, clusterId, zdoPayload, true /* handled below */); + await Wait(12000); } public async setTransmitPower(value: number): Promise { @@ -198,221 +217,152 @@ class ZiGateAdapter extends Adapter { } public async lqi(networkAddress: number): Promise { - return await this.queue.execute(async (): Promise => { - const neighbors: LQINeighbor[] = []; + const clusterId = Zdo.ClusterId.LQI_TABLE_REQUEST; + const neighbors: TsType.LQINeighbor[] = []; + const request = async (startIndex: number): Promise<[tableEntries: number, entryCount: number]> => { + const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, startIndex); + const result = await this.sendZdo(ZSpec.BLANK_EUI64, networkAddress, clusterId, zdoPayload, false); - const add = (list: Buffer[]): void => { - for (const entry of list) { - const relationByte = entry.readUInt8(18); - const extAddr: Buffer = entry.subarray(8, 16); + /* istanbul ignore else */ + if (Zdo.Buffalo.checkStatus(result)) { + const payload = result[1]; + + for (const entry of payload.entryList) { neighbors.push({ - linkquality: entry.readUInt8(21), - networkAddress: entry.readUInt16LE(16), - ieeeAddr: new Buffalo(extAddr).readIeeeAddr(), - relationship: (relationByte >> 1) & ((1 << 3) - 1), - depth: entry.readUInt8(20), + ieeeAddr: entry.eui64, + networkAddress: entry.nwkAddress, + linkquality: entry.lqi, + relationship: entry.relationship, + depth: entry.depth, }); } - }; - const request = async ( - startIndex: number, - ): Promise<{ - status: number; - tableEntrys: number; - startIndex: number; - tableListCount: number; - tableList: Buffer[]; - }> => { - try { - const resultPayload = await this.driver.sendCommand(ZiGateCommandCode.ManagementLQI, { - targetAddress: networkAddress, - startIndex: startIndex, + return [payload.neighborTableEntries, payload.entryList.length]; + } else { + // TODO: will disappear once moved upstream + throw new Zdo.StatusError(result[0]); + } + }; + + let [tableEntries, entryCount] = await request(0); + + const size = tableEntries; + let nextStartIndex = entryCount; + + while (neighbors.length < size) { + [tableEntries, entryCount] = await request(nextStartIndex); + + nextStartIndex += entryCount; + } + + return {neighbors}; + } + + public async routingTable(networkAddress: number): Promise { + const clusterId = Zdo.ClusterId.ROUTING_TABLE_REQUEST; + const table: TsType.RoutingTableEntry[] = []; + const request = async (startIndex: number): Promise<[tableEntries: number, entryCount: number]> => { + const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, startIndex); + const result = await this.sendZdo(ZSpec.BLANK_EUI64, networkAddress, clusterId, zdoPayload, false); + + /* istanbul ignore else */ + if (Zdo.Buffalo.checkStatus(result)) { + const payload = result[1]; + + for (const entry of payload.entryList) { + table.push({ + destinationAddress: entry.destinationAddress, + status: entry.status, + nextHop: entry.nextHopAddress, }); - const data = resultPayload.payload.payload; - - if (data[1] !== 0) { - // status - throw new Error(`LQI for '${networkAddress}' failed`); - } - const tableList: Buffer[] = []; - const response = { - status: data[1], - tableEntrys: data[2], - startIndex: data[3], - tableListCount: data[4], - tableList: tableList, - }; - - let tableEntry: number[] = []; - let counter = 0; - - for (let i = 5; i < response.tableListCount * 22 + 5; i++) { - // one tableentry = 22 bytes - tableEntry.push(data[i]); - counter++; - if (counter === 22) { - response.tableList.push(Buffer.from(tableEntry)); - tableEntry = []; - counter = 0; - } - } - - logger.debug( - 'LQI RESPONSE - addr: ' + - networkAddress.toString(16) + - ' status: ' + - response.status + - ' read ' + - (response.tableListCount + response.startIndex) + - '/' + - response.tableEntrys + - ' entrys', - NS, - ); - return response; - } catch (error) { - const msg = 'LQI REQUEST FAILED - addr: 0x' + networkAddress.toString(16) + ' ' + error; - logger.error(msg, NS); - throw new Error(msg); } - }; - let response = await request(0); - add(response.tableList); - let nextStartIndex = response.tableListCount; - - while (neighbors.length < response.tableEntrys) { - response = await request(nextStartIndex); - add(response.tableList); - nextStartIndex += response.tableListCount; + return [payload.routingTableEntries, payload.entryList.length]; + } else { + // TODO: will disappear once moved upstream + throw new Zdo.StatusError(result[0]); } + }; - return {neighbors}; - }, networkAddress); - } + let [tableEntries, entryCount] = await request(0); - // @TODO - // eslint-disable-next-line @typescript-eslint/no-unused-vars - public routingTable(networkAddress: number): Promise { - return Promise.resolve({table: []}); + const size = tableEntries; + let nextStartIndex = entryCount; + + while (table.length < size) { + [tableEntries, entryCount] = await request(nextStartIndex); + + nextStartIndex += entryCount; + } + + return {table}; } public async nodeDescriptor(networkAddress: number): Promise { - return await this.queue.execute(async () => { - try { - const nodeDescriptorResponse = await this.driver.sendCommand(ZiGateCommandCode.NodeDescriptor, { - targetShortAddress: networkAddress, - }); - - const data: Buffer = nodeDescriptorResponse.payload.payload; - const buf = data; - const logicaltype = data[4] & 7; - let type: DeviceType = 'Unknown'; - switch (logicaltype) { - case 1: - type = 'Router'; - break; - case 2: - type = 'EndDevice'; - break; - case 0: - type = 'Coordinator'; - break; - } - const manufacturer = buf.readUInt16LE(7); - - logger.debug( - 'RECEIVING NODE_DESCRIPTOR - addr: 0x' + - networkAddress.toString(16) + - ' type: ' + - type + - ' manufacturer: 0x' + - manufacturer.toString(16), - NS, - ); - - return {manufacturerCode: manufacturer, type}; - } catch (error) { - const msg = 'RECEIVING NODE_DESCRIPTOR FAILED - addr: 0x' + networkAddress.toString(16) + ' ' + error; - logger.error(msg, NS); - throw new Error(msg); + const clusterId = Zdo.ClusterId.NODE_DESCRIPTOR_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, networkAddress); + const result = await this.sendZdo(ZSpec.BLANK_EUI64, networkAddress, clusterId, zdoPayload, false); + + /* istanbul ignore else */ + if (Zdo.Buffalo.checkStatus(result)) { + const payload = result[1]; + let type: TsType.DeviceType = 'Unknown'; + + switch (payload.logicalType) { + case 0x0: + type = 'Coordinator'; + break; + case 0x1: + type = 'Router'; + break; + case 0x2: + type = 'EndDevice'; + break; } - }, networkAddress); + + return {type, manufacturerCode: payload.manufacturerCode}; + } else { + // TODO: will disappear once moved upstream + throw new Zdo.StatusError(result[0]); + } } public async activeEndpoints(networkAddress: number): Promise { - return await this.queue.execute(async () => { - const payload = { - targetShortAddress: networkAddress, - }; - try { - const result = await this.driver.sendCommand(ZiGateCommandCode.ActiveEndpoint, payload); - const buf = Buffer.from(result.payload.payload); - const epCount = buf.readUInt8(4); - const epList = []; - for (let i = 5; i < epCount + 5; i++) { - epList.push(buf.readUInt8(i)); - } + const clusterId = Zdo.ClusterId.ACTIVE_ENDPOINTS_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, networkAddress); + const result = await this.sendZdo(ZSpec.BLANK_EUI64, networkAddress, clusterId, zdoPayload, false); - const payloadAE: TsType.ActiveEndpoints = { - endpoints: epList, - }; + /* istanbul ignore else */ + if (Zdo.Buffalo.checkStatus(result)) { + const payload = result[1]; - logger.debug(() => `ActiveEndpoints response: ${JSON.stringify(payloadAE)}`, NS); - return payloadAE; - } catch (error) { - logger.error(`RECEIVING ActiveEndpoints FAILED, ${error}`, NS); - throw new Error('RECEIVING ActiveEndpoints FAILED ' + error); - } - }, networkAddress); + return {endpoints: payload.endpointList}; + } else { + // TODO: will disappear once moved upstream + throw new Zdo.StatusError(result[0]); + } } public async simpleDescriptor(networkAddress: number, endpointID: number): Promise { - return await this.queue.execute(async () => { - try { - const payload = { - targetShortAddress: networkAddress, - endpoint: endpointID, - }; - const result = await this.driver.sendCommand(ZiGateCommandCode.SimpleDescriptor, payload); - - const buf: Buffer = result.payload.payload; - - if (buf.length > 11) { - const inCount = buf.readUInt8(11); - const inClusters = []; - let cIndex = 12; - for (let i = 0; i < inCount; i++) { - inClusters[i] = buf.readUInt16LE(cIndex); - cIndex += 2; - } - const outCount = buf.readUInt8(12 + inCount * 2); - const outClusters = []; - cIndex = 13 + inCount * 2; - for (let l = 0; l < outCount; l++) { - outClusters[l] = buf.readUInt16LE(cIndex); - cIndex += 2; - } - - const resultPayload: TsType.SimpleDescriptor = { - profileID: buf.readUInt16LE(6), - endpointID: buf.readUInt8(5), - deviceID: buf.readUInt16LE(8), - inputClusters: inClusters, - outputClusters: outClusters, - }; - - return resultPayload; - } + const clusterId = Zdo.ClusterId.SIMPLE_DESCRIPTOR_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, networkAddress, endpointID); + const result = await this.sendZdo(ZSpec.BLANK_EUI64, networkAddress, clusterId, zdoPayload, false); - throw new Error(`Invalid buffer length ${buf.length}.`); - } catch (error) { - const msg = 'RECEIVING SIMPLE_DESCRIPTOR FAILED - addr: 0x' + networkAddress.toString(16) + ' EP:' + endpointID + ' ' + error; - logger.error(msg, NS); - throw new Error(msg); - } - }, networkAddress); + /* istanbul ignore else */ + if (Zdo.Buffalo.checkStatus(result)) { + const payload = result[1]; + + return { + profileID: payload.profileId, + endpointID: payload.endpoint, + deviceID: payload.deviceId, + inputClusters: payload.inClusterList, + outputClusters: payload.outClusterList, + }; + } else { + // TODO: will disappear once moved upstream + throw new Zdo.StatusError(result[0]); + } } public async bind( @@ -424,29 +374,25 @@ class ZiGateAdapter extends Adapter { type: 'endpoint' | 'group', destinationEndpoint?: number, ): Promise { - return await this.queue.execute(async () => { - const payload: KeyValue = { - targetExtendedAddress: sourceIeeeAddress, - targetEndpoint: sourceEndpoint, - clusterID: clusterID, - destinationAddressMode: type === 'group' ? ADDRESS_MODE.group : ADDRESS_MODE.ieee, - destinationAddress: destinationAddressOrGroup, - }; - - if (destinationEndpoint != undefined) { - payload.destinationEndpoint = destinationEndpoint; - } - const result = await this.driver.sendCommand(ZiGateCommandCode.Bind, payload, undefined, {destinationNetworkAddress}); + const clusterId = Zdo.ClusterId.BIND_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest( + this.hasZdoMessageOverhead, + clusterId, + sourceIeeeAddress as EUI64, + sourceEndpoint, + clusterID, + type === 'group' ? Zdo.MULTICAST_BINDING : Zdo.UNICAST_BINDING, + destinationAddressOrGroup as EUI64, // not used with MULTICAST_BINDING + destinationAddressOrGroup as number, // not used with UNICAST_BINDING + destinationEndpoint ?? 0, // not used with MULTICAST_BINDING + ); + const result = await this.sendZdo(ZSpec.BLANK_EUI64, destinationNetworkAddress, clusterId, zdoPayload, false); - const data = result.payload.payload; - if (data[1] === 0) { - logger.debug(`Bind ${sourceIeeeAddress} success`, NS); - } else { - const msg = `Bind ${sourceIeeeAddress} failed`; - logger.error(msg, NS); - throw new Error(msg); - } - }, destinationNetworkAddress); + /* istanbul ignore next */ + if (!Zdo.Buffalo.checkStatus(result)) { + // TODO: will disappear once moved upstream + throw new Zdo.StatusError(result[0]); + } } public async unbind( @@ -458,46 +404,121 @@ class ZiGateAdapter extends Adapter { type: 'endpoint' | 'group', destinationEndpoint?: number, ): Promise { - return await this.queue.execute(async () => { - const payload: KeyValue = { - targetExtendedAddress: sourceIeeeAddress, - targetEndpoint: sourceEndpoint, - clusterID: clusterID, - destinationAddressMode: type === 'group' ? ADDRESS_MODE.group : ADDRESS_MODE.ieee, - destinationAddress: destinationAddressOrGroup, - }; + const clusterId = Zdo.ClusterId.UNBIND_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest( + this.hasZdoMessageOverhead, + clusterId, + sourceIeeeAddress as EUI64, + sourceEndpoint, + clusterID, + type === 'group' ? Zdo.MULTICAST_BINDING : Zdo.UNICAST_BINDING, + destinationAddressOrGroup as EUI64, // not used with MULTICAST_BINDING + destinationAddressOrGroup as number, // not used with UNICAST_BINDING + destinationEndpoint ?? 0, // not used with MULTICAST_BINDING + ); + const result = await this.sendZdo(ZSpec.BLANK_EUI64, destinationNetworkAddress, clusterId, zdoPayload, false); + + /* istanbul ignore next */ + if (!Zdo.Buffalo.checkStatus(result)) { + // TODO: will disappear once moved upstream + throw new Zdo.StatusError(result[0]); + } + } + + public async removeDevice(networkAddress: number, ieeeAddr: string): Promise { + const clusterId = Zdo.ClusterId.LEAVE_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, ieeeAddr as EUI64, Zdo.LeaveRequestFlags.WITHOUT_REJOIN); + const result = await this.sendZdo(ZSpec.BLANK_EUI64, networkAddress, clusterId, zdoPayload, false); + + /* istanbul ignore next */ + if (!Zdo.Buffalo.checkStatus(result)) { + // TODO: will disappear once moved upstream + throw new Zdo.StatusError(result[0]); + } + } + + public async sendZdo( + ieeeAddress: string, + networkAddress: number, + clusterId: Zdo.ClusterId, + payload: Buffer, + disableResponse: true, + ): Promise; + public async sendZdo( + ieeeAddress: string, + networkAddress: number, + clusterId: K, + payload: Buffer, + disableResponse: false, + ): Promise; + public async sendZdo( + ieeeAddress: string, + networkAddress: number, + clusterId: K, + payload: Buffer, + disableResponse: boolean, + ): Promise { + return await this.queue.execute(async () => { + // stack-specific requirements + // https://zigate.fr/documentation/commandes-zigate/ + switch (clusterId) { + case Zdo.ClusterId.LEAVE_REQUEST: { + const prefixedPayload = Buffer.alloc(payload.length + 3); // extra zero for `removeChildren` + prefixedPayload.writeUInt16BE(networkAddress, 0); + prefixedPayload.set(payload, 2); + + payload = prefixedPayload; + break; + } - if (destinationEndpoint != undefined) { - payload.destinationEndpoint = destinationEndpoint; + case Zdo.ClusterId.BIND_REQUEST: + case Zdo.ClusterId.UNBIND_REQUEST: { + // extra zeroes for endpoint XXX: not needed? + const zeroes = 15 - payload.length; + const prefixedPayload = Buffer.alloc(payload.length + zeroes); + prefixedPayload.set(payload, 0); + + payload = prefixedPayload; + + break; + } + + case Zdo.ClusterId.PERMIT_JOINING_REQUEST: + case Zdo.ClusterId.SYSTEM_SERVER_DISCOVERY_REQUEST: + case Zdo.ClusterId.LQI_TABLE_REQUEST: + case Zdo.ClusterId.ROUTING_TABLE_REQUEST: + case Zdo.ClusterId.BINDING_TABLE_REQUEST: + case Zdo.ClusterId.NWK_UPDATE_REQUEST: { + const prefixedPayload = Buffer.alloc(payload.length + 2); + prefixedPayload.writeUInt16BE(networkAddress, 0); + prefixedPayload.set(payload, 2); + + payload = prefixedPayload; + break; + } } - const result = await this.driver.sendCommand(ZiGateCommandCode.UnBind, payload, undefined, {destinationNetworkAddress}); - const data = result.payload.payload; - if (data[1] === 0) { - logger.debug(`Unbind ${sourceIeeeAddress} success`, NS); - } else { - const msg = `Unbind ${sourceIeeeAddress} failed`; - logger.error(msg, NS); - throw new Error(msg); + let waiter; + + if (!disableResponse) { + const responseClusterId = Zdo.Utils.getResponseClusterId(clusterId); + + if (responseClusterId) { + waiter = this.driver.zdoWaitFor({ + clusterId: responseClusterId, + target: responseClusterId === Zdo.ClusterId.NETWORK_ADDRESS_RESPONSE ? ieeeAddress : networkAddress, + }); + } } - }, destinationNetworkAddress); - } - public async removeDevice(networkAddress: number, ieeeAddr: string): Promise { - return await this.queue.execute(async () => { - const payload = { - shortAddress: networkAddress, - extendedAddress: ieeeAddr, - rejoin: 0, - removeChildren: 0, - }; + await this.driver.requestZdo(clusterId, payload); - try { - await this.driver.sendCommand(ZiGateCommandCode.ManagementLeaveRequest, payload); - } catch (error) { - new Error(`ManagementLeaveRequest failed ${error}`); + if (waiter) { + const result = await waiter.start().promise; + + return result.zdo as ZdoTypes.RequestToResponseMap[K]; } - }, networkAddress); + }, networkAddress /* TODO: replace with ieeeAddress once zdo moved upstream */); } public async sendZclFrameToEndpoint( @@ -553,9 +574,9 @@ class ZiGateAdapter extends Adapter { const payload: RawAPSDataRequestPayload = { addressMode: ADDRESS_MODE.short, //nwk targetShortAddress: networkAddress, - sourceEndpoint: sourceEndpoint || 0x01, + sourceEndpoint: sourceEndpoint || ZSpec.HA_ENDPOINT, destinationEndpoint: endpoint, - profileID: 0x0104, + profileID: ZSpec.HA_PROFILE_ID, clusterID: zclFrame.cluster.ID, securityMode: 0x02, radius: 30, @@ -654,7 +675,7 @@ class ZiGateAdapter extends Adapter { targetShortAddress: destination, sourceEndpoint: sourceEndpoint, destinationEndpoint: endpoint, - profileID: /*sourceEndpoint === 242 ? 0xa1e0 :*/ 0x0104, + profileID: /*sourceEndpoint === ZSpec.GP_ENDPOINT ? ZSpec.GP_PROFILE_ID :*/ ZSpec.HA_PROFILE_ID, clusterID: zclFrame.cluster.ID, securityMode: 0x02, radius: 30, @@ -674,9 +695,9 @@ class ZiGateAdapter extends Adapter { const payload: RawAPSDataRequestPayload = { addressMode: ADDRESS_MODE.group, //nwk targetShortAddress: groupID, - sourceEndpoint: sourceEndpoint || 0x01, + sourceEndpoint: sourceEndpoint || ZSpec.HA_ENDPOINT, destinationEndpoint: 0xff, - profileID: 0x0104, + profileID: ZSpec.HA_PROFILE_ID, clusterID: zclFrame.cluster.ID, securityMode: 0x02, radius: 30, @@ -694,7 +715,9 @@ class ZiGateAdapter extends Adapter { */ private async initNetwork(): Promise { logger.debug(`Set channel mask ${this.networkOptions.channelList} key`, NS); - await this.driver.sendCommand(ZiGateCommandCode.SetChannelMask, {channelMask: channelsToMask(this.networkOptions.channelList)}); + await this.driver.sendCommand(ZiGateCommandCode.SetChannelMask, { + channelMask: ZSpec.Utils.channelsToUInt32Mask(this.networkOptions.channelList), + }); logger.debug(`Set security key`, NS); await this.driver.sendCommand(ZiGateCommandCode.SetSecurityStateKey, { @@ -771,37 +794,42 @@ class ZiGateAdapter extends Adapter { throw new Error('Not supported'); } - private deviceAnnounceListener(networkAddress: number, ieeeAddr: string): void { + private deviceAnnounceListener(response: ZdoTypes.EndDeviceAnnounce): void { // @todo debounce - const payload: Events.DeviceAnnouncePayload = {networkAddress, ieeeAddr}; + const payload: Events.DeviceAnnouncePayload = {networkAddress: response.nwkAddress, ieeeAddr: response.eui64}; if (this.joinPermitted === true) { this.emit('deviceJoined', payload); } else { - this.emit('deviceAnnounce', payload); + // convert to `zdoResponse` to avoid needing extra event upstream + this.emit('zdoResponse', Zdo.ClusterId.END_DEVICE_ANNOUNCE, [Zdo.Status.SUCCESS, response]); } } - private dataListener(data: {ziGateObject: ZiGateObject}): void { + private onZdoResponse(clusterId: Zdo.ClusterId, response: ZdoTypes.GenericZdoResponse): void { + this.emit('zdoResponse', clusterId, response); + } + + private dataListener(ziGateObject: ZiGateObject): void { const payload: Events.ZclPayload = { - address: data.ziGateObject.payload.sourceAddress, - clusterID: data.ziGateObject.payload.clusterID, - data: data.ziGateObject.payload.payload, - header: Zcl.Header.fromBuffer(data.ziGateObject.payload.payload), - endpoint: data.ziGateObject.payload.sourceEndpoint, - linkquality: data.ziGateObject.frame!.readRSSI(), // read: frame valid + address: ziGateObject.payload.sourceAddress, + clusterID: ziGateObject.payload.clusterID, + data: ziGateObject.payload.payload, + header: Zcl.Header.fromBuffer(ziGateObject.payload.payload), + endpoint: ziGateObject.payload.sourceEndpoint, + linkquality: ziGateObject.frame!.readRSSI(), // read: frame valid groupID: 0, // @todo wasBroadcast: false, // TODO - destinationEndpoint: data.ziGateObject.payload.destinationEndpoint, + destinationEndpoint: ziGateObject.payload.destinationEndpoint, }; this.waitress.resolve(payload); this.emit('zclPayload', payload); } - private leaveIndicationListener(data: {ziGateObject: ZiGateObject}): void { - logger.debug(() => `LeaveIndication ${JSON.stringify(data)}`, NS); + private leaveIndicationListener(ziGateObject: ZiGateObject): void { + logger.debug(() => `LeaveIndication ${JSON.stringify(ziGateObject)}`, NS); const payload: Events.DeviceLeavePayload = { - networkAddress: data.ziGateObject.payload.extendedAddress, - ieeeAddr: data.ziGateObject.payload.extendedAddress, + networkAddress: ziGateObject.payload.extendedAddress, + ieeeAddr: ziGateObject.payload.extendedAddress, }; this.emit('deviceLeave', payload); } diff --git a/src/adapter/zigate/driver/buffaloZiGate.ts b/src/adapter/zigate/driver/buffaloZiGate.ts index c861bde2c2..a977921441 100644 --- a/src/adapter/zigate/driver/buffaloZiGate.ts +++ b/src/adapter/zigate/driver/buffaloZiGate.ts @@ -3,6 +3,7 @@ import {Buffalo} from '../../../buffalo'; import {EUI64} from '../../../zspec/tstypes'; import {BuffaloZclOptions} from '../../../zspec/zcl/definition/tstype'; +import {getMacCapFlags} from '../../../zspec/zdo/utils'; import {LOG_LEVEL} from './constants'; import ParameterType from './parameterType'; @@ -114,25 +115,7 @@ class BuffaloZiGate extends Buffalo { return this.readInt8(); } case ParameterType.MACCAPABILITY: { - const result: {[k: string]: boolean | number} = {}; - const mac = this.readUInt8(); - // - result.alternatePanCoordinator = !!(mac & 0b00000001); - // bit 0: Alternative PAN Coordinator, always 0 - result.fullFunctionDevice = !!(mac & 0b00000010); - // bit 1: Device Type, 1 = FFD , 0 = RFD ; cf. https://fr.wikipedia.org/wiki/IEEE_802.15.4 - result.mainsPowerSource = !!(mac & 0b00000100); - // bit 2: Power Source, 1 = mains power, 0 = other - result.receiverOnWhenIdle = !!(mac & 0b00001000); - // bit 3: Receiver on when Idle, 1 = non-sleepy, 0 = sleepy - result.reserved = (mac & 0b00110000) >> 4; - // bit 4&5: Reserved - result.securityCapability = !!(mac & 0b01000000); - // bit 6: Security capacity, always 0 (standard security) - result.allocateAddress = !!(mac & 0b10000000); - // bit 7: 1 = joining device must be issued network address - - return result; + return getMacCapFlags(this.readUInt8()); } case ParameterType.ADDRESS_WITH_TYPE_DEPENDENCY: { const addressMode = this.buffer.readUInt8(this.position - 1); diff --git a/src/adapter/zigate/driver/commandType.ts b/src/adapter/zigate/driver/commandType.ts index 92c61fdf39..de0168fc6b 100644 --- a/src/adapter/zigate/driver/commandType.ts +++ b/src/adapter/zigate/driver/commandType.ts @@ -115,32 +115,32 @@ export const ZiGateCommand: {[key: string]: ZiGateCommandType} = { // SetTXpower request: [{name: 'value', parameterType: ParameterType.UINT8}], }, - [ZiGateCommandCode.ManagementLQI]: { - // 0x004E - request: [ - {name: 'targetAddress', parameterType: ParameterType.UINT16}, // Status - {name: 'startIndex', parameterType: ParameterType.UINT8}, // - ], - response: [ - [ - { - receivedProperty: 'code', - matcher: equal, - value: ZiGateMessageCode.DataIndication, - }, - { - receivedProperty: 'payload.sourceAddress', - matcher: equal, - expectedProperty: 'payload.targetAddress', - }, - { - receivedProperty: 'payload.clusterID', - matcher: equal, - value: 0x8031, - }, - ], - ], - }, + // [ZiGateCommandCode.ManagementLQI]: { + // // 0x004E + // request: [ + // {name: 'targetAddress', parameterType: ParameterType.UINT16}, // Status + // {name: 'startIndex', parameterType: ParameterType.UINT8}, // + // ], + // response: [ + // [ + // { + // receivedProperty: 'code', + // matcher: equal, + // value: ZiGateMessageCode.DataIndication, + // }, + // { + // receivedProperty: 'payload.sourceAddress', + // matcher: equal, + // expectedProperty: 'payload.targetAddress', + // }, + // { + // receivedProperty: 'payload.clusterID', + // matcher: equal, + // value: 0x8031, + // }, + // ], + // ], + // }, [ZiGateCommandCode.SetSecurityStateKey]: { // 0x0022 request: [ @@ -166,40 +166,40 @@ export const ZiGateCommand: {[key: string]: ZiGateCommandType} = { ], }, - [ZiGateCommandCode.ManagementLeaveRequest]: { - request: [ - {name: 'shortAddress', parameterType: ParameterType.UINT16}, - {name: 'extendedAddress', parameterType: ParameterType.IEEEADDR}, // - {name: 'rejoin', parameterType: ParameterType.UINT8}, - {name: 'removeChildren', parameterType: ParameterType.UINT8}, // - ], - response: [ - [ - { - receivedProperty: 'code', - matcher: equal, - value: ZiGateMessageCode.LeaveIndication, - }, - { - receivedProperty: 'payload.extendedAddress', - matcher: equal, - expectedProperty: 'payload.extendedAddress', - }, - ], - [ - { - receivedProperty: 'code', - matcher: equal, - value: ZiGateMessageCode.ManagementLeaveResponse, - }, - { - receivedProperty: 'payload.sqn', - matcher: equal, - expectedProperty: 'status.seqApsNum', - }, - ], - ], - }, + // [ZiGateCommandCode.ManagementLeaveRequest]: { + // request: [ + // {name: 'shortAddress', parameterType: ParameterType.UINT16}, + // {name: 'extendedAddress', parameterType: ParameterType.IEEEADDR}, // + // {name: 'rejoin', parameterType: ParameterType.UINT8}, + // {name: 'removeChildren', parameterType: ParameterType.UINT8}, // + // ], + // response: [ + // [ + // { + // receivedProperty: 'code', + // matcher: equal, + // value: ZiGateMessageCode.LeaveIndication, + // }, + // { + // receivedProperty: 'payload.extendedAddress', + // matcher: equal, + // expectedProperty: 'payload.extendedAddress', + // }, + // ], + // [ + // { + // receivedProperty: 'code', + // matcher: equal, + // value: ZiGateMessageCode.ManagementLeaveResponse, + // }, + // { + // receivedProperty: 'payload.sqn', + // matcher: equal, + // expectedProperty: 'status.seqApsNum', + // }, + // ], + // ], + // }, [ZiGateCommandCode.RemoveDevice]: { request: [ @@ -221,19 +221,19 @@ export const ZiGateCommand: {[key: string]: ZiGateCommandType} = { ], ], }, - [ZiGateCommandCode.PermitJoin]: { - request: [ - {name: 'targetShortAddress', parameterType: ParameterType.UINT16}, // - - // broadcast 0xfffc - {name: 'interval', parameterType: ParameterType.UINT8}, // - // 0 = Disable Joining - // 1 – 254 = Time in seconds to allow joins - // 255 = Allow all joins - // {name: 'TCsignificance', parameterType: ParameterType.UINT8}, // - // 0 = No change in authentication - // 1 = Authentication policy as spec - ], - }, + // [ZiGateCommandCode.PermitJoin]: { + // request: [ + // {name: 'targetShortAddress', parameterType: ParameterType.UINT16}, // - + // // broadcast 0xfffc + // {name: 'interval', parameterType: ParameterType.UINT8}, // + // // 0 = Disable Joining + // // 1 – 254 = Time in seconds to allow joins + // // 255 = Allow all joins + // // {name: 'TCsignificance', parameterType: ParameterType.UINT8}, // + // // 0 = No change in authentication + // // 1 = Authentication policy as spec + // ], + // }, [ZiGateCommandCode.PermitJoinStatus]: { request: [ {name: 'targetShortAddress', parameterType: ParameterType.UINT16}, // - @@ -262,151 +262,151 @@ export const ZiGateCommand: {[key: string]: ZiGateCommandType} = { {name: 'data', parameterType: ParameterType.BUFFER}, // ], }, - [ZiGateCommandCode.NodeDescriptor]: { - request: [ - {name: 'targetShortAddress', parameterType: ParameterType.UINT16}, // - ], - response: [ - [ - { - receivedProperty: 'code', - matcher: equal, - value: ZiGateMessageCode.DataIndication, - }, - { - receivedProperty: 'payload.sourceAddress', - matcher: equal, - expectedProperty: 'payload.targetShortAddress', - }, - { - receivedProperty: 'payload.clusterID', - matcher: equal, - value: 0x8002, - }, - ], - ], - }, - [ZiGateCommandCode.ActiveEndpoint]: { - request: [ - {name: 'targetShortAddress', parameterType: ParameterType.UINT16}, // - ], - response: [ - [ - { - receivedProperty: 'code', - matcher: equal, - value: ZiGateMessageCode.DataIndication, - }, - { - receivedProperty: 'payload.sourceAddress', - matcher: equal, - expectedProperty: 'payload.targetShortAddress', - }, - { - receivedProperty: 'payload.clusterID', - matcher: equal, - value: 0x8005, - }, - ], - ], - }, - [ZiGateCommandCode.SimpleDescriptor]: { - request: [ - {name: 'targetShortAddress', parameterType: ParameterType.UINT16}, // - {name: 'endpoint', parameterType: ParameterType.UINT8}, // - ], - response: [ - [ - {receivedProperty: 'code', matcher: equal, value: ZiGateMessageCode.DataIndication}, - { - receivedProperty: 'payload.sourceAddress', - matcher: equal, - expectedProperty: 'payload.targetShortAddress', - }, - { - receivedProperty: 'payload.clusterID', - matcher: equal, - value: 0x8004, - }, - ], - ], - }, - [ZiGateCommandCode.Bind]: { - request: [ - {name: 'targetExtendedAddress', parameterType: ParameterType.IEEEADDR}, // - {name: 'targetEndpoint', parameterType: ParameterType.UINT8}, // - {name: 'clusterID', parameterType: ParameterType.UINT16}, // - {name: 'destinationAddressMode', parameterType: ParameterType.UINT8}, // - { - name: 'destinationAddress', - parameterType: ParameterType.ADDRESS_WITH_TYPE_DEPENDENCY, - }, // - {name: 'destinationEndpoint', parameterType: ParameterType.UINT8}, // - ], - response: [ - [ - { - receivedProperty: 'code', - matcher: equal, - value: ZiGateMessageCode.DataIndication, - }, - { - receivedProperty: 'payload.sourceAddress', - matcher: equal, - expectedExtraParameter: 'destinationNetworkAddress', - }, - { - receivedProperty: 'payload.clusterID', - matcher: equal, - value: 0x8021, - }, - { - receivedProperty: 'payload.profileID', - matcher: equal, - value: 0x0000, - }, - ], - ], - }, - [ZiGateCommandCode.UnBind]: { - request: [ - {name: 'targetExtendedAddress', parameterType: ParameterType.IEEEADDR}, // - {name: 'targetEndpoint', parameterType: ParameterType.UINT8}, // - {name: 'clusterID', parameterType: ParameterType.UINT16}, // - {name: 'destinationAddressMode', parameterType: ParameterType.UINT8}, // - { - name: 'destinationAddress', - parameterType: ParameterType.ADDRESS_WITH_TYPE_DEPENDENCY, - }, // - {name: 'destinationEndpoint', parameterType: ParameterType.UINT8}, // - ], - response: [ - [ - { - receivedProperty: 'code', - matcher: equal, - value: ZiGateMessageCode.DataIndication, - }, - { - receivedProperty: 'payload.sourceAddress', - matcher: equal, - expectedExtraParameter: 'destinationNetworkAddress', - }, - { - receivedProperty: 'payload.clusterID', - matcher: equal, - value: 0x8022, - }, - { - receivedProperty: 'payload.profileID', - matcher: equal, - value: 0x0000, - }, - ], - ], - }, + // [ZiGateCommandCode.NodeDescriptor]: { + // request: [ + // {name: 'targetShortAddress', parameterType: ParameterType.UINT16}, // + // ], + // response: [ + // [ + // { + // receivedProperty: 'code', + // matcher: equal, + // value: ZiGateMessageCode.DataIndication, + // }, + // { + // receivedProperty: 'payload.sourceAddress', + // matcher: equal, + // expectedProperty: 'payload.targetShortAddress', + // }, + // { + // receivedProperty: 'payload.clusterID', + // matcher: equal, + // value: 0x8002, + // }, + // ], + // ], + // }, + // [ZiGateCommandCode.ActiveEndpoint]: { + // request: [ + // {name: 'targetShortAddress', parameterType: ParameterType.UINT16}, // + // ], + // response: [ + // [ + // { + // receivedProperty: 'code', + // matcher: equal, + // value: ZiGateMessageCode.DataIndication, + // }, + // { + // receivedProperty: 'payload.sourceAddress', + // matcher: equal, + // expectedProperty: 'payload.targetShortAddress', + // }, + // { + // receivedProperty: 'payload.clusterID', + // matcher: equal, + // value: 0x8005, + // }, + // ], + // ], + // }, + // [ZiGateCommandCode.SimpleDescriptor]: { + // request: [ + // {name: 'targetShortAddress', parameterType: ParameterType.UINT16}, // + // {name: 'endpoint', parameterType: ParameterType.UINT8}, // + // ], + // response: [ + // [ + // {receivedProperty: 'code', matcher: equal, value: ZiGateMessageCode.DataIndication}, + // { + // receivedProperty: 'payload.sourceAddress', + // matcher: equal, + // expectedProperty: 'payload.targetShortAddress', + // }, + // { + // receivedProperty: 'payload.clusterID', + // matcher: equal, + // value: 0x8004, + // }, + // ], + // ], + // }, + // [ZiGateCommandCode.Bind]: { + // request: [ + // {name: 'targetExtendedAddress', parameterType: ParameterType.IEEEADDR}, // + // {name: 'targetEndpoint', parameterType: ParameterType.UINT8}, // + // {name: 'clusterID', parameterType: ParameterType.UINT16}, // + // {name: 'destinationAddressMode', parameterType: ParameterType.UINT8}, // + // { + // name: 'destinationAddress', + // parameterType: ParameterType.ADDRESS_WITH_TYPE_DEPENDENCY, + // }, // + // {name: 'destinationEndpoint', parameterType: ParameterType.UINT8}, // + // ], + // response: [ + // [ + // { + // receivedProperty: 'code', + // matcher: equal, + // value: ZiGateMessageCode.DataIndication, + // }, + // { + // receivedProperty: 'payload.sourceAddress', + // matcher: equal, + // expectedExtraParameter: 'destinationNetworkAddress', + // }, + // { + // receivedProperty: 'payload.clusterID', + // matcher: equal, + // value: 0x8021, + // }, + // { + // receivedProperty: 'payload.profileID', + // matcher: equal, + // value: 0x0000, + // }, + // ], + // ], + // }, + // [ZiGateCommandCode.UnBind]: { + // request: [ + // {name: 'targetExtendedAddress', parameterType: ParameterType.IEEEADDR}, // + // {name: 'targetEndpoint', parameterType: ParameterType.UINT8}, // + // {name: 'clusterID', parameterType: ParameterType.UINT16}, // + // {name: 'destinationAddressMode', parameterType: ParameterType.UINT8}, // + // { + // name: 'destinationAddress', + // parameterType: ParameterType.ADDRESS_WITH_TYPE_DEPENDENCY, + // }, // + // {name: 'destinationEndpoint', parameterType: ParameterType.UINT8}, // + // ], + // response: [ + // [ + // { + // receivedProperty: 'code', + // matcher: equal, + // value: ZiGateMessageCode.DataIndication, + // }, + // { + // receivedProperty: 'payload.sourceAddress', + // matcher: equal, + // expectedExtraParameter: 'destinationNetworkAddress', + // }, + // { + // receivedProperty: 'payload.clusterID', + // matcher: equal, + // value: 0x8022, + // }, + // { + // receivedProperty: 'payload.profileID', + // matcher: equal, + // value: 0x0000, + // }, + // ], + // ], + // }, [ZiGateCommandCode.AddGroup]: { request: [ {name: 'addressMode', parameterType: ParameterType.UINT8}, // diff --git a/src/adapter/zigate/driver/constants.ts b/src/adapter/zigate/driver/constants.ts index cfd0f412be..85be588409 100644 --- a/src/adapter/zigate/driver/constants.ts +++ b/src/adapter/zigate/driver/constants.ts @@ -1,3 +1,5 @@ +import {ClusterId as ZdoClusterId} from '../../../zspec/zdo'; + export enum ADDRESS_MODE { bound = 0x00, //Use one or more bound nodes/endpoints, with acknowledgements group = 0x01, //Use a pre-defined group address, with acknowledgements @@ -195,7 +197,6 @@ export enum ZiGateCommandCode { Reset = 0x0011, ErasePersistentData = 0x0012, RemoveDevice = 0x0026, - PermitJoin = 0x0049, RawAPSDataRequest = 0x0530, GetTimeServer = 0x0017, SetTimeServer = 0x0016, @@ -205,30 +206,60 @@ export enum ZiGateCommandCode { StartNetwork = 0x0024, StartNetworkScan = 0x0025, SetCertification = 0x0019, - Bind = 0x0030, - UnBind = 0x0031, // ResetFactoryNew = 0x0013, OnOff = 0x0092, OnOffTimed = 0x0093, - ActiveEndpoint = 0x0045, AttributeDiscovery = 0x0140, AttributeRead = 0x0100, AttributeWrite = 0x0110, DescriptorComplex = 0x0531, + + // zdo + Bind = 0x0030, + UnBind = 0x0031, + NwkAddress = 0x0040, + IEEEAddress = 0x0041, NodeDescriptor = 0x0042, - PowerDescriptor = 0x0044, SimpleDescriptor = 0x0043, + PowerDescriptor = 0x0044, + ActiveEndpoint = 0x0045, + MatchDescriptor = 0x0046, + // ManagementLeaveRequest = 0x0047, XXX: some non-standard form of LeaveRequest? + PermitJoin = 0x0049, + ManagementNetworkUpdate = 0x004a, + SystemServerDiscovery = 0x004b, + LeaveRequest = 0x004c, + ManagementLQI = 0x004e, + // ManagementRtg = 0x004?, + // ManagementBind = 0x004?, + SetDeviceType = 0x0023, - IEEEAddress = 0x0041, LED = 0x0018, SetTXpower = 0x0806, - ManagementLeaveRequest = 0x0047, - ManagementLQI = 0x004e, SetSecurityStateKey = 0x0022, AddGroup = 0x0060, } +export const ZDO_REQ_CLUSTER_ID_TO_ZIGATE_COMMAND_ID: Readonly>> = { + [ZdoClusterId.NETWORK_ADDRESS_REQUEST]: ZiGateCommandCode.NwkAddress, + [ZdoClusterId.IEEE_ADDRESS_REQUEST]: ZiGateCommandCode.IEEEAddress, + [ZdoClusterId.NODE_DESCRIPTOR_REQUEST]: ZiGateCommandCode.NodeDescriptor, + [ZdoClusterId.POWER_DESCRIPTOR_REQUEST]: ZiGateCommandCode.PowerDescriptor, + [ZdoClusterId.SIMPLE_DESCRIPTOR_REQUEST]: ZiGateCommandCode.SimpleDescriptor, + [ZdoClusterId.MATCH_DESCRIPTORS_REQUEST]: ZiGateCommandCode.MatchDescriptor, + [ZdoClusterId.ACTIVE_ENDPOINTS_REQUEST]: ZiGateCommandCode.ActiveEndpoint, + [ZdoClusterId.SYSTEM_SERVER_DISCOVERY_REQUEST]: ZiGateCommandCode.SystemServerDiscovery, + [ZdoClusterId.BIND_REQUEST]: ZiGateCommandCode.Bind, + [ZdoClusterId.UNBIND_REQUEST]: ZiGateCommandCode.UnBind, + [ZdoClusterId.LQI_TABLE_REQUEST]: ZiGateCommandCode.ManagementLQI, + // [ZdoClusterId.ROUTING_TABLE_REQUEST]: ZiGateCommandCode.ManagementRtg, + // [ZdoClusterId.BINDING_TABLE_REQUEST]: ZiGateCommandCode.ManagementBind, + [ZdoClusterId.LEAVE_REQUEST]: ZiGateCommandCode.LeaveRequest, + [ZdoClusterId.NWK_UPDATE_REQUEST]: ZiGateCommandCode.ManagementNetworkUpdate, + [ZdoClusterId.PERMIT_JOINING_REQUEST]: ZiGateCommandCode.PermitJoin, +}; + export enum ZiGateMessageCode { DeviceAnnounce = 0x004d, Status = 0x8000, diff --git a/src/adapter/zigate/driver/messageType.ts b/src/adapter/zigate/driver/messageType.ts index 511e3e7cf7..b0ba91ff00 100644 --- a/src/adapter/zigate/driver/messageType.ts +++ b/src/adapter/zigate/driver/messageType.ts @@ -190,12 +190,12 @@ export const ZiGateMessage: {[k: number]: ZiGateMessageType} = { {name: 'rejoin', parameterType: ParameterType.UINT8}, // ], }, - [ZiGateMessageCode.ManagementLeaveResponse]: { - response: [ - {name: 'sqn', parameterType: ParameterType.UINT8}, - {name: 'status', parameterType: ParameterType.UINT8}, // - ], - }, + // [ZiGateMessageCode.ManagementLeaveResponse]: { + // response: [ + // {name: 'sqn', parameterType: ParameterType.UINT8}, + // {name: 'status', parameterType: ParameterType.UINT8}, // + // ], + // }, [ZiGateMessageCode.RouterDiscoveryConfirm]: { response: [ {name: 'status', parameterType: ParameterType.UINT8}, // @@ -203,42 +203,42 @@ export const ZiGateMessage: {[k: number]: ZiGateMessageType} = { // {name: 'dstAddress', parameterType: ParameterType.UINT16}, // ], }, - [ZiGateMessageCode.SimpleDescriptorResponse]: { - response: [ - {name: 'sourceEndpoint', parameterType: ParameterType.UINT8}, // - {name: 'profile ID', parameterType: ParameterType.UINT16}, // - {name: 'clusterID', parameterType: ParameterType.UINT16}, // - {name: 'attributeList', parameterType: ParameterType.LIST_UINT16}, // - ], - }, - [ZiGateMessageCode.ManagementLQIResponse]: { - response: [ - {name: 'sequence', parameterType: ParameterType.UINT8}, // - {name: 'status', parameterType: ParameterType.UINT8}, // - {name: 'neighbourTableEntries', parameterType: ParameterType.UINT8}, // - {name: 'neighbourTableListCount', parameterType: ParameterType.UINT8}, // - {name: 'startIndex', parameterType: ParameterType.UINT8}, // - // XXX: broken? automatic ziGateObject parsing will always read below as-is, even if it's not supposed to - // @TODO list TYPE - // - // Note: If Neighbour Table list count is 0, there are no elements in the list. - {name: 'NWKAddress', parameterType: ParameterType.UINT16}, // NWK Address : uint16_t - {name: 'Extended PAN ID', parameterType: ParameterType.IEEEADDR}, // Extended PAN ID : uint64_t - {name: 'IEEE Address', parameterType: ParameterType.IEEEADDR}, // IEEE Address : uint64_t - {name: 'Depth', parameterType: ParameterType.UINT8}, // Depth : uint_t - {name: 'linkQuality', parameterType: ParameterType.UINT8}, // Link Quality : uint8_t - {name: 'bitMap', parameterType: ParameterType.UINT8}, // Bit map of attributes Described below: uint8_t - // bit 0-1 Device Type - // (0-Coordinator 1-Router 2-End Device) - // bit 2-3 Permit Join status - // (1- On 0-Off) - // bit 4-5 Relationship - // (0-Parent 1-Child 2-Sibling) - // bit 6-7 Rx On When Idle status - // (1-On 0-Off) - {name: 'srcAddress', parameterType: ParameterType.UINT16}, // ( only from v3.1a) - ], - }, + // [ZiGateMessageCode.SimpleDescriptorResponse]: { + // response: [ + // {name: 'sourceEndpoint', parameterType: ParameterType.UINT8}, // + // {name: 'profile ID', parameterType: ParameterType.UINT16}, // + // {name: 'clusterID', parameterType: ParameterType.UINT16}, // + // {name: 'attributeList', parameterType: ParameterType.LIST_UINT16}, // + // ], + // }, + // [ZiGateMessageCode.ManagementLQIResponse]: { + // response: [ + // {name: 'sequence', parameterType: ParameterType.UINT8}, // + // {name: 'status', parameterType: ParameterType.UINT8}, // + // {name: 'neighbourTableEntries', parameterType: ParameterType.UINT8}, // + // {name: 'neighbourTableListCount', parameterType: ParameterType.UINT8}, // + // {name: 'startIndex', parameterType: ParameterType.UINT8}, // + // // XXX: broken? automatic ziGateObject parsing will always read below as-is, even if it's not supposed to + // // @TODO list TYPE + // // + // // Note: If Neighbour Table list count is 0, there are no elements in the list. + // {name: 'NWKAddress', parameterType: ParameterType.UINT16}, // NWK Address : uint16_t + // {name: 'Extended PAN ID', parameterType: ParameterType.IEEEADDR}, // Extended PAN ID : uint64_t + // {name: 'IEEE Address', parameterType: ParameterType.IEEEADDR}, // IEEE Address : uint64_t + // {name: 'Depth', parameterType: ParameterType.UINT8}, // Depth : uint_t + // {name: 'linkQuality', parameterType: ParameterType.UINT8}, // Link Quality : uint8_t + // {name: 'bitMap', parameterType: ParameterType.UINT8}, // Bit map of attributes Described below: uint8_t + // // bit 0-1 Device Type + // // (0-Coordinator 1-Router 2-End Device) + // // bit 2-3 Permit Join status + // // (1- On 0-Off) + // // bit 4-5 Relationship + // // (0-Parent 1-Child 2-Sibling) + // // bit 6-7 Rx On When Idle status + // // (1-On 0-Off) + // {name: 'srcAddress', parameterType: ParameterType.UINT16}, // ( only from v3.1a) + // ], + // }, [ZiGateMessageCode.PDMEvent]: { response: [ {name: 'eventStatus', parameterType: ParameterType.UINT8}, // diff --git a/src/adapter/zigate/driver/zigate.ts b/src/adapter/zigate/driver/zigate.ts index 790d7cdf18..9a2100ee8c 100644 --- a/src/adapter/zigate/driver/zigate.ts +++ b/src/adapter/zigate/driver/zigate.ts @@ -1,20 +1,23 @@ /* istanbul ignore file */ +import assert from 'assert'; import {EventEmitter} from 'events'; import net from 'net'; import {DelimiterParser} from '@serialport/parser-delimiter'; -import {Buffalo} from '../../../buffalo'; +import {ZSpec} from '../../..'; import {Queue} from '../../../utils'; import {logger} from '../../../utils/logger'; import Waitress from '../../../utils/waitress'; +import * as Zdo from '../../../zspec/zdo'; +import {EndDeviceAnnounce, GenericZdoResponse} from '../../../zspec/zdo/definition/tstypes'; import {SerialPort} from '../../serialPort'; import SerialPortUtils from '../../serialPortUtils'; import SocketPortUtils from '../../socketPortUtils'; import {SerialPortOptions} from '../../tstype'; import {equal, ZiGateResponseMatcher, ZiGateResponseMatcherRule} from './commandType'; -import {STATUS, ZiGateCommandCode, ZiGateMessageCode, ZiGateObjectPayload} from './constants'; +import {STATUS, ZDO_REQ_CLUSTER_ID_TO_ZIGATE_COMMAND_ID, ZiGateCommandCode, ZiGateMessageCode, ZiGateObjectPayload} from './constants'; import ZiGateFrame from './frame'; import ZiGateObject from './ziGateObject'; @@ -31,11 +34,32 @@ const timeouts = { }; type WaitressMatcher = { - ziGateObject: ZiGateObject; + ziGateObject?: ZiGateObject; rules: ZiGateResponseMatcher; extraParameters?: object; }; +type ZdoWaitressPayload = { + ziGatePayload: { + status: number; + profileID: number; + clusterID: number; + sourceEndpoint: number; + destinationEndpoint: number; + sourceAddressMode: number; + sourceAddress: number | string; + destinationAddressMode: number; + destinationAddress: number | string; + payload: Buffer; + }; + zdo: GenericZdoResponse; +}; + +type ZdoWaitressMatcher = { + clusterId: number; + target?: number | string; +}; + function zeroPad(number: number, size?: number): string { return number.toString(16).padStart(size || 4, '0'); } @@ -46,7 +70,15 @@ function resolve(path: string | [], obj: {[k: string]: any}, separator = '.'): a return properties.reduce((prev, curr) => prev && prev[curr], obj); } -export default class ZiGate extends EventEmitter { +interface ZiGateEventMap { + close: []; + zdoResponse: [Zdo.ClusterId, GenericZdoResponse]; + received: [ZiGateObject]; + LeaveIndication: [ZiGateObject]; + DeviceAnnounce: [EndDeviceAnnounce]; +} + +export default class ZiGate extends EventEmitter { private path: string; private baudRate: number; private initialized: boolean; @@ -58,6 +90,7 @@ export default class ZiGate extends EventEmitter { public portWrite?: SerialPort | net.Socket; private waitress: Waitress; + private zdoWaitress: Waitress; public constructor(path: string, serialPortOptions: SerialPortOptions) { super(); @@ -69,6 +102,7 @@ export default class ZiGate extends EventEmitter { this.queue = new Queue(1); this.waitress = new Waitress(this.waitressValidator, this.waitressTimeoutFormatter); + this.zdoWaitress = new Waitress(this.zdoWaitressValidator, this.waitressTimeoutFormatter); } public async sendCommand( @@ -138,6 +172,38 @@ export default class ZiGate extends EventEmitter { }); } + public async requestZdo(clusterId: Zdo.ClusterId, payload: Buffer): Promise { + return await this.queue.execute(async () => { + const commandCode = ZDO_REQ_CLUSTER_ID_TO_ZIGATE_COMMAND_ID[clusterId]; + assert(commandCode !== undefined, `ZDO cluster ID '${clusterId}' not supported.`); + const ruleStatus: ZiGateResponseMatcher = [ + {receivedProperty: 'code', matcher: equal, value: ZiGateMessageCode.Status}, + {receivedProperty: 'payload.packetType', matcher: equal, value: commandCode}, + ]; + + logger.debug(() => `ZDO ${Zdo.ClusterId[clusterId]}(cmd code: ${commandCode}) ${payload.toString('hex')}`, NS); + + const frame = new ZiGateFrame(); + frame.writeMsgCode(commandCode); + frame.writeMsgPayload(payload); + + logger.debug(() => `ZDO ${JSON.stringify(frame)}`, NS); + + const sendBuffer = frame.toBuffer(); + + logger.debug(`<-- ZDO send command ${sendBuffer.toString('hex')}`, NS); + + const statusWaiter = this.waitress.waitFor({rules: ruleStatus}, timeouts.default); + + // @ts-expect-error assumed proper based on port type + this.portWrite!.write(sendBuffer); + + const statusResponse: ZiGateObject = await statusWaiter.start().promise; + + return statusResponse.payload.status === STATUS.E_SL_MSG_STATUS_SUCCESS; + }); + } + public static async isValidPath(path: string): Promise { return await SerialPortUtils.is(path, autoDetectDefinitions); } @@ -175,13 +241,6 @@ export default class ZiGate extends EventEmitter { this.emit('close'); } - public waitFor( - matcher: WaitressMatcher, - timeout: number = timeouts.default, - ): {start: () => {promise: Promise; ID: number}; ID: number} { - return this.waitress.waitFor(matcher, timeout); - } - private async openSerialPort(): Promise { this.serialPort = new SerialPort({ path: this.path, @@ -277,33 +336,41 @@ export default class ZiGate extends EventEmitter { try { const ziGateObject = ZiGateObject.fromZiGateFrame(frame); logger.debug(() => `${JSON.stringify(ziGateObject.payload)}`, NS); - this.waitress.resolve(ziGateObject); + + if (code === ZiGateMessageCode.DataIndication && ziGateObject.payload.profileID === Zdo.ZDO_PROFILE_ID) { + const ziGatePayload: ZdoWaitressPayload['ziGatePayload'] = ziGateObject.payload; + // requests don't have tsn, but responses do + // https://zigate.fr/documentation/commandes-zigate/ + const zdo = Zdo.Buffalo.readResponse(true, ziGatePayload.clusterID, ziGatePayload.payload); + + this.zdoWaitress.resolve({ziGatePayload, zdo}); + this.emit('zdoResponse', ziGatePayload.clusterID, zdo); + } else { + this.waitress.resolve(ziGateObject); + } switch (code) { case ZiGateMessageCode.DataIndication: switch (ziGateObject.payload.profileID) { - case 0x0000: - switch (ziGateObject.payload.clusterID) { - case 0x0013: { - const networkAddress = ziGateObject.payload.payload.readUInt16LE(1); - const ieeeAddr = new Buffalo(ziGateObject.payload.payload.slice(3, 11)).readIeeeAddr(); - this.emit('DeviceAnnounce', networkAddress, ieeeAddr); - break; - } - } + case Zdo.ZDO_PROFILE_ID: + // handled above break; - case 0x0104: - this.emit('received', {ziGateObject}); + case ZSpec.HA_PROFILE_ID: + this.emit('received', ziGateObject); break; default: logger.debug('not implemented profile: ' + ziGateObject.payload.profileID, NS); } break; case ZiGateMessageCode.LeaveIndication: - this.emit('LeaveIndication', {ziGateObject}); + this.emit('LeaveIndication', ziGateObject); break; case ZiGateMessageCode.DeviceAnnounce: - this.emit('DeviceAnnounce', ziGateObject.payload.shortAddress, ziGateObject.payload.ieee); + this.emit('DeviceAnnounce', { + nwkAddress: ziGateObject.payload.shortAddress, + eui64: ziGateObject.payload.ieee, + capabilities: ziGateObject.payload.MACcapability, + }); break; } } catch (error) { @@ -314,7 +381,7 @@ export default class ZiGate extends EventEmitter { } } - private waitressTimeoutFormatter(matcher: WaitressMatcher, timeout: number): string { + private waitressTimeoutFormatter(matcher: WaitressMatcher | ZdoWaitressMatcher, timeout: number): string { return `${JSON.stringify(matcher)} after ${timeout}ms`; } @@ -323,6 +390,7 @@ export default class ZiGate extends EventEmitter { try { let expectedValue: string | number; if (rule.value == undefined && rule.expectedProperty != undefined) { + assert(matcher.ziGateObject, `Matcher ziGateObject expected valid.`); expectedValue = resolve(rule.expectedProperty, matcher.ziGateObject); } else if (rule.value == undefined && rule.expectedExtraParameter != undefined) { expectedValue = resolve(rule.expectedExtraParameter, matcher.extraParameters!); // XXX: assumed valid? @@ -337,4 +405,19 @@ export default class ZiGate extends EventEmitter { }; return matcher.rules.every(validator); } + + public zdoWaitFor(matcher: ZdoWaitressMatcher): ReturnType { + return this.zdoWaitress.waitFor(matcher, timeouts.default); + } + + private zdoWaitressValidator(payload: ZdoWaitressPayload, matcher: ZdoWaitressMatcher): boolean { + return ( + (matcher.target === undefined || + (typeof matcher.target === 'number' + ? matcher.target === payload.ziGatePayload.sourceAddress + : // @ts-expect-error checked with ? + matcher.target === payload.zdo?.[1]?.eui64)) && + payload.ziGatePayload.clusterID === matcher.clusterId + ); + } } diff --git a/src/controller/controller.ts b/src/controller/controller.ts index 8f15661a3a..06844f64c5 100644 --- a/src/controller/controller.ts +++ b/src/controller/controller.ts @@ -10,6 +10,8 @@ import {logger} from '../utils/logger'; import {isNumberArrayOfLength} from '../utils/utils'; import * as Zcl from '../zspec/zcl'; import {FrameControl} from '../zspec/zcl/definition/tstype'; +import * as Zdo from '../zspec/zdo'; +import {GenericZdoResponse} from '../zspec/zdo/definition/tstypes'; import Database from './database'; import * as Events from './events'; import GreenPower from './greenPower'; @@ -164,6 +166,7 @@ class Controller extends events.EventEmitter { // Register adapter events this.adapter.on('deviceJoined', this.onDeviceJoined.bind(this)); this.adapter.on('zclPayload', this.onZclPayload.bind(this)); + this.adapter.on('zdoResponse', this.onZdoResponse.bind(this)); this.adapter.on('disconnected', this.onAdapterDisconnected.bind(this)); this.adapter.on('deviceAnnounce', this.onDeviceAnnounce.bind(this)); this.adapter.on('deviceLeave', this.onDeviceLeave.bind(this)); @@ -671,6 +674,36 @@ class Controller extends events.EventEmitter { } } + private async onZdoResponse(clusterId: Zdo.ClusterId, response: GenericZdoResponse): Promise { + if (clusterId === Zdo.ClusterId.NETWORK_ADDRESS_RESPONSE) { + /* istanbul ignore else */ + if (Zdo.Buffalo.checkStatus(response)) { + const payload = response[1]; + + this.onNetworkAddress({ + networkAddress: payload.nwkAddress, + ieeeAddr: payload.eui64, + }); + } + } else if (clusterId === Zdo.ClusterId.END_DEVICE_ANNOUNCE) { + /* istanbul ignore else */ + if (Zdo.Buffalo.checkStatus(response)) { + const payload = response[1]; + + this.onDeviceAnnounce({ + networkAddress: payload.nwkAddress, + ieeeAddr: payload.eui64, + }); + } + } else { + /* istanbul ignore next */ + logger.debug( + `Received ZDO response: clusterId=${Zdo.ClusterId[clusterId]}, status=${Zdo.Status[response[0]]}, payload=${JSON.stringify(response[1])}`, + NS, + ); + } + } + private async onZclPayload(payload: AdapterEvents.ZclPayload): Promise { let frame: Zcl.Frame | undefined; let device: Device | undefined; diff --git a/src/zspec/zdo/buffaloZdo.ts b/src/zspec/zdo/buffaloZdo.ts index e9dda76214..2b69a05846 100644 --- a/src/zspec/zdo/buffaloZdo.ts +++ b/src/zspec/zdo/buffaloZdo.ts @@ -24,7 +24,6 @@ import { DeviceAuthenticationLevelTLV, DeviceCapabilityExtensionGlobalTLV, DeviceEUI64ListTLV, - EndDeviceAnnounce, FragmentationParametersGlobalTLV, GetAuthenticationLevelResponse, GetConfigurationResponse, @@ -49,6 +48,8 @@ import { PotentialParentsTLV, PowerDescriptorResponse, ProcessingStatusTLV, + RequestMap, + ResponseMap, RetrieveAuthenticationTokenResponse, RouterInformationGlobalTLV, RoutingTableEntry, @@ -63,6 +64,7 @@ import { SystemServerDiscoveryResponse, TargetIEEEAddressTLV, TLV, + ValidResponseMap, } from './definition/tstypes'; import * as Utils from './utils'; import {ZdoStatusError} from './zdoStatusError'; @@ -71,144 +73,6 @@ const NS = 'zh:zdo:buffalo'; const MAX_BUFFER_SIZE = 255; -interface RequestMap { - [ZdoClusterId.NETWORK_ADDRESS_REQUEST]: [target: EUI64, reportKids: boolean, childStartIndex: number]; - [ZdoClusterId.IEEE_ADDRESS_REQUEST]: [target: NodeId, reportKids: boolean, childStartIndex: number]; - [ZdoClusterId.NODE_DESCRIPTOR_REQUEST]: [target: NodeId, fragmentationParameters?: FragmentationParametersGlobalTLV]; - [ZdoClusterId.POWER_DESCRIPTOR_REQUEST]: [target: NodeId]; - [ZdoClusterId.SIMPLE_DESCRIPTOR_REQUEST]: [target: NodeId, targetEndpoint: number]; - [ZdoClusterId.ACTIVE_ENDPOINTS_REQUEST]: [target: NodeId]; - [ZdoClusterId.MATCH_DESCRIPTORS_REQUEST]: [target: NodeId, profileId: ProfileId, inClusterList: ClusterId[], outClusterList: ClusterId[]]; - [ZdoClusterId.SYSTEM_SERVER_DISCOVERY_REQUEST]: [serverMask: ServerMask]; - [ZdoClusterId.PARENT_ANNOUNCE]: [children: EUI64[]]; - [ZdoClusterId.BIND_REQUEST]: [ - source: EUI64, - sourceEndpoint: number, - clusterId: ClusterId, - type: number, - destination: EUI64, - groupAddress: number, - destinationEndpoint: number, - ]; - [ZdoClusterId.UNBIND_REQUEST]: [ - source: EUI64, - sourceEndpoint: number, - clusterId: ClusterId, - type: number, - destination: EUI64, - groupAddress: number, - destinationEndpoint: number, - ]; - [ZdoClusterId.CLEAR_ALL_BINDINGS_REQUEST]: [tlv: ClearAllBindingsReqEUI64TLV]; - [ZdoClusterId.LQI_TABLE_REQUEST]: [startIndex: number]; - [ZdoClusterId.ROUTING_TABLE_REQUEST]: [startIndex: number]; - [ZdoClusterId.BINDING_TABLE_REQUEST]: [startIndex: number]; - [ZdoClusterId.LEAVE_REQUEST]: [deviceAddress: EUI64, leaveRequestFlags: LeaveRequestFlags]; - [ZdoClusterId.PERMIT_JOINING_REQUEST]: [duration: number, authentication: number, tlvs: TLV[]]; - [ZdoClusterId.NWK_UPDATE_REQUEST]: [ - channels: number[], - duration: number, - count: number | undefined, - nwkUpdateId: number | undefined, - nwkManagerAddr: number | undefined, - ]; - [ZdoClusterId.NWK_ENHANCED_UPDATE_REQUEST]: [ - channelPages: number[], - duration: number, - count: number | undefined, - nwkUpdateId: number | undefined, - nwkManagerAddr: NodeId | undefined, - configurationBitmask: number | undefined, - ]; - [ZdoClusterId.NWK_IEEE_JOINING_LIST_REQUEST]: [startIndex: number]; - [ZdoClusterId.NWK_BEACON_SURVEY_REQUEST]: [tlv: BeaconSurveyConfigurationTLV]; - [ZdoClusterId.START_KEY_NEGOTIATION_REQUEST]: [tlv: Curve25519PublicPointTLV]; - [ZdoClusterId.RETRIEVE_AUTHENTICATION_TOKEN_REQUEST]: [tlv: AuthenticationTokenIdTLV]; - [ZdoClusterId.GET_AUTHENTICATION_LEVEL_REQUEST]: [tlv: TargetIEEEAddressTLV]; - [ZdoClusterId.SET_CONFIGURATION_REQUEST]: [ - nextPanIdChange: NextPanIdChangeGlobalTLV, - nextChannelChange: NextChannelChangeGlobalTLV, - configurationParameters: ConfigurationParametersGlobalTLV, - ]; - [ZdoClusterId.GET_CONFIGURATION_REQUEST]: [tlvIds: number[]]; - [ZdoClusterId.START_KEY_UPDATE_REQUEST]: [ - selectedKeyNegotiationMethod: SelectedKeyNegotiationMethodTLV, - fragmentationParameters: FragmentationParametersGlobalTLV, - ]; - [ZdoClusterId.DECOMMISSION_REQUEST]: [tlv: DeviceEUI64ListTLV]; - [ZdoClusterId.CHALLENGE_REQUEST]: [tlv: APSFrameCounterChallengeTLV]; -} - -interface ResponseMap { - [ZdoClusterId.NETWORK_ADDRESS_RESPONSE]: [Status, NetworkAddressResponse | undefined]; - [ZdoClusterId.IEEE_ADDRESS_RESPONSE]: [Status, IEEEAddressResponse | undefined]; - [ZdoClusterId.NODE_DESCRIPTOR_RESPONSE]: [Status, NodeDescriptorResponse | undefined]; - [ZdoClusterId.POWER_DESCRIPTOR_RESPONSE]: [Status, PowerDescriptorResponse | undefined]; - [ZdoClusterId.SIMPLE_DESCRIPTOR_RESPONSE]: [Status, SimpleDescriptorResponse | undefined]; - [ZdoClusterId.ACTIVE_ENDPOINTS_RESPONSE]: [Status, ActiveEndpointsResponse | undefined]; - [ZdoClusterId.MATCH_DESCRIPTORS_RESPONSE]: [Status, MatchDescriptorsResponse | undefined]; - [ZdoClusterId.END_DEVICE_ANNOUNCE]: [Status, EndDeviceAnnounce | undefined]; - [ZdoClusterId.SYSTEM_SERVER_DISCOVERY_RESPONSE]: [Status, SystemServerDiscoveryResponse | undefined]; - [ZdoClusterId.PARENT_ANNOUNCE_RESPONSE]: [Status, ParentAnnounceResponse | undefined]; - [ZdoClusterId.BIND_RESPONSE]: [Status, void | undefined]; - [ZdoClusterId.UNBIND_RESPONSE]: [Status, void | undefined]; - [ZdoClusterId.CLEAR_ALL_BINDINGS_RESPONSE]: [Status, void | undefined]; - [ZdoClusterId.LQI_TABLE_RESPONSE]: [Status, LQITableResponse | undefined]; - [ZdoClusterId.ROUTING_TABLE_RESPONSE]: [Status, RoutingTableResponse | undefined]; - [ZdoClusterId.BINDING_TABLE_RESPONSE]: [Status, BindingTableResponse | undefined]; - [ZdoClusterId.LEAVE_RESPONSE]: [Status, void | undefined]; - [ZdoClusterId.PERMIT_JOINING_RESPONSE]: [Status, void | undefined]; - [ZdoClusterId.NWK_UPDATE_RESPONSE]: [Status, NwkUpdateResponse | undefined]; - [ZdoClusterId.NWK_ENHANCED_UPDATE_RESPONSE]: [Status, NwkEnhancedUpdateResponse | undefined]; - [ZdoClusterId.NWK_IEEE_JOINING_LIST_RESPONSE]: [Status, NwkIEEEJoiningListResponse | undefined]; - [ZdoClusterId.NWK_UNSOLICITED_ENHANCED_UPDATE_RESPONSE]: [Status, NwkUnsolicitedEnhancedUpdateResponse | undefined]; - [ZdoClusterId.NWK_BEACON_SURVEY_RESPONSE]: [Status, NwkBeaconSurveyResponse | undefined]; - [ZdoClusterId.START_KEY_NEGOTIATION_RESPONSE]: [Status, StartKeyNegotiationResponse | undefined]; - [ZdoClusterId.RETRIEVE_AUTHENTICATION_TOKEN_RESPONSE]: [Status, RetrieveAuthenticationTokenResponse | undefined]; - [ZdoClusterId.GET_AUTHENTICATION_LEVEL_RESPONSE]: [Status, GetAuthenticationLevelResponse | undefined]; - [ZdoClusterId.SET_CONFIGURATION_RESPONSE]: [Status, SetConfigurationResponse | undefined]; - [ZdoClusterId.GET_CONFIGURATION_RESPONSE]: [Status, GetConfigurationResponse | undefined]; - [ZdoClusterId.START_KEY_UPDATE_RESPONSE]: [Status, void | undefined]; - [ZdoClusterId.DECOMMISSION_RESPONSE]: [Status, void | undefined]; - [ZdoClusterId.CHALLENGE_RESPONSE]: [Status, ChallengeResponse | undefined]; - // allow passing number to readResponse() from parsed payload without explicitly converting with `as` - [key: number]: [Status, unknown | undefined]; -} - -interface ValidResponseMap { - [ZdoClusterId.NETWORK_ADDRESS_RESPONSE]: [Status.SUCCESS, NetworkAddressResponse]; - [ZdoClusterId.IEEE_ADDRESS_RESPONSE]: [Status.SUCCESS, IEEEAddressResponse]; - [ZdoClusterId.NODE_DESCRIPTOR_RESPONSE]: [Status.SUCCESS, NodeDescriptorResponse]; - [ZdoClusterId.POWER_DESCRIPTOR_RESPONSE]: [Status.SUCCESS, PowerDescriptorResponse]; - [ZdoClusterId.SIMPLE_DESCRIPTOR_RESPONSE]: [Status.SUCCESS, SimpleDescriptorResponse]; - [ZdoClusterId.ACTIVE_ENDPOINTS_RESPONSE]: [Status.SUCCESS, ActiveEndpointsResponse]; - [ZdoClusterId.MATCH_DESCRIPTORS_RESPONSE]: [Status.SUCCESS, MatchDescriptorsResponse]; - [ZdoClusterId.END_DEVICE_ANNOUNCE]: [Status.SUCCESS, EndDeviceAnnounce]; - [ZdoClusterId.SYSTEM_SERVER_DISCOVERY_RESPONSE]: [Status.SUCCESS, SystemServerDiscoveryResponse]; - [ZdoClusterId.PARENT_ANNOUNCE_RESPONSE]: [Status.SUCCESS, ParentAnnounceResponse]; - [ZdoClusterId.BIND_RESPONSE]: [Status.SUCCESS, void]; - [ZdoClusterId.UNBIND_RESPONSE]: [Status.SUCCESS, void]; - [ZdoClusterId.CLEAR_ALL_BINDINGS_RESPONSE]: [Status.SUCCESS, void]; - [ZdoClusterId.LQI_TABLE_RESPONSE]: [Status.SUCCESS, LQITableResponse]; - [ZdoClusterId.ROUTING_TABLE_RESPONSE]: [Status.SUCCESS, RoutingTableResponse]; - [ZdoClusterId.BINDING_TABLE_RESPONSE]: [Status.SUCCESS, BindingTableResponse]; - [ZdoClusterId.LEAVE_RESPONSE]: [Status.SUCCESS, void]; - [ZdoClusterId.PERMIT_JOINING_RESPONSE]: [Status.SUCCESS, void]; - [ZdoClusterId.NWK_UPDATE_RESPONSE]: [Status.SUCCESS, NwkUpdateResponse]; - [ZdoClusterId.NWK_ENHANCED_UPDATE_RESPONSE]: [Status.SUCCESS, NwkEnhancedUpdateResponse]; - [ZdoClusterId.NWK_IEEE_JOINING_LIST_RESPONSE]: [Status.SUCCESS, NwkIEEEJoiningListResponse]; - [ZdoClusterId.NWK_UNSOLICITED_ENHANCED_UPDATE_RESPONSE]: [Status.SUCCESS, NwkUnsolicitedEnhancedUpdateResponse]; - [ZdoClusterId.NWK_BEACON_SURVEY_RESPONSE]: [Status.SUCCESS, NwkBeaconSurveyResponse]; - [ZdoClusterId.START_KEY_NEGOTIATION_RESPONSE]: [Status.SUCCESS, StartKeyNegotiationResponse]; - [ZdoClusterId.RETRIEVE_AUTHENTICATION_TOKEN_RESPONSE]: [Status.SUCCESS, RetrieveAuthenticationTokenResponse]; - [ZdoClusterId.GET_AUTHENTICATION_LEVEL_RESPONSE]: [Status.SUCCESS, GetAuthenticationLevelResponse]; - [ZdoClusterId.SET_CONFIGURATION_RESPONSE]: [Status.SUCCESS, SetConfigurationResponse]; - [ZdoClusterId.GET_CONFIGURATION_RESPONSE]: [Status.SUCCESS, GetConfigurationResponse]; - [ZdoClusterId.START_KEY_UPDATE_RESPONSE]: [Status.SUCCESS, void]; - [ZdoClusterId.DECOMMISSION_RESPONSE]: [Status.SUCCESS, void]; - [ZdoClusterId.CHALLENGE_RESPONSE]: [Status.SUCCESS, ChallengeResponse]; -} - export class BuffaloZdo extends Buffalo { /** * Set the position of the internal position tracker. diff --git a/src/zspec/zdo/definition/tstypes.ts b/src/zspec/zdo/definition/tstypes.ts index 96d5ea96b0..ec6423898a 100644 --- a/src/zspec/zdo/definition/tstypes.ts +++ b/src/zspec/zdo/definition/tstypes.ts @@ -1,8 +1,10 @@ import {ClusterId, EUI64, ExtendedPanId, NodeId, PanId, ProfileId} from '../../tstypes'; +import {ClusterId as ZdoClusterId} from './clusters'; import { ActiveLinkKeyType, InitialJoinMethod, JoiningPolicy, + LeaveRequestFlags, RoutingTableStatus, SelectedKeyNegotiationProtocol, SelectedPreSharedSecret, @@ -886,3 +888,175 @@ export type TLV = { export type TLVs = { tlvs: TLV[]; }; + +export interface RequestMap { + [ZdoClusterId.NETWORK_ADDRESS_REQUEST]: [target: EUI64, reportKids: boolean, childStartIndex: number]; + [ZdoClusterId.IEEE_ADDRESS_REQUEST]: [target: NodeId, reportKids: boolean, childStartIndex: number]; + [ZdoClusterId.NODE_DESCRIPTOR_REQUEST]: [target: NodeId, fragmentationParameters?: FragmentationParametersGlobalTLV]; + [ZdoClusterId.POWER_DESCRIPTOR_REQUEST]: [target: NodeId]; + [ZdoClusterId.SIMPLE_DESCRIPTOR_REQUEST]: [target: NodeId, targetEndpoint: number]; + [ZdoClusterId.ACTIVE_ENDPOINTS_REQUEST]: [target: NodeId]; + [ZdoClusterId.MATCH_DESCRIPTORS_REQUEST]: [target: NodeId, profileId: ProfileId, inClusterList: ClusterId[], outClusterList: ClusterId[]]; + [ZdoClusterId.SYSTEM_SERVER_DISCOVERY_REQUEST]: [serverMask: ServerMask]; + [ZdoClusterId.PARENT_ANNOUNCE]: [children: EUI64[]]; + [ZdoClusterId.BIND_REQUEST]: [ + source: EUI64, + sourceEndpoint: number, + clusterId: ClusterId, + type: number, + destination: EUI64, + groupAddress: number, + destinationEndpoint: number, + ]; + [ZdoClusterId.UNBIND_REQUEST]: [ + source: EUI64, + sourceEndpoint: number, + clusterId: ClusterId, + type: number, + destination: EUI64, + groupAddress: number, + destinationEndpoint: number, + ]; + [ZdoClusterId.CLEAR_ALL_BINDINGS_REQUEST]: [tlv: ClearAllBindingsReqEUI64TLV]; + [ZdoClusterId.LQI_TABLE_REQUEST]: [startIndex: number]; + [ZdoClusterId.ROUTING_TABLE_REQUEST]: [startIndex: number]; + [ZdoClusterId.BINDING_TABLE_REQUEST]: [startIndex: number]; + [ZdoClusterId.LEAVE_REQUEST]: [deviceAddress: EUI64, leaveRequestFlags: LeaveRequestFlags]; + [ZdoClusterId.PERMIT_JOINING_REQUEST]: [duration: number, authentication: number, tlvs: TLV[]]; + [ZdoClusterId.NWK_UPDATE_REQUEST]: [ + channels: number[], + duration: number, + count: number | undefined, + nwkUpdateId: number | undefined, + nwkManagerAddr: number | undefined, + ]; + [ZdoClusterId.NWK_ENHANCED_UPDATE_REQUEST]: [ + channelPages: number[], + duration: number, + count: number | undefined, + nwkUpdateId: number | undefined, + nwkManagerAddr: NodeId | undefined, + configurationBitmask: number | undefined, + ]; + [ZdoClusterId.NWK_IEEE_JOINING_LIST_REQUEST]: [startIndex: number]; + [ZdoClusterId.NWK_BEACON_SURVEY_REQUEST]: [tlv: BeaconSurveyConfigurationTLV]; + [ZdoClusterId.START_KEY_NEGOTIATION_REQUEST]: [tlv: Curve25519PublicPointTLV]; + [ZdoClusterId.RETRIEVE_AUTHENTICATION_TOKEN_REQUEST]: [tlv: AuthenticationTokenIdTLV]; + [ZdoClusterId.GET_AUTHENTICATION_LEVEL_REQUEST]: [tlv: TargetIEEEAddressTLV]; + [ZdoClusterId.SET_CONFIGURATION_REQUEST]: [ + nextPanIdChange: NextPanIdChangeGlobalTLV, + nextChannelChange: NextChannelChangeGlobalTLV, + configurationParameters: ConfigurationParametersGlobalTLV, + ]; + [ZdoClusterId.GET_CONFIGURATION_REQUEST]: [tlvIds: number[]]; + [ZdoClusterId.START_KEY_UPDATE_REQUEST]: [ + selectedKeyNegotiationMethod: SelectedKeyNegotiationMethodTLV, + fragmentationParameters: FragmentationParametersGlobalTLV, + ]; + [ZdoClusterId.DECOMMISSION_REQUEST]: [tlv: DeviceEUI64ListTLV]; + [ZdoClusterId.CHALLENGE_REQUEST]: [tlv: APSFrameCounterChallengeTLV]; +} + +export type GenericZdoResponse = [Status, unknown | undefined]; + +export interface ResponseMap { + [ZdoClusterId.NETWORK_ADDRESS_RESPONSE]: [Status, NetworkAddressResponse | undefined]; + [ZdoClusterId.IEEE_ADDRESS_RESPONSE]: [Status, IEEEAddressResponse | undefined]; + [ZdoClusterId.NODE_DESCRIPTOR_RESPONSE]: [Status, NodeDescriptorResponse | undefined]; + [ZdoClusterId.POWER_DESCRIPTOR_RESPONSE]: [Status, PowerDescriptorResponse | undefined]; + [ZdoClusterId.SIMPLE_DESCRIPTOR_RESPONSE]: [Status, SimpleDescriptorResponse | undefined]; + [ZdoClusterId.ACTIVE_ENDPOINTS_RESPONSE]: [Status, ActiveEndpointsResponse | undefined]; + [ZdoClusterId.MATCH_DESCRIPTORS_RESPONSE]: [Status, MatchDescriptorsResponse | undefined]; + [ZdoClusterId.END_DEVICE_ANNOUNCE]: [Status, EndDeviceAnnounce | undefined]; + [ZdoClusterId.SYSTEM_SERVER_DISCOVERY_RESPONSE]: [Status, SystemServerDiscoveryResponse | undefined]; + [ZdoClusterId.PARENT_ANNOUNCE_RESPONSE]: [Status, ParentAnnounceResponse | undefined]; + [ZdoClusterId.BIND_RESPONSE]: [Status, void | undefined]; + [ZdoClusterId.UNBIND_RESPONSE]: [Status, void | undefined]; + [ZdoClusterId.CLEAR_ALL_BINDINGS_RESPONSE]: [Status, void | undefined]; + [ZdoClusterId.LQI_TABLE_RESPONSE]: [Status, LQITableResponse | undefined]; + [ZdoClusterId.ROUTING_TABLE_RESPONSE]: [Status, RoutingTableResponse | undefined]; + [ZdoClusterId.BINDING_TABLE_RESPONSE]: [Status, BindingTableResponse | undefined]; + [ZdoClusterId.LEAVE_RESPONSE]: [Status, void | undefined]; + [ZdoClusterId.PERMIT_JOINING_RESPONSE]: [Status, void | undefined]; + [ZdoClusterId.NWK_UPDATE_RESPONSE]: [Status, NwkUpdateResponse | undefined]; + [ZdoClusterId.NWK_ENHANCED_UPDATE_RESPONSE]: [Status, NwkEnhancedUpdateResponse | undefined]; + [ZdoClusterId.NWK_IEEE_JOINING_LIST_RESPONSE]: [Status, NwkIEEEJoiningListResponse | undefined]; + [ZdoClusterId.NWK_UNSOLICITED_ENHANCED_UPDATE_RESPONSE]: [Status, NwkUnsolicitedEnhancedUpdateResponse | undefined]; + [ZdoClusterId.NWK_BEACON_SURVEY_RESPONSE]: [Status, NwkBeaconSurveyResponse | undefined]; + [ZdoClusterId.START_KEY_NEGOTIATION_RESPONSE]: [Status, StartKeyNegotiationResponse | undefined]; + [ZdoClusterId.RETRIEVE_AUTHENTICATION_TOKEN_RESPONSE]: [Status, RetrieveAuthenticationTokenResponse | undefined]; + [ZdoClusterId.GET_AUTHENTICATION_LEVEL_RESPONSE]: [Status, GetAuthenticationLevelResponse | undefined]; + [ZdoClusterId.SET_CONFIGURATION_RESPONSE]: [Status, SetConfigurationResponse | undefined]; + [ZdoClusterId.GET_CONFIGURATION_RESPONSE]: [Status, GetConfigurationResponse | undefined]; + [ZdoClusterId.START_KEY_UPDATE_RESPONSE]: [Status, void | undefined]; + [ZdoClusterId.DECOMMISSION_RESPONSE]: [Status, void | undefined]; + [ZdoClusterId.CHALLENGE_RESPONSE]: [Status, ChallengeResponse | undefined]; + // allow passing number to readResponse() from parsed payload without explicitly converting with `as` + [key: number]: GenericZdoResponse; +} + +export interface ValidResponseMap { + [ZdoClusterId.NETWORK_ADDRESS_RESPONSE]: [Status.SUCCESS, NetworkAddressResponse]; + [ZdoClusterId.IEEE_ADDRESS_RESPONSE]: [Status.SUCCESS, IEEEAddressResponse]; + [ZdoClusterId.NODE_DESCRIPTOR_RESPONSE]: [Status.SUCCESS, NodeDescriptorResponse]; + [ZdoClusterId.POWER_DESCRIPTOR_RESPONSE]: [Status.SUCCESS, PowerDescriptorResponse]; + [ZdoClusterId.SIMPLE_DESCRIPTOR_RESPONSE]: [Status.SUCCESS, SimpleDescriptorResponse]; + [ZdoClusterId.ACTIVE_ENDPOINTS_RESPONSE]: [Status.SUCCESS, ActiveEndpointsResponse]; + [ZdoClusterId.MATCH_DESCRIPTORS_RESPONSE]: [Status.SUCCESS, MatchDescriptorsResponse]; + [ZdoClusterId.END_DEVICE_ANNOUNCE]: [Status.SUCCESS, EndDeviceAnnounce]; + [ZdoClusterId.SYSTEM_SERVER_DISCOVERY_RESPONSE]: [Status.SUCCESS, SystemServerDiscoveryResponse]; + [ZdoClusterId.PARENT_ANNOUNCE_RESPONSE]: [Status.SUCCESS, ParentAnnounceResponse]; + [ZdoClusterId.BIND_RESPONSE]: [Status.SUCCESS, void]; + [ZdoClusterId.UNBIND_RESPONSE]: [Status.SUCCESS, void]; + [ZdoClusterId.CLEAR_ALL_BINDINGS_RESPONSE]: [Status.SUCCESS, void]; + [ZdoClusterId.LQI_TABLE_RESPONSE]: [Status.SUCCESS, LQITableResponse]; + [ZdoClusterId.ROUTING_TABLE_RESPONSE]: [Status.SUCCESS, RoutingTableResponse]; + [ZdoClusterId.BINDING_TABLE_RESPONSE]: [Status.SUCCESS, BindingTableResponse]; + [ZdoClusterId.LEAVE_RESPONSE]: [Status.SUCCESS, void]; + [ZdoClusterId.PERMIT_JOINING_RESPONSE]: [Status.SUCCESS, void]; + [ZdoClusterId.NWK_UPDATE_RESPONSE]: [Status.SUCCESS, NwkUpdateResponse]; + [ZdoClusterId.NWK_ENHANCED_UPDATE_RESPONSE]: [Status.SUCCESS, NwkEnhancedUpdateResponse]; + [ZdoClusterId.NWK_IEEE_JOINING_LIST_RESPONSE]: [Status.SUCCESS, NwkIEEEJoiningListResponse]; + [ZdoClusterId.NWK_UNSOLICITED_ENHANCED_UPDATE_RESPONSE]: [Status.SUCCESS, NwkUnsolicitedEnhancedUpdateResponse]; + [ZdoClusterId.NWK_BEACON_SURVEY_RESPONSE]: [Status.SUCCESS, NwkBeaconSurveyResponse]; + [ZdoClusterId.START_KEY_NEGOTIATION_RESPONSE]: [Status.SUCCESS, StartKeyNegotiationResponse]; + [ZdoClusterId.RETRIEVE_AUTHENTICATION_TOKEN_RESPONSE]: [Status.SUCCESS, RetrieveAuthenticationTokenResponse]; + [ZdoClusterId.GET_AUTHENTICATION_LEVEL_RESPONSE]: [Status.SUCCESS, GetAuthenticationLevelResponse]; + [ZdoClusterId.SET_CONFIGURATION_RESPONSE]: [Status.SUCCESS, SetConfigurationResponse]; + [ZdoClusterId.GET_CONFIGURATION_RESPONSE]: [Status.SUCCESS, GetConfigurationResponse]; + [ZdoClusterId.START_KEY_UPDATE_RESPONSE]: [Status.SUCCESS, void]; + [ZdoClusterId.DECOMMISSION_RESPONSE]: [Status.SUCCESS, void]; + [ZdoClusterId.CHALLENGE_RESPONSE]: [Status.SUCCESS, ChallengeResponse]; +} + +export interface RequestToResponseMap { + [ZdoClusterId.NETWORK_ADDRESS_REQUEST]: ResponseMap[ZdoClusterId.NETWORK_ADDRESS_RESPONSE]; + [ZdoClusterId.IEEE_ADDRESS_REQUEST]: ResponseMap[ZdoClusterId.IEEE_ADDRESS_RESPONSE]; + [ZdoClusterId.NODE_DESCRIPTOR_REQUEST]: ResponseMap[ZdoClusterId.NODE_DESCRIPTOR_RESPONSE]; + [ZdoClusterId.POWER_DESCRIPTOR_REQUEST]: ResponseMap[ZdoClusterId.POWER_DESCRIPTOR_RESPONSE]; + [ZdoClusterId.SIMPLE_DESCRIPTOR_REQUEST]: ResponseMap[ZdoClusterId.SIMPLE_DESCRIPTOR_RESPONSE]; + [ZdoClusterId.ACTIVE_ENDPOINTS_REQUEST]: ResponseMap[ZdoClusterId.ACTIVE_ENDPOINTS_RESPONSE]; + [ZdoClusterId.MATCH_DESCRIPTORS_REQUEST]: ResponseMap[ZdoClusterId.MATCH_DESCRIPTORS_RESPONSE]; + [ZdoClusterId.SYSTEM_SERVER_DISCOVERY_REQUEST]: ResponseMap[ZdoClusterId.SYSTEM_SERVER_DISCOVERY_RESPONSE]; + [ZdoClusterId.PARENT_ANNOUNCE]: ResponseMap[ZdoClusterId.PARENT_ANNOUNCE_RESPONSE]; + [ZdoClusterId.BIND_REQUEST]: ResponseMap[ZdoClusterId.BIND_RESPONSE]; + [ZdoClusterId.UNBIND_REQUEST]: ResponseMap[ZdoClusterId.UNBIND_RESPONSE]; + [ZdoClusterId.CLEAR_ALL_BINDINGS_REQUEST]: ResponseMap[ZdoClusterId.CLEAR_ALL_BINDINGS_RESPONSE]; + [ZdoClusterId.LQI_TABLE_REQUEST]: ResponseMap[ZdoClusterId.LQI_TABLE_RESPONSE]; + [ZdoClusterId.ROUTING_TABLE_REQUEST]: ResponseMap[ZdoClusterId.ROUTING_TABLE_RESPONSE]; + [ZdoClusterId.BINDING_TABLE_REQUEST]: ResponseMap[ZdoClusterId.BINDING_TABLE_RESPONSE]; + [ZdoClusterId.LEAVE_REQUEST]: ResponseMap[ZdoClusterId.LEAVE_RESPONSE]; + [ZdoClusterId.PERMIT_JOINING_REQUEST]: ResponseMap[ZdoClusterId.PERMIT_JOINING_RESPONSE]; + [ZdoClusterId.NWK_UPDATE_REQUEST]: ResponseMap[ZdoClusterId.NWK_UPDATE_RESPONSE]; + [ZdoClusterId.NWK_ENHANCED_UPDATE_REQUEST]: ResponseMap[ZdoClusterId.NWK_ENHANCED_UPDATE_RESPONSE]; + [ZdoClusterId.NWK_IEEE_JOINING_LIST_REQUEST]: ResponseMap[ZdoClusterId.NWK_IEEE_JOINING_LIST_RESPONSE]; + [ZdoClusterId.NWK_BEACON_SURVEY_REQUEST]: ResponseMap[ZdoClusterId.NWK_BEACON_SURVEY_RESPONSE]; + [ZdoClusterId.START_KEY_NEGOTIATION_REQUEST]: ResponseMap[ZdoClusterId.START_KEY_NEGOTIATION_RESPONSE]; + [ZdoClusterId.RETRIEVE_AUTHENTICATION_TOKEN_REQUEST]: ResponseMap[ZdoClusterId.RETRIEVE_AUTHENTICATION_TOKEN_RESPONSE]; + [ZdoClusterId.GET_AUTHENTICATION_LEVEL_REQUEST]: ResponseMap[ZdoClusterId.GET_AUTHENTICATION_LEVEL_RESPONSE]; + [ZdoClusterId.SET_CONFIGURATION_REQUEST]: ResponseMap[ZdoClusterId.SET_CONFIGURATION_RESPONSE]; + [ZdoClusterId.GET_CONFIGURATION_REQUEST]: ResponseMap[ZdoClusterId.GET_CONFIGURATION_RESPONSE]; + [ZdoClusterId.START_KEY_UPDATE_REQUEST]: ResponseMap[ZdoClusterId.START_KEY_UPDATE_RESPONSE]; + [ZdoClusterId.DECOMMISSION_REQUEST]: ResponseMap[ZdoClusterId.DECOMMISSION_RESPONSE]; + [ZdoClusterId.CHALLENGE_REQUEST]: ResponseMap[ZdoClusterId.CHALLENGE_RESPONSE]; +} diff --git a/src/zspec/zdo/utils.ts b/src/zspec/zdo/utils.ts index 1d17c4f16a..36261ce2df 100644 --- a/src/zspec/zdo/utils.ts +++ b/src/zspec/zdo/utils.ts @@ -3,19 +3,18 @@ import {MACCapabilityFlags, ServerMask} from './definition/tstypes'; /** * Get a the response cluster ID corresponding to a request. - * May be null if cluster does not have a response. * @param requestClusterId - * @returns + * @returns Response cluster ID or undefined if unknown/invalid */ -export const getResponseClusterId = (requestClusterId: ClusterId): ClusterId | null => { +export const getResponseClusterId = (requestClusterId: ClusterId): ClusterId | undefined => { if (0x8000 < requestClusterId || requestClusterId === ClusterId.END_DEVICE_ANNOUNCE) { - return null; + return undefined; } const responseClusterId = requestClusterId + 0x8000; if (ClusterId[responseClusterId] == undefined) { - return null; + return undefined; } return responseClusterId; diff --git a/test/adapter/ember/emberAdapter.test.ts b/test/adapter/ember/emberAdapter.test.ts index fc72b345d5..dfb084f2b1 100644 --- a/test/adapter/ember/emberAdapter.test.ts +++ b/test/adapter/ember/emberAdapter.test.ts @@ -43,7 +43,7 @@ import { SecManNetworkKeyInfo, } from '../../../src/adapter/ember/types'; import {lowHighBytes} from '../../../src/adapter/ember/utils/math'; -import {DeviceAnnouncePayload, DeviceJoinedPayload, DeviceLeavePayload, NetworkAddressPayload, ZclPayload} from '../../../src/adapter/events'; +import {DeviceJoinedPayload, DeviceLeavePayload, ZclPayload} from '../../../src/adapter/events'; import {AdapterOptions, NetworkOptions, SerialPortOptions} from '../../../src/adapter/tstype'; import {Backup} from '../../../src/models/backup'; import {UnifiedBackupStorage} from '../../../src/models/backup-storage-unified'; @@ -842,28 +842,28 @@ describe('Ember Adapter Layer', () => { .mockResolvedValueOnce([SLStatus.OK, EmberNodeType.COORDINATOR, deepClone(DEFAULT_ADAPTER_NETWORK_PARAMETERS)]) .mockResolvedValueOnce([SLStatus.FAIL, 0, {}]); }, - `Failed to get network parameters with status=${SLStatus[SLStatus.FAIL]}.`, + `Failed to get network parameters with status=FAIL.`, ], [ 'if could not set concentrator', () => { mockEzspSetConcentrator.mockResolvedValueOnce(SLStatus.FAIL); }, - `[CONCENTRATOR] Failed to set concentrator with status=${SLStatus[SLStatus.FAIL]}.`, + `[CONCENTRATOR] Failed to set concentrator with status=FAIL.`, ], [ 'if could not add endpoint', () => { mockEzspAddEndpoint.mockResolvedValueOnce(SLStatus.FAIL); }, - `Failed to register endpoint '1' with status=${SLStatus[SLStatus.FAIL]}.`, + `Failed to register endpoint '1' with status=FAIL.`, ], [ 'if could not set multicast table entry', () => { mockEzspSetMulticastTableEntry.mockResolvedValueOnce(SLStatus.FAIL); }, - `Failed to register group '0' in multicast table with status=${SLStatus[SLStatus.FAIL]}.`, + `Failed to register group '0' in multicast table with status=FAIL.`, ], [ 'if could not set TC key request policy', @@ -873,7 +873,7 @@ describe('Ember Adapter Layer', () => { .mockResolvedValueOnce(SLStatus.OK) // EzspPolicyId.MESSAGE_CONTENTS_IN_CALLBACK_POLICY .mockResolvedValueOnce(SLStatus.FAIL); // EzspPolicyId.TC_KEY_REQUEST_POLICY }, - `[INIT TC] Failed to set EzspPolicyId TC_KEY_REQUEST_POLICY to ALLOW_TC_KEY_REQUESTS_AND_SEND_CURRENT_KEY with status=${SLStatus[SLStatus.FAIL]}.`, + `[INIT TC] Failed to set EzspPolicyId TC_KEY_REQUEST_POLICY to ALLOW_TC_KEY_REQUESTS_AND_SEND_CURRENT_KEY with status=FAIL.`, ], [ 'if could not set app key request policy', @@ -884,7 +884,7 @@ describe('Ember Adapter Layer', () => { .mockResolvedValueOnce(SLStatus.OK) // EzspPolicyId.TC_KEY_REQUEST_POLICY .mockResolvedValueOnce(SLStatus.FAIL); // EzspPolicyId.APP_KEY_REQUEST_POLICY }, - `[INIT TC] Failed to set EzspPolicyId APP_KEY_REQUEST_POLICY to DENY_APP_KEY_REQUESTS with status=${SLStatus[SLStatus.FAIL]}.`, + `[INIT TC] Failed to set EzspPolicyId APP_KEY_REQUEST_POLICY to DENY_APP_KEY_REQUESTS with status=FAIL.`, ], [ 'if could not set app key request policy', @@ -896,21 +896,21 @@ describe('Ember Adapter Layer', () => { .mockResolvedValueOnce(SLStatus.OK) // EzspPolicyId.APP_KEY_REQUEST_POLICY .mockResolvedValueOnce(SLStatus.FAIL); // EzspPolicyId.TRUST_CENTER_POLICY }, - `[INIT TC] Failed to set join policy to USE_PRECONFIGURED_KEY with status=${SLStatus[SLStatus.FAIL]}.`, + `[INIT TC] Failed to set join policy to USE_PRECONFIGURED_KEY with status=FAIL.`, ], [ 'if could not init network', () => { mockEzspNetworkInit.mockResolvedValueOnce(SLStatus.FAIL); }, - `[INIT TC] Failed network init request with status=${SLStatus[SLStatus.FAIL]}.`, + `[INIT TC] Failed network init request with status=FAIL.`, ], [ 'if could not export network key', () => { mockEzspExportKey.mockResolvedValueOnce([SLStatus.FAIL, Buffer.alloc(16)]); }, - `[INIT TC] Failed to export Network Key with status=${SLStatus[SLStatus.FAIL]}.`, + `[INIT TC] Failed to export Network Key with status=FAIL.`, ], [ 'if could not leave network', @@ -919,7 +919,7 @@ describe('Ember Adapter Layer', () => { mockEzspGetNetworkParameters.mockResolvedValueOnce([SLStatus.FAIL, 0, {}]); mockEzspLeaveNetwork.mockResolvedValueOnce(SLStatus.FAIL); }, - `[INIT TC] Failed leave network request with status=${SLStatus[SLStatus.FAIL]}.`, + `[INIT TC] Failed leave network request with status=FAIL.`, ], [ 'if form could not set initial security state', @@ -927,7 +927,7 @@ describe('Ember Adapter Layer', () => { takeResetCodePath(); mockEzspSetInitialSecurityState.mockResolvedValueOnce(SLStatus.FAIL); }, - `[INIT FORM] Failed to set initial security state with status=${SLStatus[SLStatus.FAIL]}.`, + `[INIT FORM] Failed to set initial security state with status=FAIL.`, ], [ 'if form could not set extended security bitmask', @@ -935,7 +935,7 @@ describe('Ember Adapter Layer', () => { takeResetCodePath(); mockEzspSetExtendedSecurityBitmask.mockResolvedValueOnce(SLStatus.FAIL); }, - `[INIT FORM] Failed to set extended security bitmask to 272 with status=${SLStatus[SLStatus.FAIL]}.`, + `[INIT FORM] Failed to set extended security bitmask to 272 with status=FAIL.`, ], [ 'if could not form network', @@ -943,7 +943,7 @@ describe('Ember Adapter Layer', () => { takeResetCodePath(); mockEzspFormNetwork.mockResolvedValueOnce(SLStatus.FAIL); }, - `[INIT FORM] Failed form network request with status=${SLStatus[SLStatus.FAIL]}.`, + `[INIT FORM] Failed form network request with status=FAIL.`, ], [ 'if backup corrupted', @@ -1109,7 +1109,7 @@ describe('Ember Adapter Layer', () => { await jest.advanceTimersByTimeAsync(5000); await expect(result).resolves.toStrictEqual('reset'); - expect(loggerSpies.error).toHaveBeenCalledWith(`[INIT FORM] Failed to clear key table with status=${SLStatus[SLStatus.FAIL]}.`, 'zh:ember'); + expect(loggerSpies.error).toHaveBeenCalledWith(`[INIT FORM] Failed to clear key table with status=FAIL.`, 'zh:ember'); }); it('Starts but ignores backup if unsupported version', async () => { @@ -1202,21 +1202,21 @@ describe('Ember Adapter Layer', () => { const p1 = defuseRejection(adapter.emberGetPanId()); await jest.advanceTimersByTimeAsync(5000); - await expect(p1).rejects.toThrow(`Failed to get PAN ID (via network parameters) with status=${SLStatus[SLStatus.FAIL]}.`); + await expect(p1).rejects.toThrow(`Failed to get PAN ID (via network parameters) with status=FAIL.`); adapter.clearNetworkCache(); const p2 = defuseRejection(adapter.emberGetExtendedPanId()); await jest.advanceTimersByTimeAsync(5000); - await expect(p2).rejects.toThrow(`Failed to get Extended PAN ID (via network parameters) with status=${SLStatus[SLStatus.FAIL]}.`); + await expect(p2).rejects.toThrow(`Failed to get Extended PAN ID (via network parameters) with status=FAIL.`); adapter.clearNetworkCache(); const p3 = defuseRejection(adapter.emberGetRadioChannel()); await jest.advanceTimersByTimeAsync(5000); - await expect(p3).rejects.toThrow(`Failed to get radio channel (via network parameters) with status=${SLStatus[SLStatus.FAIL]}.`); + await expect(p3).rejects.toThrow(`Failed to get radio channel (via network parameters) with status=FAIL.`); }); it('Logs stack status change', async () => { @@ -1243,10 +1243,7 @@ describe('Ember Adapter Layer', () => { mockEzspEmitter.emit('messageSent', SLStatus.ZIGBEE_DELIVERY_FAILED, EmberOutgoingMessageType.BROADCAST, 1234, apsFrame, 1); await flushPromises(); - expect(loggerSpies.error).toHaveBeenCalledWith( - `Delivery of BROADCAST failed for '1234' [apsFrame=${JSON.stringify(apsFrame)} messageTag=1]`, - 'zh:ember', - ); + expect(loggerSpies.error).toHaveBeenCalledWith(`Delivery of BROADCAST failed for '1234'.`, 'zh:ember'); const spyDeliveryFailedFor = jest.spyOn( // @ts-expect-error private @@ -1355,17 +1352,19 @@ describe('Ember Adapter Layer', () => { ); await flushPromises(); + const zdoResponse = [ + Zdo.Status.SUCCESS, + { + eui64: '0x332211eeddccbbaa', + nwkAddress: sender, + startIndex: 0, + assocDevList: [], + } as ZdoTypes.NetworkAddressResponse, + ]; + expect(spyResolveZDO).toHaveBeenCalledTimes(1); - expect(spyResolveZDO).toHaveBeenCalledWith(sender, apsFrame, { - eui64: '0x332211eeddccbbaa', - nwkAddress: sender, - startIndex: 0, - assocDevList: [], - } as ZdoTypes.NetworkAddressResponse); - expect(spyEmit).toHaveBeenCalledWith('networkAddress', { - networkAddress: sender, - ieeeAddr: '0x332211eeddccbbaa', - } as NetworkAddressPayload); + expect(spyResolveZDO).toHaveBeenCalledWith('0x332211eeddccbbaa', apsFrame, zdoResponse); + expect(spyEmit).toHaveBeenCalledWith('zdoResponse', Zdo.ClusterId.NETWORK_ADDRESS_RESPONSE, zdoResponse); }); it('Emits device announce event on ZDO END_DEVICE_ANNOUNCE', async () => { @@ -1390,25 +1389,26 @@ describe('Ember Adapter Layer', () => { await flushPromises(); + const zdoResponse = [ + Zdo.Status.SUCCESS, + { + nwkAddress: sender, + eui64: '0x332211eeddccbbaa', + capabilities: { + alternatePANCoordinator: 0, + deviceType: 1, + powerSource: 1, + rxOnWhenIdle: 0, + reserved1: 0, + reserved2: 0, + securityCapability: 0, + allocateAddress: 0, + }, + } as ZdoTypes.EndDeviceAnnounce, + ]; expect(spyResolveZDO).toHaveBeenCalledTimes(1); - expect(spyResolveZDO).toHaveBeenCalledWith(sender, apsFrame, { - nwkAddress: sender, - eui64: '0x332211eeddccbbaa', - capabilities: { - alternatePANCoordinator: 0, - deviceType: 1, - powerSource: 1, - rxOnWhenIdle: 0, - reserved1: 0, - reserved2: 0, - securityCapability: 0, - allocateAddress: 0, - }, - } as ZdoTypes.EndDeviceAnnounce); - expect(spyEmit).toHaveBeenCalledWith('deviceAnnounce', { - networkAddress: sender, - ieeeAddr: '0x332211eeddccbbaa', - } as DeviceAnnouncePayload); + expect(spyResolveZDO).toHaveBeenCalledWith(sender, apsFrame, zdoResponse); + expect(spyEmit).toHaveBeenCalledWith('zdoResponse', Zdo.ClusterId.END_DEVICE_ANNOUNCE, zdoResponse); }); it('Emits ZCL payload on incoming message', async () => { @@ -1721,9 +1721,7 @@ describe('Ember Adapter Layer', () => { it('Fails to export link keys due to failed table size retrieval', async () => { mockEzspGetConfigurationValue.mockResolvedValueOnce([SLStatus.FAIL, 0]); - await expect(adapter.exportLinkKeys()).rejects.toThrow( - `[BACKUP] Failed to retrieve key table size from NCP with status=${SLStatus[SLStatus.FAIL]}.`, - ); + await expect(adapter.exportLinkKeys()).rejects.toThrow(`[BACKUP] Failed to retrieve key table size from NCP with status=FAIL.`); }); it('Fails to export link keys due to failed AES hashing', async () => { @@ -1751,7 +1749,7 @@ describe('Ember Adapter Layer', () => { await adapter.exportLinkKeys(); expect(loggerSpies.error).toHaveBeenCalledWith( - `[BACKUP] Failed to hash link key at index 0 with status=${SLStatus[SLStatus.FAIL]}. Omitting from backup.`, + `[BACKUP] Failed to hash link key at index 0 with status=FAIL. Omitting from backup.`, 'zh:ember', ); }); @@ -1845,7 +1843,7 @@ describe('Ember Adapter Layer', () => { // @ts-expect-error mock, unnecessary {}, ]), - ).rejects.toThrow(`[BACKUP] Failed to retrieve key table size from NCP with status=${SLStatus[SLStatus.FAIL]}.`); + ).rejects.toThrow(`[BACKUP] Failed to retrieve key table size from NCP with status=FAIL.`); }); it('Failed to import link keys due to insufficient table size', async () => { @@ -1904,7 +1902,7 @@ describe('Ember Adapter Layer', () => { incomingFrameCounter: k1Metadata.incomingFrameCounter, }, ]), - ).rejects.toThrow(`[BACKUP] Failed to set key table entry at index 0 with status=${SLStatus[SLStatus.FAIL]}.`); + ).rejects.toThrow(`[BACKUP] Failed to set key table entry at index 0 with status=FAIL.`); }); it('Failed to import link keys due to failed key erase', async () => { @@ -1938,7 +1936,7 @@ describe('Ember Adapter Layer', () => { incomingFrameCounter: k1Metadata.incomingFrameCounter, }, ]), - ).rejects.toThrow(`[BACKUP] Failed to erase key table entry at index 1 with status=${SLStatus[SLStatus.FAIL]}.`); + ).rejects.toThrow(`[BACKUP] Failed to erase key table entry at index 1 with status=FAIL.`); }); it('Broadcasts network key update', async () => { @@ -1956,7 +1954,7 @@ describe('Ember Adapter Layer', () => { const p = defuseRejection(adapter.broadcastNetworkKeyUpdate()); await jest.advanceTimersByTimeAsync(100000); - await expect(p).rejects.toThrow(`[TRUST CENTER] Failed to broadcast next network key with status=${SLStatus[SLStatus.FAIL]}.`); + await expect(p).rejects.toThrow(`[TRUST CENTER] Failed to broadcast next network key with status=FAIL.`); expect(mockEzspBroadcastNextNetworkKey).toHaveBeenCalledTimes(1); expect(mockEzspBroadcastNetworkKeySwitch).toHaveBeenCalledTimes(0); }); @@ -1967,7 +1965,7 @@ describe('Ember Adapter Layer', () => { const p = defuseRejection(adapter.broadcastNetworkKeyUpdate()); await jest.advanceTimersByTimeAsync(100000); - await expect(p).rejects.toThrow(`[TRUST CENTER] Failed to broadcast network key switch with status=${SLStatus[SLStatus.FAIL]}.`); + await expect(p).rejects.toThrow(`[TRUST CENTER] Failed to broadcast network key switch with status=FAIL.`); expect(mockEzspBroadcastNextNetworkKey).toHaveBeenCalledTimes(1); expect(mockEzspBroadcastNetworkKeySwitch).toHaveBeenCalledTimes(1); }); @@ -2021,7 +2019,6 @@ describe('Ember Adapter Layer', () => { ['bind', [1234, '0x1122334455667788', 1, 0, 54, 'group', 1]], ['unbind', [1234, '0x1122334455667788', 1, 0, '0xaabbccddee112233', 'endpoint', 1]], ['unbind', [1234, '0x1122334455667788', 1, 0, 54, 'group', 1]], - ['removeDevice', [1234]], ['removeDevice', [1234, '0x1122334455667788']], [ 'sendZclFrameToEndpoint', @@ -2131,14 +2128,14 @@ describe('Ember Adapter Layer', () => { () => { mockEzspGetNetworkParameters.mockResolvedValueOnce([SLStatus.FAIL, 0, {}]); }, - `[BACKUP] Failed to get network parameters with status=${SLStatus[SLStatus.FAIL]}.`, + `[BACKUP] Failed to get network parameters with status=FAIL.`, ], [ 'failed get network keys info', () => { mockEzspGetNetworkKeyInfo.mockResolvedValueOnce([SLStatus.FAIL, {}]); }, - `[BACKUP] Failed to get network keys info with status=${SLStatus[SLStatus.FAIL]}.`, + `[BACKUP] Failed to get network keys info with status=FAIL.`, ], [ 'no network key set', @@ -2161,7 +2158,7 @@ describe('Ember Adapter Layer', () => { () => { mockEzspExportKey.mockResolvedValueOnce([SLStatus.FAIL, {}]); }, - `[BACKUP] Failed to export TC Link Key with status=${SLStatus[SLStatus.FAIL]}.`, + `[BACKUP] Failed to export TC Link Key with status=FAIL.`, ], [ 'failed export network key', @@ -2173,7 +2170,7 @@ describe('Ember Adapter Layer', () => { ]) .mockResolvedValueOnce([SLStatus.FAIL, {}]); }, - `[BACKUP] Failed to export Network Key with status=${SLStatus[SLStatus.FAIL]}.`, + `[BACKUP] Failed to export Network Key with status=FAIL.`, ], ])('Adapter impl: throws when backup fails due to %s', async (_command, setup, error) => { setup(); @@ -2225,11 +2222,11 @@ describe('Ember Adapter Layer', () => { expect(spyResolveEvent).toHaveBeenCalledWith(OneWaitressEvents.STACK_STATUS_CHANNEL_CHANGED); }); - it('Adapter impl: throws when changeChannel fails', async () => { + it('Adapter impl: throws when changeChannel request fails', async () => { mockEzspSendBroadcast.mockResolvedValueOnce([SLStatus.FAIL, 0]); await expect(adapter.changeChannel(25)).rejects.toThrow( - `[ZDO] Failed broadcast channel change to '25' with status=${SLStatus[SLStatus.FAIL]}.`, + `~x~> [ZDO NWK_UPDATE_REQUEST BROADCAST to=65535 messageTag=1] Failed to send request with status=FAIL.`, ); expect(mockEzspSendBroadcast).toHaveBeenCalledTimes(1); }); @@ -2242,7 +2239,7 @@ describe('Ember Adapter Layer', () => { it('Adapter impl: throws when setTransmitPower fails', async () => { mockEzspSetRadioPower.mockResolvedValueOnce(SLStatus.FAIL); - await expect(adapter.setTransmitPower(10)).rejects.toThrow(`Failed to set transmit power to 10 status=${SLStatus[SLStatus.FAIL]}.`); + await expect(adapter.setTransmitPower(10)).rejects.toThrow(`Failed to set transmit power to 10 status=FAIL.`); expect(mockEzspSetRadioPower).toHaveBeenCalledTimes(1); }); @@ -2267,7 +2264,7 @@ describe('Ember Adapter Layer', () => { mockEzspAesMmoHash.mockResolvedValueOnce([SLStatus.FAIL, Buffer.alloc(16)]); await expect(adapter.addInstallCode('0x1122334455667788', Buffer.alloc(16))).rejects.toThrow( - `[ADD INSTALL CODE] Failed AES hash for '0x1122334455667788' with status=${SLStatus[SLStatus.FAIL]}.`, + `[ADD INSTALL CODE] Failed AES hash for '0x1122334455667788' with status=FAIL.`, ); expect(mockEzspAesMmoHash).toHaveBeenCalledTimes(1); expect(mockEzspImportTransientKey).toHaveBeenCalledTimes(0); @@ -2277,7 +2274,7 @@ describe('Ember Adapter Layer', () => { mockEzspImportTransientKey.mockResolvedValueOnce(SLStatus.FAIL); await expect(adapter.addInstallCode('0x1122334455667788', Buffer.alloc(16))).rejects.toThrow( - `[ADD INSTALL CODE] Failed for '0x1122334455667788' with status=${SLStatus[SLStatus.FAIL]}.`, + `[ADD INSTALL CODE] Failed for '0x1122334455667788' with status=FAIL.`, ); expect(mockEzspAesMmoHash).toHaveBeenCalledTimes(1); expect(mockEzspImportTransientKey).toHaveBeenCalledTimes(1); @@ -2384,19 +2381,20 @@ describe('Ember Adapter Layer', () => { mockEzspSendUnicast.mockImplementationOnce(emitResponse).mockImplementationOnce(emitResponse); + let zdoResponse = [Zdo.Status.SUCCESS, undefined]; let p = adapter.permitJoin(250, sender); await jest.advanceTimersByTimeAsync(1000); await p; expect(mockEzspSendUnicast).toHaveBeenCalledTimes(1); expect(spyResolveZDO).toHaveBeenCalledTimes(1); - expect(spyResolveZDO).toHaveBeenCalledWith(sender, apsFrame, undefined); + expect(spyResolveZDO).toHaveBeenCalledWith(sender, apsFrame, zdoResponse); p = adapter.permitJoin(0, sender); await jest.advanceTimersByTimeAsync(1000); await p; expect(mockEzspSendUnicast).toHaveBeenCalledTimes(2); expect(spyResolveZDO).toHaveBeenCalledTimes(2); - expect(spyResolveZDO).toHaveBeenCalledWith(sender, apsFrame, undefined); + expect(spyResolveZDO).toHaveBeenCalledWith(sender, apsFrame, zdoResponse); expect(mockEzspSetPolicy).toHaveBeenNthCalledWith( 1, @@ -2434,26 +2432,21 @@ describe('Ember Adapter Layer', () => { expect(mockManufCode).toStrictEqual(Zcl.ManufacturerCode.SILICON_LABORATORIES); }); - it('Adapter impl: throws when permitJoin on coordinator fails due to failed request', async () => { + it('Adapter impl: throws when permitJoin request on coordinator fails', async () => { mockEzspPermitJoining.mockResolvedValueOnce(SLStatus.FAIL); - await expect(adapter.permitJoin(250, 0)).rejects.toThrow( - `[ZDO] Failed coordinator permit joining request with status=${SLStatus[SLStatus.FAIL]}.`, - ); + await expect(adapter.permitJoin(250, 0)).rejects.toThrow(`[ZDO] Failed coordinator permit joining request with status=FAIL.`); }); - it('Adapter impl: log error when permitJoin broadcast fails due to failed request', async () => { + it('Adapter impl: throws when permitJoin broadcast request fails', async () => { mockEzspSendBroadcast.mockResolvedValueOnce([SLStatus.FAIL, 0]); - await adapter.permitJoin(250, undefined); - - expect(loggerSpies.error).toHaveBeenCalledWith( - `[ZDO] Failed broadcast permit joining request with status=${SLStatus[SLStatus.FAIL]}.`, - 'zh:ember', + await expect(defuseRejection(adapter.permitJoin(250, undefined))).rejects.toThrow( + `~x~> [ZDO PERMIT_JOINING_REQUEST BROADCAST to=65532 messageTag=1] Failed to send request with status=FAIL.`, ); }); - it('Adapter impl: throws when permitJoin on router fails due to failed ZDO status', async () => { + it('Adapter impl: resolves undefined when permitJoin on router fails due to failed ZDO status', async () => { const spyResolveZDO = jest.spyOn( // @ts-expect-error private adapter.oneWaitress, @@ -2473,34 +2466,35 @@ describe('Ember Adapter Layer', () => { mockEzspEmitter.emit('zdoResponse', apsFrame, sender, Buffer.from([1, Zdo.Status.NOT_AUTHORIZED])); await flushPromises(); + const zdoResponse = [Zdo.Status.NOT_AUTHORIZED, undefined]; expect(spyResolveZDO).toHaveBeenCalledTimes(1); - expect(spyResolveZDO).toHaveBeenCalledWith(sender, apsFrame, new Zdo.StatusError(Zdo.Status.NOT_AUTHORIZED)); + expect(spyResolveZDO).toHaveBeenCalledWith(sender, apsFrame, zdoResponse); }); - it('Adapter impl: throws when permitJoin on router fails due to failed request', async () => { + it('Adapter impl: throws when permitJoin request on router fails', async () => { mockEzspSendUnicast.mockResolvedValueOnce([SLStatus.FAIL, 0]); await expect(adapter.permitJoin(250, 1234)).rejects.toThrow( - `[ZDO] Failed permit joining request for '1234' with status=${SLStatus[SLStatus.FAIL]}.`, + `~x~> [ZDO PERMIT_JOINING_REQUEST UNICAST to=0xFFFFFFFFFFFFFFFF:1234 messageTag=1] Failed to send request with status=FAIL.`, ); }); it('Adapter impl: throws when permitJoin fails to import ZIGBEE_PROFILE_INTEROPERABILITY_LINK_KEY', async () => { mockEzspImportTransientKey.mockResolvedValueOnce(SLStatus.FAIL); - await expect(adapter.permitJoin(250)).rejects.toThrow(`[ZDO] Failed import transient key with status=${SLStatus[SLStatus.FAIL]}.`); + await expect(adapter.permitJoin(250)).rejects.toThrow(`[ZDO] Failed import transient key with status=FAIL.`); }); it('Adapter impl: throws when permitJoin fails to set TC policy', async () => { mockEzspSetPolicy.mockResolvedValueOnce(SLStatus.FAIL); - await expect(adapter.permitJoin(250)).rejects.toThrow(`[ZDO] Failed set join policy with status=${SLStatus[SLStatus.FAIL]}.`); + await expect(adapter.permitJoin(250)).rejects.toThrow(`[ZDO] Failed set join policy with status=FAIL.`); }); it('Adapter impl: throws when stop permitJoin fails to restore TC policy', async () => { mockEzspSetPolicy.mockResolvedValueOnce(SLStatus.FAIL); - await expect(adapter.permitJoin(0)).rejects.toThrow(`[ZDO] Failed set join policy with status=${SLStatus[SLStatus.FAIL]}.`); + await expect(adapter.permitJoin(0)).rejects.toThrow(`[ZDO] Failed set join policy with status=FAIL.`); }); it('Adapter impl: lqi', async () => { @@ -2609,14 +2603,14 @@ describe('Ember Adapter Layer', () => { }); it('Adapter impl: throws when lqi fails request', async () => { - const sender: NodeId = 1234; - mockEzspSendUnicast.mockResolvedValueOnce([SLStatus.FAIL, 0]); - const p = defuseRejection(adapter.lqi(sender)); + const p = defuseRejection(adapter.lqi(1234)); await jest.advanceTimersByTimeAsync(5000); - await expect(p).rejects.toThrow(`[ZDO] Failed LQI request for '${sender}' (index '0') with status=${SLStatus[SLStatus.FAIL]}.`); + await expect(p).rejects.toThrow( + `~x~> [ZDO LQI_TABLE_REQUEST UNICAST to=0xFFFFFFFFFFFFFFFF:1234 messageTag=1] Failed to send request with status=FAIL.`, + ); }); it('Adapter impl: routingTable', async () => { @@ -2701,14 +2695,14 @@ describe('Ember Adapter Layer', () => { }); it('Adapter impl: throws when routingTable fails request', async () => { - const sender: NodeId = 1234; - mockEzspSendUnicast.mockResolvedValueOnce([SLStatus.FAIL, 0]); - const p = defuseRejection(adapter.routingTable(sender)); + const p = defuseRejection(adapter.routingTable(1234)); await jest.advanceTimersByTimeAsync(5000); - await expect(p).rejects.toThrow(`[ZDO] Failed routing table request for '${sender}' (index '0') with status=${SLStatus[SLStatus.FAIL]}.`); + await expect(p).rejects.toThrow( + `~x~> [ZDO ROUTING_TABLE_REQUEST UNICAST to=0xFFFFFFFFFFFFFFFF:1234 messageTag=1] Failed to send request with status=FAIL.`, + ); }); it('Adapter impl: nodeDescriptor for coordinator', async () => { @@ -2905,14 +2899,14 @@ describe('Ember Adapter Layer', () => { }); it('Adapter impl: throws when nodeDescriptor fails request', async () => { - const sender: NodeId = 1234; - mockEzspSendUnicast.mockResolvedValueOnce([SLStatus.FAIL, 0]); - const p = defuseRejection(adapter.nodeDescriptor(sender)); + const p = defuseRejection(adapter.nodeDescriptor(1234)); await jest.advanceTimersByTimeAsync(5000); - await expect(p).rejects.toThrow(`[ZDO] Failed node descriptor request for '${sender}' with status=${SLStatus[SLStatus.FAIL]}.`); + await expect(p).rejects.toThrow( + `~x~> [ZDO NODE_DESCRIPTOR_REQUEST UNICAST to=0xFFFFFFFFFFFFFFFF:1234 messageTag=1] Failed to send request with status=FAIL.`, + ); }); it('Adapter impl: activeEndpoints', async () => { @@ -2958,14 +2952,14 @@ describe('Ember Adapter Layer', () => { }); it('Adapter impl: throws when activeEndpoints fails request', async () => { - const sender: NodeId = 1234; - mockEzspSendUnicast.mockResolvedValueOnce([SLStatus.FAIL, 0]); - const p = defuseRejection(adapter.activeEndpoints(sender)); + const p = defuseRejection(adapter.activeEndpoints(1234)); await jest.advanceTimersByTimeAsync(5000); - await expect(p).rejects.toThrow(`[ZDO] Failed active endpoints request for '${sender}' with status=${SLStatus[SLStatus.FAIL]}.`); + await expect(p).rejects.toThrow( + `~x~> [ZDO ACTIVE_ENDPOINTS_REQUEST UNICAST to=0xFFFFFFFFFFFFFFFF:1234 messageTag=1] Failed to send request with status=FAIL.`, + ); }); it('Adapter impl: simpleDescriptor', async () => { @@ -3032,15 +3026,13 @@ describe('Ember Adapter Layer', () => { }); it('Adapter impl: throws when simpleDescriptor fails request', async () => { - const sender: NodeId = 1234; - mockEzspSendUnicast.mockResolvedValueOnce([SLStatus.FAIL, 0]); - const p = defuseRejection(adapter.simpleDescriptor(sender, 1)); + const p = defuseRejection(adapter.simpleDescriptor(1234, 1)); await jest.advanceTimersByTimeAsync(5000); await expect(p).rejects.toThrow( - `[ZDO] Failed simple descriptor request for '${sender}' endpoint '1' with status=${SLStatus[SLStatus.FAIL]}.`, + `~x~> [ZDO SIMPLE_DESCRIPTOR_REQUEST UNICAST to=0xFFFFFFFFFFFFFFFF:1234 messageTag=1] Failed to send request with status=FAIL.`, ); }); @@ -3100,17 +3092,13 @@ describe('Ember Adapter Layer', () => { }); it('Adapter impl: throws when bind endpoint fails request', async () => { - const sender: NodeId = 1234; - mockEzspSendUnicast.mockResolvedValueOnce([SLStatus.FAIL, 0]); - const p = defuseRejection( - adapter.bind(sender, '0x1122334455667788', 1, Zcl.Clusters.genBasic.ID, DEFAULT_COORDINATOR_IEEE, 'endpoint', 1), - ); + const p = defuseRejection(adapter.bind(1234, '0x1122334455667788', 1, Zcl.Clusters.genBasic.ID, DEFAULT_COORDINATOR_IEEE, 'endpoint', 1)); await jest.advanceTimersByTimeAsync(5000); await expect(p).rejects.toThrow( - `[ZDO] Failed bind request for '${sender}' destination '${DEFAULT_COORDINATOR_IEEE}' endpoint '1' with status=${SLStatus[SLStatus.FAIL]}.`, + `~x~> [ZDO BIND_REQUEST UNICAST to=0xFFFFFFFFFFFFFFFF:1234 messageTag=1] Failed to send request with status=FAIL.`, ); }); @@ -3163,16 +3151,13 @@ describe('Ember Adapter Layer', () => { }); it('Adapter impl: throws when bind group fails request', async () => { - const sender: NodeId = 1234; - const groupId: number = 987; - mockEzspSendUnicast.mockResolvedValueOnce([SLStatus.FAIL, 0]); - const p = defuseRejection(adapter.bind(sender, '0x1122334455667788', 1, Zcl.Clusters.genBasic.ID, groupId, 'group')); + const p = defuseRejection(adapter.bind(1234, '0x1122334455667788', 1, Zcl.Clusters.genBasic.ID, 987, 'group')); await jest.advanceTimersByTimeAsync(5000); await expect(p).rejects.toThrow( - `[ZDO] Failed bind request for '${sender}' destination '${groupId}' endpoint 'undefined' with status=${SLStatus[SLStatus.FAIL]}.`, + `~x~> [ZDO BIND_REQUEST UNICAST to=0xFFFFFFFFFFFFFFFF:1234 messageTag=1] Failed to send request with status=FAIL.`, ); }); @@ -3232,17 +3217,15 @@ describe('Ember Adapter Layer', () => { }); it('Adapter impl: throws when unbind endpoint fails request', async () => { - const sender: NodeId = 1234; - mockEzspSendUnicast.mockResolvedValueOnce([SLStatus.FAIL, 0]); const p = defuseRejection( - adapter.unbind(sender, '0x1122334455667788', 1, Zcl.Clusters.genBasic.ID, DEFAULT_COORDINATOR_IEEE, 'endpoint', 1), + adapter.unbind(1234, '0x1122334455667788', 1, Zcl.Clusters.genBasic.ID, DEFAULT_COORDINATOR_IEEE, 'endpoint', 1), ); await jest.advanceTimersByTimeAsync(5000); await expect(p).rejects.toThrow( - `[ZDO] Failed unbind request for '${sender}' destination '${DEFAULT_COORDINATOR_IEEE}' endpoint '1' with status=${SLStatus[SLStatus.FAIL]}.`, + `~x~> [ZDO UNBIND_REQUEST UNICAST to=0xFFFFFFFFFFFFFFFF:1234 messageTag=1] Failed to send request with status=FAIL.`, ); }); @@ -3295,16 +3278,13 @@ describe('Ember Adapter Layer', () => { }); it('Adapter impl: throws when unbind group fails request', async () => { - const sender: NodeId = 1234; - const groupId: number = 987; - mockEzspSendUnicast.mockResolvedValueOnce([SLStatus.FAIL, 0]); - const p = defuseRejection(adapter.unbind(sender, '0x1122334455667788', 1, Zcl.Clusters.genBasic.ID, groupId, 'group')); + const p = defuseRejection(adapter.unbind(1234, '0x1122334455667788', 1, Zcl.Clusters.genBasic.ID, 987, 'group')); await jest.advanceTimersByTimeAsync(5000); await expect(p).rejects.toThrow( - `[ZDO] Failed unbind request for '${sender}' destination '${groupId}' endpoint 'undefined' with status=${SLStatus[SLStatus.FAIL]}.`, + `~x~> [ZDO UNBIND_REQUEST UNICAST to=0xFFFFFFFFFFFFFFFF:1234 messageTag=1] Failed to send request with status=FAIL.`, ); }); @@ -3345,7 +3325,7 @@ describe('Ember Adapter Layer', () => { await jest.advanceTimersByTimeAsync(5000); await expect(p).rejects.toThrow( - `[ZDO] Failed remove device request for '${sender}' target '${ieee}' with status=${SLStatus[SLStatus.FAIL]}.`, + `~x~> [ZDO LEAVE_REQUEST UNICAST to=0xFFFFFFFFFFFFFFFF:1234 messageTag=1] Failed to send request with status=FAIL.`, ); }); @@ -3704,7 +3684,9 @@ describe('Ember Adapter Layer', () => { ); await jest.advanceTimersByTimeAsync(10000); - await expect(p).rejects.toThrow(`~x~> [ZCL to=${networkAddress}] Failed to send request with status=${SLStatus[SLStatus.BUSY]}.`); + await expect(p).rejects.toThrow( + `~x~> [ZCL to=0x1122334455667788:1234 apsFrame={"profileId":260,"clusterId":0,"sourceEndpoint":1,"destinationEndpoint":1,"options":4416,"groupId":0,"sequence":0}] Failed to send request with status=${SLStatus[SLStatus.BUSY]}.`, + ); expect(mockEzspSend).toHaveBeenCalledTimes(1); expect(mockEzspSend).toHaveBeenCalledWith(EmberOutgoingMessageType.DIRECT, networkAddress, apsFrame, zclFrame.toBuffer(), 0, 0); }); @@ -3750,7 +3732,9 @@ describe('Ember Adapter Layer', () => { ); await jest.advanceTimersByTimeAsync(10000); - await expect(p).rejects.toThrow(`~x~> [ZCL to=${networkAddress}] Failed to send request with status=${SLStatus[SLStatus.BUSY]}.`); + await expect(p).rejects.toThrow( + `~x~> [ZCL to=0x1122334455667788:1234 apsFrame={"profileId":260,"clusterId":0,"sourceEndpoint":1,"destinationEndpoint":1,"options":4416,"groupId":0,"sequence":0}] Failed to send request with status=${SLStatus[SLStatus.BUSY]}.`, + ); expect(mockEzspSend).toHaveBeenCalledTimes(1); expect(mockEzspSend).toHaveBeenCalledWith(EmberOutgoingMessageType.DIRECT, networkAddress, apsFrame, zclFrame.toBuffer(), 0, 0); }); @@ -3799,7 +3783,9 @@ describe('Ember Adapter Layer', () => { ); await jest.advanceTimersByTimeAsync(10000); - await expect(p).rejects.toThrow(`~x~> [ZCL to=${networkAddress}] Failed to send request with status=${SLStatus[SLStatus.BUSY]}.`); + await expect(p).rejects.toThrow( + `~x~> [ZCL to=0x1122334455667788:1234 apsFrame={"profileId":260,"clusterId":0,"sourceEndpoint":1,"destinationEndpoint":1,"options":4416,"groupId":0,"sequence":0}] Failed to send request with status=${SLStatus[SLStatus.BUSY]}.`, + ); expect(mockEzspSend).toHaveBeenCalledTimes(3); expect(mockEzspSend).toHaveBeenCalledWith(EmberOutgoingMessageType.DIRECT, networkAddress, apsFrame, zclFrame.toBuffer(), 0, 0); }); @@ -3845,11 +3831,74 @@ describe('Ember Adapter Layer', () => { ); await jest.advanceTimersByTimeAsync(10000); - await expect(p).rejects.toThrow(`~x~> [ZCL to=${networkAddress}] Failed to send request with status=${SLStatus[SLStatus.FAIL]}.`); + await expect(p).rejects.toThrow( + `~x~> [ZCL to=0x1122334455667788:1234 apsFrame={"profileId":260,"clusterId":0,"sourceEndpoint":1,"destinationEndpoint":1,"options":4416,"groupId":0,"sequence":0}] Failed to send request with status=FAIL.`, + ); expect(mockEzspSend).toHaveBeenCalledTimes(1); expect(mockEzspSend).toHaveBeenCalledWith(EmberOutgoingMessageType.DIRECT, networkAddress, apsFrame, zclFrame.toBuffer(), 0, 0); }); + it('Adapter impl: sendZdo with EUI64', async () => { + const sender: NodeId = 0x6789; + const senderEUI64: EUI64 = '0x1122334455667788'; + const apsFrame: EmberApsFrame = { + profileId: Zdo.ZDO_PROFILE_ID, + clusterId: Zdo.ClusterId.NETWORK_ADDRESS_RESPONSE, + sourceEndpoint: Zdo.ZDO_ENDPOINT, + destinationEndpoint: Zdo.ZDO_ENDPOINT, + options: 0, + groupId: 0, + sequence: 0, + }; + + mockEzspSendBroadcast.mockImplementationOnce(() => { + setTimeout(async () => { + mockEzspEmitter.emit( + 'zdoResponse', + apsFrame, + sender, + Buffer.from([ + 1, + Zdo.Status.SUCCESS, + 0x88, + 0x77, + 0x66, + 0x55, + 0x44, + 0x33, + 0x22, + 0x11, + 0x89, // nwkAddress + 0x67, // nwkAddress + ]), + ); + await flushPromises(); + }, 300); + + return [SLStatus.OK, ++mockAPSSequence]; + }); + + const zdoPayload = Zdo.Buffalo.buildRequest(false, Zdo.ClusterId.NETWORK_ADDRESS_REQUEST, senderEUI64, false, 0); + const p = adapter.sendZdo( + senderEUI64, + ZSpec.NULL_NODE_ID /* same as broadcast SLEEPY */, + Zdo.ClusterId.NETWORK_ADDRESS_REQUEST, + zdoPayload, + false, + ); + + await jest.advanceTimersByTimeAsync(1000); + await expect(p).resolves.toStrictEqual([ + Zdo.Status.SUCCESS, + { + eui64: senderEUI64, + nwkAddress: sender, + startIndex: 0, + assocDevList: [], + } as ZdoTypes.NetworkAddressResponse, + ]); + }); + it('Adapter impl: sendZclFrameToEndpoint with default response', async () => { const networkAddress: NodeId = 1234; const endpoint: number = 3; @@ -3995,7 +4044,7 @@ describe('Ember Adapter Layer', () => { const p = defuseRejection(adapter.sendZclFrameToGroup(groupId, zclFrame, 1)); await jest.advanceTimersByTimeAsync(5000); - await expect(p).rejects.toThrow(`~x~> [ZCL GROUP] Failed to send with status=${SLStatus[SLStatus.FAIL]}.`); + await expect(p).rejects.toThrow(`~x~> [ZCL GROUP groupId=32] Failed to send with status=FAIL.`); expect(mockEzspSend).toHaveBeenCalledTimes(1); }); @@ -4065,7 +4114,7 @@ describe('Ember Adapter Layer', () => { const p = defuseRejection(adapter.sendZclFrameToAll(endpoint, zclFrame, 1, ZSpec.BroadcastAddress.DEFAULT)); await jest.advanceTimersByTimeAsync(5000); - await expect(p).rejects.toThrow(`~x~> [ZCL BROADCAST] Failed to send with status=${SLStatus[SLStatus.FAIL]}.`); + await expect(p).rejects.toThrow(`~x~> [ZCL BROADCAST destination=65532] Failed to send with status=FAIL.`); expect(mockEzspSend).toHaveBeenCalledTimes(1); }); @@ -4077,9 +4126,7 @@ describe('Ember Adapter Layer', () => { it('Adapter impl: throws when setChannelInterPAN fails request', async () => { mockEzspSetLogicalAndRadioChannel.mockResolvedValueOnce(SLStatus.FAIL); - await expect(adapter.setChannelInterPAN(15)).rejects.toThrow( - `Failed to set InterPAN channel to '15' with status=${SLStatus[SLStatus.FAIL]}.`, - ); + await expect(adapter.setChannelInterPAN(15)).rejects.toThrow(`Failed to set InterPAN channel to '15' with status=FAIL.`); expect(mockEzspSetLogicalAndRadioChannel).toHaveBeenCalledWith(15); }); @@ -4230,9 +4277,7 @@ describe('Ember Adapter Layer', () => { const p = defuseRejection(adapter.restoreChannelInterPAN()); await jest.advanceTimersByTimeAsync(10000); - await expect(p).rejects.toThrow( - `Failed to restore InterPAN channel to '${DEFAULT_NETWORK_OPTIONS.channelList[0]}' with status=${SLStatus[SLStatus.FAIL]}.`, - ); + await expect(p).rejects.toThrow(`Failed to restore InterPAN channel to '${DEFAULT_NETWORK_OPTIONS.channelList[0]}' with status=FAIL.`); expect(mockEzspSetLogicalAndRadioChannel).toHaveBeenCalledWith(DEFAULT_NETWORK_OPTIONS.channelList[0]); }); }); diff --git a/test/adapter/z-stack/adapter.test.ts b/test/adapter/z-stack/adapter.test.ts index 87f3e0e035..84f833b78f 100644 --- a/test/adapter/z-stack/adapter.test.ts +++ b/test/adapter/z-stack/adapter.test.ts @@ -10,7 +10,7 @@ import {ZclPayload} from '../../../src/adapter/events'; import {ZStackAdapter} from '../../../src/adapter/z-stack/adapter'; import {ZnpVersion} from '../../../src/adapter/z-stack/adapter/tstype'; import * as Constants from '../../../src/adapter/z-stack/constants'; -import {DevStates, NvItemsIds, NvSystemIds, ZnpCommandStatus} from '../../../src/adapter/z-stack/constants/common'; +import {AddressMode, DevStates, NvItemsIds, NvSystemIds, ZnpCommandStatus} from '../../../src/adapter/z-stack/constants/common'; import * as Structs from '../../../src/adapter/z-stack/structs'; import {Subsystem, Type} from '../../../src/adapter/z-stack/unpi/constants'; import {Znp, ZpiObject} from '../../../src/adapter/z-stack/znp'; @@ -18,6 +18,7 @@ import Definition from '../../../src/adapter/z-stack/znp/definition'; import {ZpiObjectPayload} from '../../../src/adapter/z-stack/znp/tstype'; import {UnifiedBackupStorage} from '../../../src/models'; import {setLogger} from '../../../src/utils/logger'; +import * as ZSpec from '../../../src/zspec'; import {BroadcastAddress} from '../../../src/zspec/enums'; import * as Zcl from '../../../src/zspec/zcl'; import * as Zdo from '../../../src/zspec/zdo'; @@ -56,7 +57,7 @@ jest.mock('../../../src/utils/wait', () => { return jest.fn(); }); -const waitForResult = (payloadOrPromise: Promise | ZpiObjectPayload, ID = null) => { +const waitForResult = (payloadOrPromise: Promise | ZpiObjectPayload, ID?: number) => { ID = ID || 1; if (payloadOrPromise instanceof Promise) { return { @@ -629,14 +630,8 @@ const baseZnpRequestMock = new ZnpRequestMockBuilder() .handle(Subsystem.ZDO, 'bindReq', () => ({})) .handle(Subsystem.ZDO, 'unbindReq', () => ({})) .handle(Subsystem.ZDO, 'mgmtLeaveReq', () => ({})) - .handle(Subsystem.ZDO, 'mgmtLqiReq', (payload) => { - lastStartIndex = payload.startindex; - return {}; - }) - .handle(Subsystem.ZDO, 'mgmtRtgReq', (payload) => { - lastStartIndex = payload.startindex; - return {}; - }) + .handle(Subsystem.ZDO, 'mgmtLqiReq', () => ({})) + .handle(Subsystem.ZDO, 'mgmtRtgReq', () => ({})) .handle(Subsystem.ZDO, 'mgmtNwkUpdateReq', () => ({})) .handle(Subsystem.AF, 'interPanCtl', () => ({})) .handle(Subsystem.ZDO, 'extRouteDisc', () => ({})) @@ -680,51 +675,6 @@ const baseZnpRequestMock = new ZnpRequestMockBuilder() return {payload: {panid: nib.nwkPanId, extendedpanid: `0x${nib.extendedPANID.toString('hex')}`, channel: nib.nwkLogicalChannel}}; }) .handle(Subsystem.ZDO, 'startupFromApp', () => ({})) - .handle(Subsystem.ZDO, 'simpleDescReq', (payload: ZpiObjectPayload) => { - let responsePayload: SimpleDescriptorResponse; - if (payload.endpoint === 1) { - responsePayload = { - length: 1, // bogus - endpoint: 1, - profileId: 123, - deviceId: 5, - inClusterList: [1], - outClusterList: [2], - nwkAddress: 0, - deviceVersion: 0, - }; - } else if (payload.endpoint === 99) { - responsePayload = { - length: 1, // bogus - endpoint: 99, - profileId: 123, - deviceId: 5, - inClusterList: [1], - outClusterList: [2], - nwkAddress: 0, - deviceVersion: 0, - }; - } else { - responsePayload = { - length: 1, // bogus - endpoint: payload.endpoint, - profileId: 124, - deviceId: 7, - inClusterList: [8], - outClusterList: [9], - nwkAddress: 0, - deviceVersion: 0, - }; - } - - resolveZnpWaitFors( - Type.AREQ, - Subsystem.ZDO, - 'simpleDescRsp', - {srcaddr: payload.dstaddr}, - mockZdoZpiObject('simpleDescRsp', Zdo.Status.SUCCESS, responsePayload), - ); - }) .nv(NvItemsIds.CHANLIST, Buffer.from([0, 8, 0, 0])) .nv(NvItemsIds.PRECFGKEY, Buffer.alloc(16, 0)) .nv(NvItemsIds.PRECFGKEYS_ENABLE, Buffer.from([0])) @@ -974,6 +924,7 @@ const mockZnpRequest = jest (subsystem: Subsystem, command: string, payload: any, expectedStatus: ZnpCommandStatus) => new Promise((resolve) => resolve(baseZnpRequestMock.execute({subsystem, command, payload}))), ); +const mockZnpRequestZdo = jest.fn(); const mockZnpWaitFor = jest.fn(); const mockZnpOpen = jest.fn(); const mockZnpClose = jest.fn(); @@ -989,22 +940,25 @@ const mockZnpRequestWith = (builder: ZnpRequestMockBuilder) => { }; const mockZnpWaitForDefault = () => { - mockZnpWaitFor.mockImplementation((type, subsystem, command, payload) => { + mockZnpWaitFor.mockImplementation((type, subsystem, command, target, transid, state, timeout) => { const missing = () => { - const msg = `Not implemented - ${Type[type]} - ${Subsystem[subsystem]} - ${command} - ${JSON.stringify(payload)}`; + const msg = `Not implemented - ${Type[type]} - ${Subsystem[subsystem]} - ${command} - ${target} - ${transid} - ${state} - ${timeout}`; console.log(msg); throw new Error(msg); }; if (type === Type.AREQ && subsystem === Subsystem.ZDO && command === 'activeEpRsp') { return waitForResult( - mockZdoZpiObject('activeEpRsp', Zdo.Status.SUCCESS, { - nwkAddress: 0, - endpointList: [], - }), + mockZdoZpiObject('activeEpRsp', target, [ + Zdo.Status.SUCCESS, + { + nwkAddress: 0, + endpointList: [], + }, + ]), ); } else if (type === Type.AREQ && subsystem === Subsystem.ZDO && command === 'mgmtPermitJoinRsp') { - return waitForResult(mockZdoZpiObject('mgmtPermitJoinRsp', Zdo.Status.SUCCESS, {})); + return waitForResult(mockZdoZpiObject('mgmtPermitJoinRsp', target, [Zdo.Status.SUCCESS, undefined])); } else if (type === Type.AREQ && subsystem === Subsystem.ZDO && command === 'stateChangeInd') { return waitForResult({payload: {state: 9}}); } else { @@ -1014,19 +968,22 @@ const mockZnpWaitForDefault = () => { }; const mockZnpWaitForStateChangeIndTimeout = () => { - mockZnpWaitFor.mockImplementation((type, subsystem, command, payload) => { + mockZnpWaitFor.mockImplementation((type, subsystem, command, target, transid, state, timeout) => { const missing = () => { - const msg = `Not implemented - ${Type[type]} - ${Subsystem[subsystem]} - ${command} - ${JSON.stringify(payload)}`; + const msg = `Not implemented - ${Type[type]} - ${Subsystem[subsystem]} - ${command} - ${target} - ${transid} - ${state} - ${timeout}`; console.log(msg); throw new Error(msg); }; if (type === Type.AREQ && subsystem === Subsystem.ZDO && command === 'activeEpRsp') { return waitForResult( - mockZdoZpiObject('activeEpRsp', Zdo.Status.SUCCESS, { - nwkAddress: 0, - endpointList: [], - }), + mockZdoZpiObject('activeEpRsp', target, [ + Zdo.Status.SUCCESS, + { + nwkAddress: 0, + endpointList: [], + }, + ]), ); } else if (type === Type.AREQ && subsystem === Subsystem.ZDO && command === 'stateChangeInd') { return; @@ -1038,15 +995,14 @@ const mockZnpWaitForStateChangeIndTimeout = () => { let bindStatusResponse = 0; -const mockZdoZpiObject = (commandName: string, status: Zdo.Status, payload: T | undefined = undefined): ZpiObject => { +const mockZdoZpiObject = (commandName: string, srcaddr: number | undefined, payload: [Zdo.Status, T | undefined]): ZpiObject => { const subsystem = Subsystem.ZDO; - const command = Definition[subsystem].find((c) => c.name === commandName); + const command = Definition[subsystem].find((c) => c.name === commandName)!; return { type: Type.AREQ, subsystem, command, - payload: {}, - parseZdoPayload: () => [status, payload], + payload: {srcaddr, zdo: payload}, }; }; @@ -1059,47 +1015,74 @@ interface ZnpWaitFor { type: Type; subsystem: Subsystem; command: string; - payload: ZpiObjectPayload; + target?: number | string; + transid?: number; + state?: number; resolve: (object: unknown) => unknown; reject: (reason: string) => void; } -const znpWaitFors: ZnpWaitFor[] = []; -function resolveZnpWaitFors(type: Type, subsystem: Subsystem, command: string, payload: ZpiObjectPayload, result: ZpiObject): void { - const matches = znpWaitFors.filter( - (i) => i.type === type && i.subsystem === subsystem && i.command === command && (!i.payload || equals(i.payload, payload)), - ); - if (matches.length > 0) { - matches.forEach((m) => m.resolve(result)); - } else { - throw new Error(`No waiters for ${Type[type]}: ${Subsystem[subsystem]} - ${command} - ${JSON.stringify(payload)}`); - } -} const basicMocks = () => { mockZnpRequestWith(commissioned3x0AlignedRequestMock); - mockZnpWaitFor.mockImplementation((type, subsystem, command, payload) => { + mockZnpWaitFor.mockImplementation((type, subsystem, command, target, transid, state, timeout) => { const missing = () => { - const msg = `Not implemented - ${Type[type]} - ${Subsystem[subsystem]} - ${command} - ${JSON.stringify(payload)}`; + const msg = `Not implemented - ${Type[type]} - ${Subsystem[subsystem]} - ${command} - ${target} - ${transid} - ${state} - ${timeout}`; console.log(msg); throw new Error(msg); }; if (type === Type.AREQ && subsystem === Subsystem.ZDO && command === 'activeEpRsp') { return waitForResult( - mockZdoZpiObject('activeEpRsp', Zdo.Status.SUCCESS, { - nwkAddress: 0, - endpointList: [1, 2, 3, 4, 5, 6, 8, 10, 11, 110, 12, 13, 47, 242], - }), + mockZdoZpiObject('activeEpRsp', target, [ + Zdo.Status.SUCCESS, + { + nwkAddress: 0, + endpointList: [1, 2, 3, 4, 5, 6, 8, 10, 11, 110, 12, 13, 47, 242], + }, + ]), ); } else if (type === Type.AREQ && subsystem === Subsystem.ZDO && command === 'stateChangeInd') { return waitForResult({payload: {}}); } else if (type === Type.AREQ && subsystem === Subsystem.ZDO && command === 'mgmtPermitJoinRsp') { - return waitForResult(mockZdoZpiObject('mgmtPermitJoinRsp', Zdo.Status.SUCCESS, {})); + return waitForResult(mockZdoZpiObject('mgmtPermitJoinRsp', target, [Zdo.Status.SUCCESS, undefined])); } else if (type === Type.AREQ && subsystem === Subsystem.ZDO && command === 'simpleDescRsp') { - const promise = new Promise((resolve, reject) => { - znpWaitFors.push({type, subsystem, command, payload, resolve, reject}); - }); - return waitForResult(promise); + let responsePayload: SimpleDescriptorResponse; + if (simpleDescriptorEndpoint === 1) { + responsePayload = { + length: 1, // bogus + endpoint: 1, + profileId: 123, + deviceId: 5, + inClusterList: [1], + outClusterList: [2], + nwkAddress: target, + deviceVersion: 0, + }; + } else if (simpleDescriptorEndpoint === 99) { + responsePayload = { + length: 1, // bogus + endpoint: 99, + profileId: 123, + deviceId: 5, + inClusterList: [1], + outClusterList: [2], + nwkAddress: target, + deviceVersion: 0, + }; + } else { + responsePayload = { + length: 1, // bogus + endpoint: simpleDescriptorEndpoint, + profileId: 124, + deviceId: 7, + inClusterList: [8], + outClusterList: [9], + nwkAddress: target, + deviceVersion: 0, + }; + } + + return waitForResult(mockZdoZpiObject('simpleDescRsp', target, [Zdo.Status.SUCCESS, responsePayload])); } else if (type === Type.AREQ && subsystem === Subsystem.ZDO && command === 'nodeDescRsp') { if (nodeDescRspErrorOnce) { nodeDescRspErrorOnce = false; @@ -1116,32 +1099,35 @@ const basicMocks = () => { } return waitForResult( - mockZdoZpiObject('nodeDescRsp', Zdo.Status.SUCCESS, { - manufacturerCode: payload.srcaddr * 2, - apsFlags: 0, - capabilities: DUMMY_NODE_DESC_RSP_CAPABILITIES, - deprecated1: 0, - fragmentationSupported: true, - frequencyBand: 0, - logicalType: payload.srcaddr - 1, - maxBufSize: 0, - maxIncTxSize: 0, - maxOutTxSize: 0, - nwkAddress: payload.srcaddr, - serverMask: { - backupTrustCenter: 0, + mockZdoZpiObject('nodeDescRsp', target, [ + Zdo.Status.SUCCESS, + { + manufacturerCode: target * 2, + apsFlags: 0, + capabilities: DUMMY_NODE_DESC_RSP_CAPABILITIES, deprecated1: 0, - deprecated2: 0, - deprecated3: 0, - deprecated4: 0, - networkManager: 0, - primaryTrustCenter: 0, - reserved1: 0, - reserved2: 0, - stackComplianceRevision: 0, + fragmentationSupported: true, + frequencyBand: 0, + logicalType: target - 1, + maxBufSize: 0, + maxIncTxSize: 0, + maxOutTxSize: 0, + nwkAddress: target, + serverMask: { + backupTrustCenter: 0, + deprecated1: 0, + deprecated2: 0, + deprecated3: 0, + deprecated4: 0, + networkManager: 0, + primaryTrustCenter: 0, + reserved1: 0, + reserved2: 0, + stackComplianceRevision: 0, + }, + tlvs: [], }, - tlvs: [], - }), + ]), ); } else if (type === Type.AREQ && subsystem === Subsystem.AF && command === 'dataConfirm') { const status = dataConfirmCode; @@ -1163,43 +1149,54 @@ const basicMocks = () => { } else { return waitForResult({payload: {status}}, 99); } - } else if (type === Type.AREQ && subsystem === Subsystem.ZDO && command === 'mgmtLqiRsp' && equals(payload, {srcaddr: 203})) { + } else if (type === Type.AREQ && subsystem === Subsystem.ZDO && command === 'mgmtLqiRsp' && target === 203) { const defaults = {deviceType: 0, extendedPanId: [0], permitJoining: 0, reserved1: 0, reserved2: 0, rxOnWhenIdle: 0}; if (lastStartIndex === 0) { + lastStartIndex += 2; return waitForResult( - mockZdoZpiObject('mgmtLqiRsp', Zdo.Status.SUCCESS, { - neighborTableEntries: 5, - startIndex: 0, - entryList: [ - {lqi: 10, nwkAddress: 2, eui64: '0x3', relationship: 3, depth: 1, ...defaults}, - {lqi: 15, nwkAddress: 3, eui64: '0x4', relationship: 2, depth: 5, ...defaults}, - ], - }), + mockZdoZpiObject('mgmtLqiRsp', target, [ + Zdo.Status.SUCCESS, + { + neighborTableEntries: 5, + startIndex: 0, + entryList: [ + {lqi: 10, nwkAddress: 2, eui64: '0x3', relationship: 3, depth: 1, ...defaults}, + {lqi: 15, nwkAddress: 3, eui64: '0x4', relationship: 2, depth: 5, ...defaults}, + ], + }, + ]), ); } else if (lastStartIndex === 2) { + lastStartIndex += 2; return waitForResult( - mockZdoZpiObject('mgmtLqiRsp', Zdo.Status.SUCCESS, { - neighborTableEntries: 5, - startIndex: 0, - entryList: [ - {lqi: 10, nwkAddress: 5, eui64: '0x6', relationship: 3, depth: 1, ...defaults}, - {lqi: 15, nwkAddress: 7, eui64: '0x8', relationship: 2, depth: 5, ...defaults}, - ], - }), + mockZdoZpiObject('mgmtLqiRsp', target, [ + Zdo.Status.SUCCESS, + { + neighborTableEntries: 5, + startIndex: 0, + entryList: [ + {lqi: 10, nwkAddress: 5, eui64: '0x6', relationship: 3, depth: 1, ...defaults}, + {lqi: 15, nwkAddress: 7, eui64: '0x8', relationship: 2, depth: 5, ...defaults}, + ], + }, + ]), ); } else if (lastStartIndex === 4) { return waitForResult( - mockZdoZpiObject('mgmtLqiRsp', Zdo.Status.SUCCESS, { - neighborTableEntries: 5, - startIndex: 0, - entryList: [{lqi: 10, nwkAddress: 9, eui64: '0x10', relationship: 3, depth: 1, ...defaults}], - }), + mockZdoZpiObject('mgmtLqiRsp', target, [ + Zdo.Status.SUCCESS, + { + neighborTableEntries: 5, + startIndex: 0, + entryList: [{lqi: 10, nwkAddress: 9, eui64: '0x10', relationship: 3, depth: 1, ...defaults}], + }, + ]), ); } - } else if (type === Type.AREQ && subsystem === Subsystem.ZDO && command === 'mgmtLqiRsp' && equals(payload, {srcaddr: 204})) { - return waitForResult(mockZdoZpiObject('mgmtLqiRsp', Zdo.Status.NOT_AUTHORIZED)); - } else if (type === Type.AREQ && subsystem === Subsystem.ZDO && command === 'mgmtRtgRsp' && equals(payload, {srcaddr: 205})) { + } else if (type === Type.AREQ && subsystem === Subsystem.ZDO && command === 'mgmtLqiRsp' && target === 204) { + return waitForResult(mockZdoZpiObject('mgmtLqiRsp', target, [Zdo.Status.NOT_AUTHORIZED, undefined])); + } else if (type === Type.AREQ && subsystem === Subsystem.ZDO && command === 'mgmtRtgRsp' && target === 205) { const defaultEntryList = { manyToOne: 0, memoryConstrained: 0, @@ -1208,61 +1205,78 @@ const basicMocks = () => { status: Zdo.RoutingTableStatus[0] as keyof typeof Zdo.RoutingTableStatus, }; if (lastStartIndex === 0) { + lastStartIndex += 2; return waitForResult( - mockZdoZpiObject('mgmtRtgRsp', Zdo.Status.SUCCESS, { - startIndex: 0, - routingTableEntries: 5, - entryList: [ - {destinationAddress: 10, nextHopAddress: 3, ...defaultEntryList}, - {destinationAddress: 11, nextHopAddress: 3, ...defaultEntryList}, - ], - }), + mockZdoZpiObject('mgmtRtgRsp', target, [ + Zdo.Status.SUCCESS, + { + startIndex: 0, + routingTableEntries: 5, + entryList: [ + {destinationAddress: 10, nextHopAddress: 3, ...defaultEntryList}, + {destinationAddress: 11, nextHopAddress: 3, ...defaultEntryList}, + ], + }, + ]), ); } else if (lastStartIndex === 2) { + lastStartIndex += 2; return waitForResult( - mockZdoZpiObject('mgmtRtgRsp', Zdo.Status.SUCCESS, { - startIndex: 0, - routingTableEntries: 5, - entryList: [ - {destinationAddress: 12, nextHopAddress: 3, ...defaultEntryList}, - {destinationAddress: 13, nextHopAddress: 3, ...defaultEntryList}, - ], - }), + mockZdoZpiObject('mgmtRtgRsp', target, [ + Zdo.Status.SUCCESS, + { + startIndex: 0, + routingTableEntries: 5, + entryList: [ + {destinationAddress: 12, nextHopAddress: 3, ...defaultEntryList}, + {destinationAddress: 13, nextHopAddress: 3, ...defaultEntryList}, + ], + }, + ]), ); } else if (lastStartIndex === 4) { return waitForResult( - mockZdoZpiObject('mgmtRtgRsp', Zdo.Status.SUCCESS, { - startIndex: 0, - routingTableEntries: 5, - entryList: [{destinationAddress: 14, nextHopAddress: 3, ...defaultEntryList}], - }), + mockZdoZpiObject('mgmtRtgRsp', target, [ + Zdo.Status.SUCCESS, + { + startIndex: 0, + routingTableEntries: 5, + entryList: [{destinationAddress: 14, nextHopAddress: 3, ...defaultEntryList}], + }, + ]), ); } - } else if (type === Type.AREQ && subsystem === Subsystem.ZDO && command === 'mgmtRtgRsp' && equals(payload, {srcaddr: 206})) { - return waitForResult(mockZdoZpiObject('mgmtRtgRsp', Zdo.Status.INSUFFICIENT_SPACE)); - } else if (type === Type.AREQ && subsystem === Subsystem.ZDO && command === 'bindRsp' && equals(payload, {srcaddr: 301})) { - return waitForResult(mockZdoZpiObject('bindRsp', bindStatusResponse, {})); - } else if (type === Type.AREQ && subsystem === Subsystem.ZDO && command === 'unbindRsp' && equals(payload, {srcaddr: 301})) { - return waitForResult(mockZdoZpiObject('unbindRsp', bindStatusResponse, {})); - } else if (type === Type.AREQ && subsystem === Subsystem.ZDO && command === 'mgmtLeaveRsp' && equals(payload, {srcaddr: 401})) { - return waitForResult(mockZdoZpiObject('mgmtLeaveRsp', Zdo.Status.SUCCESS, {})); - } else if (type === Type.AREQ && subsystem === Subsystem.ZDO && command === 'nwkAddrRsp' && payload.ieeeaddr === '0x03') { + } else if (type === Type.AREQ && subsystem === Subsystem.ZDO && command === 'mgmtRtgRsp' && target === 206) { + return waitForResult(mockZdoZpiObject('mgmtRtgRsp', target, [Zdo.Status.INSUFFICIENT_SPACE, undefined])); + } else if (type === Type.AREQ && subsystem === Subsystem.ZDO && command === 'bindRsp' && target === 301) { + return waitForResult(mockZdoZpiObject('bindRsp', target, [bindStatusResponse, undefined])); + } else if (type === Type.AREQ && subsystem === Subsystem.ZDO && command === 'unbindRsp' && target === 301) { + return waitForResult(mockZdoZpiObject('unbindRsp', target, [bindStatusResponse, undefined])); + } else if (type === Type.AREQ && subsystem === Subsystem.ZDO && command === 'mgmtLeaveRsp' && target === 401) { + return waitForResult(mockZdoZpiObject('mgmtLeaveRsp', target, [Zdo.Status.SUCCESS, undefined])); + } else if (type === Type.AREQ && subsystem === Subsystem.ZDO && command === 'nwkAddrRsp' && target === '0x03') { return waitForResult( - mockZdoZpiObject('nwkAddrRsp', Zdo.Status.SUCCESS, { - nwkAddress: 3, - eui64: '0x03', - assocDevList: [], - startIndex: 0, - }), + mockZdoZpiObject('nwkAddrRsp', target, [ + Zdo.Status.SUCCESS, + { + nwkAddress: 3, + eui64: '0x03', + assocDevList: [], + startIndex: 0, + }, + ]), ); - } else if (type === Type.AREQ && subsystem === Subsystem.ZDO && command === 'nwkAddrRsp' && payload.ieeeaddr === '0x02') { + } else if (type === Type.AREQ && subsystem === Subsystem.ZDO && command === 'nwkAddrRsp' && target === '0x02') { return waitForResult( - mockZdoZpiObject('nwkAddrRsp', Zdo.Status.SUCCESS, { - nwkAddress: 2, - eui64: '0x02', - assocDevList: [], - startIndex: 0, - }), + mockZdoZpiObject('nwkAddrRsp', target, [ + Zdo.Status.SUCCESS, + { + nwkAddress: 2, + eui64: '0x02', + assocDevList: [], + startIndex: 0, + }, + ]), ); } else { missing(); @@ -1340,6 +1354,7 @@ let nodeDescRspErrorOnce = false; let dataRequestCode = 0; let dataRequestExtCode = 0; let lastStartIndex = 0; +let simpleDescriptorEndpoint = 0; let assocGetWithAddressNodeRelation; jest.mock('../../../src/adapter/z-stack/znp/znp', () => { @@ -1354,6 +1369,7 @@ jest.mock('../../../src/adapter/z-stack/znp/znp', () => { }, open: mockZnpOpen, request: mockZnpRequest, + requestZdo: mockZnpRequestZdo, requestWithReply: mockZnpRequest, waitFor: mockZnpWaitFor, close: mockZnpClose, @@ -1403,6 +1419,8 @@ describe('zstack-adapter', () => { networkOptions.networkKeyDistribute = false; dataConfirmCodeReset = false; nodeDescRspErrorOnce = false; + lastStartIndex = 0; + simpleDescriptorEndpoint = 0; }); it('should commission network with 3.0.x adapter', async () => { @@ -1900,19 +1918,22 @@ describe('zstack-adapter', () => { mockZnpRequestWith( commissioned3AlignedRequestMock.clone().handle(Subsystem.UTIL, 'getDeviceInfo', () => ({payload: {devicestate: DevStates.ZB_COORD}})), ); - mockZnpWaitFor.mockImplementation((type, subsystem, command, payload) => { + mockZnpWaitFor.mockImplementation((type, subsystem, command, target, transid, state, timeout) => { const missing = () => { - const msg = `Not implemented - ${Type[type]} - ${Subsystem[subsystem]} - ${command} - ${JSON.stringify(payload)}`; + const msg = `Not implemented - ${Type[type]} - ${Subsystem[subsystem]} - ${command} - ${target} - ${transid} - ${state} - ${timeout}`; console.log(msg); throw new Error(msg); }; if (type === Type.AREQ && subsystem === Subsystem.ZDO && command === 'activeEpRsp') { return waitForResult( - mockZdoZpiObject('activeEpRsp', Zdo.Status.SUCCESS, { - nwkAddress: 0, - endpointList: [1, 2, 3], - }), + mockZdoZpiObject('activeEpRsp', target, [ + Zdo.Status.SUCCESS, + { + nwkAddress: 0, + endpointList: [1, 2, 3], + }, + ]), ); } else if (type === Type.AREQ && subsystem === Subsystem.ZDO && command === 'stateChangeInd') { return waitForResult({payload: {state: 9}}); @@ -2085,6 +2106,12 @@ describe('zstack-adapter', () => { it('Get coordinator', async () => { basicMocks(); await adapter.start(); + const simpleDescritorOriginal = adapter.simpleDescriptor; + + const spyDesc = jest.spyOn(adapter, 'simpleDescriptor').mockImplementation(async (networkAddress, endpointID) => { + simpleDescriptorEndpoint = endpointID; + return await simpleDescritorOriginal.bind(adapter)(networkAddress, endpointID); + }); const info = await adapter.getCoordinator(); const expected = { networkAddress: 0, @@ -2192,21 +2219,35 @@ describe('zstack-adapter', () => { ], }; expect(info).toStrictEqual(expected); + spyDesc.mockRestore(); }); it('Permit join all', async () => { basicMocks(); await adapter.start(); mockZnpRequest.mockClear(); + mockZnpRequestZdo.mockClear(); mockQueueExecute.mockClear(); await adapter.permitJoin(100); - expect(mockZnpRequest).toHaveBeenCalledTimes(2); - expect(mockZnpRequest).toHaveBeenCalledWith(Subsystem.ZDO, 'mgmtPermitJoinReq', { - addrmode: 0x0f, - dstaddr: 0xfffc, - duration: 100, - tcsignificance: 0, - }); + expect(mockZnpRequest).toHaveBeenCalledTimes(1); + expect(mockZnpRequestZdo).toHaveBeenCalledTimes(1); + const zdoPayload = Zdo.Buffalo.buildRequest(false, Zdo.ClusterId.PERMIT_JOINING_REQUEST, 100, 1, []); + expect(mockZnpRequestZdo).toHaveBeenCalledWith( + Zdo.ClusterId.PERMIT_JOINING_REQUEST, + Buffer.from([ + AddressMode.ADDR_BROADCAST, + ZSpec.BroadcastAddress.DEFAULT & 0xff, + (ZSpec.BroadcastAddress.DEFAULT >> 8) & 0xff, + ...zdoPayload, + ]), + undefined, + // Subsystem.ZDO, 'mgmtPermitJoinReq', { + // addrmode: 0x0f, + // dstaddr: 0xfffc, + // duration: 100, + // tcsignificance: 0, + // } + ); expect(mockZnpRequest).toHaveBeenCalledWith(Subsystem.UTIL, 'ledControl', {ledid: 3, mode: 1}, undefined, 500); }); @@ -2214,15 +2255,23 @@ describe('zstack-adapter', () => { basicMocks(); await adapter.start(); mockZnpRequest.mockClear(); + mockZnpRequestZdo.mockClear(); mockQueueExecute.mockClear(); await adapter.permitJoin(102, 42102); - expect(mockZnpRequest).toHaveBeenCalledTimes(2); - expect(mockZnpRequest).toHaveBeenCalledWith(Subsystem.ZDO, 'mgmtPermitJoinReq', { - addrmode: 2, - dstaddr: 42102, - duration: 102, - tcsignificance: 0, - }); + expect(mockZnpRequest).toHaveBeenCalledTimes(1); + expect(mockZnpRequestZdo).toHaveBeenCalledTimes(1); + const zdoPayload = Zdo.Buffalo.buildRequest(false, Zdo.ClusterId.PERMIT_JOINING_REQUEST, 102, 1, []); + expect(mockZnpRequestZdo).toHaveBeenCalledWith( + Zdo.ClusterId.PERMIT_JOINING_REQUEST, + Buffer.from([AddressMode.ADDR_16BIT, 42102 & 0xff, (42102 >> 8) & 0xff, ...zdoPayload]), + expect.any(Number), + // Subsystem.ZDO, 'mgmtPermitJoinReq', { + // addrmode: 2, + // dstaddr: 42102, + // duration: 102, + // tcsignificance: 0, + // } + ); expect(mockZnpRequest).toHaveBeenCalledWith(Subsystem.UTIL, 'ledControl', {ledid: 3, mode: 1}, undefined, 500); }); @@ -2257,18 +2306,32 @@ describe('zstack-adapter', () => { it('Change channel', async () => { basicMocks(); await adapter.start(); - mockZnpRequest.mockClear(); + mockZnpRequestZdo.mockClear(); mockQueueExecute.mockClear(); await adapter.changeChannel(25); - expect(mockZnpRequest).toHaveBeenCalledTimes(1); - expect(mockZnpRequest).toHaveBeenCalledWith(Subsystem.ZDO, 'mgmtNwkUpdateReq', { - dstaddr: 0xffff, - dstaddrmode: 15, - channelmask: 0x2000000, - scanduration: 0xfe, - scancount: 0, - nwkmanageraddr: 0, - }); + expect(mockZnpRequestZdo).toHaveBeenCalledTimes(1); + const zdoPayload = Zdo.Buffalo.buildRequest(false, Zdo.ClusterId.NWK_UPDATE_REQUEST, [25], 0xfe, 0, undefined, 0); + expect(mockZnpRequestZdo).toHaveBeenCalledWith( + Zdo.ClusterId.NWK_UPDATE_REQUEST, + Buffer.from([ + ZSpec.BroadcastAddress.SLEEPY & 0xff, + (ZSpec.BroadcastAddress.SLEEPY >> 8) & 0xff, + AddressMode.ADDR_BROADCAST, + ...zdoPayload, + 0, + 0, + 0, + ]), + undefined, + // Subsystem.ZDO, 'mgmtNwkUpdateReq', { + // dstaddr: 0xffff, + // dstaddrmode: 15, + // channelmask: 0x2000000, + // scanduration: 0xfe, + // scancount: 0, + // nwkmanageraddr: 0, + // } + ); }); it('Start with transmit power set', async () => { @@ -2313,46 +2376,36 @@ describe('zstack-adapter', () => { let result; await adapter.start(); - mockZnpRequest.mockClear(); + mockZnpRequestZdo.mockClear(); mockQueueExecute.mockClear(); result = await adapter.nodeDescriptor(2); - expect(mockZnpWaitFor).toHaveBeenCalledWith(Type.AREQ, Subsystem.ZDO, 'nodeDescRsp', {srcaddr: 2}); - expect(mockZnpRequest).toHaveBeenCalledTimes(1); - expect(mockZnpRequest).toHaveBeenCalledWith(Subsystem.ZDO, 'nodeDescReq', {dstaddr: 2, nwkaddrofinterest: 2}, 1); + expect(mockZnpWaitFor).toHaveBeenCalledWith(Type.AREQ, Subsystem.ZDO, 'nodeDescRsp', 2, undefined, undefined); + expect(mockZnpRequestZdo).toHaveBeenCalledTimes(1); + expect(mockZnpRequestZdo).toHaveBeenCalledWith( + Zdo.ClusterId.NODE_DESCRIPTOR_REQUEST, + Buffer.from([2 & 0xff, (2 >> 8) & 0xff, ...Zdo.Buffalo.buildRequest(false, Zdo.ClusterId.NODE_DESCRIPTOR_REQUEST, 2)]), + expect.any(Number), + //Subsystem.ZDO, 'nodeDescReq', {dstaddr: 2, nwkaddrofinterest: 2}, 1 + ); expect(mockQueueExecute.mock.calls[0][1]).toBe(2); expect(result).toStrictEqual({manufacturerCode: 4, type: 'Router'}); - mockZnpRequest.mockClear(); + mockZnpRequestZdo.mockClear(); mockQueueExecute.mockClear(); result = await adapter.nodeDescriptor(1); expect(result).toStrictEqual({manufacturerCode: 2, type: 'Coordinator'}); - mockZnpRequest.mockClear(); + mockZnpRequestZdo.mockClear(); mockQueueExecute.mockClear(); result = await adapter.nodeDescriptor(3); expect(result).toStrictEqual({manufacturerCode: 6, type: 'EndDevice'}); - mockZnpRequest.mockClear(); + mockZnpRequestZdo.mockClear(); mockQueueExecute.mockClear(); result = await adapter.nodeDescriptor(5); expect(result).toStrictEqual({manufacturerCode: 10, type: 'Unknown'}); }); - it('Node descriptor fails, should retry after route discovery', async () => { - basicMocks(); - await adapter.start(); - nodeDescRspErrorOnce = true; - mockZnpRequest.mockClear(); - mockQueueExecute.mockClear(); - - const result = await adapter.nodeDescriptor(1); - - expect(mockZnpRequest).toHaveBeenNthCalledWith(1, 5, 'nodeDescReq', {dstaddr: 1, nwkaddrofinterest: 1}, 89); - expect(mockZnpRequest).toHaveBeenNthCalledWith(2, 5, 'extRouteDisc', {dstAddr: 1, options: 0, radius: 30}); - expect(mockZnpRequest).toHaveBeenNthCalledWith(3, 5, 'nodeDescReq', {dstaddr: 1, nwkaddrofinterest: 1}, 1); - expect(result).toStrictEqual({manufacturerCode: 2, type: 'Coordinator'}); - }); - it('Active endpoints', async () => { basicMocks(); await adapter.start(); @@ -2369,9 +2422,10 @@ describe('zstack-adapter', () => { mockZnpRequest.mockClear(); mockQueueExecute.mockClear(); - const result = await adapter.simpleDescriptor(1, 20); + simpleDescriptorEndpoint = 20; + const result = await adapter.simpleDescriptor(1, simpleDescriptorEndpoint); expect(mockQueueExecute.mock.calls[0][1]).toBe(1); - expect(result).toStrictEqual({deviceID: 7, endpointID: 20, inputClusters: [8], outputClusters: [9], profileID: 124}); + expect(result).toStrictEqual({deviceID: 7, endpointID: simpleDescriptorEndpoint, inputClusters: [8], outputClusters: [9], profileID: 124}); }); it('Send zcl frame network address', async () => { @@ -2510,6 +2564,7 @@ describe('zstack-adapter', () => { await adapter.start(); dataConfirmCode = 240; mockZnpRequest.mockClear(); + mockZnpRequestZdo.mockClear(); mockQueueExecute.mockClear(); const frame = Zcl.Frame.create( Zcl.FrameType.GLOBAL, @@ -2531,7 +2586,8 @@ describe('zstack-adapter', () => { } expect(error.message).toStrictEqual("Data request failed with error: 'MAC transaction expired' (240)"); - expect(mockZnpRequest).toHaveBeenCalledTimes(10); + expect(mockZnpRequest).toHaveBeenCalledTimes(9); + expect(mockZnpRequestZdo).toHaveBeenCalledTimes(1); expect(mockZnpRequest).toHaveBeenNthCalledWith( 1, 4, @@ -2564,9 +2620,14 @@ describe('zstack-adapter', () => { {clusterid: 0, data: frame.toBuffer(), destendpoint: 20, dstaddr: 2, len: 6, options: 0, radius: 30, srcendpoint: 1, transid: 4}, 99, ); - expect(mockZnpRequest).toHaveBeenNthCalledWith(9, 5, 'nwkAddrReq', {ieeeaddr: '0x02', reqtype: 0, startindex: 0}); + expect(mockZnpRequestZdo).toHaveBeenCalledWith( + Zdo.ClusterId.NETWORK_ADDRESS_REQUEST, + Zdo.Buffalo.buildRequest(false, Zdo.ClusterId.NETWORK_ADDRESS_REQUEST, '0x02', false, 0), + expect.any(Number), + //9, 5, 'nwkAddrReq', {ieeeaddr: '0x02', reqtype: 0, startindex: 0} + ); expect(mockZnpRequest).toHaveBeenNthCalledWith( - 10, + 9, 4, 'dataRequest', {clusterid: 0, data: frame.toBuffer(), destendpoint: 20, dstaddr: 2, len: 6, options: 0, radius: 30, srcendpoint: 1, transid: 5}, @@ -2580,6 +2641,7 @@ describe('zstack-adapter', () => { dataConfirmCode = 240; assocGetWithAddressNodeRelation = 255; mockZnpRequest.mockClear(); + mockZnpRequestZdo.mockClear(); mockQueueExecute.mockClear(); const frame = Zcl.Frame.create( Zcl.FrameType.GLOBAL, @@ -2601,7 +2663,8 @@ describe('zstack-adapter', () => { } expect(error.message).toStrictEqual("Data request failed with error: 'MAC transaction expired' (240)"); - expect(mockZnpRequest).toHaveBeenCalledTimes(8); + expect(mockZnpRequest).toHaveBeenCalledTimes(7); + expect(mockZnpRequestZdo).toHaveBeenCalledTimes(1); expect(mockZnpRequest).toHaveBeenNthCalledWith( 1, 4, @@ -2625,16 +2688,21 @@ describe('zstack-adapter', () => { {clusterid: 0, data: frame.toBuffer(), destendpoint: 20, dstaddr: 2, len: 6, options: 0, radius: 30, srcendpoint: 1, transid: 3}, 99, ); - expect(mockZnpRequest).toHaveBeenNthCalledWith(6, 5, 'nwkAddrReq', {ieeeaddr: '0x02', reqtype: 0, startindex: 0}); + expect(mockZnpRequestZdo).toHaveBeenCalledWith( + Zdo.ClusterId.NETWORK_ADDRESS_REQUEST, + Zdo.Buffalo.buildRequest(false, Zdo.ClusterId.NETWORK_ADDRESS_REQUEST, '0x02', false, 0), + expect.any(Number), + //6, 5, 'nwkAddrReq', {ieeeaddr: '0x02', reqtype: 0, startindex: 0} + ); expect(mockZnpRequest).toHaveBeenNthCalledWith( - 7, + 6, 4, 'dataRequest', {clusterid: 0, data: frame.toBuffer(), destendpoint: 20, dstaddr: 2, len: 6, options: 0, radius: 30, srcendpoint: 1, transid: 4}, 99, ); expect(mockZnpRequest).toHaveBeenNthCalledWith( - 8, + 7, 4, 'dataRequest', {clusterid: 0, data: frame.toBuffer(), destendpoint: 20, dstaddr: 2, len: 6, options: 0, radius: 30, srcendpoint: 1, transid: 5}, @@ -2647,6 +2715,7 @@ describe('zstack-adapter', () => { await adapter.start(); dataConfirmCode = 233; mockZnpRequest.mockClear(); + mockZnpRequestZdo.mockClear(); mockQueueExecute.mockClear(); const frame = Zcl.Frame.create( Zcl.FrameType.GLOBAL, @@ -2668,7 +2737,8 @@ describe('zstack-adapter', () => { } expect(error.message).toStrictEqual("Data request failed with error: 'MAC no ack' (233)"); - expect(mockZnpRequest).toHaveBeenCalledTimes(7); + expect(mockZnpRequest).toHaveBeenCalledTimes(6); + expect(mockZnpRequestZdo).toHaveBeenCalledTimes(1); expect(mockZnpRequest).toHaveBeenNthCalledWith( 1, 4, @@ -2691,16 +2761,21 @@ describe('zstack-adapter', () => { {clusterid: 0, data: frame.toBuffer(), destendpoint: 20, dstaddr: 2, len: 6, options: 0, radius: 30, srcendpoint: 1, transid: 3}, 99, ); - expect(mockZnpRequest).toHaveBeenNthCalledWith(5, 5, 'nwkAddrReq', {ieeeaddr: '0x02', reqtype: 0, startindex: 0}); + expect(mockZnpRequestZdo).toHaveBeenCalledWith( + Zdo.ClusterId.NETWORK_ADDRESS_REQUEST, + Zdo.Buffalo.buildRequest(false, Zdo.ClusterId.NETWORK_ADDRESS_REQUEST, '0x02', false, 0), + expect.any(Number), + //5, 5, 'nwkAddrReq', {ieeeaddr: '0x02', reqtype: 0, startindex: 0} + ); expect(mockZnpRequest).toHaveBeenNthCalledWith( - 6, + 5, 4, 'dataRequest', {clusterid: 0, data: frame.toBuffer(), destendpoint: 20, dstaddr: 2, len: 6, options: 0, radius: 30, srcendpoint: 1, transid: 4}, 99, ); expect(mockZnpRequest).toHaveBeenNthCalledWith( - 7, + 6, 4, 'dataRequest', {clusterid: 0, data: frame.toBuffer(), destendpoint: 20, dstaddr: 2, len: 6, options: 0, radius: 30, srcendpoint: 1, transid: 5}, @@ -2713,6 +2788,7 @@ describe('zstack-adapter', () => { await adapter.start(); dataConfirmCode = 233; mockZnpRequest.mockClear(); + mockZnpRequestZdo.mockClear(); mockQueueExecute.mockClear(); const frame = Zcl.Frame.create( Zcl.FrameType.GLOBAL, @@ -2734,7 +2810,8 @@ describe('zstack-adapter', () => { } expect(error.message).toStrictEqual("Data request failed with error: 'MAC no ack' (233)"); - expect(mockZnpRequest).toHaveBeenCalledTimes(8); + // expect(mockZnpRequest).toHaveBeenCalledTimes(7); + // expect(mockZnpRequestZdo).toHaveBeenCalledTimes(1); expect(mockZnpRequest).toHaveBeenNthCalledWith( 1, 4, @@ -2757,17 +2834,22 @@ describe('zstack-adapter', () => { {clusterid: 0, data: frame.toBuffer(), destendpoint: 20, dstaddr: 2, len: 6, options: 0, radius: 30, srcendpoint: 1, transid: 3}, 99, ); - expect(mockZnpRequest).toHaveBeenNthCalledWith(5, 5, 'nwkAddrReq', {ieeeaddr: '0x03', reqtype: 0, startindex: 0}); - expect(mockZnpRequest).toHaveBeenNthCalledWith(6, 5, 'extRouteDisc', {dstAddr: 3, options: 0, radius: 30}); + expect(mockZnpRequestZdo).toHaveBeenCalledWith( + Zdo.ClusterId.NETWORK_ADDRESS_REQUEST, + Zdo.Buffalo.buildRequest(false, Zdo.ClusterId.NETWORK_ADDRESS_REQUEST, '0x03', false, 0), + expect.any(Number), + //5, 5, 'nwkAddrReq', {ieeeaddr: '0x03', reqtype: 0, startindex: 0} + ); + expect(mockZnpRequest).toHaveBeenNthCalledWith(5, 5, 'extRouteDisc', {dstAddr: 3, options: 0, radius: 30}); expect(mockZnpRequest).toHaveBeenNthCalledWith( - 7, + 6, 4, 'dataRequest', {clusterid: 0, data: frame.toBuffer(), destendpoint: 20, dstaddr: 3, len: 6, options: 0, radius: 30, srcendpoint: 1, transid: 4}, 99, ); expect(mockZnpRequest).toHaveBeenNthCalledWith( - 8, + 7, 4, 'dataRequest', {clusterid: 0, data: frame.toBuffer(), destendpoint: 20, dstaddr: 3, len: 6, options: 0, radius: 30, srcendpoint: 1, transid: 5}, @@ -3432,161 +3514,195 @@ describe('zstack-adapter', () => { it('LQI', async () => { basicMocks(); await adapter.start(); - mockZnpRequest.mockClear(); + mockZnpRequestZdo.mockClear(); mockQueueExecute.mockClear(); const result = await adapter.lqi(203); expect(mockQueueExecute.mock.calls[0][1]).toBe(203); - expect(mockZnpRequest).toHaveBeenCalledTimes(3); - expect(mockZnpRequest).toHaveBeenCalledWith(Subsystem.ZDO, 'mgmtLqiReq', {dstaddr: 203, startindex: 0}, 1); - expect(mockZnpRequest).toHaveBeenCalledWith(Subsystem.ZDO, 'mgmtLqiReq', {dstaddr: 203, startindex: 2}, 1); - expect(mockZnpRequest).toHaveBeenCalledWith(Subsystem.ZDO, 'mgmtLqiReq', {dstaddr: 203, startindex: 4}, 1); + expect(mockZnpRequestZdo).toHaveBeenCalledTimes(3); + let zdoPayload = Zdo.Buffalo.buildRequest(false, Zdo.ClusterId.LQI_TABLE_REQUEST, 0); + expect(mockZnpRequestZdo).toHaveBeenCalledWith( + Zdo.ClusterId.LQI_TABLE_REQUEST, + Buffer.from([203 & 0xff, (203 >> 8) & 0xff, ...zdoPayload]), + expect.any(Number), + //Subsystem.ZDO, 'mgmtLqiReq', {dstaddr: 203, startindex: 0}, 1 + ); + zdoPayload = Zdo.Buffalo.buildRequest(false, Zdo.ClusterId.LQI_TABLE_REQUEST, 2); + expect(mockZnpRequestZdo).toHaveBeenCalledWith( + Zdo.ClusterId.LQI_TABLE_REQUEST, + Buffer.from([203 & 0xff, (203 >> 8) & 0xff, ...zdoPayload]), + expect.any(Number), + //Subsystem.ZDO, 'mgmtLqiReq', {dstaddr: 203, startindex: 2}, 1 + ); + zdoPayload = Zdo.Buffalo.buildRequest(false, Zdo.ClusterId.LQI_TABLE_REQUEST, 4); + expect(mockZnpRequestZdo).toHaveBeenCalledWith( + Zdo.ClusterId.LQI_TABLE_REQUEST, + Buffer.from([203 & 0xff, (203 >> 8) & 0xff, ...zdoPayload]), + expect.any(Number), + //Subsystem.ZDO, 'mgmtLqiReq', {dstaddr: 203, startindex: 4}, 1 + ); expect(result).toStrictEqual({ neighbors: [ - {linkquality: 10, networkAddress: 2, ieeeAddr: '0x3', relationship: 3, depth: 1}, - {linkquality: 15, networkAddress: 3, ieeeAddr: '0x4', relationship: 2, depth: 5}, {linkquality: 10, networkAddress: 2, ieeeAddr: '0x3', relationship: 3, depth: 1}, {linkquality: 15, networkAddress: 3, ieeeAddr: '0x4', relationship: 2, depth: 5}, {linkquality: 10, networkAddress: 5, ieeeAddr: '0x6', relationship: 3, depth: 1}, {linkquality: 15, networkAddress: 7, ieeeAddr: '0x8', relationship: 2, depth: 5}, + {linkquality: 10, networkAddress: 9, ieeeAddr: '0x10', relationship: 3, depth: 1}, ], }); }); - it('LQI fails', async () => { - basicMocks(); - await adapter.start(); - mockZnpRequest.mockClear(); - mockQueueExecute.mockClear(); - await expect(adapter.lqi(204)).rejects.toThrow("Status 'NOT_AUTHORIZED'"); - expect(mockQueueExecute.mock.calls[0][1]).toBe(204); - expect(mockZnpRequest).toHaveBeenCalledTimes(1); - expect(mockZnpRequest).toHaveBeenCalledWith(Subsystem.ZDO, 'mgmtLqiReq', {dstaddr: 204, startindex: 0}, 1); - }); - it('Routing table', async () => { basicMocks(); await adapter.start(); - mockZnpRequest.mockClear(); + mockZnpRequestZdo.mockClear(); mockQueueExecute.mockClear(); const result = await adapter.routingTable(205); expect(mockQueueExecute.mock.calls[0][1]).toBe(205); - expect(mockZnpRequest).toHaveBeenCalledTimes(3); - expect(mockZnpRequest).toHaveBeenCalledWith(Subsystem.ZDO, 'mgmtRtgReq', {dstaddr: 205, startindex: 0}, 1); - expect(mockZnpRequest).toHaveBeenCalledWith(Subsystem.ZDO, 'mgmtRtgReq', {dstaddr: 205, startindex: 2}, 1); - expect(mockZnpRequest).toHaveBeenCalledWith(Subsystem.ZDO, 'mgmtRtgReq', {dstaddr: 205, startindex: 4}, 1); + expect(mockZnpRequestZdo).toHaveBeenCalledTimes(3); + let zdoPayload = Zdo.Buffalo.buildRequest(false, Zdo.ClusterId.ROUTING_TABLE_REQUEST, 0); + expect(mockZnpRequestZdo).toHaveBeenCalledWith( + Zdo.ClusterId.ROUTING_TABLE_REQUEST, + Buffer.from([205 & 0xff, (205 >> 8) & 0xff, ...zdoPayload]), + expect.any(Number), + //Subsystem.ZDO, 'mgmtRtgReq', {dstaddr: 205, startindex: 0}, 1 + ); + zdoPayload = Zdo.Buffalo.buildRequest(false, Zdo.ClusterId.ROUTING_TABLE_REQUEST, 2); + expect(mockZnpRequestZdo).toHaveBeenCalledWith( + Zdo.ClusterId.ROUTING_TABLE_REQUEST, + Buffer.from([205 & 0xff, (205 >> 8) & 0xff, ...zdoPayload]), + expect.any(Number), + // Subsystem.ZDO, 'mgmtRtgReq', {dstaddr: 205, startindex: 2}, 1 + ); + zdoPayload = Zdo.Buffalo.buildRequest(false, Zdo.ClusterId.ROUTING_TABLE_REQUEST, 4); + expect(mockZnpRequestZdo).toHaveBeenCalledWith( + Zdo.ClusterId.ROUTING_TABLE_REQUEST, + Buffer.from([205 & 0xff, (205 >> 8) & 0xff, ...zdoPayload]), + expect.any(Number), + // Subsystem.ZDO, 'mgmtRtgReq', {dstaddr: 205, startindex: 4}, 1 + ); expect(result).toStrictEqual({ table: [ - {destinationAddress: 10, status: 'ACTIVE', nextHop: 3}, - {destinationAddress: 11, status: 'ACTIVE', nextHop: 3}, {destinationAddress: 10, status: 'ACTIVE', nextHop: 3}, {destinationAddress: 11, status: 'ACTIVE', nextHop: 3}, {destinationAddress: 12, status: 'ACTIVE', nextHop: 3}, {destinationAddress: 13, status: 'ACTIVE', nextHop: 3}, + {destinationAddress: 14, status: 'ACTIVE', nextHop: 3}, ], }); }); - it('Routing table fails', async () => { - basicMocks(); - await adapter.start(); - mockZnpRequest.mockClear(); - mockQueueExecute.mockClear(); - - await expect(adapter.routingTable(206)).rejects.toThrow("Status 'INSUFFICIENT_SPACE'"); - expect(mockQueueExecute.mock.calls[0][1]).toBe(206); - expect(mockZnpRequest).toHaveBeenCalledTimes(1); - expect(mockZnpRequest).toHaveBeenCalledWith(Subsystem.ZDO, 'mgmtRtgReq', {dstaddr: 206, startindex: 0}, 1); - }); - it('Bind endpoint', async () => { basicMocks(); await adapter.start(); - mockZnpRequest.mockClear(); + mockZnpRequestZdo.mockClear(); mockQueueExecute.mockClear(); + const zdoPayload = Zdo.Buffalo.buildRequest(false, Zdo.ClusterId.BIND_REQUEST, '0x01', 1, 1, Zdo.UNICAST_BINDING, '0x02', 0, 1); const result = await adapter.bind(301, '0x01', 1, 1, '0x02', 'endpoint', 1); expect(mockQueueExecute.mock.calls[0][1]).toBe(301); - expect(mockZnpRequest).toHaveBeenCalledTimes(1); - expect(mockZnpRequest).toHaveBeenCalledWith( - Subsystem.ZDO, - 'bindReq', - {clusterid: 1, dstaddr: 301, dstaddress: '0x02', dstaddrmode: 3, dstendpoint: 1, srcaddr: '0x01', srcendpoint: 1}, - 1, + expect(mockZnpRequestZdo).toHaveBeenCalledTimes(1); + expect(mockZnpRequestZdo).toHaveBeenCalledWith( + Zdo.ClusterId.BIND_REQUEST, + Buffer.from([301 & 0xff, (301 >> 8) & 0xff, ...zdoPayload]), + expect.any(Number), + // Subsystem.ZDO, + // 'bindReq', + // {clusterid: 1, dstaddr: 301, dstaddress: '0x02', dstaddrmode: 3, dstendpoint: 1, srcaddr: '0x01', srcendpoint: 1}, + // 1, ); }); - it('Bind fails', async () => { - basicMocks(); - await adapter.start(); - mockZnpRequest.mockClear(); - mockQueueExecute.mockClear(); - bindStatusResponse = 0x8e; - await expect(adapter.bind(301, '0x129', 1, 1, 4, 'endpoint', 9)).rejects.toThrow(`Status 'DEVICE_BINDING_TABLE_FULL'`); - }); - it('Bind group', async () => { basicMocks(); await adapter.start(); - mockZnpRequest.mockClear(); + mockZnpRequestZdo.mockClear(); mockQueueExecute.mockClear(); + const zdoPayload = Zdo.Buffalo.buildRequest(false, Zdo.ClusterId.BIND_REQUEST, '0x129', 1, 1, Zdo.MULTICAST_BINDING, ZSpec.BLANK_EUI64, 4, 0); const result = await adapter.bind(301, '0x129', 1, 1, 4, 'group', undefined); expect(mockQueueExecute.mock.calls[0][1]).toBe(301); - expect(mockZnpRequest).toHaveBeenCalledTimes(1); - expect(mockZnpRequest).toHaveBeenCalledWith( - Subsystem.ZDO, - 'bindReq', - {clusterid: 1, dstaddr: 301, dstaddress: '0x0000000000000004', dstaddrmode: 1, dstendpoint: 0xff, srcaddr: '0x129', srcendpoint: 1}, - 1, + expect(mockZnpRequestZdo).toHaveBeenCalledTimes(1); + expect(mockZnpRequestZdo).toHaveBeenCalledWith( + Zdo.ClusterId.BIND_REQUEST, + Buffer.from([301 & 0xff, (301 >> 8) & 0xff, ...zdoPayload, 0, 0, 0, 0, 0, 0, 0]), + expect.any(Number), + // Subsystem.ZDO, + // 'bindReq', + // {clusterid: 1, dstaddr: 301, dstaddress: '0x0000000000000004', dstaddrmode: 1, dstendpoint: 0xff, srcaddr: '0x129', srcendpoint: 1}, + // 1, ); }); - it('Unbind', async () => { + it('Unbind endpoint', async () => { basicMocks(); await adapter.start(); - mockZnpRequest.mockClear(); + mockZnpRequestZdo.mockClear(); mockQueueExecute.mockClear(); + const zdoPayload = Zdo.Buffalo.buildRequest(false, Zdo.ClusterId.UNBIND_REQUEST, '0x01', 1, 1, Zdo.UNICAST_BINDING, '0x02', 0, 1); const result = await adapter.unbind(301, '0x01', 1, 1, '0x02', 'endpoint', 1); expect(mockQueueExecute.mock.calls[0][1]).toBe(301); - expect(mockZnpRequest).toHaveBeenCalledTimes(1); - expect(mockZnpRequest).toHaveBeenCalledWith( - Subsystem.ZDO, - 'unbindReq', - {clusterid: 1, dstaddr: 301, dstaddress: '0x02', dstaddrmode: 3, dstendpoint: 1, srcaddr: '0x01', srcendpoint: 1}, - 1, + expect(mockZnpRequestZdo).toHaveBeenCalledTimes(1); + expect(mockZnpRequestZdo).toHaveBeenCalledWith( + Zdo.ClusterId.UNBIND_REQUEST, + Buffer.from([301 & 0xff, (301 >> 8) & 0xff, ...zdoPayload]), + expect.any(Number), + // Subsystem.ZDO, + // 'unbindReq', + // {clusterid: 1, dstaddr: 301, dstaddress: '0x02', dstaddrmode: 3, dstendpoint: 1, srcaddr: '0x01', srcendpoint: 1}, + // 1, ); }); it('Unbind group', async () => { basicMocks(); await adapter.start(); - mockZnpRequest.mockClear(); + mockZnpRequestZdo.mockClear(); mockQueueExecute.mockClear(); - const result = await adapter.unbind(301, '0x129', 1, 1, 4, 'group', null); - expect(mockQueueExecute.mock.calls[0][1]).toBe(301); - expect(mockZnpRequest).toHaveBeenCalledTimes(1); - expect(mockZnpRequest).toHaveBeenCalledWith( - Subsystem.ZDO, - 'unbindReq', - {clusterid: 1, dstaddr: 301, dstaddress: '0x0000000000000004', dstaddrmode: 1, dstendpoint: 0xff, srcaddr: '0x129', srcendpoint: 1}, + const zdoPayload = Zdo.Buffalo.buildRequest( + false, + Zdo.ClusterId.UNBIND_REQUEST, + '0x129', + 1, 1, + Zdo.MULTICAST_BINDING, + ZSpec.BLANK_EUI64, + 4, + 0, + ); + const result = await adapter.unbind(301, '0x129', 1, 1, 4, 'group', undefined); + expect(mockQueueExecute.mock.calls[0][1]).toBe(301); + expect(mockZnpRequestZdo).toHaveBeenCalledTimes(1); + expect(mockZnpRequestZdo).toHaveBeenCalledWith( + Zdo.ClusterId.UNBIND_REQUEST, + Buffer.from([301 & 0xff, (301 >> 8) & 0xff, ...zdoPayload, 0, 0, 0, 0, 0, 0, 0]), + expect.any(Number), + // Subsystem.ZDO, + // 'unbindReq', + // {clusterid: 1, dstaddr: 301, dstaddress: '0x0000000000000004', dstaddrmode: 1, dstendpoint: 0xff, srcaddr: '0x129', srcendpoint: 1}, + // 1, ); }); it('Remove device', async () => { basicMocks(); await adapter.start(); - mockZnpRequest.mockClear(); + mockZnpRequestZdo.mockClear(); mockQueueExecute.mockClear(); - const result = await adapter.removeDevice(401, '0x01'); + const zdoPayload = Zdo.Buffalo.buildRequest(false, Zdo.ClusterId.LEAVE_REQUEST, '0x1122334455667788', Zdo.LeaveRequestFlags.WITHOUT_REJOIN); + const result = await adapter.removeDevice(401, '0x1122334455667788'); expect(mockQueueExecute.mock.calls[0][1]).toBe(401); - expect(mockZnpRequest).toHaveBeenCalledTimes(1); - expect(mockZnpRequest).toHaveBeenCalledWith(Subsystem.ZDO, 'mgmtLeaveReq', {deviceaddress: '0x01', dstaddr: 401, removechildrenRejoin: 0}, 1); + expect(mockZnpRequestZdo).toHaveBeenCalledTimes(1); + expect(mockZnpRequestZdo).toHaveBeenCalledWith( + Zdo.ClusterId.LEAVE_REQUEST, + Buffer.from([401 & 0xff, (401 >> 8) & 0xff, ...zdoPayload]), + expect.any(Number), + //Subsystem.ZDO, 'mgmtLeaveReq', {deviceaddress: '0x01', dstaddr: 401, removechildrenRejoin: 0}, 1 + ); }); it('Incoming message extended', async () => { @@ -3687,44 +3803,49 @@ describe('zstack-adapter', () => { it('Device announce', async () => { basicMocks(); await adapter.start(); - let deviceAnnounce; mockZnpRequest.mockClear(); mockQueueExecute.mockClear(); - const object = mockZdoZpiObject('endDeviceAnnceInd', Zdo.Status.SUCCESS, { - capabilities: DUMMY_NODE_DESC_RSP_CAPABILITIES, - eui64: '0x123', - nwkAddress: 123, - }); - adapter.on('deviceAnnounce', (p) => { - deviceAnnounce = p; + const object = mockZdoZpiObject('endDeviceAnnceInd', 123, [ + Zdo.Status.SUCCESS, + { + capabilities: DUMMY_NODE_DESC_RSP_CAPABILITIES, + eui64: '0x123', + nwkAddress: 123, + }, + ]); + adapter.on('zdoResponse', (clusterId, payload) => { + expect(clusterId).toStrictEqual(Zdo.ClusterId.END_DEVICE_ANNOUNCE); + expect(payload[0]).toStrictEqual(Zdo.Status.SUCCESS); + expect(payload[1]).toStrictEqual({eui64: '0x123', nwkAddress: 123, capabilities: DUMMY_NODE_DESC_RSP_CAPABILITIES}); }); znpReceived(object); - expect(deviceAnnounce).toStrictEqual({ieeeAddr: '0x123', networkAddress: 123}); expect(mockZnpRequest).toHaveBeenCalledTimes(0); }); it('Device announce should discover route to end devices', async () => { basicMocks(); await adapter.start(); - let deviceAnnounce; mockZnpRequest.mockClear(); mockQueueExecute.mockClear(); - const object = mockZdoZpiObject('endDeviceAnnceInd', Zdo.Status.SUCCESS, { - capabilities: {...DUMMY_NODE_DESC_RSP_CAPABILITIES, deviceType: 0}, - eui64: '0x123', - nwkAddress: 123, - }); - adapter.on('deviceAnnounce', (p) => { - deviceAnnounce = p; + const object = mockZdoZpiObject('endDeviceAnnceInd', 123, [ + Zdo.Status.SUCCESS, + { + capabilities: {...DUMMY_NODE_DESC_RSP_CAPABILITIES, deviceType: 0}, + eui64: '0x123', + nwkAddress: 123, + }, + ]); + adapter.on('zdoResponse', (clusterId, payload) => { + expect(clusterId).toStrictEqual(Zdo.ClusterId.END_DEVICE_ANNOUNCE); + expect(payload[0]).toStrictEqual(Zdo.Status.SUCCESS); + expect(payload[1]).toStrictEqual({eui64: '0x123', nwkAddress: 123, capabilities: {...DUMMY_NODE_DESC_RSP_CAPABILITIES, deviceType: 0}}); }); znpReceived(object); - expect(deviceAnnounce).toStrictEqual({ieeeAddr: '0x123', networkAddress: 123}); expect(mockZnpRequest).toHaveBeenCalledTimes(1); expect(mockZnpRequest).toHaveBeenCalledWith(Subsystem.ZDO, 'extRouteDisc', {dstAddr: 123, options: 0, radius: 30}); // Should debounce route discovery. znpReceived(object); - expect(deviceAnnounce).toStrictEqual({ieeeAddr: '0x123', networkAddress: 123}); expect(mockZnpRequest).toHaveBeenCalledTimes(1); expect(mockZnpRequest).toHaveBeenCalledWith(Subsystem.ZDO, 'extRouteDisc', {dstAddr: 123, options: 0, radius: 30}); }); @@ -3732,30 +3853,33 @@ describe('zstack-adapter', () => { it('Network address response', async () => { basicMocks(); await adapter.start(); - let networkAddress; - const object = mockZdoZpiObject('nwkAddrRsp', Zdo.Status.SUCCESS, { - eui64: '0x123', - nwkAddress: 124, - assocDevList: [], - startIndex: 0, - }); - adapter.on('networkAddress', (p) => { - networkAddress = p; + const object = mockZdoZpiObject('nwkAddrRsp', 124, [ + Zdo.Status.SUCCESS, + { + eui64: '0x123', + nwkAddress: 124, + assocDevList: [], + startIndex: 0, + }, + ]); + adapter.on('zdoResponse', (clusterId, payload) => { + expect(clusterId).toStrictEqual(Zdo.ClusterId.NETWORK_ADDRESS_RESPONSE); + expect(payload[0]).toStrictEqual(Zdo.Status.SUCCESS); + expect(payload[1]).toStrictEqual({eui64: '0x123', nwkAddress: 124, assocDevList: [], startIndex: 0}); }); znpReceived(object); - expect(networkAddress).toStrictEqual({ieeeAddr: '0x123', networkAddress: 124}); }); it('Concentrator Callback Indication', async () => { basicMocks(); await adapter.start(); - let networkAddress; const object = mockZpiObject(Type.AREQ, Subsystem.ZDO, 'concentratorIndCb', {srcaddr: 124, extaddr: '0x123'}); - adapter.on('networkAddress', (p) => { - networkAddress = p; + adapter.on('zdoResponse', (clusterId, payload) => { + expect(clusterId).toStrictEqual(Zdo.ClusterId.NETWORK_ADDRESS_RESPONSE); + expect(payload[0]).toStrictEqual(Zdo.Status.SUCCESS); + expect(payload[1]).toStrictEqual({eui64: '0x123', nwkAddress: 124, assocDevList: [], startIndex: 0}); }); znpReceived(object); - expect(networkAddress).toStrictEqual({ieeeAddr: '0x123', networkAddress: 124}); }); it('Device leave', async () => { @@ -4036,4 +4160,211 @@ describe('zstack-adapter', () => { await adapter.sendZclFrameToEndpoint('0x02', 2, 20, frame, 10000, false, false); expect(mockZnpRequest).toHaveBeenCalledTimes(1); }); + + it('Sends proper ZDO request payload for PERMIT_JOINING_REQUEST to target', async () => { + basicMocks(); + await adapter.start(); + mockZnpRequestZdo.mockClear(); + + const clusterId = Zdo.ClusterId.PERMIT_JOINING_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest(false, clusterId, 250, 1, []); + + await adapter.sendZdo('0x1122334455667788', 1234, clusterId, zdoPayload, true); + + expect(mockZnpRequestZdo).toHaveBeenCalledWith( + clusterId, + Buffer.from([AddressMode.ADDR_16BIT, 1234 & 0xff, (1234 >> 8) & 0xff, ...zdoPayload]), + undefined, + ); + }); + + it('Sends proper ZDO request payload for PERMIT_JOINING_REQUEST broadcast', async () => { + basicMocks(); + await adapter.start(); + mockZnpRequestZdo.mockClear(); + + const clusterId = Zdo.ClusterId.PERMIT_JOINING_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest(false, clusterId, 250, 1, []); + + await adapter.sendZdo(ZSpec.BLANK_EUI64, ZSpec.BroadcastAddress.DEFAULT, clusterId, zdoPayload, true); + + expect(mockZnpRequestZdo).toHaveBeenCalledWith( + clusterId, + Buffer.from([ + AddressMode.ADDR_BROADCAST, + ZSpec.BroadcastAddress.DEFAULT & 0xff, + (ZSpec.BroadcastAddress.DEFAULT >> 8) & 0xff, + ...zdoPayload, + ]), + undefined, + ); + }); + + it('Sends proper ZDO request payload for NWK_UPDATE_REQUEST UNICAST', async () => { + basicMocks(); + await adapter.start(); + mockZnpRequestZdo.mockClear(); + + const clusterId = Zdo.ClusterId.NWK_UPDATE_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest(false, clusterId, [15], 0xfe, 0, undefined, 0); + await adapter.sendZdo(ZSpec.BLANK_EUI64, 0x123, clusterId, zdoPayload, true); + + expect(mockZnpRequestZdo).toHaveBeenCalledWith( + clusterId, + Buffer.from([ + 0x123 & 0xff, + (0x123 >> 8) & 0xff, + AddressMode.ADDR_16BIT, + ...zdoPayload, + 0, // scancount + 0, // nwkmanageraddr + 0, // nwkmanageraddr + ]), + undefined, + ); + }); + + it('Sends proper ZDO request payload for NWK_UPDATE_REQUEST BROADCAST', async () => { + basicMocks(); + await adapter.start(); + mockZnpRequestZdo.mockClear(); + + const clusterId = Zdo.ClusterId.NWK_UPDATE_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest(false, clusterId, [15], 0xfe, 0, undefined, 0); + await adapter.sendZdo(ZSpec.BLANK_EUI64, ZSpec.BroadcastAddress.SLEEPY, clusterId, zdoPayload, true); + + expect(mockZnpRequestZdo).toHaveBeenCalledWith( + clusterId, + Buffer.from([ + ZSpec.BroadcastAddress.SLEEPY & 0xff, + (ZSpec.BroadcastAddress.SLEEPY >> 8) & 0xff, + AddressMode.ADDR_BROADCAST, + ...zdoPayload, + 0, // scancount + 0, // nwkmanageraddr + 0, // nwkmanageraddr + ]), + undefined, + ); + }); + + it('Sends proper ZDO request payload for BIND_REQUEST/UNBIND_REQUEST UNICAST', async () => { + basicMocks(); + await adapter.start(); + mockZnpRequestZdo.mockClear(); + + const clusterId = Zdo.ClusterId.BIND_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest( + false, + clusterId, + '0x1122334455667788', + 3, + Zcl.Clusters.barrierControl.ID, + Zdo.UNICAST_BINDING, + '0x5544332211667788', + 0, + 5, + ); + + await adapter.sendZdo(ZSpec.BLANK_EUI64, 1234, clusterId, zdoPayload, true); + + expect(mockZnpRequestZdo).toHaveBeenCalledWith(clusterId, Buffer.from([1234 & 0xff, (1234 >> 8) & 0xff, ...zdoPayload]), undefined); + }); + + it('Sends proper ZDO request payload for BIND_REQUEST/UNBIND_REQUEST MULTICAST', async () => { + basicMocks(); + await adapter.start(); + mockZnpRequestZdo.mockClear(); + + const clusterId = Zdo.ClusterId.BIND_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest( + false, + clusterId, + '0x1122334455667788', + 3, + Zcl.Clusters.barrierControl.ID, + Zdo.MULTICAST_BINDING, + ZSpec.BLANK_EUI64, + 32, + 0, + ); + + await adapter.sendZdo(ZSpec.BLANK_EUI64, 1234, clusterId, zdoPayload, true); + + expect(mockZnpRequestZdo).toHaveBeenCalledWith( + clusterId, + Buffer.from([ + 1234 & 0xff, + (1234 >> 8) & 0xff, + ...zdoPayload, + 0, // match destination EUI64 length + 0, // match destination EUI64 length + 0, // match destination EUI64 length + 0, // match destination EUI64 length + 0, // match destination EUI64 length + 0, // match destination EUI64 length + 0, // endpoint + ]), + undefined, + ); + }); + + it('Sends proper ZDO request payload for NETWORK_ADDRESS_REQUEST', async () => { + basicMocks(); + await adapter.start(); + mockZnpRequestZdo.mockClear(); + + const clusterId = Zdo.ClusterId.NETWORK_ADDRESS_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest(false, clusterId, '0x1122334455667788', false, 0); + + await adapter.sendZdo(ZSpec.BLANK_EUI64, 1234, clusterId, zdoPayload, true); + + expect(mockZnpRequestZdo).toHaveBeenCalledWith(clusterId, zdoPayload, undefined); + }); + + it('Sends proper ZDO request payload for generic logic request', async () => { + basicMocks(); + await adapter.start(); + mockZnpRequestZdo.mockClear(); + + const clusterId = Zdo.ClusterId.NODE_DESCRIPTOR_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest(false, clusterId, 1234); + + await adapter.sendZdo(ZSpec.BLANK_EUI64, 1234, clusterId, zdoPayload, true); + + expect(mockZnpRequestZdo).toHaveBeenCalledWith(clusterId, Buffer.from([1234 & 0xff, (1234 >> 8) & 0xff, ...zdoPayload]), undefined); + }); + + it('Node descriptor request should discover route to fix potential fails', async () => { + // https://github.com/Koenkk/zigbee2mqtt/issues/3276 + basicMocks(); + await adapter.start(); + mockZnpRequest.mockClear(); + mockZnpRequestZdo.mockClear(); + mockZnpRequestZdo.mockRejectedValueOnce('Failed'); + + const clusterId = Zdo.ClusterId.NODE_DESCRIPTOR_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest(false, clusterId, 1234); + + await adapter.sendZdo(ZSpec.BLANK_EUI64, 1234, clusterId, zdoPayload, true); + + expect(mockZnpRequestZdo).toHaveBeenCalledTimes(2); + expect(mockZnpRequestZdo).toHaveBeenNthCalledWith(1, clusterId, Buffer.from([1234 & 0xff, (1234 >> 8) & 0xff, ...zdoPayload]), undefined); + expect(mockZnpRequestZdo).toHaveBeenNthCalledWith(2, clusterId, Buffer.from([1234 & 0xff, (1234 >> 8) & 0xff, ...zdoPayload]), undefined); + expect(mockZnpRequest).toHaveBeenCalledTimes(1); + expect(mockZnpRequest).toHaveBeenCalledWith(Subsystem.ZDO, 'extRouteDisc', {dstAddr: 1234, options: 0, radius: 30}); + }); + + it('Should throw error when ZDO call fails', async () => { + basicMocks(); + await adapter.start(); + mockZnpRequest.mockClear(); + mockZnpRequestZdo.mockClear(); + mockZnpRequestZdo.mockRejectedValueOnce(new Error('Failed')); + + const clusterId = Zdo.ClusterId.SIMPLE_DESCRIPTOR_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest(false, clusterId, 123, 0); + + await expect(adapter.sendZdo(ZSpec.BLANK_EUI64, 1234, clusterId, zdoPayload, true)).rejects.toThrow('Failed'); + }); }); diff --git a/test/adapter/z-stack/znp.test.ts b/test/adapter/z-stack/znp.test.ts index 80f75d3aef..d88804220f 100644 --- a/test/adapter/z-stack/znp.test.ts +++ b/test/adapter/z-stack/znp.test.ts @@ -5,7 +5,7 @@ import {Constants as UnpiConstants, Frame as UnpiFrame} from '../../../src/adapt import {Znp, ZpiObject} from '../../../src/adapter/z-stack/znp'; import BuffaloZnp from '../../../src/adapter/z-stack/znp/buffaloZnp'; import ParameterType from '../../../src/adapter/z-stack/znp/parameterType'; -import {logger, setLogger} from '../../../src/utils/logger'; +import {logger} from '../../../src/utils/logger'; import * as Zdo from '../../../src/zspec/zdo'; import {duplicateArray, ieeeaAddr1, ieeeaAddr2} from '../../testUtils'; @@ -746,7 +746,7 @@ describe('ZNP', () => { expect(error).toStrictEqual(new Error('SRSP - SYS - osalNvRead after 6000ms')); }); - it('znp request, waitfor', async () => { + it('znp request, waitFor', async () => { let parsedCb; mockUnpiParserOn.mockImplementationOnce((event, cb) => { if (event === 'parsed') { @@ -766,7 +766,113 @@ describe('ZNP', () => { expect(object.payload).toStrictEqual({len: 2, status: 0, value: Buffer.from([1, 2])}); }); - it('znp request, waitfor with payload', async () => { + it('znp request ZDO', async () => { + let parsedCb; + + mockUnpiParserOn.mockImplementationOnce((event, cb) => { + if (event === 'parsed') { + parsedCb = cb; + } + }); + mockUnpiWriterWriteFrame.mockImplementationOnce(() => { + parsedCb(new UnpiFrame(UnpiConstants.Type.SRSP, UnpiConstants.Subsystem.ZDO, 2, Buffer.from([0x00]))); + }); + + await znp.open(); + + const zdoPayload = Buffer.from([2 & 0xff, (2 >> 8) & 0xff, ...Zdo.Buffalo.buildRequest(false, Zdo.ClusterId.NODE_DESCRIPTOR_REQUEST, 2)]); + const result = await znp.requestZdo(Zdo.ClusterId.NODE_DESCRIPTOR_REQUEST, zdoPayload, 1); + + const frame = mockUnpiWriterWriteFrame.mock.calls[0][0]; + expect(mockUnpiWriterWriteFrame).toHaveBeenCalledTimes(1); + expect(frame.commandID).toBe(2); + expect(frame.subsystem).toBe(UnpiConstants.Subsystem.ZDO); + expect(frame.type).toBe(UnpiConstants.Type.SREQ); + expect(frame.data).toStrictEqual(zdoPayload); + + expect(result).toBe(undefined); + }); + + it('znp request ZDO SUCCESS', async () => { + let parsedCb; + mockUnpiParserOn.mockImplementationOnce((event, cb) => { + if (event === 'parsed') { + parsedCb = cb; + } + }); + + await znp.open(); + + const waiter = znp.waitFor(UnpiConstants.Type.SRSP, UnpiConstants.Subsystem.ZDO, 'nodeDescReq'); + const zdoPayload = Buffer.from([2 & 0xff, (2 >> 8) & 0xff, ...Zdo.Buffalo.buildRequest(false, Zdo.ClusterId.NODE_DESCRIPTOR_REQUEST, 2)]); + znp.requestZdo(Zdo.ClusterId.NODE_DESCRIPTOR_REQUEST, zdoPayload, 1); + + parsedCb(new UnpiFrame(UnpiConstants.Type.SRSP, UnpiConstants.Subsystem.ZDO, 2, Buffer.from([0x00]))); + + const object = await waiter.start().promise; + expect(object.payload).toStrictEqual({status: 0x00}); + }); + + it('znp request ZDO FAILURE', async () => { + let parsedCb; + mockUnpiParserOn.mockImplementationOnce((event, cb) => { + if (event === 'parsed') { + parsedCb = cb; + } + }); + + mockUnpiWriterWriteFrame.mockImplementationOnce(() => { + parsedCb(new UnpiFrame(UnpiConstants.Type.SRSP, UnpiConstants.Subsystem.ZDO, 2, Buffer.from([0x01]))); + }); + + await znp.open(); + + const zdoPayload = Buffer.from([2 & 0xff, (2 >> 8) & 0xff, ...Zdo.Buffalo.buildRequest(false, Zdo.ClusterId.NODE_DESCRIPTOR_REQUEST, 2)]); + let error; + try { + await znp.requestZdo(Zdo.ClusterId.NODE_DESCRIPTOR_REQUEST, zdoPayload, undefined); + } catch (e) { + error = e; + } + + expect(error).toStrictEqual( + new Error(`--> 'SREQ: ZDO - NODE_DESCRIPTOR_REQUEST - ${zdoPayload.toString('hex')}' failed with status '(0x01: FAILURE)'`), + ); + }); + + it('znp request ZDO failed should cancel waiter when provided', async () => { + let parsedCb; + mockUnpiParserOn.mockImplementationOnce((event, cb) => { + if (event === 'parsed') { + parsedCb = cb; + } + }); + + mockUnpiWriterWriteFrame.mockImplementationOnce(() => { + parsedCb(new UnpiFrame(UnpiConstants.Type.SRSP, UnpiConstants.Subsystem.ZDO, 2, Buffer.from([0x01]))); + }); + + await znp.open(); + + expect(znp.waitress.waiters.size).toBe(0); + const waiter = znp.waitFor(UnpiConstants.Type.AREQ, UnpiConstants.Subsystem.ZDO, 'nodeDescRsp'); + expect(znp.waitress.waiters.size).toBe(1); + + const zdoPayload = Buffer.from([2 & 0xff, (2 >> 8) & 0xff, ...Zdo.Buffalo.buildRequest(false, Zdo.ClusterId.NODE_DESCRIPTOR_REQUEST, 2)]); + let error; + try { + await znp.requestZdo(Zdo.ClusterId.NODE_DESCRIPTOR_REQUEST, zdoPayload, waiter.ID); + } catch (e) { + expect(znp.waitress.waiters.size).toBe(0); + error = e; + } + + expect(error).toStrictEqual( + new Error(`--> 'SREQ: ZDO - NODE_DESCRIPTOR_REQUEST - ${zdoPayload.toString('hex')}' failed with status '(0x01: FAILURE)'`), + ); + }); + + it('znp waitFor with transid', async () => { let parsedCb; mockUnpiParserOn.mockImplementationOnce((event, cb) => { if (event === 'parsed') { @@ -777,16 +883,15 @@ describe('ZNP', () => { await znp.open(); requestSpy.mockRestore(); - const waiter = znp.waitFor(UnpiConstants.Type.SRSP, UnpiConstants.Subsystem.SYS, 'osalNvRead', {status: 0, value: Buffer.from([1, 2])}); - znp.request(UnpiConstants.Subsystem.SYS, 'osalNvRead', {id: 1, offset: 2}); + const waiter = znp.waitFor(UnpiConstants.Type.AREQ, UnpiConstants.Subsystem.AF, 'dataConfirm', undefined, 123); - parsedCb(new UnpiFrame(UnpiConstants.Type.SRSP, UnpiConstants.Subsystem.SYS, 0x08, Buffer.from([0x00, 0x02, 0x01, 0x02]))); + parsedCb(new UnpiFrame(UnpiConstants.Type.AREQ, UnpiConstants.Subsystem.AF, 128, Buffer.from([0, 1, 123]))); const object = await waiter.start().promise; - expect(object.payload).toStrictEqual({len: 2, status: 0, value: Buffer.from([1, 2])}); + expect(object.payload).toStrictEqual({status: 0, endpoint: 1, transid: 123}); }); - it('znp request, waitfor with payload mismatch', (done) => { + it('znp waitFor with target as network address', async () => { let parsedCb; mockUnpiParserOn.mockImplementationOnce((event, cb) => { if (event === 'parsed') { @@ -794,23 +899,115 @@ describe('ZNP', () => { } }); - znp.open().then(() => { - requestSpy.mockRestore(); - const waiter = znp.waitFor(UnpiConstants.Type.SRSP, UnpiConstants.Subsystem.SYS, 'osalNvRead', {status: 3, value: Buffer.from([1, 3])}); - znp.request(UnpiConstants.Subsystem.SYS, 'osalNvRead', {id: 1, offset: 2}); + await znp.open(); + requestSpy.mockRestore(); - parsedCb(new UnpiFrame(UnpiConstants.Type.SRSP, UnpiConstants.Subsystem.SYS, 0x08, Buffer.from([0x00, 0x02, 0x01, 0x02]))); + const waiter = znp.waitFor(UnpiConstants.Type.AREQ, UnpiConstants.Subsystem.ZDO, 'activeEpRsp', 0x1234); + + parsedCb(new UnpiFrame(UnpiConstants.Type.AREQ, UnpiConstants.Subsystem.ZDO, 133, Buffer.from([0x34, 0x12, 0x00, 0x34, 0x12, 0x00]))); + + const object = await waiter.start().promise; + expect(object.payload.zdo).toStrictEqual([ + Zdo.Status.SUCCESS, + { + nwkAddress: 0x1234, + endpointList: [], + }, + ]); + }); + + it('znp waitFor with target as IEEE', async () => { + let parsedCb; + mockUnpiParserOn.mockImplementationOnce((event, cb) => { + if (event === 'parsed') { + parsedCb = cb; + } + }); + + await znp.open(); + requestSpy.mockRestore(); + + const waiter = znp.waitFor(UnpiConstants.Type.AREQ, UnpiConstants.Subsystem.ZDO, 'nwkAddrRsp', '0x0807060504030201'); + + parsedCb( + new UnpiFrame( + UnpiConstants.Type.AREQ, + UnpiConstants.Subsystem.ZDO, + 128, + Buffer.from([0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x01, 0x00, 0x02, 0x10, 0x10, 0x11, 0x11]), + ), + ); + + const object = await waiter.start().promise; + expect(object.payload.zdo).toStrictEqual([ + Zdo.Status.SUCCESS, + { + assocDevList: [4112, 4369], + eui64: '0x0807060504030201', + // numassocdev: 2, + nwkAddress: 257, + startIndex: 0, + }, + ]); + }); + + it('znp waitFor with target as IEEE forced to timeout because invalid ZDO status (no payload to match against)', async () => { + let parsedCb; + mockUnpiParserOn.mockImplementationOnce((event, cb) => { + if (event === 'parsed') { + parsedCb = cb; + } + }); + + await znp.open(); + requestSpy.mockRestore(); - waiter - .start() - .promise.then(() => done("Shouldn't end up here")) - .catch((e) => { - expect(e).toStrictEqual(new Error('SRSP - SYS - osalNvRead after 10000ms')); - done(); - }); + const waiter = znp.waitFor(UnpiConstants.Type.AREQ, UnpiConstants.Subsystem.ZDO, 'nwkAddrRsp', '0x0807060504030201'); - jest.runOnlyPendingTimers(); + parsedCb(new UnpiFrame(UnpiConstants.Type.AREQ, UnpiConstants.Subsystem.ZDO, 128, Buffer.from([Zdo.Status.INVALID_INDEX]))); + + expect(async () => { + await waiter.start().promise; + }).rejects.toThrow('AREQ - ZDO - nwkAddrRsp after 10000ms'); + }); + + it('znp waitFor with state', async () => { + let parsedCb; + mockUnpiParserOn.mockImplementationOnce((event, cb) => { + if (event === 'parsed') { + parsedCb = cb; + } }); + + await znp.open(); + requestSpy.mockRestore(); + + const waiter = znp.waitFor(UnpiConstants.Type.AREQ, UnpiConstants.Subsystem.ZDO, 'stateChangeInd', undefined, undefined, 9); + + parsedCb(new UnpiFrame(UnpiConstants.Type.AREQ, UnpiConstants.Subsystem.ZDO, 192, Buffer.from([9]))); + + const object = await waiter.start().promise; + expect(object.payload).toStrictEqual({state: 9}); + }); + + it('znp waitFor with payload mismatch', async () => { + let parsedCb; + mockUnpiParserOn.mockImplementationOnce((event, cb) => { + if (event === 'parsed') { + parsedCb = cb; + } + }); + + await znp.open(); + requestSpy.mockRestore(); + + const waiter = znp.waitFor(UnpiConstants.Type.SRSP, UnpiConstants.Subsystem.SYS, 'osalNvRead', 'abcd'); + + parsedCb(new UnpiFrame(UnpiConstants.Type.SRSP, UnpiConstants.Subsystem.SYS, 0x08, Buffer.from([0x00, 0x02, 0x01, 0x02]))); + + expect(async () => { + await waiter.start().promise; + }).rejects.toThrow('SRSP - SYS - osalNvRead after 10000ms'); }); it('znp requestWithReply should throw error when request as no reply', async () => { @@ -852,11 +1049,11 @@ describe('ZNP', () => { expect(obj.isResetCommand()).toBeFalsy(); }); - it('ZpiObject parseZdoPayload - endDeviceAnnceInd', async () => { + it('ZpiObject parse payload for endDeviceAnnceInd', async () => { const buffer = Buffer.from([0, 0, 0, 1, 1, 2, 3, 4, 5, 6, 7, 8, 5]); const frame = new UnpiFrame(UnpiConstants.Type.AREQ, UnpiConstants.Subsystem.ZDO, 193, buffer); const obj = ZpiObject.fromUnpiFrame(frame); - expect(obj.parseZdoPayload()).toStrictEqual([ + expect(obj.payload.zdo).toStrictEqual([ Zdo.Status.SUCCESS, { capabilities: { @@ -875,11 +1072,11 @@ describe('ZNP', () => { ]); }); - it('ZpiObject parseZdoPayload - nwkAddrRsp', async () => { + it('ZpiObject parse payload for nwkAddrRsp', async () => { const buffer = Buffer.from([0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x01, 0x00, 0x02, 0x10, 0x10, 0x11, 0x11]); const frame = new UnpiFrame(UnpiConstants.Type.AREQ, UnpiConstants.Subsystem.ZDO, 128, buffer); const obj = ZpiObject.fromUnpiFrame(frame); - expect(obj.parseZdoPayload()).toStrictEqual([ + expect(obj.payload.zdo).toStrictEqual([ Zdo.Status.SUCCESS, { assocDevList: [4112, 4369], diff --git a/test/adapter/zboss/fixZdoResponse.test.ts b/test/adapter/zboss/fixZdoResponse.test.ts new file mode 100644 index 0000000000..0235273d04 --- /dev/null +++ b/test/adapter/zboss/fixZdoResponse.test.ts @@ -0,0 +1,178 @@ +import {CommandId} from '../../../src/adapter/zboss/enums'; +import {FrameType, readZBOSSFrame, ZBOSSFrame} from '../../../src/adapter/zboss/frame'; +import * as Zdo from '../../../src/zspec/zdo'; +import * as ZdoTypes from '../../../src/zspec/zdo/definition/tstypes'; + +describe('ZBOSS fix non-standard ZDO response payloads', () => { + it('No fix needed FrameType.RESPONSE', async () => { + expect(readZBOSSFrame(Buffer.from('0001010211000088776655443322113412', 'hex'))).toStrictEqual({ + version: 0, + type: FrameType.RESPONSE, + commandId: CommandId.ZDO_NWK_ADDR_REQ, + tsn: 17, + payload: { + category: 0, + zdoClusterId: Zdo.ClusterId.NETWORK_ADDRESS_RESPONSE, + zdo: [ + Zdo.Status.SUCCESS, + { + nwkAddress: 0x1234, + eui64: '0x1122334455667788', + startIndex: 0, + assocDevList: [], + } as ZdoTypes.NetworkAddressResponse, + ], + }, + } as ZBOSSFrame); + }); + + it('No fix needed FrameType.INDICATION', async () => { + expect(readZBOSSFrame(Buffer.from('00020c020cda603602602bd5b3708e', 'hex'))).toStrictEqual({ + version: 0, + type: FrameType.INDICATION, + commandId: CommandId.ZDO_DEV_ANNCE_IND, + tsn: 0, + payload: { + category: undefined, + zdoClusterId: Zdo.ClusterId.END_DEVICE_ANNOUNCE, + zdo: [ + Zdo.Status.SUCCESS, + { + nwkAddress: 0xda0c, + eui64: '0x70b3d52b60023660', + capabilities: Zdo.Utils.getMacCapFlags(0x8e), + } as ZdoTypes.EndDeviceAnnounce, + ], + }, + } as ZBOSSFrame); + }); + + it('NODE_DESCRIPTOR_RESPONSE', async () => { + expect(readZBOSSFrame(Buffer.from('000104021100000000000000000000432c0000003412', 'hex'))).toStrictEqual({ + version: 0, + type: FrameType.RESPONSE, + commandId: CommandId.ZDO_NODE_DESC_REQ, + tsn: 17, + payload: { + category: 0, + zdoClusterId: Zdo.ClusterId.NODE_DESCRIPTOR_RESPONSE, + zdo: [ + Zdo.Status.SUCCESS, + { + nwkAddress: 0x1234, + logicalType: 0, + fragmentationSupported: undefined, + apsFlags: 0, + frequencyBand: 0, + capabilities: { + alternatePANCoordinator: 0, + deviceType: 0, + powerSource: 0, + rxOnWhenIdle: 0, + reserved1: 0, + reserved2: 0, + securityCapability: 0, + allocateAddress: 0, + }, + manufacturerCode: 0, + maxBufSize: 0, + maxIncTxSize: 0, + serverMask: Zdo.Utils.getServerMask(0x2c43), + maxOutTxSize: 0, + deprecated1: 0, + tlvs: [], + } as ZdoTypes.NodeDescriptorResponse, + ], + }, + } as ZBOSSFrame); + }); + + it('POWER_DESCRIPTOR_RESPONSE', async () => { + expect(readZBOSSFrame(Buffer.from('0001030211000001023412', 'hex'))).toStrictEqual({ + version: 0, + type: FrameType.RESPONSE, + commandId: CommandId.ZDO_POWER_DESC_REQ, + tsn: 17, + payload: { + category: 0, + zdoClusterId: Zdo.ClusterId.POWER_DESCRIPTOR_RESPONSE, + zdo: [ + Zdo.Status.SUCCESS, + { + nwkAddress: 0x1234, + currentPowerMode: 1, + availPowerSources: 0, + currentPowerSource: 2, + currentPowerSourceLevel: 0, + } as ZdoTypes.PowerDescriptorResponse, + ], + }, + } as ZBOSSFrame); + }); + + it('MATCH_DESCRIPTORS_RESPONSE', async () => { + expect(readZBOSSFrame(Buffer.from('0001070211000002f2013412', 'hex'))).toStrictEqual({ + version: 0, + type: FrameType.RESPONSE, + commandId: CommandId.ZDO_MATCH_DESC_REQ, + tsn: 17, + payload: { + category: 0, + zdoClusterId: Zdo.ClusterId.MATCH_DESCRIPTORS_RESPONSE, + zdo: [ + Zdo.Status.SUCCESS, + { + nwkAddress: 0x1234, + endpointList: [242, 1], + } as ZdoTypes.MatchDescriptorsResponse, + ], + }, + } as ZBOSSFrame); + }); + + it('ACTIVE_ENDPOINTS_RESPONSE', async () => { + expect(readZBOSSFrame(Buffer.from('0001060211000002f2013412', 'hex'))).toStrictEqual({ + version: 0, + type: FrameType.RESPONSE, + commandId: CommandId.ZDO_ACTIVE_EP_REQ, + tsn: 17, + payload: { + category: 0, + zdoClusterId: Zdo.ClusterId.ACTIVE_ENDPOINTS_RESPONSE, + zdo: [ + Zdo.Status.SUCCESS, + { + nwkAddress: 0x1234, + endpointList: [242, 1], + } as ZdoTypes.ActiveEndpointsResponse, + ], + }, + } as ZBOSSFrame); + }); + + it('SIMPLE_DESCRIPTOR_RESPONSE', async () => { + expect(readZBOSSFrame(Buffer.from('0001050211000001040100000301022c2ffefebcbc3412', 'hex'))).toStrictEqual({ + version: 0, + type: FrameType.RESPONSE, + commandId: CommandId.ZDO_SIMPLE_DESC_REQ, + tsn: 17, + payload: { + category: 0, + zdoClusterId: Zdo.ClusterId.SIMPLE_DESCRIPTOR_RESPONSE, + zdo: [ + Zdo.Status.SUCCESS, + { + nwkAddress: 0x1234, + length: 14, + endpoint: 1, + profileId: 0x0104, + deviceId: 0x0000, + deviceVersion: 3, + inClusterList: [0x2f2c], + outClusterList: [0xfefe, 0xbcbc], + } as ZdoTypes.SimpleDescriptorResponse, + ], + }, + } as ZBOSSFrame); + }); +}); diff --git a/test/adapter/zigate/patchZdoBuffaloBE.test.ts b/test/adapter/zigate/patchZdoBuffaloBE.test.ts new file mode 100644 index 0000000000..0c2541ed2a --- /dev/null +++ b/test/adapter/zigate/patchZdoBuffaloBE.test.ts @@ -0,0 +1,86 @@ +import * as Zdo from '../../../src/zspec/zdo'; + +describe('ZiGate Patch BuffaloZdo to use BE variants', () => { + let BuffaloZdo: typeof Zdo.Buffalo; + + beforeAll(async () => { + await jest.isolateModulesAsync(async () => { + const buf = await import('../../../src/zspec/zdo/buffaloZdo'); + BuffaloZdo = buf.BuffaloZdo; + const {ZiGateAdapter} = await import('../../../src/adapter/zigate/adapter'); + // @ts-expect-error bogus, just need to trigger constructor + const adapter = new ZiGateAdapter({}, {}, '', {}); + }); + }); + + it('writeUInt16', async () => { + expect(BuffaloZdo.buildRequest(false, Zdo.ClusterId.IEEE_ADDRESS_REQUEST, 0x1234, false, 0)).toStrictEqual( + Buffer.from([0x12, 0x34, 0x00, 0x00]), + ); + + // ensure regular parsing OK + expect(Zdo.Buffalo.buildRequest(false, Zdo.ClusterId.IEEE_ADDRESS_REQUEST, 0x1234, false, 0)).toStrictEqual( + Buffer.from([0x34, 0x12, 0x00, 0x00]), + ); + }); + + it('writeUInt32', async () => { + expect(BuffaloZdo.buildRequest(false, Zdo.ClusterId.NWK_UPDATE_REQUEST, [15], 0xfe, undefined, undefined, undefined)).toStrictEqual( + Buffer.from([0x00, 0x00, 0x80, 0x00, 0xfe]), + ); + + // ensure regular parsing OK + expect(Zdo.Buffalo.buildRequest(false, Zdo.ClusterId.NWK_UPDATE_REQUEST, [15], 0xfe, undefined, undefined, undefined)).toStrictEqual( + Buffer.from([0x00, 0x80, 0x00, 0x00, 0xfe]), + ); + }); + + it('readUInt16 + readUInt32', async () => { + expect( + BuffaloZdo.readResponse( + true, + Zdo.ClusterId.NWK_UPDATE_RESPONSE, + Buffer.from([0x01, 0x00, 0x00, 0x00, 0x80, 0x00, 0x12, 0x34, 0x00, 0x01, 0x01, 0x12]), + ), + ).toStrictEqual([Zdo.Status.SUCCESS, {scannedChannels: 32768, totalTransmissions: 0x1234, totalFailures: 0x01, entryList: [0x12]}]); + + // ensure regular parsing OK + expect( + Zdo.Buffalo.readResponse( + true, + Zdo.ClusterId.NWK_UPDATE_RESPONSE, + Buffer.from([0x01, 0x00, 0x00, 0x80, 0x00, 0x00, 0x34, 0x12, 0x01, 0x00, 0x01, 0x12]), + ), + ).toStrictEqual([Zdo.Status.SUCCESS, {scannedChannels: 32768, totalTransmissions: 0x1234, totalFailures: 0x01, entryList: [0x12]}]); + }); + + it('writeIeeeAddr', async () => { + expect(BuffaloZdo.buildRequest(false, Zdo.ClusterId.NETWORK_ADDRESS_REQUEST, '0x1122334455667788', false, 0)).toStrictEqual( + Buffer.from([0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x00, 0x00]), + ); + + // ensure regular parsing OK + expect(Zdo.Buffalo.buildRequest(false, Zdo.ClusterId.NETWORK_ADDRESS_REQUEST, '0x1122334455667788', false, 0)).toStrictEqual( + Buffer.from([0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x00, 0x00]), + ); + }); + + it('readIeeeAddr', async () => { + expect( + BuffaloZdo.readResponse( + true, + Zdo.ClusterId.IEEE_ADDRESS_RESPONSE, + Buffer.from([0x01, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x12, 0x34]), + ), + ).toStrictEqual([Zdo.Status.SUCCESS, {eui64: '0x1122334455667788', nwkAddress: 0x1234, startIndex: 0, assocDevList: []}]); + + // ensure regular parsing OK + expect( + Zdo.Buffalo.readResponse( + true, + Zdo.ClusterId.IEEE_ADDRESS_RESPONSE, + Buffer.from([0x01, 0x00, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x34, 0x12]), + ), + ).toStrictEqual([Zdo.Status.SUCCESS, {eui64: '0x1122334455667788', nwkAddress: 0x1234, startIndex: 0, assocDevList: []}]); + }); +}); diff --git a/test/controller.test.ts b/test/controller.test.ts index 0bf0d25638..04d09e9136 100755 --- a/test/controller.test.ts +++ b/test/controller.test.ts @@ -21,6 +21,7 @@ import * as Utils from '../src/utils'; import {setLogger} from '../src/utils/logger'; import {BroadcastAddress} from '../src/zspec/enums'; import * as Zcl from '../src/zspec/zcl'; +import * as Zdo from '../src/zspec/zdo'; const globalSetImmediate = setImmediate; const flushPromises = () => new Promise(globalSetImmediate); @@ -664,7 +665,7 @@ const mocksRestore = [ mockZiGateAdapterAutoDetectPath, ]; -const events = { +const events: Record = { deviceJoined: [], deviceInterview: [], adapterDisconnected: [], @@ -2138,8 +2139,8 @@ describe('Controller', () => { await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); expect(controller.getDeviceByNetworkAddress(129)?.ieeeAddr).toStrictEqual('0x129'); await mockAdapterEvents['networkAddress']({networkAddress: 9999, ieeeAddr: '0x129'}); - expect(controller.getDeviceByIeeeAddr('0x129').networkAddress).toBe(9999); - expect(controller.getDeviceByIeeeAddr('0x129').getEndpoint(1).deviceNetworkAddress).toBe(9999); + expect(controller.getDeviceByIeeeAddr('0x129')?.networkAddress).toBe(9999); + expect(controller.getDeviceByIeeeAddr('0x129')?.getEndpoint(1)?.deviceNetworkAddress).toBe(9999); expect(controller.getDeviceByNetworkAddress(129)).toBeUndefined(); expect(controller.getDeviceByNetworkAddress(9999)?.ieeeAddr).toStrictEqual('0x129'); }); @@ -2148,8 +2149,8 @@ describe('Controller', () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); await mockAdapterEvents['networkAddress']({networkAddress: 129, ieeeAddr: '0x129'}); - expect(controller.getDeviceByIeeeAddr('0x129').networkAddress).toBe(129); - expect(controller.getDeviceByIeeeAddr('0x129').getEndpoint(1).deviceNetworkAddress).toBe(129); + expect(controller.getDeviceByIeeeAddr('0x129')?.networkAddress).toBe(129); + expect(controller.getDeviceByIeeeAddr('0x129')?.getEndpoint(1)?.deviceNetworkAddress).toBe(129); }); it('Network address event from unknown device', async () => { @@ -2161,11 +2162,139 @@ describe('Controller', () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); Date.now = jest.fn(); + // @ts-expect-error mock Date.now.mockReturnValue(200); await mockAdapterEvents['networkAddress']({networkAddress: 129, ieeeAddr: '0x129'}); expect(events.lastSeenChanged[1].device.lastSeen).toBe(200); }); + it('ZDO response for NETWORK_ADDRESS_RESPONSE should update network address when different', async () => { + await controller.start(); + await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); + expect(controller.getDeviceByNetworkAddress(129)?.ieeeAddr).toStrictEqual('0x129'); + await mockAdapterEvents['zdoResponse'](Zdo.ClusterId.NETWORK_ADDRESS_RESPONSE, [ + Zdo.Status.SUCCESS, + {nwkAddress: 9999, eui64: '0x129', assocDevList: [], startIndex: 0}, + ]); + expect(controller.getDeviceByIeeeAddr('0x129')?.networkAddress).toBe(9999); + expect(controller.getDeviceByIeeeAddr('0x129')?.getEndpoint(1)?.deviceNetworkAddress).toBe(9999); + expect(controller.getDeviceByNetworkAddress(129)).toBeUndefined(); + expect(controller.getDeviceByNetworkAddress(9999)?.ieeeAddr).toStrictEqual('0x129'); + }); + + it('ZDO response for NETWORK_ADDRESS_RESPONSE shouldnt update network address when the same', async () => { + await controller.start(); + await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); + await mockAdapterEvents['zdoResponse'](Zdo.ClusterId.NETWORK_ADDRESS_RESPONSE, [ + Zdo.Status.SUCCESS, + {nwkAddress: 129, eui64: '0x129', assocDevList: [], startIndex: 0}, + ]); + expect(controller.getDeviceByIeeeAddr('0x129')?.networkAddress).toBe(129); + expect(controller.getDeviceByIeeeAddr('0x129')?.getEndpoint(1)?.deviceNetworkAddress).toBe(129); + }); + + it('ZDO response for NETWORK_ADDRESS_RESPONSE from unknown device', async () => { + await controller.start(); + await mockAdapterEvents['zdoResponse'](Zdo.ClusterId.NETWORK_ADDRESS_RESPONSE, [ + Zdo.Status.SUCCESS, + {nwkAddress: 19321, eui64: '0x19321', assocDevList: [], startIndex: 0}, + ]); + }); + + it('ZDO response for NETWORK_ADDRESS_RESPONSE should update the last seen value', async () => { + await controller.start(); + await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); + Date.now = jest.fn(); + // @ts-expect-error mock + Date.now.mockReturnValue(200); + await mockAdapterEvents['zdoResponse'](Zdo.ClusterId.NETWORK_ADDRESS_RESPONSE, [ + Zdo.Status.SUCCESS, + {nwkAddress: 129, eui64: '0x129', assocDevList: [], startIndex: 0}, + ]); + expect(events.lastSeenChanged[1].device.lastSeen).toBe(200); + }); + + it('ZDO response for END_DEVICE_ANNOUNCE should bubble up event', async () => { + await controller.start(); + await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); + expect(events.deviceAnnounce.length).toBe(0); + await mockAdapterEvents['zdoResponse'](Zdo.ClusterId.END_DEVICE_ANNOUNCE, [ + Zdo.Status.SUCCESS, + { + nwkAddress: 129, + eui64: '0x129', + capabilities: { + allocateAddress: 0, + alternatePANCoordinator: 0, + deviceType: 2, + powerSource: 0, + reserved1: 0, + reserved2: 0, + rxOnWhenIdle: 0, + securityCapability: 0, + }, + }, + ]); + expect(events.deviceAnnounce.length).toBe(1); + expect(events.deviceAnnounce[0].device).toBeInstanceOf(Device); + expect(events.deviceAnnounce[0].device.ieeeAddr).toBe('0x129'); + expect(events.deviceAnnounce[0].device.modelID).toBe('myModelID'); + }); + + it('ZDO response for END_DEVICE_ANNOUNCE should update network address when different', async () => { + await controller.start(); + await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); + expect(controller.getDeviceByNetworkAddress(129)?.ieeeAddr).toStrictEqual('0x129'); + await mockAdapterEvents['zdoResponse'](Zdo.ClusterId.END_DEVICE_ANNOUNCE, [ + Zdo.Status.SUCCESS, + { + nwkAddress: 9999, + eui64: '0x129', + capabilities: { + allocateAddress: 0, + alternatePANCoordinator: 0, + deviceType: 2, + powerSource: 0, + reserved1: 0, + reserved2: 0, + rxOnWhenIdle: 0, + securityCapability: 0, + }, + }, + ]); + expect(controller.getDeviceByIeeeAddr('0x129')?.networkAddress).toBe(9999); + expect(controller.getDeviceByIeeeAddr('0x129')?.getEndpoint(1)?.deviceNetworkAddress).toBe(9999); + expect(controller.getDeviceByNetworkAddress(129)).toBeUndefined(); + expect(controller.getDeviceByNetworkAddress(9999)?.ieeeAddr).toStrictEqual('0x129'); + }); + + it('ZDO response for END_DEVICE_ANNOUNCE from unknown device', async () => { + await controller.start(); + await mockAdapterEvents['zdoResponse'](Zdo.ClusterId.END_DEVICE_ANNOUNCE, [ + Zdo.Status.SUCCESS, + { + nwkAddress: 12999, + eui64: '0x12999', + capabilities: { + allocateAddress: 0, + alternatePANCoordinator: 0, + deviceType: 2, + powerSource: 0, + reserved1: 0, + reserved2: 0, + rxOnWhenIdle: 0, + securityCapability: 0, + }, + }, + ]); + expect(events.deviceAnnounce.length).toBe(0); + }); + + it('ZDO response for cluster ID with no extra processing', async () => { + await controller.start(); + await mockAdapterEvents['zdoResponse'](Zdo.ClusterId.BIND_RESPONSE, [Zdo.Status.SUCCESS, undefined]); + }); + it('Emit lastSeenChanged event even when no message is emitted from it', async () => { // Default response const buffer = Buffer.from([0x18, 0x04, 0x0b, 0x0c, 0x82]); diff --git a/test/zspec/zdo/utils.test.ts b/test/zspec/zdo/utils.test.ts index f0a84e5d3f..929b88309a 100644 --- a/test/zspec/zdo/utils.test.ts +++ b/test/zspec/zdo/utils.test.ts @@ -15,11 +15,11 @@ describe('ZDO Utils', () => { [Zdo.ClusterId.CHALLENGE_REQUEST, Zdo.ClusterId.CHALLENGE_RESPONSE], [Zdo.ClusterId.NODE_DESCRIPTOR_REQUEST, Zdo.ClusterId.NODE_DESCRIPTOR_RESPONSE], [Zdo.ClusterId.NETWORK_ADDRESS_REQUEST, Zdo.ClusterId.NETWORK_ADDRESS_RESPONSE], - [Zdo.ClusterId.END_DEVICE_ANNOUNCE, null], - [Zdo.ClusterId.NWK_UNSOLICITED_ENHANCED_UPDATE_RESPONSE, null], - [Zdo.ClusterId.ACTIVE_ENDPOINTS_RESPONSE, null], - [Zdo.ClusterId.CHALLENGE_RESPONSE, null], - [0x7999, null], + [Zdo.ClusterId.END_DEVICE_ANNOUNCE, undefined], // not a request + [Zdo.ClusterId.NWK_UNSOLICITED_ENHANCED_UPDATE_RESPONSE, undefined], + [Zdo.ClusterId.ACTIVE_ENDPOINTS_RESPONSE, undefined], + [Zdo.ClusterId.CHALLENGE_RESPONSE, undefined], + [0x7999, undefined], ])('Gets response cluster ID for request %s', (request, response) => { expect(Zdo.Utils.getResponseClusterId(request)).toStrictEqual(response); });