diff --git a/src/adapter/adapter.ts b/src/adapter/adapter.ts index d98797eb0e..2f4b36498c 100644 --- a/src/adapter/adapter.ts +++ b/src/adapter/adapter.ts @@ -18,13 +18,12 @@ interface AdapterEventMap { zclPayload: [payload: AdapterEvents.ZclPayload]; zdoResponse: [clusterId: Zdo.ClusterId, response: ZdoTypes.GenericZdoResponse]; disconnected: []; - deviceAnnounce: [payload: AdapterEvents.DeviceAnnouncePayload]; deviceLeave: [payload: AdapterEvents.DeviceLeavePayload]; - networkAddress: [payload: AdapterEvents.NetworkAddressPayload]; } abstract class Adapter extends events.EventEmitter { public hasZdoMessageOverhead: boolean; + public manufacturerID: Zcl.ManufacturerCode; protected networkOptions: TsType.NetworkOptions; protected adapterOptions: TsType.AdapterOptions; protected serialPortOptions: TsType.SerialPortOptions; @@ -38,6 +37,7 @@ abstract class Adapter extends events.EventEmitter { ) { super(); this.hasZdoMessageOverhead = true; + this.manufacturerID = Zcl.ManufacturerCode.RESERVED_10; this.networkOptions = networkOptions; this.adapterOptions = adapterOptions; this.serialPortOptions = serialPortOptions; @@ -189,7 +189,7 @@ abstract class Adapter extends events.EventEmitter { public abstract stop(): Promise; - public abstract getCoordinator(): Promise; + public abstract getCoordinatorIEEE(): Promise; public abstract getCoordinatorVersion(): Promise; @@ -201,8 +201,6 @@ abstract class Adapter extends events.EventEmitter { public abstract getNetworkParameters(): Promise; - public abstract changeChannel(newChannel: number): Promise; - public abstract setTransmitPower(value: number): Promise; public abstract addInstallCode(ieeeAddress: string, key: Buffer): Promise; @@ -246,38 +244,6 @@ abstract class Adapter extends events.EventEmitter { public abstract permitJoin(seconds: number, networkAddress?: number): Promise; - public abstract lqi(networkAddress: number): Promise; - - public abstract routingTable(networkAddress: number): Promise; - - public abstract nodeDescriptor(networkAddress: number): Promise; - - public abstract activeEndpoints(networkAddress: number): Promise; - - public abstract simpleDescriptor(networkAddress: number, endpointID: number): Promise; - - public abstract bind( - destinationNetworkAddress: number, - sourceIeeeAddress: string, - sourceEndpoint: number, - clusterID: number, - destinationAddressOrGroup: string | number, - type: 'endpoint' | 'group', - destinationEndpoint?: number, - ): Promise; - - public abstract unbind( - destinationNetworkAddress: number, - sourceIeeeAddress: string, - sourceEndpoint: number, - clusterID: number, - destinationAddressOrGroup: string | number, - type: 'endpoint' | 'group', - destinationEndpoint?: number, - ): Promise; - - public abstract removeDevice(networkAddress: number, ieeeAddr: string): Promise; - /** * ZCL */ diff --git a/src/adapter/deconz/adapter/deconzAdapter.ts b/src/adapter/deconz/adapter/deconzAdapter.ts index 3f497d4967..d44f40f2a1 100644 --- a/src/adapter/deconz/adapter/deconzAdapter.ts +++ b/src/adapter/deconz/adapter/deconzAdapter.ts @@ -2,35 +2,18 @@ import assert from 'assert'; -import {ZSpec} from '../../..'; import Device from '../../../controller/model/device'; import * as Models from '../../../models'; import {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 { - ActiveEndpoints, - AdapterOptions, - Coordinator, - CoordinatorVersion, - DeviceType, - LQI, - LQINeighbor, - NetworkOptions, - NetworkParameters, - NodeDescriptor, - RoutingTable, - RoutingTableEntry, - SerialPortOptions, - SimpleDescriptor, - StartResult, -} from '../../tstype'; +import {AdapterOptions, CoordinatorVersion, NetworkOptions, NetworkParameters, SerialPortOptions, StartResult} from '../../tstype'; import PARAM, {ApsDataRequest, gpDataInd, ReceivedDataResponse, WaitForDataRequest} from '../driver/constants'; import Driver from '../driver/driver'; import processFrame, {frameParserEvents} from '../driver/frameParser'; @@ -60,6 +43,7 @@ class DeconzAdapter extends Adapter { public constructor(networkOptions: NetworkOptions, serialPortOptions: SerialPortOptions, backupPath: string, adapterOptions: AdapterOptions) { super(networkOptions, serialPortOptions, backupPath, adapterOptions); this.hasZdoMessageOverhead = true; + this.manufacturerID = Zcl.ManufacturerCode.DRESDEN_ELEKTRONIK_INGENIEURTECHNIK_GMBH; // const concurrent = this.adapterOptions && this.adapterOptions.concurrent ? this.adapterOptions.concurrent : 2; @@ -92,9 +76,6 @@ class DeconzAdapter extends Adapter { setInterval(() => { this.checkReceivedDataPayload(null); }, 1000); - setTimeout(async () => { - await this.checkCoordinatorSimpleDescriptor(false); - }, 3000); } public static async isValidPath(path: string): Promise { @@ -251,6 +232,15 @@ class DeconzAdapter extends Adapter { await Wait(2000); } + // write endpoints + //[ sd1 ep proId devId vers #inCl iCl1 iCl2 iCl3 iCl4 iCl5 #outC oCl1 oCl2 oCl3 oCl4 ] + const sd = [ + 0x00, 0x01, 0x04, 0x01, 0x05, 0x00, 0x01, 0x05, 0x00, 0x00, 0x00, 0x06, 0x0a, 0x00, 0x19, 0x00, 0x01, 0x05, 0x04, 0x01, 0x00, 0x20, 0x00, + 0x00, 0x05, 0x02, 0x05, + ]; + const sd1 = sd.reverse(); + await this.driver.writeParameterRequest(PARAM.PARAM.STK.Endpoint, sd1); + return 'resumed'; } @@ -258,33 +248,8 @@ class DeconzAdapter extends Adapter { await this.driver.close(); } - public async getCoordinator(): Promise { - const ieeeAddr = await this.driver.readParameterRequest(PARAM.PARAM.Network.MAC); - const nwkAddr = await this.driver.readParameterRequest(PARAM.PARAM.Network.NWK_ADDRESS); - - const endpoints = [ - { - ID: 0x01, - profileID: 0x0104, - deviceID: 0x0005, - inputClusters: [0x0000, 0x0006, 0x000a, 0x0019, 0x0501], - outputClusters: [0x0001, 0x0020, 0x0500, 0x0502], - }, - { - ID: 0xf2, - profileID: 0xa1e0, - deviceID: 0x0064, - inputClusters: [], - outputClusters: [0x0021], - }, - ]; - - return { - networkAddress: nwkAddr as number, - manufacturerID: 0x1135, - ieeeAddr: ieeeAddr as string, - endpoints, - }; + public async getCoordinatorIEEE(): Promise { + return (await this.driver.readParameterRequest(PARAM.PARAM.Network.MAC)) as string; } public async permitJoin(seconds: number, networkAddress?: number): Promise { @@ -354,221 +319,6 @@ class DeconzAdapter extends Adapter { return await Promise.reject(new Error('Reset is not supported')); } - public async lqi(networkAddress: number): Promise { - 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); - - /* 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, - }); - } - - 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: 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 { - 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; - } - - 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 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 { - 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: 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]); - } - } - - private async checkCoordinatorSimpleDescriptor(skip: boolean): Promise { - logger.debug('checking coordinator simple descriptor', NS); - let simpleDesc: SimpleDescriptor | undefined; - - if (skip === false) { - try { - simpleDesc = await this.simpleDescriptor(0x0, 1); - } catch { - /* empty */ - } - - if (simpleDesc == undefined) { - await this.checkCoordinatorSimpleDescriptor(false); - return; - } - logger.debug('EP: ' + simpleDesc.endpointID, NS); - logger.debug('profile ID: ' + simpleDesc.profileID, NS); - logger.debug('device ID: ' + simpleDesc.deviceID, NS); - for (let i = 0; i < simpleDesc.inputClusters.length; i++) { - logger.debug('input cluster: 0x' + simpleDesc.inputClusters[i].toString(16), NS); - } - - for (let o = 0; o < simpleDesc.outputClusters.length; o++) { - logger.debug('output cluster: 0x' + simpleDesc.outputClusters[o].toString(16), NS); - } - - let ok = true; - if (simpleDesc.endpointID === 0x1) { - if ( - !simpleDesc.inputClusters.includes(0x0) || - !simpleDesc.inputClusters.includes(0x0a) || - !simpleDesc.inputClusters.includes(0x06) || - !simpleDesc.inputClusters.includes(0x19) || - !simpleDesc.inputClusters.includes(0x0501) || - !simpleDesc.outputClusters.includes(0x01) || - !simpleDesc.outputClusters.includes(0x20) || - !simpleDesc.outputClusters.includes(0x500) || - !simpleDesc.outputClusters.includes(0x502) - ) { - logger.debug('missing cluster', NS); - ok = false; - } - - if (ok === true) { - return; - } - } - } - - logger.debug('setting new simple descriptor', NS); - try { - //[ sd1 ep proId devId vers #inCl iCl1 iCl2 iCl3 iCl4 iCl5 #outC oCl1 oCl2 oCl3 oCl4 ] - const sd = [ - 0x00, 0x01, 0x04, 0x01, 0x05, 0x00, 0x01, 0x05, 0x00, 0x00, 0x00, 0x06, 0x0a, 0x00, 0x19, 0x00, 0x01, 0x05, 0x04, 0x01, 0x00, 0x20, - 0x00, 0x00, 0x05, 0x02, 0x05, - ]; - const sd1 = sd.reverse(); - await this.driver.writeParameterRequest(PARAM.PARAM.STK.Endpoint, sd1); - } catch (error) { - logger.debug(`error setting simple descriptor: ${error} - try again`, NS); - await this.checkCoordinatorSimpleDescriptor(true); - return; - } - logger.debug('success setting simple descriptor', NS); - } - public waitFor( networkAddress: number | undefined, endpoint: number, @@ -785,78 +535,6 @@ class DeconzAdapter extends Adapter { return await (this.driver.enqueueSendDataRequest(request) as Promise); } - public async bind( - destinationNetworkAddress: number, - sourceIeeeAddress: string, - sourceEndpoint: number, - clusterID: number, - destinationAddressOrGroup: string | number, - type: 'endpoint' | 'group', - destinationEndpoint?: number, - ): Promise { - 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( - destinationNetworkAddress: number, - sourceIeeeAddress: string, - sourceEndpoint: number, - clusterID: number, - destinationAddressOrGroup: string | number, - type: 'endpoint' | 'group', - destinationEndpoint?: number, - ): Promise { - 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 supportsBackup(): Promise { return false; } @@ -906,14 +584,6 @@ class DeconzAdapter extends Adapter { throw new Error('not supported'); } - public async changeChannel(newChannel: number): Promise { - 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 public async setTransmitPower(value: number): Promise { throw new Error('not supported'); diff --git a/src/adapter/ember/adapter/emberAdapter.ts b/src/adapter/ember/adapter/emberAdapter.ts index 810a9e144d..12302bcb8e 100644 --- a/src/adapter/ember/adapter/emberAdapter.ts +++ b/src/adapter/ember/adapter/emberAdapter.ts @@ -128,8 +128,6 @@ const autoDetectDefinitions = [ const APPLICATION_ZDO_SEQUENCE_MASK = 0x7f; /* Default radius used for broadcast ZDO requests. uint8_t */ const ZDO_REQUEST_RADIUS = 0xff; -/** Current revision of the spec by zigbee alliance supported by Z2M. */ -const CURRENT_ZIGBEE_SPEC_REVISION = 22; /** Oldest supported EZSP version for backups. Don't take the risk to restore a broken network until older backup versions can be investigated. */ const BACKUP_OLDEST_SUPPORTED_EZSP_VERSION = 12; /** @@ -277,6 +275,8 @@ export class EmberAdapter extends Adapter { adapterOptions: TsType.AdapterOptions, ) { super(networkOptions, serialPortOptions, backupPath, adapterOptions); + this.hasZdoMessageOverhead = true; + this.manufacturerID = Zcl.ManufacturerCode.SILICON_LABORATORIES; this.version = { ezsp: 0, @@ -417,7 +417,6 @@ export class EmberAdapter extends Adapter { break; } case SLStatus.ZIGBEE_CHANNEL_CHANGED: { - this.oneWaitress.resolveEvent(OneWaitressEvents.STACK_STATUS_CHANNEL_CHANGED); // invalidate cache this.networkCache.parameters.radioChannel = INVALID_RADIO_CHANNEL; logger.info(`[STACK STATUS] Channel changed.`, NS); @@ -527,8 +526,6 @@ export class EmberAdapter extends Adapter { private async onZDOResponse(apsFrame: EmberApsFrame, sender: NodeId, messageContents: Buffer): Promise { 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 @@ -1596,28 +1593,12 @@ export class EmberAdapter extends Adapter { logger.info(`======== Ember Adapter Stopped ========`, NS); } - // queued, non-InterPAN - public async getCoordinator(): Promise { - return await this.queue.execute(async () => { + public async getCoordinatorIEEE(): Promise { + return await this.queue.execute(async () => { this.checkInterpanLock(); // in all likelihood this will be retrieved from cache - const ieeeAddr = await this.emberGetEui64(); - - return { - ieeeAddr, - networkAddress: ZSpec.COORDINATOR_ADDRESS, - manufacturerID: DEFAULT_MANUFACTURER_CODE, - endpoints: FIXED_ENDPOINTS.map((ep) => { - return { - profileID: ep.profileId, - ID: ep.endpoint, - deviceID: ep.deviceId, - inputClusters: ep.inClusterList.slice(), // copy - outputClusters: ep.outClusterList.slice(), // copy - }; - }), - }; + return await this.emberGetEui64(); }); } @@ -1739,19 +1720,6 @@ export class EmberAdapter extends Adapter { }); } - // queued - public async changeChannel(newChannel: number): Promise { - 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 this.oneWaitress.startWaitingForEvent( - {eventName: OneWaitressEvents.STACK_STATUS_CHANNEL_CHANGED}, - DEFAULT_NETWORK_REQUEST_TIMEOUT * 2, // observed to ~9sec - '[ZDO] Change Channel', - ); - } - // queued public async setTransmitPower(value: number): Promise { return await this.queue.execute(async () => { @@ -1954,7 +1922,7 @@ export class EmberAdapter extends Adapter { ); } } - }, networkAddress /* TODO: replace with ieeeAddress once zdo moved upstream */); + }, networkAddress); } // queued, non-InterPAN @@ -2034,246 +2002,6 @@ export class EmberAdapter extends Adapter { } } - // queued, non-InterPAN - public async lqi(networkAddress: number): Promise { - 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); - - /* 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, - }); - } - - 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}; - } - - // queued, non-InterPAN - 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, - }); - } - - 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}; - } - - // queued, non-InterPAN - public async nodeDescriptor(networkAddress: number): Promise { - 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; - } - - /* istanbul ignore else */ - if (payload.serverMask.stackComplianceRevision < CURRENT_ZIGBEE_SPEC_REVISION) { - // always 0 before rev. 21 where field was added - 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}).`, - NS, - ); - } - - 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 { - 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]); - } - } - - // queued, non-InterPAN - public async simpleDescriptor(networkAddress: number, endpointID: number): Promise { - 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: 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]); - } - } - - // queued, non-InterPAN - public async bind( - destinationNetworkAddress: number, - sourceIeeeAddress: string, - sourceEndpoint: number, - clusterID: number, - destinationAddressOrGroup: string | number, - type: 'endpoint' | 'group', - destinationEndpoint?: number, - ): Promise { - 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]); - } - } - - // queued, non-InterPAN - public async unbind( - destinationNetworkAddress: number, - sourceIeeeAddress: string, - sourceEndpoint: number, - clusterID: number, - destinationAddressOrGroup: string | number, - type: 'endpoint' | 'group', - destinationEndpoint?: number, - ): Promise { - 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]); - } - } - - // queued, non-InterPAN - 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]); - } - } - //---- ZCL // queued, non-InterPAN diff --git a/src/adapter/ember/adapter/oneWaitress.ts b/src/adapter/ember/adapter/oneWaitress.ts index 5bf2dd1b2c..1e54f5449c 100644 --- a/src/adapter/ember/adapter/oneWaitress.ts +++ b/src/adapter/ember/adapter/oneWaitress.ts @@ -13,7 +13,6 @@ export enum OneWaitressEvents { STACK_STATUS_NETWORK_DOWN = 'STACK_STATUS_NETWORK_DOWN', STACK_STATUS_NETWORK_OPENED = 'STACK_STATUS_NETWORK_OPENED', STACK_STATUS_NETWORK_CLOSED = 'STACK_STATUS_NETWORK_CLOSED', - STACK_STATUS_CHANNEL_CHANGED = 'STACK_STATUS_CHANNEL_CHANGED', } type OneWaitressMatcher = { diff --git a/src/adapter/events.ts b/src/adapter/events.ts index a27d842243..10eb3763d9 100644 --- a/src/adapter/events.ts +++ b/src/adapter/events.ts @@ -5,16 +5,6 @@ type DeviceJoinedPayload = { ieeeAddr: string; }; -type DeviceAnnouncePayload = { - networkAddress: number; - ieeeAddr: string; -}; - -type NetworkAddressPayload = { - networkAddress: number; - ieeeAddr: string; -}; - type DeviceLeavePayload = {networkAddress?: number; ieeeAddr: string} | {networkAddress: number; ieeeAddr?: string}; interface ZclPayload { @@ -30,4 +20,4 @@ interface ZclPayload { destinationEndpoint: number; } -export {DeviceJoinedPayload, ZclPayload, DeviceAnnouncePayload, NetworkAddressPayload, DeviceLeavePayload}; +export {DeviceJoinedPayload, ZclPayload, DeviceLeavePayload}; diff --git a/src/adapter/ezsp/adapter/ezspAdapter.ts b/src/adapter/ezsp/adapter/ezspAdapter.ts index 835c6a87b6..4ec74905b1 100644 --- a/src/adapter/ezsp/adapter/ezspAdapter.ts +++ b/src/adapter/ezsp/adapter/ezspAdapter.ts @@ -6,7 +6,6 @@ import * as Models from '../../../models'; import {Queue, RealpathSync, Wait, Waitress} from '../../../utils'; import {logger} from '../../../utils/logger'; 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'; @@ -14,23 +13,7 @@ import Adapter from '../../adapter'; import {ZclPayload} from '../../events'; import SerialPortUtils from '../../serialPortUtils'; import SocketPortUtils from '../../socketPortUtils'; -import { - ActiveEndpoints, - AdapterOptions, - Coordinator, - CoordinatorVersion, - DeviceType, - LQI, - LQINeighbor, - NetworkOptions, - NetworkParameters, - NodeDescriptor, - RoutingTable, - RoutingTableEntry, - SerialPortOptions, - SimpleDescriptor, - StartResult, -} from '../../tstype'; +import {AdapterOptions, CoordinatorVersion, NetworkOptions, NetworkParameters, SerialPortOptions, StartResult} from '../../tstype'; import {Driver, EmberIncomingMessage} from '../driver'; import {EmberEUI64, EmberStatus} from '../driver/types'; @@ -59,6 +42,7 @@ class EZSPAdapter extends Adapter { public constructor(networkOptions: NetworkOptions, serialPortOptions: SerialPortOptions, backupPath: string, adapterOptions: AdapterOptions) { super(networkOptions, serialPortOptions, backupPath, adapterOptions); this.hasZdoMessageOverhead = true; + this.manufacturerID = Zcl.ManufacturerCode.SILICON_LABORATORIES; this.waitress = new Waitress(this.waitressValidator, this.waitressTimeoutFormatter); this.interpanLock = false; @@ -201,31 +185,8 @@ class EZSPAdapter extends Adapter { return paths.length > 0 ? paths[0] : undefined; } - public async getCoordinator(): Promise { - return await this.queue.execute(async () => { - this.checkInterpanLock(); - const message = await this.activeEndpoints(ZSpec.COORDINATOR_ADDRESS); - const endpoints = []; - - for (const endpoint of message.endpoints) { - const descriptor = await this.simpleDescriptor(ZSpec.COORDINATOR_ADDRESS, endpoint); - - endpoints.push({ - profileID: descriptor.profileID, - ID: descriptor.endpointID, - deviceID: descriptor.deviceID, - inputClusters: descriptor.inputClusters, - outputClusters: descriptor.outputClusters, - }); - } - - return { - networkAddress: ZSpec.COORDINATOR_ADDRESS, - manufacturerID: 0, - ieeeAddr: `0x${this.driver.ieee.toString()}`, - endpoints, - }; - }); + public async getCoordinatorIEEE(): Promise { + return `0x${this.driver.ieee.toString()}`; } public async permitJoin(seconds: number, networkAddress?: number): Promise { @@ -293,155 +254,6 @@ class EZSPAdapter extends Adapter { return await Promise.reject(new Error('Not supported')); } - public async lqi(networkAddress: number): Promise { - 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); - - /* 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, - }); - } - - 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: 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 { - 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; - } - - 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 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 { - 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: 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 sendZdo( ieeeAddress: string, networkAddress: number, @@ -515,7 +327,7 @@ class EZSPAdapter extends Adapter { return response.zdoResponse! as ZdoTypes.RequestToResponseMap[K]; } - }, networkAddress /* TODO: replace with ieeeAddress once zdo moved upstream */); + }, networkAddress); } public async sendZclFrameToEndpoint( @@ -673,78 +485,6 @@ class EZSPAdapter extends Adapter { }); } - public async bind( - destinationNetworkAddress: number, - sourceIeeeAddress: string, - sourceEndpoint: number, - clusterID: number, - destinationAddressOrGroup: string | number, - type: 'endpoint' | 'group', - destinationEndpoint?: number, - ): Promise { - 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( - destinationNetworkAddress: number, - sourceIeeeAddress: string, - sourceEndpoint: number, - clusterID: number, - destinationAddressOrGroup: string | number, - type: 'endpoint' | 'group', - destinationEndpoint?: number, - ): Promise { - 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 getNetworkParameters(): Promise { return { panID: this.driver.networkParams.panId, @@ -829,14 +569,6 @@ class EZSPAdapter extends Adapter { }); } - public async changeChannel(newChannel: number): Promise { - 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 { logger.debug(`setTransmitPower to ${value}`, NS); return await this.queue.execute(async () => { diff --git a/src/adapter/tstype.ts b/src/adapter/tstype.ts index 1363ca1d5a..cbb35f9389 100644 --- a/src/adapter/tstype.ts +++ b/src/adapter/tstype.ts @@ -30,15 +30,6 @@ type DeviceType = 'Coordinator' | 'EndDevice' | 'Router' | 'Unknown'; type StartResult = 'resumed' | 'reset' | 'restored'; -interface NodeDescriptor { - type: DeviceType; - manufacturerCode: number; -} - -interface ActiveEndpoints { - endpoints: number[]; -} - interface LQINeighbor { ieeeAddr: string; networkAddress: number; @@ -61,27 +52,6 @@ interface RoutingTable { table: RoutingTableEntry[]; } -interface SimpleDescriptor { - profileID: number; - endpointID: number; - deviceID: number; - inputClusters: number[]; - outputClusters: number[]; -} - -interface Coordinator { - ieeeAddr: string; - networkAddress: number; - manufacturerID: number; - endpoints: { - ID: number; - profileID: number; - deviceID: number; - inputClusters: number[]; - outputClusters: number[]; - }[]; -} - interface Backup { adapterType: 'zStack'; time: string; @@ -99,12 +69,8 @@ interface NetworkParameters { export { SerialPortOptions, NetworkOptions, - Coordinator, CoordinatorVersion, - NodeDescriptor, DeviceType, - ActiveEndpoints, - SimpleDescriptor, LQI, LQINeighbor, RoutingTable, diff --git a/src/adapter/z-stack/adapter/manager.ts b/src/adapter/z-stack/adapter/manager.ts index f436a1eba3..fdad9b1816 100644 --- a/src/adapter/z-stack/adapter/manager.ts +++ b/src/adapter/z-stack/adapter/manager.ts @@ -4,6 +4,8 @@ import {TsType} from '../../'; import * as Models from '../../../models'; import {Wait} from '../../../utils'; import {logger} from '../../../utils/logger'; +import * as ZSpec from '../../../zspec'; +import * as Zdo from '../../../zspec/zdo'; import * as ZnpConstants from '../constants'; import {DevStates, NvItemsIds, ZnpCommandStatus} from '../constants/common'; import * as ZStackModels from '../models'; @@ -452,14 +454,23 @@ export class ZnpAdapterManager { * Registers endpoints before beginning normal operation. */ private async registerEndpoints(): Promise { - const activeEp = await this.adapter.activeEndpoints(0); - for (const endpoint of Endpoints) { - if (activeEp.endpoints.includes(endpoint.endpoint)) { - logger.debug(`endpoint '${endpoint.endpoint}' already registered`, NS); - } else { - logger.debug(`registering endpoint '${endpoint.endpoint}'`, NS); - await this.znp.request(Subsystem.AF, 'register', endpoint); + const clusterId = Zdo.ClusterId.ACTIVE_ENDPOINTS_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest(this.adapter.hasZdoMessageOverhead, clusterId, ZSpec.COORDINATOR_ADDRESS); + const response = await this.adapter.sendZdo(ZSpec.BLANK_EUI64, ZSpec.COORDINATOR_ADDRESS, clusterId, zdoPayload, false); + + if (Zdo.Buffalo.checkStatus(response)) { + const activeEndpoints = response[1].endpointList; + + for (const endpoint of Endpoints) { + if (activeEndpoints.includes(endpoint.endpoint)) { + logger.debug(`endpoint '${endpoint.endpoint}' already registered`, NS); + } else { + logger.debug(`registering endpoint '${endpoint.endpoint}'`, NS); + await this.znp.request(Subsystem.AF, 'register', endpoint); + } } + } else { + throw new Zdo.StatusError(response[0]); } } diff --git a/src/adapter/z-stack/adapter/zStackAdapter.ts b/src/adapter/z-stack/adapter/zStackAdapter.ts index 861f20e0b4..bf93c6b4ff 100644 --- a/src/adapter/z-stack/adapter/zStackAdapter.ts +++ b/src/adapter/z-stack/adapter/zStackAdapter.ts @@ -13,23 +13,7 @@ import * as Zdo from '../../../zspec/zdo'; import * as ZdoTypes from '../../../zspec/zdo/definition/tstypes'; import Adapter from '../../adapter'; import * as Events from '../../events'; -import { - ActiveEndpoints, - AdapterOptions, - Coordinator, - CoordinatorVersion, - DeviceType, - LQI, - LQINeighbor, - NetworkOptions, - NetworkParameters, - NodeDescriptor, - RoutingTable, - RoutingTableEntry, - SerialPortOptions, - SimpleDescriptor, - StartResult, -} from '../../tstype'; +import {AdapterOptions, CoordinatorVersion, NetworkOptions, NetworkParameters, SerialPortOptions, StartResult} from '../../tstype'; import * as Constants from '../constants'; import {Constants as UnpiConstants} from '../unpi'; import {Znp, ZpiObject} from '../znp'; @@ -99,6 +83,7 @@ class ZStackAdapter extends Adapter { public constructor(networkOptions: NetworkOptions, serialPortOptions: SerialPortOptions, backupPath: string, adapterOptions: AdapterOptions) { super(networkOptions, serialPortOptions, backupPath, adapterOptions); this.hasZdoMessageOverhead = false; + this.manufacturerID = Zcl.ManufacturerCode.TEXAS_INSTRUMENTS; this.znp = new Znp(this.serialPortOptions.path!, this.serialPortOptions.baudRate!, this.serialPortOptions.rtscts!); this.transactionID = 0; @@ -186,33 +171,19 @@ class ZStackAdapter extends Adapter { return await Znp.autoDetectPath(); } - public async getCoordinator(): Promise { - return await this.queue.execute(async () => { + public async getCoordinatorIEEE(): Promise { + return await this.queue.execute(async () => { this.checkInterpanLock(); - const activeEp = await this.activeEndpoints(0); const deviceInfo = await this.znp.requestWithReply(Subsystem.UTIL, 'getDeviceInfo', {}); - const endpoints = []; - for (const endpoint of activeEp.endpoints) { - const simpleDesc = await this.simpleDescriptor(0, endpoint); - endpoints.push({ - ID: simpleDesc.endpointID, - deviceID: simpleDesc.deviceID, - profileID: simpleDesc.profileID, - inputClusters: simpleDesc.inputClusters, - outputClusters: simpleDesc.outputClusters, - }); - } - - return { - networkAddress: 0, - manufacturerID: 0, - ieeeAddr: deviceInfo.payload.ieeeaddr, - endpoints, - }; + return deviceInfo.payload.ieeeaddr; }); } + public async getCoordinatorVersion(): Promise { + return {type: ZnpVersion[this.version.product], meta: this.version}; + } + public async permitJoin(seconds: number, networkAddress?: number): Promise { const clusterId = Zdo.ClusterId.PERMIT_JOINING_REQUEST; // `authentication`: TC significance always 1 (zb specs) @@ -237,10 +208,6 @@ class ZStackAdapter extends Adapter { }); } - public async getCoordinatorVersion(): Promise { - return {type: ZnpVersion[this.version.product], meta: this.version}; - } - public async reset(type: 'soft' | 'hard'): Promise { if (type === 'soft') { await this.znp.request(Subsystem.SYS, 'resetReq', {type: Constants.SYS.resetType.SOFT}); @@ -320,71 +287,6 @@ class ZStackAdapter extends Adapter { } } - public async nodeDescriptor(networkAddress: number): Promise { - 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; - } - - 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 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 { - 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: 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 sendZdo( ieeeAddress: string, networkAddress: number, @@ -502,7 +404,7 @@ class ZStackAdapter extends Adapter { return response.payload.zdo; } - }, networkAddress /* TODO: replace with ieeeAddress once zdo moved upstream */); + }, networkAddress); } public async sendZclFrameToEndpoint( @@ -841,166 +743,12 @@ class ZStackAdapter extends Adapter { }); } - public async lqi(networkAddress: number): Promise { - 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); - - /* 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, - }); - } - - 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: 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 addInstallCode(ieeeAddress: string, key: Buffer): Promise { assert(this.version.product !== ZnpVersion.zStack12, 'Install code is not supported for ZStack 1.2 adapter'); const payload = {installCodeFormat: key.length === 18 ? 1 : 2, ieeeaddr: ieeeAddress, installCode: key}; await this.znp.request(Subsystem.APP_CNF, 'bdbAddInstallCode', payload); } - public async bind( - destinationNetworkAddress: number, - sourceIeeeAddress: string, - sourceEndpoint: number, - clusterID: number, - destinationAddressOrGroup: string | number, - type: 'endpoint' | 'group', - destinationEndpoint?: number, - ): Promise { - 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( - destinationNetworkAddress: number, - sourceIeeeAddress: string, - sourceEndpoint: number, - clusterID: number, - destinationAddressOrGroup: string | number, - type: 'endpoint' | 'group', - destinationEndpoint?: number, - ): Promise { - 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]); - } - } - /** * Event handlers */ @@ -1032,15 +780,11 @@ class ZStackAdapter extends Adapter { /* istanbul ignore else */ if (Zdo.Buffalo.checkStatus(object.payload.zdo)) { const zdoPayload = object.payload.zdo[1]; - const payload: Events.DeviceAnnouncePayload = { - 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 = zdoPayload.capabilities.deviceType === 0; + if (isEndDevice) { - if (!this.deviceAnnounceRouteDiscoveryDebouncers.has(payload.networkAddress)) { + if (!this.deviceAnnounceRouteDiscoveryDebouncers.has(zdoPayload.nwkAddress)) { // If a device announces multiple times in a very short time, it makes no sense // to rediscover the route every time. const debouncer = debounce( @@ -1048,16 +792,16 @@ class ZStackAdapter extends Adapter { // eslint-disable-next-line @typescript-eslint/no-floating-promises this.queue.execute(async () => { /* istanbul ignore next */ - this.discoverRoute(payload.networkAddress, false).catch(() => {}); - }, payload.networkAddress); + this.discoverRoute(zdoPayload.nwkAddress, false).catch(() => {}); + }, zdoPayload.nwkAddress); }, 60 * 1000, {immediate: true}, ); - this.deviceAnnounceRouteDiscoveryDebouncers.set(payload.networkAddress, debouncer); + this.deviceAnnounceRouteDiscoveryDebouncers.set(zdoPayload.nwkAddress, debouncer); } - const debouncer = this.deviceAnnounceRouteDiscoveryDebouncers.get(payload.networkAddress); + const debouncer = this.deviceAnnounceRouteDiscoveryDebouncers.get(zdoPayload.nwkAddress); assert(debouncer); debouncer(); } @@ -1217,15 +961,6 @@ class ZStackAdapter extends Adapter { }); } - public async changeChannel(newChannel: number): Promise { - const clusterId = Zdo.ClusterId.NWK_UPDATE_REQUEST; - const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, [newChannel], 0xfe, 0, undefined, 0); - - 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 { return await this.queue.execute(async () => { await this.znp.request(Subsystem.SYS, 'stackTune', {operation: 0, value}); diff --git a/src/adapter/zboss/adapter/zbossAdapter.ts b/src/adapter/zboss/adapter/zbossAdapter.ts index 4fa08082d5..ef42201e99 100644 --- a/src/adapter/zboss/adapter/zbossAdapter.ts +++ b/src/adapter/zboss/adapter/zbossAdapter.ts @@ -4,17 +4,15 @@ import assert from 'assert'; import {Adapter, TsType} from '../..'; import {Backup} from '../../../models'; -import {Queue, RealpathSync, Wait, Waitress} from '../../../utils'; +import {Queue, RealpathSync, Waitress} from '../../../utils'; import {logger} from '../../../utils/logger'; 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 {ZclPayload} from '../../events'; import SerialPortUtils from '../../serialPortUtils'; import SocketPortUtils from '../../socketPortUtils'; -import {Coordinator} from '../../tstype'; import {ZBOSSDriver} from '../driver'; import {CommandId, DeviceUpdateStatus} from '../enums'; import {FrameType, ZBOSSFrame} from '../frame'; @@ -47,6 +45,7 @@ export class ZBOSSAdapter extends Adapter { ) { super(networkOptions, serialPortOptions, backupPath, adapterOptions); this.hasZdoMessageOverhead = false; + this.manufacturerID = Zcl.ManufacturerCode.NORDIC_SEMICONDUCTOR_ASA; const concurrent = adapterOptions && adapterOptions.concurrent ? adapterOptions.concurrent : 8; logger.debug(`Adapter concurrent: ${concurrent}`, NS); this.queue = new Queue(concurrent); @@ -144,30 +143,8 @@ export class ZBOSSAdapter extends Adapter { logger.info(`ZBOSS Adapter stopped`, NS); } - public async getCoordinator(): Promise { - return await this.queue.execute(async () => { - 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 { - ieeeAddr: this.driver.netInfo.ieeeAddr, - networkAddress: ZSpec.COORDINATOR_ADDRESS, - manufacturerID: 0x0000, - endpoints: ap, - }; - }); + public async getCoordinatorIEEE(): Promise { + return this.driver.netInfo.ieeeAddr; } public async getCoordinatorVersion(): Promise { @@ -221,14 +198,6 @@ export class ZBOSSAdapter extends Adapter { }); } - public async changeChannel(newChannel: number): Promise { - 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 () => { @@ -275,228 +244,6 @@ export class ZBOSSAdapter extends Adapter { } } - public async lqi(networkAddress: number): Promise { - 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); - - /* 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, - }); - } - - 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 { - 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 { - 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 { - 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 { - 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: 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( - destinationNetworkAddress: number, - sourceIeeeAddress: string, - sourceEndpoint: number, - clusterID: number, - destinationAddressOrGroup: string | number, - type: 'endpoint' | 'group', - destinationEndpoint?: number, - ): Promise { - 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( - destinationNetworkAddress: number, - sourceIeeeAddress: string, - sourceEndpoint: number, - clusterID: number, - destinationAddressOrGroup: string | number, - type: 'endpoint' | 'group', - destinationEndpoint: number, - ): Promise { - 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, @@ -547,7 +294,7 @@ export class ZBOSSAdapter extends Adapter { return frame.payload.zdo as ZdoTypes.RequestToResponseMap[K]; } - }, networkAddress /* TODO: replace with ieeeAddress once zdo moved upstream */); + }, networkAddress); } public async sendZclFrameToEndpoint( diff --git a/src/adapter/zigate/adapter/zigateAdapter.ts b/src/adapter/zigate/adapter/zigateAdapter.ts index b7fed30624..a8c09edd83 100644 --- a/src/adapter/zigate/adapter/zigateAdapter.ts +++ b/src/adapter/zigate/adapter/zigateAdapter.ts @@ -5,7 +5,6 @@ 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'; @@ -13,7 +12,7 @@ import Adapter from '../../adapter'; import * as Events from '../../events'; import * as TsType from '../../tstype'; import {RawAPSDataRequestPayload} from '../driver/commandType'; -import {ADDRESS_MODE, coordinatorEndpoints, DEVICE_TYPE, ZiGateCommandCode, ZiGateMessageCode, ZPSNwkKeyState} from '../driver/constants'; +import {ADDRESS_MODE, DEVICE_TYPE, ZiGateCommandCode, ZiGateMessageCode, ZPSNwkKeyState} from '../driver/constants'; import Driver from '../driver/zigate'; import ZiGateObject from '../driver/ziGateObject'; import {patchZdoBuffaloBE} from './patchZdoBuffaloBE'; @@ -46,6 +45,7 @@ class ZiGateAdapter extends Adapter { patchZdoBuffaloBE(); super(networkOptions, serialPortOptions, backupPath, adapterOptions); this.hasZdoMessageOverhead = false; // false for requests, true for responses + this.manufacturerID = Zcl.ManufacturerCode.RESERVED_10; this.joinPermitted = false; this.closing = false; @@ -103,19 +103,9 @@ class ZiGateAdapter extends Adapter { await this.driver.close(); } - public async getCoordinator(): Promise { + public async getCoordinatorIEEE(): Promise { const networkResponse = await this.driver.sendCommand(ZiGateCommandCode.GetNetworkState); - - // @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: ZSpec.COORDINATOR_ADDRESS, - manufacturerID: 0, - ieeeAddr: networkResponse.payload.extendedAddress, - endpoints: coordinatorEndpoints.slice(), // copy - }; - logger.debug(() => `getCoordinator ${JSON.stringify(response)}`, NS); - return response; + return networkResponse.payload.extendedAddress; } public async getCoordinatorVersion(): Promise { @@ -200,14 +190,6 @@ class ZiGateAdapter extends Adapter { throw new Error('This adapter does not support backup'); } - public async changeChannel(newChannel: number): Promise { - 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 { try { await this.driver.sendCommand(ZiGateCommandCode.SetTXpower, {value: value}); @@ -216,227 +198,6 @@ class ZiGateAdapter extends Adapter { } } - public async lqi(networkAddress: number): Promise { - 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); - - /* 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, - }); - } - - 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, - }); - } - - 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 { - 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 { - 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 { - 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: 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( - destinationNetworkAddress: number, - sourceIeeeAddress: string, - sourceEndpoint: number, - clusterID: number, - destinationAddressOrGroup: string | number, - type: 'endpoint' | 'group', - destinationEndpoint?: number, - ): Promise { - 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( - destinationNetworkAddress: number, - sourceIeeeAddress: string, - sourceEndpoint: number, - clusterID: number, - destinationAddressOrGroup: string | number, - type: 'endpoint' | 'group', - destinationEndpoint?: number, - ): Promise { - 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, @@ -518,7 +279,7 @@ class ZiGateAdapter extends Adapter { return result.zdo as ZdoTypes.RequestToResponseMap[K]; } - }, networkAddress /* TODO: replace with ieeeAddress once zdo moved upstream */); + }, networkAddress); } public async sendZclFrameToEndpoint( @@ -796,9 +557,8 @@ class ZiGateAdapter extends Adapter { private deviceAnnounceListener(response: ZdoTypes.EndDeviceAnnounce): void { // @todo debounce - const payload: Events.DeviceAnnouncePayload = {networkAddress: response.nwkAddress, ieeeAddr: response.eui64}; if (this.joinPermitted === true) { - this.emit('deviceJoined', payload); + this.emit('deviceJoined', {networkAddress: response.nwkAddress, ieeeAddr: response.eui64}); } else { // convert to `zdoResponse` to avoid needing extra event upstream this.emit('zdoResponse', Zdo.ClusterId.END_DEVICE_ANNOUNCE, [Zdo.Status.SUCCESS, response]); diff --git a/src/adapter/zigate/driver/zigate.ts b/src/adapter/zigate/driver/zigate.ts index 9a2100ee8c..1d44555bd1 100644 --- a/src/adapter/zigate/driver/zigate.ts +++ b/src/adapter/zigate/driver/zigate.ts @@ -6,10 +6,10 @@ import net from 'net'; import {DelimiterParser} from '@serialport/parser-delimiter'; -import {ZSpec} from '../../..'; import {Queue} from '../../../utils'; import {logger} from '../../../utils/logger'; import Waitress from '../../../utils/waitress'; +import * as ZSpec from '../../../zspec'; import * as Zdo from '../../../zspec/zdo'; import {EndDeviceAnnounce, GenericZdoResponse} from '../../../zspec/zdo/definition/tstypes'; import {SerialPort} from '../../serialPort'; diff --git a/src/controller/controller.ts b/src/controller/controller.ts index 06844f64c5..8f7a92f6bf 100644 --- a/src/controller/controller.ts +++ b/src/controller/controller.ts @@ -5,13 +5,15 @@ import fs from 'fs'; import mixinDeep from 'mixin-deep'; import {Adapter, Events as AdapterEvents, TsType as AdapterTsType} from '../adapter'; -import {BackupUtils} from '../utils'; +import {BackupUtils, Wait} from '../utils'; import {logger} from '../utils/logger'; import {isNumberArrayOfLength} from '../utils/utils'; +import * as ZSpec from '../zspec'; +import {EUI64} from '../zspec/tstypes'; 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 * as ZdoTypes from '../zspec/zdo/definition/tstypes'; import Database from './database'; import * as Events from './events'; import GreenPower from './greenPower'; @@ -38,14 +40,6 @@ interface Options { acceptJoiningDeviceHandler: (ieeeAddr: string) => Promise; } -async function catcho(func: () => Promise, errorMessage: string): Promise { - try { - await func(); - } catch (error) { - logger.error(`${errorMessage}: ${error}`, NS); - } -} - const DefaultOptions: Pick = { network: { networkKeyDistribute: false, @@ -92,6 +86,8 @@ class Controller extends events.EventEmitter { private stopping: boolean; private adapterDisconnected: boolean; private networkParametersCached: AdapterTsType.NetworkParameters | undefined; + /** List of unknown devices detected during a single runtime session. Serves as de-dupe and anti-spam. */ + private unknownDevices: Set; /** * Create a controller @@ -103,6 +99,7 @@ class Controller extends events.EventEmitter { this.stopping = false; this.adapterDisconnected = true; // set false after adapter.start() is successfully called this.options = mixinDeep(JSON.parse(JSON.stringify(DefaultOptions)), options); + this.unknownDevices = new Set(); // Validate options for (const channel of this.options.network.channelList) { @@ -168,11 +165,10 @@ class Controller extends events.EventEmitter { 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)); - this.adapter.on('networkAddress', this.onNetworkAddress.bind(this)); if (startResult === 'reset') { + /* istanbul ignore else */ if (this.options.databaseBackupPath && fs.existsSync(this.options.databasePath)) { fs.copyFileSync(this.options.databasePath, this.options.databaseBackupPath); } @@ -192,28 +188,35 @@ class Controller extends events.EventEmitter { } // Add coordinator to the database if it is not there yet. - const coordinator = await this.adapter.getCoordinator(); + const coordinatorIEEE = await this.adapter.getCoordinatorIEEE(); if (Device.byType('Coordinator').length === 0) { logger.debug('No coordinator in database, querying...', NS); - Device.create( + const coordinator = Device.create( 'Coordinator', - coordinator.ieeeAddr, - coordinator.networkAddress, - coordinator.manufacturerID, + coordinatorIEEE, + ZSpec.COORDINATOR_ADDRESS, + this.adapter.manufacturerID, undefined, undefined, undefined, true, - coordinator.endpoints, ); + + await coordinator.updateActiveEndpoints(); + + for (const endpoint of coordinator.endpoints) { + await endpoint.updateSimpleDescriptor(); + } + + coordinator.save(); } // Update coordinator ieeeAddr if changed, can happen due to e.g. reflashing const databaseCoordinator = Device.byType('Coordinator')[0]; - if (databaseCoordinator.ieeeAddr !== coordinator.ieeeAddr) { - logger.info(`Coordinator address changed, updating to '${coordinator.ieeeAddr}'`, NS); - databaseCoordinator.changeIeeeAddress(coordinator.ieeeAddr); + if (databaseCoordinator.ieeeAddr !== coordinatorIEEE) { + logger.info(`Coordinator address changed, updating to '${coordinatorIEEE}'`, NS); + databaseCoordinator.changeIeeeAddress(coordinatorIEEE); } // Set backup timer to 1 day. @@ -286,14 +289,18 @@ class Controller extends events.EventEmitter { await this.adapter.permitJoin(254, device?.networkAddress); await this.greenPower.permitJoin(254, device?.networkAddress); + // TODO: remove https://github.com/Koenkk/zigbee-herdsman/issues/940 // Zigbee 3 networks automatically close after max 255 seconds, keep network open. this.permitJoinNetworkClosedTimer = setInterval(async (): Promise => { - await catcho(async () => { + try { await this.adapter.permitJoin(254, device?.networkAddress); await this.greenPower.permitJoin(254, device?.networkAddress); - }, 'Failed to keep permit join alive'); + } catch (error) { + logger.error(`Failed to keep permit join alive: ${error}`, NS); + } }, 200 * 1000); + // TODO: prevent many mqtt messages by doing this on the other end? if (typeof time === 'number') { this.permitJoinTimeout = time; this.permitJoinTimeoutTimer = setInterval(async (): Promise => { @@ -346,7 +353,12 @@ class Controller extends events.EventEmitter { if (this.adapterDisconnected) { this.databaseSave(); } else { - await catcho(() => this.permitJoinInternal(false, 'manual'), 'Failed to disable join on stop'); + try { + await this.permitJoinInternal(false, 'manual'); + } catch (error) { + logger.error(`Failed to disable join on stop: ${error}`, NS); + } + await this.backup(); // always calls databaseSave() await this.adapter.stop(); @@ -498,10 +510,17 @@ class Controller extends events.EventEmitter { */ private async changeChannel(oldChannel: number, newChannel: number): Promise { logger.warning(`Changing channel from '${oldChannel}' to '${newChannel}'`, NS); - await this.adapter.changeChannel(newChannel); + + const clusterId = Zdo.ClusterId.NWK_UPDATE_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest(this.adapter.hasZdoMessageOverhead, clusterId, [newChannel], 0xfe, undefined, 0, undefined); + + await this.adapter.sendZdo(ZSpec.BLANK_EUI64, ZSpec.BroadcastAddress.SLEEPY, clusterId, zdoPayload, true); logger.info(`Channel changed to '${newChannel}'`, NS); this.networkParametersCached = undefined; // invalidate cache + // wait for the broadcast to propagate and the adapter to actually change + // NOTE: observed to ~9sec on `ember` with actual stack event + await Wait(12000); } /** @@ -511,33 +530,82 @@ class Controller extends events.EventEmitter { return await this.adapter.setTransmitPower(value); } - private onNetworkAddress(payload: AdapterEvents.NetworkAddressPayload): void { - logger.debug(`Network address '${payload.ieeeAddr}'`, NS); - const device = Device.byIeeeAddr(payload.ieeeAddr); + public async identifyUnknownDevice(nwkAddress: number): Promise { + if (this.unknownDevices.has(nwkAddress)) { + // prevent duplicate triggering + return; + } + + logger.debug(`Trying to identify unknown device with address '${nwkAddress}'`, NS); + this.unknownDevices.add(nwkAddress); + const clusterId = Zdo.ClusterId.IEEE_ADDRESS_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest(this.adapter.hasZdoMessageOverhead, clusterId, nwkAddress, false, 0); + const response = await this.adapter.sendZdo(ZSpec.BLANK_EUI64, nwkAddress, clusterId, zdoPayload, false); + + if (Zdo.Buffalo.checkStatus(response)) { + // XXX: race with onIEEEAddress triggered from onZdoResponse or not? + // this duplicates the triggering but makes sure device is updated before going further... + this.onIEEEAddress(response[1]); + + return Device.byIeeeAddr(response[1].eui64); + } else { + logger.debug(`Failed to retrieve IEEE address for device '${nwkAddress}': ${Zdo.Status[response[0]]}`, NS); + } + + // NOTE: by keeping nwkAddress in `this.unknownDevices` on fail, it prevents a non-responding device from potentially spamming identify. + // This only lasts until next reboot (runtime Set), allowing to 'force' another trigger if necessary. + } + + private onNetworkAddress(payload: ZdoTypes.NetworkAddressResponse): void { + logger.debug(`Network address from '${payload.eui64}:${payload.nwkAddress}'`, NS); + const device = Device.byIeeeAddr(payload.eui64); if (!device) { - logger.debug(`Network address is from unknown device '${payload.ieeeAddr}'`, NS); + logger.debug(`Network address is from unknown device '${payload.eui64}:${payload.nwkAddress}'`, NS); return; } device.updateLastSeen(); this.selfAndDeviceEmit(device, 'lastSeenChanged', {device, reason: 'networkAddress'}); - if (device.networkAddress !== payload.networkAddress) { - logger.debug(`Device '${payload.ieeeAddr}' got new networkAddress '${payload.networkAddress}'`, NS); - device.networkAddress = payload.networkAddress; + if (device.networkAddress !== payload.nwkAddress) { + logger.debug(`Device '${payload.eui64}' got new networkAddress '${payload.nwkAddress}'`, NS); + device.networkAddress = payload.nwkAddress; + device.save(); + + this.selfAndDeviceEmit(device, 'deviceNetworkAddressChanged', {device}); + } + } + + private onIEEEAddress(payload: ZdoTypes.IEEEAddressResponse): void { + logger.debug(`IEEE address from '${payload.eui64}:${payload.nwkAddress}'`, NS); + const device = Device.byIeeeAddr(payload.eui64); + + if (!device) { + logger.debug(`IEEE address is from unknown device '${payload.eui64}:${payload.nwkAddress}'`, NS); + return; + } + + device.updateLastSeen(); + this.selfAndDeviceEmit(device, 'lastSeenChanged', {device, reason: 'networkAddress'}); + + if (device.networkAddress !== payload.nwkAddress) { + logger.debug(`Device '${payload.eui64}' got new networkAddress '${payload.nwkAddress}'`, NS); + device.networkAddress = payload.nwkAddress; device.save(); this.selfAndDeviceEmit(device, 'deviceNetworkAddressChanged', {device}); } + + this.unknownDevices.delete(payload.nwkAddress); } - private onDeviceAnnounce(payload: AdapterEvents.DeviceAnnouncePayload): void { - logger.debug(`Device announce '${payload.ieeeAddr}'`, NS); - const device = Device.byIeeeAddr(payload.ieeeAddr); + private onDeviceAnnounce(payload: ZdoTypes.EndDeviceAnnounce): void { + logger.debug(`Device announce from '${payload.eui64}:${payload.nwkAddress}'`, NS); + const device = Device.byIeeeAddr(payload.eui64); if (!device) { - logger.debug(`Device announce is from unknown device '${payload.ieeeAddr}'`, NS); + logger.debug(`Device announce is from unknown device '${payload.eui64}:${payload.nwkAddress}'`, NS); return; } @@ -545,10 +613,12 @@ class Controller extends events.EventEmitter { this.selfAndDeviceEmit(device, 'lastSeenChanged', {device, reason: 'deviceAnnounce'}); device.implicitCheckin(); - if (device.networkAddress !== payload.networkAddress) { - logger.debug(`Device '${payload.ieeeAddr}' announced with new networkAddress '${payload.networkAddress}'`, NS); - device.networkAddress = payload.networkAddress; + if (device.networkAddress !== payload.nwkAddress) { + logger.debug(`Device '${payload.eui64}' announced with new networkAddress '${payload.nwkAddress}'`, NS); + device.networkAddress = payload.nwkAddress; device.save(); + + this.selfAndDeviceEmit(device, 'deviceNetworkAddressChanged', {device}); } this.selfAndDeviceEmit(device, 'deviceAnnounce', {device}); @@ -576,7 +646,11 @@ class Controller extends events.EventEmitter { this.adapterDisconnected = true; - await catcho(() => this.adapter.stop(), 'Failed to stop adapter on disconnect'); + try { + await this.adapter.stop(); + } catch (error) { + logger.error(`Failed to stop adapter on disconnect: ${error}`, NS); + } this.emit('adapterDisconnected'); } @@ -595,7 +669,7 @@ class Controller extends events.EventEmitter { if (!device) { logger.debug(`New green power device '${ieeeAddr}' joined`, NS); logger.debug(`Creating device '${ieeeAddr}'`, NS); - device = Device.create('GreenPower', ieeeAddr, payload.networkAddress, undefined, undefined, undefined, modelID, true, []); + device = Device.create('GreenPower', ieeeAddr, payload.networkAddress, undefined, undefined, undefined, modelID, true); device.save(); this.selfAndDeviceEmit(device, 'deviceJoined', {device}); @@ -622,10 +696,30 @@ class Controller extends events.EventEmitter { private async onDeviceJoined(payload: AdapterEvents.DeviceJoinedPayload): Promise { logger.debug(`Device '${payload.ieeeAddr}' joined`, NS); + /* istanbul ignore else */ if (this.options.acceptJoiningDeviceHandler) { if (!(await this.options.acceptJoiningDeviceHandler(payload.ieeeAddr))) { logger.debug(`Device '${payload.ieeeAddr}' rejected by handler, removing it`, NS); - await catcho(() => this.adapter.removeDevice(payload.networkAddress, payload.ieeeAddr), 'Failed to remove rejected device'); + + // XXX: GP devices? see Device.removeFromNetwork + try { + const clusterId = Zdo.ClusterId.LEAVE_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest( + this.adapter.hasZdoMessageOverhead, + clusterId, + payload.ieeeAddr as EUI64, + Zdo.LeaveRequestFlags.WITHOUT_REJOIN, + ); + const response = await this.adapter.sendZdo(payload.ieeeAddr, payload.networkAddress, clusterId, zdoPayload, false); + + /* istanbul ignore else */ + if (!Zdo.Buffalo.checkStatus(response)) { + throw new Zdo.StatusError(response[0]); + } + } catch (error) { + logger.error(`Failed to remove rejected device: ${(error as Error).message}`, NS); + } + return; } else { logger.debug(`Device '${payload.ieeeAddr}' accepted by handler`, NS); @@ -636,7 +730,7 @@ class Controller extends events.EventEmitter { if (!device) { logger.debug(`New device '${payload.ieeeAddr}' joined`, NS); logger.debug(`Creating device '${payload.ieeeAddr}'`, NS); - device = Device.create('Unknown', payload.ieeeAddr, payload.networkAddress, undefined, undefined, undefined, undefined, false, []); + device = Device.create('Unknown', payload.ieeeAddr, payload.networkAddress, undefined, undefined, undefined, undefined, false); this.selfAndDeviceEmit(device, 'deviceJoined', {device}); } else if (device.isDeleted) { logger.debug(`Deleted device '${payload.ieeeAddr}' joined, undeleting`, NS); @@ -674,33 +768,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]; + private async onZdoResponse(clusterId: Zdo.ClusterId, response: ZdoTypes.GenericZdoResponse): Promise { + logger.debug( + `Received ZDO response: clusterId=${Zdo.ClusterId[clusterId]}, status=${Zdo.Status[response[0]]}, payload=${JSON.stringify(response[1])}`, + NS, + ); - this.onNetworkAddress({ - networkAddress: payload.nwkAddress, - ieeeAddr: payload.eui64, - }); + switch (clusterId) { + case Zdo.ClusterId.NETWORK_ADDRESS_RESPONSE: { + /* istanbul ignore else */ + if (Zdo.Buffalo.checkStatus(response)) { + this.onNetworkAddress(response[1]); + } + break; } - } 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, - }); + case Zdo.ClusterId.IEEE_ADDRESS_RESPONSE: { + /* istanbul ignore else */ + if (Zdo.Buffalo.checkStatus(response)) { + this.onIEEEAddress(response[1]); + } + break; + } + + case Zdo.ClusterId.END_DEVICE_ANNOUNCE: { + /* istanbul ignore else */ + if (Zdo.Buffalo.checkStatus(response)) { + this.onDeviceAnnounce(response[1]); + } + break; } - } else { - /* istanbul ignore next */ - logger.debug( - `Received ZDO response: clusterId=${Zdo.ClusterId[clusterId]}, status=${Zdo.Status[response[0]]}, payload=${JSON.stringify(response[1])}`, - NS, - ); } } @@ -752,8 +849,14 @@ class Controller extends events.EventEmitter { } if (!device) { - logger.debug(`Data is from unknown device with address '${payload.address}', skipping...`, NS); - return; + if (typeof payload.address === 'number') { + device = await this.identifyUnknownDevice(payload.address); + } + + if (!device) { + logger.debug(`Data is from unknown device with address '${payload.address}', skipping...`, NS); + return; + } } logger.debug( diff --git a/src/controller/greenPower.ts b/src/controller/greenPower.ts index 69485f0629..78113e5104 100644 --- a/src/controller/greenPower.ts +++ b/src/controller/greenPower.ts @@ -4,7 +4,7 @@ import events from 'events'; import {Adapter, Events as AdapterEvents} from '../adapter'; import {logger} from '../utils/logger'; -import {GP_ENDPOINT, GP_GROUP_ID} from '../zspec/consts'; +import {COORDINATOR_ADDRESS, GP_ENDPOINT, GP_GROUP_ID} from '../zspec/consts'; import {BroadcastAddress} from '../zspec/enums'; import * as Zcl from '../zspec/zcl'; import ZclTransactionSequenceNumber from './helpers/zclTransactionSequenceNumber'; @@ -62,9 +62,9 @@ class GreenPower extends events.EventEmitter { case 0b00: // Full unicast forwarding case 0b11: { // Lightweight unicast forwarding - const coordinator = await this.adapter.getCoordinator(); - payload.sinkIEEEAddr = coordinator.ieeeAddr; - payload.sinkNwkAddr = coordinator.networkAddress; + const coordinatorIEEE = await this.adapter.getCoordinatorIEEE(); + payload.sinkIEEEAddr = coordinatorIEEE; + payload.sinkNwkAddr = COORDINATOR_ADDRESS; break; } /* istanbul ignore next */ diff --git a/src/controller/model/device.ts b/src/controller/model/device.ts index dad2617578..cef81ce8d8 100755 --- a/src/controller/model/device.ts +++ b/src/controller/model/device.ts @@ -1,11 +1,15 @@ import assert from 'assert'; import {Events as AdapterEvents} from '../../adapter'; +import {LQINeighbor, RoutingTableEntry} from '../../adapter/tstype'; import {Wait} 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 {ClusterDefinition, CustomClusters} from '../../zspec/zcl/definition/tstype'; +import * as Zdo from '../../zspec/zdo'; import {ControllerEventMap} from '../controller'; import {ZclFrameConverter} from '../helpers'; import ZclTransactionSequenceNumber from '../helpers/zclTransactionSequenceNumber'; @@ -693,13 +697,6 @@ class Device extends Entity { powerSource: string | undefined, modelID: string | undefined, interviewCompleted: boolean, - endpoints: { - ID: number; - profileID: number; - deviceID: number; - inputClusters: number[]; - outputClusters: number[]; - }[], ): Device { Device.loadFromDatabaseIfNecessary(); @@ -707,10 +704,6 @@ class Device extends Entity { throw new Error(`Device with IEEE address '${ieeeAddr}' already exists`); } - const endpointsMapped = endpoints.map((e): Endpoint => { - return Endpoint.create(e.ID, e.profileID, e.deviceID, e.inputClusters, e.outputClusters, networkAddress, ieeeAddr); - }); - const ID = Entity.database!.newID(); const device = new Device( ID, @@ -718,7 +711,7 @@ class Device extends Entity { ieeeAddr, networkAddress, manufacturerID, - endpointsMapped, + [], manufacturerName, powerSource, modelID, @@ -855,19 +848,12 @@ class Device extends Entity { } private async interviewInternal(ignoreCache: boolean): Promise { - const nodeDescriptorQuery = async (): Promise => { - const nodeDescriptor = await Entity.adapter!.nodeDescriptor(this.networkAddress); - this._manufacturerID = nodeDescriptor.manufacturerCode; - this._type = nodeDescriptor.type; - logger.debug(`Interview - got node descriptor for device '${this.ieeeAddr}'`, NS); - }; - const hasNodeDescriptor = (): boolean => this._manufacturerID !== undefined && this._type !== 'Unknown'; if (ignoreCache || !hasNodeDescriptor()) { for (let attempt = 0; attempt < 6; attempt++) { try { - await nodeDescriptorQuery(); + await this.updateNodeDescriptor(); break; } catch (error) { if (this.interviewQuirks()) { @@ -913,41 +899,32 @@ class Device extends Entity { // e.g. Xiaomi Aqara Opple devices fail to respond to the first active endpoints request, therefore try 2 times // https://github.com/Koenkk/zigbee-herdsman/pull/103 - let activeEndpoints; + let gotActiveEndpoints: boolean = false; + for (let attempt = 0; attempt < 2; attempt++) { try { - activeEndpoints = await Entity.adapter!.activeEndpoints(this.networkAddress); + await this.updateActiveEndpoints(); + gotActiveEndpoints = true; break; } catch (error) { logger.debug(`Interview - active endpoints request failed for '${this.ieeeAddr}', attempt ${attempt + 1} (${error})`, NS); } } - if (!activeEndpoints) { + + if (!gotActiveEndpoints) { throw new Error(`Interview failed because can not get active endpoints ('${this.ieeeAddr}')`); } - // Make sure that the endpoint are sorted. - activeEndpoints.endpoints.sort((a, b) => a - b); - - // Some devices, e.g. TERNCY return endpoint 0 in the active endpoints request. - // This is not a valid endpoint number according to the ZCL, requesting a simple descriptor will result - // into an error. Therefore we filter it, more info: https://github.com/Koenkk/zigbee-herdsman/issues/82 - activeEndpoints.endpoints - .filter((e) => e !== 0 && !this.getEndpoint(e)) - .forEach((e) => this._endpoints.push(Endpoint.create(e, undefined, undefined, [], [], this.networkAddress, this.ieeeAddr))); logger.debug(`Interview - got active endpoints for device '${this.ieeeAddr}'`, NS); - for (const endpointID of activeEndpoints.endpoints.filter((e) => e !== 0)) { - const endpoint = this.getEndpoint(endpointID)!; // XXX: should never be undefined? - const simpleDescriptor = await Entity.adapter!.simpleDescriptor(this.networkAddress, endpoint.ID); - endpoint.profileID = simpleDescriptor.profileID; - endpoint.deviceID = simpleDescriptor.deviceID; - endpoint.inputClusters = simpleDescriptor.inputClusters; - endpoint.outputClusters = simpleDescriptor.outputClusters; + const coordinator = Device.byType('Coordinator')[0]; + + for (const endpoint of this._endpoints) { + await endpoint.updateSimpleDescriptor(); logger.debug(`Interview - got simple descriptor for endpoint '${endpoint.ID}' device '${this.ieeeAddr}'`, NS); - // Read attributes, nice to have but not required for succesfull pairing as most of the attributes - // are not mandatory in ZCL specification. + // Read attributes + // nice to have but not required for successful pairing as most of the attributes are not mandatory in ZCL specification if (endpoint.supportsInputCluster('genBasic')) { for (const key in Device.ReportablePropertiesMapping) { const item = Device.ReportablePropertiesMapping[key]; @@ -980,53 +957,51 @@ class Device extends Entity { } } } - } - const coordinator = Device.byType('Coordinator')[0]; + // Enroll IAS device + if (endpoint.supportsInputCluster('ssIasZone')) { + logger.debug(`Interview - IAS - enrolling '${this.ieeeAddr}' endpoint '${endpoint.ID}'`, NS); - // Enroll IAS device - for (const endpoint of this.endpoints.filter((e): boolean => e.supportsInputCluster('ssIasZone'))) { - logger.debug(`Interview - IAS - enrolling '${this.ieeeAddr}' endpoint '${endpoint.ID}'`, NS); - - const stateBefore = await endpoint.read('ssIasZone', ['iasCieAddr', 'zoneState'], {sendPolicy: 'immediate'}); - logger.debug(`Interview - IAS - before enrolling state: '${JSON.stringify(stateBefore)}'`, NS); - - // Do not enroll when device has already been enrolled - if (stateBefore.zoneState !== 1 || stateBefore.iasCieAddr !== coordinator.ieeeAddr) { - logger.debug(`Interview - IAS - not enrolled, enrolling`, NS); - - await endpoint.write('ssIasZone', {iasCieAddr: coordinator.ieeeAddr}, {sendPolicy: 'immediate'}); - logger.debug(`Interview - IAS - wrote iasCieAddr`, NS); - - // There are 2 enrollment procedures: - // - Auto enroll: coordinator has to send enrollResponse without receiving an enroll request - // this case is handled below. - // - Manual enroll: coordinator replies to enroll request with an enroll response. - // this case in hanled in onZclData(). - // https://github.com/Koenkk/zigbee2mqtt/issues/4569#issuecomment-706075676 - await Wait(500); - logger.debug(`IAS - '${this.ieeeAddr}' sending enroll response (auto enroll)`, NS); - const payload = {enrollrspcode: 0, zoneid: 23}; - await endpoint.command('ssIasZone', 'enrollRsp', payload, {disableDefaultResponse: true, sendPolicy: 'immediate'}); - - let enrolled = false; - for (let attempt = 0; attempt < 20; attempt++) { + const stateBefore = await endpoint.read('ssIasZone', ['iasCieAddr', 'zoneState'], {sendPolicy: 'immediate'}); + logger.debug(`Interview - IAS - before enrolling state: '${JSON.stringify(stateBefore)}'`, NS); + + // Do not enroll when device has already been enrolled + if (stateBefore.zoneState !== 1 || stateBefore.iasCieAddr !== coordinator.ieeeAddr) { + logger.debug(`Interview - IAS - not enrolled, enrolling`, NS); + + await endpoint.write('ssIasZone', {iasCieAddr: coordinator.ieeeAddr}, {sendPolicy: 'immediate'}); + logger.debug(`Interview - IAS - wrote iasCieAddr`, NS); + + // There are 2 enrollment procedures: + // - Auto enroll: coordinator has to send enrollResponse without receiving an enroll request + // this case is handled below. + // - Manual enroll: coordinator replies to enroll request with an enroll response. + // this case in hanled in onZclData(). + // https://github.com/Koenkk/zigbee2mqtt/issues/4569#issuecomment-706075676 await Wait(500); - const stateAfter = await endpoint.read('ssIasZone', ['iasCieAddr', 'zoneState'], {sendPolicy: 'immediate'}); - logger.debug(`Interview - IAS - after enrolling state (${attempt}): '${JSON.stringify(stateAfter)}'`, NS); - if (stateAfter.zoneState === 1) { - enrolled = true; - break; + logger.debug(`IAS - '${this.ieeeAddr}' sending enroll response (auto enroll)`, NS); + const payload = {enrollrspcode: 0, zoneid: 23}; + await endpoint.command('ssIasZone', 'enrollRsp', payload, {disableDefaultResponse: true, sendPolicy: 'immediate'}); + + let enrolled = false; + for (let attempt = 0; attempt < 20; attempt++) { + await Wait(500); + const stateAfter = await endpoint.read('ssIasZone', ['iasCieAddr', 'zoneState'], {sendPolicy: 'immediate'}); + logger.debug(`Interview - IAS - after enrolling state (${attempt}): '${JSON.stringify(stateAfter)}'`, NS); + if (stateAfter.zoneState === 1) { + enrolled = true; + break; + } } - } - if (enrolled) { - logger.debug(`Interview - IAS successfully enrolled '${this.ieeeAddr}' endpoint '${endpoint.ID}'`, NS); + if (enrolled) { + logger.debug(`Interview - IAS successfully enrolled '${this.ieeeAddr}' endpoint '${endpoint.ID}'`, NS); + } else { + throw new Error(`Interview failed because of failed IAS enroll (zoneState didn't change ('${this.ieeeAddr}')`); + } } else { - throw new Error(`Interview failed because of failed IAS enroll (zoneState didn't change ('${this.ieeeAddr}')`); + logger.debug(`Interview - IAS - already enrolled, skipping enroll`, NS); } - } else { - logger.debug(`Interview - IAS - already enrolled, skipping enroll`, NS); } } @@ -1045,13 +1020,93 @@ class Device extends Entity { } } + public async updateNodeDescriptor(): Promise { + const clusterId = Zdo.ClusterId.NODE_DESCRIPTOR_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest(Entity.adapter!.hasZdoMessageOverhead, clusterId, this.networkAddress); + const response = await Entity.adapter!.sendZdo(this.ieeeAddr, this.networkAddress, clusterId, zdoPayload, false); + + if (!Zdo.Buffalo.checkStatus(response)) { + throw new Zdo.StatusError(response[0]); + } + + // TODO: make use of: capabilities.rxOnWhenIdle, maxIncTxSize, maxOutTxSize, serverMask.stackComplianceRevision + const nodeDescriptor = response[1]; + this._manufacturerID = nodeDescriptor.manufacturerCode; + + switch (nodeDescriptor.logicalType) { + case 0x0: + this._type = 'Coordinator'; + break; + case 0x1: + this._type = 'Router'; + break; + case 0x2: + this._type = 'EndDevice'; + break; + } + + logger.debug(`Interview - got node descriptor for device '${this.ieeeAddr}'`, NS); + + // TODO: define a property on Device for this value (would be good to have it displayed) + // log for devices older than 1 from current revision + if (nodeDescriptor.serverMask.stackComplianceRevision < ZSpec.ZIGBEE_REVISION - 1) { + // always 0 before revision 21 where field was added + const rev = nodeDescriptor.serverMask.stackComplianceRevision < 21 ? 'pre-21' : nodeDescriptor.serverMask.stackComplianceRevision; + + logger.info( + `Device '${this.ieeeAddr}' is only compliant to revision '${rev}' of the ZigBee specification (current revision: ${ZSpec.ZIGBEE_REVISION}).`, + NS, + ); + } + } + + public async updateActiveEndpoints(): Promise { + const clusterId = Zdo.ClusterId.ACTIVE_ENDPOINTS_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest(Entity.adapter!.hasZdoMessageOverhead, clusterId, this.networkAddress); + + const response = await Entity.adapter!.sendZdo(this.ieeeAddr, this.networkAddress, clusterId, zdoPayload, false); + + if (!Zdo.Buffalo.checkStatus(response)) { + throw new Zdo.StatusError(response[0]); + } + + const activeEndpoints = response[1]; + + // Make sure that the endpoint are sorted. + activeEndpoints.endpointList.sort((a, b) => a - b); + + // TODO: this does not take care of removing endpoint (changing custom devices)? + for (const endpoint of activeEndpoints.endpointList) { + // Some devices, e.g. TERNCY return endpoint 0 in the active endpoints request. + // This is not a valid endpoint number according to the ZCL, requesting a simple descriptor will result + // into an error. Therefore we filter it, more info: https://github.com/Koenkk/zigbee-herdsman/issues/82 + /* istanbul ignore else */ + if (endpoint !== 0 && !this.getEndpoint(endpoint)) { + this._endpoints.push(Endpoint.create(endpoint, undefined, undefined, [], [], this.networkAddress, this.ieeeAddr)); + } + } + } + + /** + * Request device to advertise its network address. + * Note: This does not actually update the device property (if needed), as this is already done with `zdoResponse` event in Controller. + */ + public async updateNetworkAddress(): Promise { + const clusterId = Zdo.ClusterId.NETWORK_ADDRESS_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest(Entity.adapter!.hasZdoMessageOverhead, clusterId, this.ieeeAddr as EUI64, false, 0); + const response = await Entity.adapter!.sendZdo(this.ieeeAddr, ZSpec.BroadcastAddress.RX_ON_WHEN_IDLE, clusterId, zdoPayload, false); + + if (!Zdo.Buffalo.checkStatus(response)) { + throw new Zdo.StatusError(response[0]); + } + } + public async removeFromNetwork(): Promise { if (this._type === 'GreenPower') { const payload = { options: 0x002550, srcID: Number(this.ieeeAddr), }; - const frame = Zcl.Frame.create( Zcl.FrameType.SPECIFIC, Zcl.Direction.SERVER_TO_CLIENT, @@ -1065,7 +1120,21 @@ class Device extends Entity { ); await Entity.adapter!.sendZclFrameToAll(242, frame, 242, BroadcastAddress.RX_ON_WHEN_IDLE); - } else await Entity.adapter!.removeDevice(this.networkAddress, this.ieeeAddr); + } else { + const clusterId = Zdo.ClusterId.LEAVE_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest( + Entity.adapter!.hasZdoMessageOverhead, + clusterId, + this.ieeeAddr as EUI64, + Zdo.LeaveRequestFlags.WITHOUT_REJOIN, + ); + const response = await Entity.adapter!.sendZdo(this.ieeeAddr, this.networkAddress, clusterId, zdoPayload, false); + + if (!Zdo.Buffalo.checkStatus(response)) { + throw new Zdo.StatusError(response[0]); + } + } + this.removeFromDatabase(); } @@ -1105,11 +1174,83 @@ class Device extends Entity { } public async lqi(): Promise { - return await Entity.adapter!.lqi(this.networkAddress); + const clusterId = Zdo.ClusterId.LQI_TABLE_REQUEST; + // TODO return Zdo.LQITableEntry directly (requires updates in other repos) + const neighbors: LQINeighbor[] = []; + const request = async (startIndex: number): Promise<[tableEntries: number, entryCount: number]> => { + const zdoPayload = Zdo.Buffalo.buildRequest(Entity.adapter!.hasZdoMessageOverhead, clusterId, startIndex); + const response = await Entity.adapter!.sendZdo(this.ieeeAddr, this.networkAddress, clusterId, zdoPayload, false); + + if (!Zdo.Buffalo.checkStatus(response)) { + throw new Zdo.StatusError(response[0]); + } + + const result = response[1]; + + for (const entry of result.entryList) { + neighbors.push({ + ieeeAddr: entry.eui64, + networkAddress: entry.nwkAddress, + linkquality: entry.lqi, + relationship: entry.relationship, + depth: entry.depth, + }); + } + + return [result.neighborTableEntries, result.entryList.length]; + }; + + 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(): Promise { - return await Entity.adapter!.routingTable(this.networkAddress); + const clusterId = Zdo.ClusterId.ROUTING_TABLE_REQUEST; + // TODO return Zdo.RoutingTableEntry directly (requires updates in other repos) + const table: RoutingTableEntry[] = []; + const request = async (startIndex: number): Promise<[tableEntries: number, entryCount: number]> => { + const zdoPayload = Zdo.Buffalo.buildRequest(Entity.adapter!.hasZdoMessageOverhead, clusterId, startIndex); + const response = await Entity.adapter!.sendZdo(this.ieeeAddr, this.networkAddress, clusterId, zdoPayload, false); + + if (!Zdo.Buffalo.checkStatus(response)) { + throw new Zdo.StatusError(response[0]); + } + + const result = response[1]; + + for (const entry of result.entryList) { + table.push({ + destinationAddress: entry.destinationAddress, + status: entry.status, + nextHop: entry.nextHopAddress, + }); + } + + return [result.routingTableEntries, result.entryList.length]; + }; + + 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 ping(disableRecovery = true): Promise { diff --git a/src/controller/model/endpoint.ts b/src/controller/model/endpoint.ts index 16bfea886e..87a7f1356c 100644 --- a/src/controller/model/endpoint.ts +++ b/src/controller/model/endpoint.ts @@ -2,9 +2,12 @@ import assert from 'assert'; import {Events as AdapterEvents} from '../../adapter'; 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 ZclTypes from '../../zspec/zcl/definition/tstype'; +import * as Zdo from '../../zspec/zdo'; import Request from '../helpers/request'; import RequestQueue from '../helpers/requestQueue'; import * as ZclFrameConverter from '../helpers/zclFrameConverter'; @@ -479,6 +482,23 @@ class Endpoint extends Entity { ); } + public async updateSimpleDescriptor(): Promise { + const clusterId = Zdo.ClusterId.SIMPLE_DESCRIPTOR_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest(Entity.adapter!.hasZdoMessageOverhead, clusterId, this.deviceNetworkAddress, this.ID); + const response = await Entity.adapter!.sendZdo(this.deviceIeeeAddress, this.deviceNetworkAddress, clusterId, zdoPayload, false); + + if (!Zdo.Buffalo.checkStatus(response)) { + throw new Zdo.StatusError(response[0]); + } + + const simpleDescriptor = response[1]; + + this.profileID = simpleDescriptor.profileId; + this.deviceID = simpleDescriptor.deviceId; + this.inputClusters = simpleDescriptor.inClusterList; + this.outputClusters = simpleDescriptor.outClusterList; + } + public hasBind(clusterId: number, target: Endpoint | Group): boolean { return this.getBindIndex(clusterId, target) !== -1; } @@ -516,7 +536,6 @@ class Endpoint extends Entity { public async bind(clusterKey: number | string, target: Endpoint | Group | number): Promise { const cluster = this.getCluster(clusterKey); - const type = target instanceof Endpoint ? 'endpoint' : 'group'; if (typeof target === 'number') { target = Group.byGroupID(target) || Group.create(target); @@ -528,16 +547,25 @@ class Endpoint extends Entity { logger.debug(log, NS); try { - await Entity.adapter!.bind( - this.deviceNetworkAddress, - this.deviceIeeeAddress, + const zdoClusterId = Zdo.ClusterId.BIND_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest( + Entity.adapter!.hasZdoMessageOverhead, + zdoClusterId, + this.deviceIeeeAddress as EUI64, this.ID, cluster.ID, - destinationAddress, - type, - target instanceof Endpoint ? target.ID : undefined, + target instanceof Endpoint ? Zdo.UNICAST_BINDING : Zdo.MULTICAST_BINDING, + target instanceof Endpoint ? (target.deviceIeeeAddress as EUI64) : ZSpec.BLANK_EUI64, + target instanceof Group ? target.groupID : 0, + target instanceof Endpoint ? target.ID : 0xff, ); + const response = await Entity.adapter!.sendZdo(this.deviceIeeeAddress, this.deviceNetworkAddress, zdoClusterId, zdoPayload, false); + + if (!Zdo.Buffalo.checkStatus(response)) { + throw new Zdo.StatusError(response[0]); + } + this.addBindingInternal(cluster, target); } catch (error) { const err = error as Error; @@ -565,7 +593,6 @@ class Endpoint extends Entity { target = groupTarget; } - const type = target instanceof Endpoint ? 'endpoint' : 'group'; const destinationAddress = target instanceof Endpoint ? target.deviceIeeeAddress : target.groupID; const log = `${action} from '${target instanceof Endpoint ? `${destinationAddress}/${target.ID}` : destinationAddress}'`; const index = this.getBindIndex(cluster.ID, target); @@ -578,16 +605,25 @@ class Endpoint extends Entity { logger.debug(log, NS); try { - await Entity.adapter!.unbind( - this.deviceNetworkAddress, - this.deviceIeeeAddress, + const zdoClusterId = Zdo.ClusterId.UNBIND_REQUEST; + const zdoPayload = Zdo.Buffalo.buildRequest( + Entity.adapter!.hasZdoMessageOverhead, + zdoClusterId, + this.deviceIeeeAddress as EUI64, this.ID, cluster.ID, - destinationAddress, - type, - target instanceof Endpoint ? target.ID : undefined, + target instanceof Endpoint ? Zdo.UNICAST_BINDING : Zdo.MULTICAST_BINDING, + target instanceof Endpoint ? (target.deviceIeeeAddress as EUI64) : ZSpec.BLANK_EUI64, + target instanceof Group ? target.groupID : 0, + target instanceof Endpoint ? target.ID : 0xff, ); + const response = await Entity.adapter!.sendZdo(this.deviceIeeeAddress, this.deviceNetworkAddress, zdoClusterId, zdoPayload, false); + + if (!Zdo.Buffalo.checkStatus(response)) { + throw new Zdo.StatusError(response[0]); + } + this._binds.splice(index, 1); this.save(); } catch (error) { diff --git a/src/zspec/zdo/buffaloZdo.ts b/src/zspec/zdo/buffaloZdo.ts index 2b69a05846..3512c02d70 100644 --- a/src/zspec/zdo/buffaloZdo.ts +++ b/src/zspec/zdo/buffaloZdo.ts @@ -1067,7 +1067,7 @@ export class BuffaloZdo extends Buffalo { source: EUI64, sourceEndpoint: number, clusterId: ClusterId, - type: ClusterId, + type: number, destination: EUI64, groupAddress: number, destinationEndpoint: number, diff --git a/test/adapter/ember/emberAdapter.test.ts b/test/adapter/ember/emberAdapter.test.ts index dfb084f2b1..581371902a 100644 --- a/test/adapter/ember/emberAdapter.test.ts +++ b/test/adapter/ember/emberAdapter.test.ts @@ -2004,22 +2004,17 @@ describe('Ember Adapter Layer', () => { expect(spyEmit).toHaveBeenCalledWith('disconnected'); }); + it('Handles channel changed stack status', async () => { + mockEzspEmitter.emit('stackStatus', SLStatus.ZIGBEE_CHANNEL_CHANGED); + await flushPromises(); + expect(loggerSpies.info).toHaveBeenCalledWith(`[STACK STATUS] Channel changed.`, 'zh:ember'); + }); + it.each([ - ['getCoordinator', []], + ['getCoordinatorIEEE', []], ['getNetworkParameters', []], - ['changeChannel', [15]], ['permitJoin', [250, 1234]], ['permitJoin', [250]], - ['lqi', [1234]], - ['routingTable', [1234]], - ['nodeDescriptor', [1234]], - ['activeEndpoints', [1234]], - ['simpleDescriptor', [1234, 1]], - ['bind', [1234, '0x1122334455667788', 1, 0, '0xaabbccddee112233', 'endpoint', 1]], - ['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, '0x1122334455667788']], [ 'sendZclFrameToEndpoint', [ @@ -2052,21 +2047,8 @@ describe('Ember Adapter Layer', () => { await expect(adapter[funcName](...args)).rejects.toThrow(`[INTERPAN MODE] Cannot execute non-InterPAN commands.`); }); - it('Adapter impl: getCoordinator', async () => { - await expect(adapter.getCoordinator()).resolves.toStrictEqual({ - ieeeAddr: DEFAULT_COORDINATOR_IEEE, - networkAddress: ZSpec.COORDINATOR_ADDRESS, - manufacturerID: Zcl.ManufacturerCode.SILICON_LABORATORIES, - endpoints: FIXED_ENDPOINTS.map((ep) => { - return { - profileID: ep.profileId, - ID: ep.endpoint, - deviceID: ep.deviceId, - inputClusters: ep.inClusterList.slice(), // copy - outputClusters: ep.outClusterList.slice(), // copy - }; - }), - } as TsType.Coordinator); + it('Adapter impl: getCoordinatorIEEE', async () => { + await expect(adapter.getCoordinatorIEEE()).resolves.toStrictEqual(DEFAULT_COORDINATOR_IEEE); }); it('Adapter impl: getCoordinatorVersion', async () => { @@ -2198,39 +2180,6 @@ describe('Ember Adapter Layer', () => { expect(mockEzspGetNetworkParameters).toHaveBeenCalledTimes(1); }); - it('Adapter impl: changeChannel', async () => { - const spyResolveEvent = jest.spyOn( - // @ts-expect-error private - adapter.oneWaitress, - 'resolveEvent', - ); - - mockEzspSendBroadcast.mockImplementationOnce(() => { - setTimeout(async () => { - mockEzspEmitter.emit('stackStatus', SLStatus.ZIGBEE_CHANNEL_CHANGED); - await flushPromises(); - }, 300); - - return [SLStatus.OK, ++mockAPSSequence]; - }); - - const p = adapter.changeChannel(25); - - await jest.advanceTimersByTimeAsync(1000); - await p; - expect(mockEzspSendBroadcast).toHaveBeenCalledTimes(1); - expect(spyResolveEvent).toHaveBeenCalledWith(OneWaitressEvents.STACK_STATUS_CHANNEL_CHANGED); - }); - - it('Adapter impl: throws when changeChannel request fails', async () => { - mockEzspSendBroadcast.mockResolvedValueOnce([SLStatus.FAIL, 0]); - - await expect(adapter.changeChannel(25)).rejects.toThrow( - `~x~> [ZDO NWK_UPDATE_REQUEST BROADCAST to=65535 messageTag=1] Failed to send request with status=FAIL.`, - ); - expect(mockEzspSendBroadcast).toHaveBeenCalledTimes(1); - }); - it('Adapter impl: setTransmitPower', async () => { await expect(adapter.setTransmitPower(10)).resolves.toStrictEqual(undefined); expect(mockEzspSetRadioPower).toHaveBeenCalledTimes(1); @@ -2497,838 +2446,6 @@ describe('Ember Adapter Layer', () => { await expect(adapter.permitJoin(0)).rejects.toThrow(`[ZDO] Failed set join policy with status=FAIL.`); }); - it('Adapter impl: lqi', async () => { - const sender: NodeId = 1234; - const apsFrame: EmberApsFrame = { - profileId: Zdo.ZDO_PROFILE_ID, - clusterId: Zdo.ClusterId.LQI_TABLE_RESPONSE, - sourceEndpoint: Zdo.ZDO_ENDPOINT, - destinationEndpoint: Zdo.ZDO_ENDPOINT, - options: 0, - groupId: 0, - sequence: 0, - }; - - mockEzspSendUnicast - .mockImplementationOnce(() => { - setTimeout(async () => { - mockEzspEmitter.emit( - 'zdoResponse', - apsFrame, - sender, - Buffer.from([ - 1, - Zdo.Status.SUCCESS, - 2, // neighborTableEntries - 0, // startIndex - 1, // entryCount - ...DEFAULT_NETWORK_OPTIONS.extendedPanID!, // extendedPanId - 0x88, - 0x77, - 0x66, - 0x55, - 0x44, - 0x33, - 0x22, - 0x11, // eui64 - 0x67, - 0x45, // nwkAddress - 0b00110010, // deviceTypeByte - 0, // permitJoiningByte - 0, // depth - 234, // lqi - ]), - ); - await flushPromises(); - }, 300); - - return [SLStatus.OK, ++mockAPSSequence]; - }) - .mockImplementationOnce(() => { - setTimeout(async () => { - mockEzspEmitter.emit( - 'zdoResponse', - apsFrame, - sender, - Buffer.from([ - 1, - Zdo.Status.SUCCESS, - 2, // neighborTableEntries - 1, // startIndex - 1, // entryCount - ...DEFAULT_NETWORK_OPTIONS.extendedPanID!, // extendedPanId - 0x44, - 0x33, - 0x22, - 0x11, - 0x88, - 0x77, - 0x66, - 0x55, // eui64 - 0x23, - 0x32, // nwkAddress - 0b00010010, // deviceTypeByte - 0, // permitJoiningByte - 0, // depth - 145, // lqi - ]), - ); - await flushPromises(); - }, 300); - - return [SLStatus.OK, ++mockAPSSequence]; - }); - - const p = adapter.lqi(sender); - - await jest.advanceTimersByTimeAsync(5000); - await expect(p).resolves.toStrictEqual({ - neighbors: [ - { - ieeeAddr: '0x1122334455667788', - networkAddress: 0x4567, - linkquality: 234, - relationship: 0x03, - depth: 0, - }, - { - ieeeAddr: '0x5566778811223344', - networkAddress: 0x3223, - linkquality: 145, - relationship: 0x01, - depth: 0, - }, - ], - } as TsType.LQI); - }); - - it('Adapter impl: throws when lqi fails request', async () => { - mockEzspSendUnicast.mockResolvedValueOnce([SLStatus.FAIL, 0]); - - const p = defuseRejection(adapter.lqi(1234)); - - await jest.advanceTimersByTimeAsync(5000); - 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 () => { - const sender: NodeId = 1234; - const apsFrame: EmberApsFrame = { - profileId: Zdo.ZDO_PROFILE_ID, - clusterId: Zdo.ClusterId.ROUTING_TABLE_RESPONSE, - sourceEndpoint: Zdo.ZDO_ENDPOINT, - destinationEndpoint: Zdo.ZDO_ENDPOINT, - options: 0, - groupId: 0, - sequence: 0, - }; - - mockEzspSendUnicast - .mockImplementationOnce(() => { - setTimeout(async () => { - mockEzspEmitter.emit( - 'zdoResponse', - apsFrame, - sender, - Buffer.from([ - 1, - Zdo.Status.SUCCESS, - 2, // routingTableEntries - 0, // startIndex - 1, // entryCount - 0x98, - 0x76, // destinationAddress - 0, // statusByte - 0x56, - 0x34, // nextHopAddress - ]), - ); - await flushPromises(); - }, 300); - - return [SLStatus.OK, ++mockAPSSequence]; - }) - .mockImplementationOnce(() => { - setTimeout(async () => { - mockEzspEmitter.emit( - 'zdoResponse', - apsFrame, - sender, - Buffer.from([ - 1, - Zdo.Status.SUCCESS, - 2, // routingTableEntries - 1, // startIndex - 1, // entryCount - 0x67, - 0x45, // destinationAddress - 0b011, // statusByte - 0x85, - 0x34, // nextHopAddress - ]), - ); - await flushPromises(); - }, 300); - - return [SLStatus.OK, ++mockAPSSequence]; - }); - - const p = adapter.routingTable(sender); - - await jest.advanceTimersByTimeAsync(5000); - await expect(p).resolves.toStrictEqual({ - table: [ - { - destinationAddress: 0x7698, - status: 'ACTIVE', - nextHop: 0x3456, - }, - { - destinationAddress: 0x4567, - status: 'INACTIVE', - nextHop: 0x3485, - }, - ], - } as TsType.RoutingTable); - }); - - it('Adapter impl: throws when routingTable fails request', async () => { - mockEzspSendUnicast.mockResolvedValueOnce([SLStatus.FAIL, 0]); - - const p = defuseRejection(adapter.routingTable(1234)); - - await jest.advanceTimersByTimeAsync(5000); - 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 () => { - const sender: NodeId = 0x1234; - const apsFrame: EmberApsFrame = { - profileId: Zdo.ZDO_PROFILE_ID, - clusterId: Zdo.ClusterId.NODE_DESCRIPTOR_RESPONSE, - sourceEndpoint: Zdo.ZDO_ENDPOINT, - destinationEndpoint: Zdo.ZDO_ENDPOINT, - options: 0, - groupId: 0, - sequence: 0, - }; - - mockEzspSendUnicast.mockImplementationOnce(() => { - setTimeout(async () => { - mockEzspEmitter.emit( - 'zdoResponse', - apsFrame, - sender, - Buffer.from([ - 1, - Zdo.Status.SUCCESS, - 0x34, - 0x12, // nwkAddress - 0b00000000, // nodeDescByte1 - 0, // nodeDescByte2 - 0, // macCapFlags - 0x49, - 0x10, // manufacturerCode - 60, // maxBufSize - 0, - 0, // maxIncTxSize - 0, - 0, // serverMask - 0, - 0, // maxOutTxSize - 0, // deprecated1 - ]), - ); - await flushPromises(); - }, 300); - - return [SLStatus.OK, ++mockAPSSequence]; - }); - - const p = adapter.nodeDescriptor(sender); - - await jest.advanceTimersByTimeAsync(1000); - await expect(p).resolves.toStrictEqual({ - type: 'Coordinator', - manufacturerCode: 0x1049, - } as TsType.NodeDescriptor); - }); - - it('Adapter impl: nodeDescriptor for router', async () => { - const sender: NodeId = 0x1234; - const apsFrame: EmberApsFrame = { - profileId: Zdo.ZDO_PROFILE_ID, - clusterId: Zdo.ClusterId.NODE_DESCRIPTOR_RESPONSE, - sourceEndpoint: Zdo.ZDO_ENDPOINT, - destinationEndpoint: Zdo.ZDO_ENDPOINT, - options: 0, - groupId: 0, - sequence: 0, - }; - // for coverage of stackComplianceRevision detection - const serverMask = Zdo.Utils.createServerMask({ - primaryTrustCenter: 0, - backupTrustCenter: 0, - deprecated1: 0, - deprecated2: 0, - deprecated3: 0, - deprecated4: 0, - networkManager: 0, - reserved1: 0, - reserved2: 0, - stackComplianceRevision: 0, - }); - - mockEzspSendUnicast.mockImplementationOnce(() => { - setTimeout(async () => { - mockEzspEmitter.emit( - 'zdoResponse', - apsFrame, - sender, - Buffer.from([ - 1, - Zdo.Status.SUCCESS, - 0x34, - 0x12, // nwkAddress - 0b00000001, // nodeDescByte1 - 0, // nodeDescByte2 - 0, // macCapFlags - 0x56, - 0x67, // manufacturerCode - 60, // maxBufSize - 0, - 0, // maxIncTxSize - serverMask & 0xff, - (serverMask >> 8) & 0xff, // serverMask - 0, - 0, // maxOutTxSize - 0, // deprecated1 - ]), - ); - await flushPromises(); - }, 300); - - return [SLStatus.OK, ++mockAPSSequence]; - }); - - const p = adapter.nodeDescriptor(sender); - - await jest.advanceTimersByTimeAsync(1000); - await expect(p).resolves.toStrictEqual({ - type: 'Router', - manufacturerCode: 0x6756, - } as TsType.NodeDescriptor); - expect(loggerSpies.warning).toHaveBeenCalledWith( - `[ZDO] Device '${sender}' is only compliant to revision 'pre-21' of the ZigBee specification (current revision: 22).`, - 'zh:ember', - ); - }); - - it('Adapter impl: nodeDescriptor for end device', async () => { - const sender: NodeId = 0x1234; - const apsFrame: EmberApsFrame = { - profileId: Zdo.ZDO_PROFILE_ID, - clusterId: Zdo.ClusterId.NODE_DESCRIPTOR_RESPONSE, - sourceEndpoint: Zdo.ZDO_ENDPOINT, - destinationEndpoint: Zdo.ZDO_ENDPOINT, - options: 0, - groupId: 0, - sequence: 0, - }; - // for coverage of stackComplianceRevision detection - const serverMask = Zdo.Utils.createServerMask({ - primaryTrustCenter: 0, - backupTrustCenter: 0, - deprecated1: 0, - deprecated2: 0, - deprecated3: 0, - deprecated4: 0, - networkManager: 0, - reserved1: 0, - reserved2: 0, - stackComplianceRevision: 21, - }); - - mockEzspSendUnicast.mockImplementationOnce(() => { - setTimeout(async () => { - mockEzspEmitter.emit( - 'zdoResponse', - apsFrame, - sender, - Buffer.from([ - 1, - Zdo.Status.SUCCESS, - 0x34, - 0x12, // nwkAddress - 0b00000010, // nodeDescByte1 - 0, // nodeDescByte2 - 0, // macCapFlags - 0x56, - 0x67, // manufacturerCode - 60, // maxBufSize - 0, - 0, // maxIncTxSize - serverMask & 0xff, - (serverMask >> 8) & 0xff, // serverMask - 0, - 0, // maxOutTxSize - 0, // deprecated1 - ]), - ); - await flushPromises(); - }, 300); - - return [SLStatus.OK, ++mockAPSSequence]; - }); - - const p = adapter.nodeDescriptor(sender); - - await jest.advanceTimersByTimeAsync(1000); - await expect(p).resolves.toStrictEqual({ - type: 'EndDevice', - manufacturerCode: 0x6756, - } as TsType.NodeDescriptor); - expect(loggerSpies.warning).toHaveBeenCalledWith( - `[ZDO] Device '${sender}' is only compliant to revision '21' of the ZigBee specification (current revision: 22).`, - 'zh:ember', - ); - }); - - it('Adapter impl: throws when nodeDescriptor fails request', async () => { - mockEzspSendUnicast.mockResolvedValueOnce([SLStatus.FAIL, 0]); - - const p = defuseRejection(adapter.nodeDescriptor(1234)); - - await jest.advanceTimersByTimeAsync(5000); - 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 () => { - const sender: NodeId = 0x1234; - const apsFrame: EmberApsFrame = { - profileId: Zdo.ZDO_PROFILE_ID, - clusterId: Zdo.ClusterId.ACTIVE_ENDPOINTS_RESPONSE, - sourceEndpoint: Zdo.ZDO_ENDPOINT, - destinationEndpoint: Zdo.ZDO_ENDPOINT, - options: 0, - groupId: 0, - sequence: 0, - }; - - mockEzspSendUnicast.mockImplementationOnce(() => { - setTimeout(async () => { - mockEzspEmitter.emit( - 'zdoResponse', - apsFrame, - sender, - Buffer.from([ - 1, - Zdo.Status.SUCCESS, - 0x34, - 0x12, // nwkAddress - 2, // endpointCount - 1, - 43, // endpointList - ]), - ); - await flushPromises(); - }, 300); - - return [SLStatus.OK, ++mockAPSSequence]; - }); - - const p = adapter.activeEndpoints(sender); - - await jest.advanceTimersByTimeAsync(1000); - await expect(p).resolves.toStrictEqual({ - endpoints: [1, 43], - } as TsType.ActiveEndpoints); - }); - - it('Adapter impl: throws when activeEndpoints fails request', async () => { - mockEzspSendUnicast.mockResolvedValueOnce([SLStatus.FAIL, 0]); - - const p = defuseRejection(adapter.activeEndpoints(1234)); - - await jest.advanceTimersByTimeAsync(5000); - 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 () => { - const sender: NodeId = 0x1234; - const endpoint: number = 1; - const apsFrame: EmberApsFrame = { - profileId: Zdo.ZDO_PROFILE_ID, - clusterId: Zdo.ClusterId.SIMPLE_DESCRIPTOR_RESPONSE, - sourceEndpoint: Zdo.ZDO_ENDPOINT, - destinationEndpoint: Zdo.ZDO_ENDPOINT, - options: 0, - groupId: 0, - sequence: 0, - }; - - mockEzspSendUnicast.mockImplementationOnce(() => { - setTimeout(async () => { - mockEzspEmitter.emit( - 'zdoResponse', - apsFrame, - sender, - Buffer.from([ - 1, - Zdo.Status.SUCCESS, - 0x34, - 0x12, // nwkAddress - 18, // length - endpoint, // endpoint - 0x33, - 0x44, // profileId - 0x00, - 0x66, // deviceId - 1, // deviceVersion - 2, // inClusterCount - 0x00, - 0x00, - 0x03, - 0x00, // inClusterList - 3, // outClusterCount - 0x01, - 0x00, - 0x08, - 0x00, - 0x79, - 0x23, // outClusterList - ]), - ); - await flushPromises(); - }, 300); - - return [SLStatus.OK, ++mockAPSSequence]; - }); - - const p = adapter.simpleDescriptor(sender, endpoint); - - await jest.advanceTimersByTimeAsync(1000); - await expect(p).resolves.toStrictEqual({ - profileID: 0x4433, - endpointID: endpoint, - deviceID: 0x6600, - inputClusters: [0x00, 0x03], - outputClusters: [0x01, 0x08, 0x2379], - } as TsType.SimpleDescriptor); - }); - - it('Adapter impl: throws when simpleDescriptor fails request', async () => { - mockEzspSendUnicast.mockResolvedValueOnce([SLStatus.FAIL, 0]); - - const p = defuseRejection(adapter.simpleDescriptor(1234, 1)); - - await jest.advanceTimersByTimeAsync(5000); - await expect(p).rejects.toThrow( - `~x~> [ZDO SIMPLE_DESCRIPTOR_REQUEST UNICAST to=0xFFFFFFFFFFFFFFFF:1234 messageTag=1] Failed to send request with status=FAIL.`, - ); - }); - - it('Adapter impl: bind endpoint', async () => { - const sender: NodeId = 0x1234; - const apsFrame: EmberApsFrame = { - profileId: Zdo.ZDO_PROFILE_ID, - clusterId: Zdo.ClusterId.BIND_RESPONSE, - sourceEndpoint: Zdo.ZDO_ENDPOINT, - destinationEndpoint: Zdo.ZDO_ENDPOINT, - options: 0, - groupId: 0, - sequence: 0, - }; - - mockEzspSendUnicast.mockImplementationOnce(() => { - setTimeout(async () => { - mockEzspEmitter.emit('zdoResponse', apsFrame, sender, Buffer.from([1, Zdo.Status.SUCCESS])); - await flushPromises(); - }, 300); - - return [SLStatus.OK, ++mockAPSSequence]; - }); - - const p = adapter.bind(sender, '0x1122334455667788', 1, Zcl.Clusters.genBasic.ID, DEFAULT_COORDINATOR_IEEE, 'endpoint', 1); - - await jest.advanceTimersByTimeAsync(1000); - await expect(p).resolves.toStrictEqual(undefined); - - // verify ZDO payload - expect(mockEzspSendUnicast.mock.calls[0][4]).toStrictEqual( - Buffer.from([ - 0x01, // seq - 0x88, - 0x77, - 0x66, - 0x55, - 0x44, - 0x33, - 0x22, - 0x11, // sourceIeeeAddress - 0x01, // sourceEndpoint - 0x00, - 0x00, // clusterID - 0x03, // type - 0x11, - 0x22, - 0x33, - 0x44, - 0x55, - 0x66, - 0x77, - 0x88, // destination DEFAULT_COORDINATOR_IEEE - 0x01, // destinationEndpoint - ]), - ); - }); - - it('Adapter impl: throws when bind endpoint fails request', async () => { - mockEzspSendUnicast.mockResolvedValueOnce([SLStatus.FAIL, 0]); - - 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( - `~x~> [ZDO BIND_REQUEST UNICAST to=0xFFFFFFFFFFFFFFFF:1234 messageTag=1] Failed to send request with status=FAIL.`, - ); - }); - - it('Adapter impl: bind group', async () => { - const sender: NodeId = 0x1234; - const apsFrame: EmberApsFrame = { - profileId: Zdo.ZDO_PROFILE_ID, - clusterId: Zdo.ClusterId.BIND_RESPONSE, - sourceEndpoint: Zdo.ZDO_ENDPOINT, - destinationEndpoint: Zdo.ZDO_ENDPOINT, - options: 0, - groupId: 0, - sequence: 0, - }; - - mockEzspSendUnicast.mockImplementationOnce(() => { - setTimeout(async () => { - mockEzspEmitter.emit('zdoResponse', apsFrame, sender, Buffer.from([1, Zdo.Status.SUCCESS])); - await flushPromises(); - }, 300); - - return [SLStatus.OK, ++mockAPSSequence]; - }); - - const p = adapter.bind(sender, '0x1122334455667788', 1, Zcl.Clusters.genBasic.ID, 987, 'group', 1); - - await jest.advanceTimersByTimeAsync(1000); - await expect(p).resolves.toStrictEqual(undefined); - - // verify ZDO payload - expect(mockEzspSendUnicast.mock.calls[0][4]).toStrictEqual( - Buffer.from([ - 0x01, // seq - 0x88, - 0x77, - 0x66, - 0x55, - 0x44, - 0x33, - 0x22, - 0x11, // sourceIeeeAddress - 0x01, // sourceEndpoint - 0x00, - 0x00, // clusterID - 0x01, // type - 0xdb, - 0x03, // destination - ]), - ); - }); - - it('Adapter impl: throws when bind group fails request', async () => { - mockEzspSendUnicast.mockResolvedValueOnce([SLStatus.FAIL, 0]); - - const p = defuseRejection(adapter.bind(1234, '0x1122334455667788', 1, Zcl.Clusters.genBasic.ID, 987, 'group')); - - await jest.advanceTimersByTimeAsync(5000); - await expect(p).rejects.toThrow( - `~x~> [ZDO BIND_REQUEST UNICAST to=0xFFFFFFFFFFFFFFFF:1234 messageTag=1] Failed to send request with status=FAIL.`, - ); - }); - - it('Adapter impl: unbind endpoint', async () => { - const sender: NodeId = 0x1234; - const apsFrame: EmberApsFrame = { - profileId: Zdo.ZDO_PROFILE_ID, - clusterId: Zdo.ClusterId.UNBIND_RESPONSE, - sourceEndpoint: Zdo.ZDO_ENDPOINT, - destinationEndpoint: Zdo.ZDO_ENDPOINT, - options: 0, - groupId: 0, - sequence: 0, - }; - - mockEzspSendUnicast.mockImplementationOnce(() => { - setTimeout(async () => { - mockEzspEmitter.emit('zdoResponse', apsFrame, sender, Buffer.from([1, Zdo.Status.SUCCESS])); - await flushPromises(); - }, 300); - - return [SLStatus.OK, ++mockAPSSequence]; - }); - - const p = adapter.unbind(sender, '0x1122334455667788', 1, Zcl.Clusters.genBasic.ID, DEFAULT_COORDINATOR_IEEE, 'endpoint', 1); - - await jest.advanceTimersByTimeAsync(1000); - await expect(p).resolves.toStrictEqual(undefined); - - // verify ZDO payload - expect(mockEzspSendUnicast.mock.calls[0][4]).toStrictEqual( - Buffer.from([ - 0x01, // seq - 0x88, - 0x77, - 0x66, - 0x55, - 0x44, - 0x33, - 0x22, - 0x11, // sourceIeeeAddress - 0x01, // sourceEndpoint - 0x00, - 0x00, // clusterID - 0x03, // type - 0x11, - 0x22, - 0x33, - 0x44, - 0x55, - 0x66, - 0x77, - 0x88, // destination DEFAULT_COORDINATOR_IEEE - 0x01, // destinationEndpoint - ]), - ); - }); - - it('Adapter impl: throws when unbind endpoint fails request', async () => { - mockEzspSendUnicast.mockResolvedValueOnce([SLStatus.FAIL, 0]); - - const p = defuseRejection( - adapter.unbind(1234, '0x1122334455667788', 1, Zcl.Clusters.genBasic.ID, DEFAULT_COORDINATOR_IEEE, 'endpoint', 1), - ); - - await jest.advanceTimersByTimeAsync(5000); - await expect(p).rejects.toThrow( - `~x~> [ZDO UNBIND_REQUEST UNICAST to=0xFFFFFFFFFFFFFFFF:1234 messageTag=1] Failed to send request with status=FAIL.`, - ); - }); - - it('Adapter impl: unbind group', async () => { - const sender: NodeId = 0x1234; - const apsFrame: EmberApsFrame = { - profileId: Zdo.ZDO_PROFILE_ID, - clusterId: Zdo.ClusterId.UNBIND_RESPONSE, - sourceEndpoint: Zdo.ZDO_ENDPOINT, - destinationEndpoint: Zdo.ZDO_ENDPOINT, - options: 0, - groupId: 0, - sequence: 0, - }; - - mockEzspSendUnicast.mockImplementationOnce(() => { - setTimeout(async () => { - mockEzspEmitter.emit('zdoResponse', apsFrame, sender, Buffer.from([1, Zdo.Status.SUCCESS])); - await flushPromises(); - }, 300); - - return [SLStatus.OK, ++mockAPSSequence]; - }); - - const p = adapter.unbind(sender, '0x1122334455667788', 1, Zcl.Clusters.genBasic.ID, 987, 'group', 1); - - await jest.advanceTimersByTimeAsync(1000); - await expect(p).resolves.toStrictEqual(undefined); - - // verify ZDO payload - expect(mockEzspSendUnicast.mock.calls[0][4]).toStrictEqual( - Buffer.from([ - 0x01, // seq - 0x88, - 0x77, - 0x66, - 0x55, - 0x44, - 0x33, - 0x22, - 0x11, // sourceIeeeAddress - 0x01, // sourceEndpoint - 0x00, - 0x00, // clusterID - 0x01, // type - 0xdb, - 0x03, // destination - ]), - ); - }); - - it('Adapter impl: throws when unbind group fails request', async () => { - mockEzspSendUnicast.mockResolvedValueOnce([SLStatus.FAIL, 0]); - - const p = defuseRejection(adapter.unbind(1234, '0x1122334455667788', 1, Zcl.Clusters.genBasic.ID, 987, 'group')); - - await jest.advanceTimersByTimeAsync(5000); - await expect(p).rejects.toThrow( - `~x~> [ZDO UNBIND_REQUEST UNICAST to=0xFFFFFFFFFFFFFFFF:1234 messageTag=1] Failed to send request with status=FAIL.`, - ); - }); - - it('Adapter impl: removeDevice', async () => { - const sender: NodeId = 0x1234; - const apsFrame: EmberApsFrame = { - profileId: Zdo.ZDO_PROFILE_ID, - clusterId: Zdo.ClusterId.LEAVE_RESPONSE, - sourceEndpoint: Zdo.ZDO_ENDPOINT, - destinationEndpoint: Zdo.ZDO_ENDPOINT, - options: 0, - groupId: 0, - sequence: 0, - }; - - mockEzspSendUnicast.mockImplementationOnce(() => { - setTimeout(async () => { - mockEzspEmitter.emit('zdoResponse', apsFrame, sender, Buffer.from([1, Zdo.Status.SUCCESS])); - await flushPromises(); - }, 300); - - return [SLStatus.OK, ++mockAPSSequence]; - }); - - const p = adapter.removeDevice(sender, '0x1122334455667788'); - - await jest.advanceTimersByTimeAsync(1000); - await expect(p).resolves.toStrictEqual(undefined); - }); - - it('Adapter impl: throws when removeDevice fails request', async () => { - const sender: NodeId = 1234; - const ieee: EUI64 = '0x1122334455667788'; - - mockEzspSendUnicast.mockResolvedValueOnce([SLStatus.FAIL, 0]); - - const p = defuseRejection(adapter.removeDevice(sender, ieee)); - - await jest.advanceTimersByTimeAsync(5000); - await expect(p).rejects.toThrow( - `~x~> [ZDO LEAVE_REQUEST UNICAST to=0xFFFFFFFFFFFFFFFF:1234 messageTag=1] Failed to send request with status=FAIL.`, - ); - }); - it('Adapter impl: sendZclFrameToEndpoint with command response with fixed source endpoint', async () => { const networkAddress: NodeId = 1234; const endpoint: number = 1; diff --git a/test/adapter/z-stack/adapter.test.ts b/test/adapter/z-stack/adapter.test.ts index 84f833b78f..1048334dc6 100644 --- a/test/adapter/z-stack/adapter.test.ts +++ b/test/adapter/z-stack/adapter.test.ts @@ -1,6 +1,5 @@ import 'regenerator-runtime/runtime'; -import assert from 'assert'; import * as fs from 'fs'; import * as path from 'path'; @@ -31,7 +30,6 @@ import { RoutingTableResponse, SimpleDescriptorResponse, } from '../../../src/zspec/zdo/definition/tstypes'; -import {ZdoStatusError} from '../../../src/zspec/zdo/zdoStatusError'; const DUMMY_NODE_DESC_RSP_CAPABILITIES = { allocateAddress: 0, @@ -2103,123 +2101,11 @@ describe('zstack-adapter', () => { expect(mockZnpClose).toHaveBeenCalledTimes(1); }); - it('Get coordinator', async () => { + it('Get coordinator IEEE', 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, - manufacturerID: 0, - ieeeAddr: '0x00124b0009d80ba7', - endpoints: [ - { - ID: 1, - profileID: 123, - deviceID: 5, - inputClusters: [1], - outputClusters: [2], - }, - { - ID: 2, - profileID: 124, - deviceID: 7, - inputClusters: [8], - outputClusters: [9], - }, - { - ID: 3, - profileID: 124, - deviceID: 7, - inputClusters: [8], - outputClusters: [9], - }, - { - ID: 4, - profileID: 124, - deviceID: 7, - inputClusters: [8], - outputClusters: [9], - }, - { - ID: 5, - profileID: 124, - deviceID: 7, - inputClusters: [8], - outputClusters: [9], - }, - { - ID: 6, - profileID: 124, - deviceID: 7, - inputClusters: [8], - outputClusters: [9], - }, - { - ID: 8, - profileID: 124, - deviceID: 7, - inputClusters: [8], - outputClusters: [9], - }, - { - ID: 10, - profileID: 124, - deviceID: 7, - inputClusters: [8], - outputClusters: [9], - }, - { - ID: 11, - profileID: 124, - deviceID: 7, - inputClusters: [8], - outputClusters: [9], - }, - { - ID: 110, - profileID: 124, - deviceID: 7, - inputClusters: [8], - outputClusters: [9], - }, - { - ID: 12, - profileID: 124, - deviceID: 7, - inputClusters: [8], - outputClusters: [9], - }, - { - ID: 13, - profileID: 124, - deviceID: 7, - inputClusters: [8], - outputClusters: [9], - }, - { - ID: 47, - profileID: 124, - deviceID: 7, - inputClusters: [8], - outputClusters: [9], - }, - { - ID: 242, - profileID: 124, - deviceID: 7, - inputClusters: [8], - outputClusters: [9], - }, - ], - }; - expect(info).toStrictEqual(expected); - spyDesc.mockRestore(); + const ieee = await adapter.getCoordinatorIEEE(); + expect(ieee).toStrictEqual('0x00124b0009d80ba7'); }); it('Permit join all', async () => { @@ -2303,37 +2189,6 @@ describe('zstack-adapter', () => { expect(mockZnpRequest).toHaveBeenCalledWith(Subsystem.SYS, 'resetReq', {type: 0}); }); - it('Change channel', async () => { - basicMocks(); - await adapter.start(); - mockZnpRequestZdo.mockClear(); - mockQueueExecute.mockClear(); - await adapter.changeChannel(25); - 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 () => { basicMocks(); adapter = new ZStackAdapter(networkOptions, serialPortOptions, 'backup.json', {transmitPower: 2, disableLED: false}); @@ -2371,63 +2226,6 @@ describe('zstack-adapter', () => { expect(mockZnpRequest).not.toHaveBeenCalledWith(Subsystem.UTIL, 'ledControl', {ledid: 3, mode: 0}, undefined, 500); }); - it('Node descriptor', async () => { - basicMocks(); - let result; - await adapter.start(); - - mockZnpRequestZdo.mockClear(); - mockQueueExecute.mockClear(); - result = await adapter.nodeDescriptor(2); - 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'}); - - mockZnpRequestZdo.mockClear(); - mockQueueExecute.mockClear(); - result = await adapter.nodeDescriptor(1); - expect(result).toStrictEqual({manufacturerCode: 2, type: 'Coordinator'}); - - mockZnpRequestZdo.mockClear(); - mockQueueExecute.mockClear(); - result = await adapter.nodeDescriptor(3); - expect(result).toStrictEqual({manufacturerCode: 6, type: 'EndDevice'}); - - mockZnpRequestZdo.mockClear(); - mockQueueExecute.mockClear(); - result = await adapter.nodeDescriptor(5); - expect(result).toStrictEqual({manufacturerCode: 10, type: 'Unknown'}); - }); - - it('Active endpoints', async () => { - basicMocks(); - await adapter.start(); - mockQueueExecute.mockClear(); - - const result = await adapter.activeEndpoints(3); - expect(mockQueueExecute.mock.calls[0][1]).toBe(3); - expect(result).toStrictEqual({endpoints: [1, 2, 3, 4, 5, 6, 8, 10, 11, 110, 12, 13, 47, 242]}); - }); - - it('Simple descriptor', async () => { - basicMocks(); - await adapter.start(); - mockZnpRequest.mockClear(); - mockQueueExecute.mockClear(); - - simpleDescriptorEndpoint = 20; - const result = await adapter.simpleDescriptor(1, simpleDescriptorEndpoint); - expect(mockQueueExecute.mock.calls[0][1]).toBe(1); - expect(result).toStrictEqual({deviceID: 7, endpointID: simpleDescriptorEndpoint, inputClusters: [8], outputClusters: [9], profileID: 124}); - }); - it('Send zcl frame network address', async () => { basicMocks(); await adapter.start(); @@ -3511,200 +3309,6 @@ describe('zstack-adapter', () => { expect(await adapter.supportsBackup()).toBeTruthy(); }); - it('LQI', async () => { - basicMocks(); - await adapter.start(); - mockZnpRequestZdo.mockClear(); - mockQueueExecute.mockClear(); - - const result = await adapter.lqi(203); - expect(mockQueueExecute.mock.calls[0][1]).toBe(203); - 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: 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('Routing table', async () => { - basicMocks(); - await adapter.start(); - mockZnpRequestZdo.mockClear(); - mockQueueExecute.mockClear(); - - const result = await adapter.routingTable(205); - expect(mockQueueExecute.mock.calls[0][1]).toBe(205); - 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: 12, status: 'ACTIVE', nextHop: 3}, - {destinationAddress: 13, status: 'ACTIVE', nextHop: 3}, - {destinationAddress: 14, status: 'ACTIVE', nextHop: 3}, - ], - }); - }); - - it('Bind endpoint', async () => { - basicMocks(); - await adapter.start(); - 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(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 group', async () => { - basicMocks(); - await adapter.start(); - 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(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 endpoint', async () => { - basicMocks(); - await adapter.start(); - 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(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(); - mockZnpRequestZdo.mockClear(); - mockQueueExecute.mockClear(); - - 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(); - mockZnpRequestZdo.mockClear(); - mockQueueExecute.mockClear(); - - 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(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 () => { basicMocks(); await adapter.start(); @@ -4367,4 +3971,13 @@ describe('zstack-adapter', () => { await expect(adapter.sendZdo(ZSpec.BLANK_EUI64, 1234, clusterId, zdoPayload, true)).rejects.toThrow('Failed'); }); + + it('Should throw error when registerEndpoints fails', async () => { + basicMocks(); + jest.spyOn(adapter, 'sendZdo').mockResolvedValueOnce([Zdo.Status.NOT_ACTIVE, undefined]); + + expect(async () => { + await adapter.start(); + }).rejects.toThrow(`Status 'NOT_ACTIVE'`); + }); }); diff --git a/test/controller.test.ts b/test/controller.test.ts index 04d09e9136..20ced46dfc 100755 --- a/test/controller.test.ts +++ b/test/controller.test.ts @@ -3,7 +3,7 @@ import 'regenerator-runtime/runtime'; import fs from 'fs'; import path from 'path'; -import Bonjour, {BrowserConfig, Service} from 'bonjour-service'; +import Bonjour, {Browser, BrowserConfig, Service} from 'bonjour-service'; import equals from 'fast-deep-equal/es6'; import {Adapter} from '../src/adapter'; @@ -19,9 +19,12 @@ import * as Models from '../src/models'; import {Wait} from '../src/utils'; import * as Utils from '../src/utils'; 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'; +import {IEEEAddressResponse, NetworkAddressResponse} from '../src/zspec/zdo/definition/tstypes'; +import {DEFAULT_184_CHECKIN_INTERVAL, LQI_TABLE_ENTRY_DEFAULTS, MOCK_DEVICES, ROUTING_TABLE_ENTRY_DEFAULTS} from './mockDevices'; const globalSetImmediate = setImmediate; const flushPromises = () => new Promise(globalSetImmediate); @@ -36,9 +39,9 @@ const mockLogger = { }; let skipWait = true; -Wait.mockImplementation((milliseconds) => { +(Wait as ReturnType).mockImplementation((milliseconds) => { if (!skipWait) { - return new Promise((resolve): void => { + return new Promise((resolve) => { setTimeout((): void => resolve(), milliseconds); }); } @@ -61,59 +64,108 @@ const mockAdapterReset = jest.fn(); const mockAdapterStop = jest.fn(); const mockAdapterStart = jest.fn().mockReturnValue('resumed'); const mockAdapterSetTransmitPower = jest.fn(); -const mockAdapterChangeChannel = jest.fn(); -const mockAdapterGetCoordinator = jest.fn().mockReturnValue({ - ieeeAddr: '0x0000012300000000', - networkAddress: 123, - manufacturerID: 100, - endpoints: [ - {ID: 1, profileID: 2, deviceID: 3, inputClusters: [10], outputClusters: [11]}, - {ID: 2, profileID: 3, deviceID: 5, inputClusters: [1], outputClusters: [0]}, - ], -}); -const mockAdapterNodeDescriptor = jest.fn().mockImplementation(async (networkAddress) => { - const descriptor = mockDevices[networkAddress].nodeDescriptor; - if (typeof descriptor === 'string' && descriptor.startsWith('xiaomi')) { - const frame = Zcl.Frame.create( - 0, - 1, - true, - undefined, - 10, - 'readRsp', - 0, - [{attrId: 5, status: 0, dataType: 66, attrData: 'lumi.occupancy'}], - {}, - ); - await mockAdapterEvents['zclPayload']({ - wasBroadcast: false, - address: networkAddress, - clusterID: frame.cluster.ID, - data: frame.toBuffer(), - header: frame.header, - endpoint: 1, - linkquality: 50, - groupID: 1, - }); - - if (descriptor.endsWith('error')) { - throw new Error('failed'); - } else { - return {type: 'EndDevice', manufacturerCode: 1219}; - } - } else { - return descriptor; - } -}); - +const mockAdapterGetCoordinatorIEEE = jest.fn().mockReturnValue('0x0000012300000000'); const mockAdapterGetNetworkParameters = jest.fn().mockReturnValue({panID: 1, extendedPanID: 3, channel: 15}); -const mockAdapterBind = jest.fn(); const mocksendZclFrameToGroup = jest.fn(); const mocksendZclFrameToAll = jest.fn(); const mockAddInstallCode = jest.fn(); -const mockAdapterUnbind = jest.fn(); -const mockAdapterRemoveDevice = jest.fn(); const mocksendZclFrameToEndpoint = jest.fn(); +let sendZdoResponseStatus = Zdo.Status.SUCCESS; +const mockAdapterSendZdo = jest + .fn() + .mockImplementation(async (ieeeAddress: string, networkAddress: number, clusterId: Zdo.ClusterId, payload: Buffer, disableResponse: true) => { + if (sendZdoResponseStatus !== Zdo.Status.SUCCESS) { + return [sendZdoResponseStatus, undefined]; + } + + if (ZSpec.BroadcastAddress[networkAddress]) { + // TODO + } else { + const device = MOCK_DEVICES[networkAddress]; + + if (!device) { + throw new Error(`Mock device ${networkAddress} not found`); + } + + switch (clusterId) { + case Zdo.ClusterId.NODE_DESCRIPTOR_REQUEST: { + if (device.key === 'xiaomi') { + const frame = Zcl.Frame.create( + 0, + 1, + true, + undefined, + 10, + 'readRsp', + 0, + [{attrId: 5, status: 0, dataType: 66, attrData: 'lumi.occupancy'}], + {}, + ); + await mockAdapterEvents['zclPayload']({ + wasBroadcast: false, + address: networkAddress, + clusterID: frame.cluster.ID, + data: frame.toBuffer(), + header: frame.header, + endpoint: 1, + linkquality: 50, + groupID: 1, + }); + } + + if (!device.nodeDescriptor) { + throw new Error('NODE_DESCRIPTOR_REQUEST timeout'); + } else { + return device.nodeDescriptor; + } + } + + case Zdo.ClusterId.ACTIVE_ENDPOINTS_REQUEST: { + if (!device.activeEndpoints) { + throw new Error('ACTIVE_ENDPOINTS_REQUEST timeout'); + } else { + return device.activeEndpoints; + } + } + + case Zdo.ClusterId.SIMPLE_DESCRIPTOR_REQUEST: { + if (!device.simpleDescriptor) { + throw new Error('SIMPLE_DESCRIPTOR_REQUEST timeout'); + } + + // XXX: only valid if hasZdoMessageOverhead === false + const endpoint = payload[2]; + + if (device.simpleDescriptor[endpoint] === undefined) { + throw new Error(`SIMPLE_DESCRIPTOR_REQUEST(${endpoint}) timeout`); + } + + return device.simpleDescriptor[endpoint]; + } + + case Zdo.ClusterId.LQI_TABLE_REQUEST: { + if (!device.lqiTable) { + throw new Error('LQI_TABLE_REQUEST timeout'); + } else { + return device.lqiTable; + } + } + + case Zdo.ClusterId.ROUTING_TABLE_REQUEST: { + if (!device.routingTable) { + throw new Error('ROUTING_TABLE_REQUEST timeout'); + } else { + return device.routingTable; + } + } + + default: { + // Zdo.ClusterId.LEAVE_REQUEST, Zdo.ClusterId.BIND_REQUEST, Zdo.ClusterId.UNBIND_REQUEST + return [Zdo.Status.SUCCESS, undefined]; + } + } + } + }); let iasZoneReadState170Count = 0; let enroll170 = true; @@ -127,7 +179,7 @@ const restoreMocksendZclFrameToEndpoint = () => { frame.isCommand('read') && (frame.isCluster('genBasic') || frame.isCluster('ssIasZone') || frame.isCluster('genPollCtrl') || frame.isCluster('hvacThermostat')) ) { - const payload = []; + const payload: {[key: string]: unknown}[] = []; const cluster = frame.cluster; for (const item of frame.payload) { if (item.attrId !== 65314) { @@ -144,7 +196,7 @@ const restoreMocksendZclFrameToEndpoint = () => { payload.push({ attrId: item.attrId, dataType: attribute.type, - attrData: mockDevices[networkAddress].attributes[endpoint][attribute.name], + attrData: MOCK_DEVICES[networkAddress]!.attributes![endpoint][attribute.name], status: 0, }); } @@ -203,7 +255,7 @@ const restoreMocksendZclFrameToEndpoint = () => { } if (frame.header.isGlobal && frame.isCommand('write')) { - const payload = []; + const payload: {[key: string]: unknown}[] = []; for (const item of frame.payload) { payload.push({attrId: item.attrId, status: 0}); } @@ -238,13 +290,12 @@ const mocksClear = [ mockAdapterReset, mocksendZclFrameToGroup, mockSetChannelInterPAN, - mockAdapterNodeDescriptor, mocksendZclFrameInterPANToIeeeAddr, mocksendZclFrameInterPANBroadcast, mockRestoreChannelInterPAN, mockAddInstallCode, mockAdapterGetNetworkParameters, - mockAdapterChangeChannel, + mockAdapterSendZdo, mockLogger.debug, mockLogger.info, mockLogger.warning, @@ -262,220 +313,6 @@ const equalsPartial = (object, expected) => { return true; }; -const default174CheckinInterval = 50; -const mockDevices = { - 129: { - nodeDescriptor: {type: 'Router', manufacturerCode: 1212}, - activeEndpoints: {endpoints: [1]}, - simpleDescriptor: {1: {endpointID: 1, deviceID: 5, inputClusters: [0, 1], outputClusters: [2], profileID: 99}}, - attributes: { - 1: { - modelId: 'myModelID', - manufacturerName: 'KoenAndCo', - zclVersion: 1, - appVersion: 2, - hwVersion: 3, - dateCode: '201901', - swBuildId: '1.01', - powerSource: 1, - stackVersion: 101, - }, - }, - }, - 140: { - nodeDescriptor: undefined, - }, - 150: { - nodeDescriptor: 'xiaomi_error', - }, - 151: { - nodeDescriptor: 'xiaomi', - activeEndpoints: 'error', - }, - 160: { - nodeDescriptor: {type: 'Router', manufacturerCode: 1212}, - activeEndpoints: {endpoints: []}, - attributes: {}, - }, - 161: { - nodeDescriptor: {type: 'Router', manufacturerCode: 1213}, - activeEndpoints: {endpoints: [4, 1]}, - simpleDescriptor: { - 1: {endpointID: 1, deviceID: 5, inputClusters: [0, 1], outputClusters: [2], profileID: 99}, - 4: {endpointID: 1, deviceID: 5, inputClusters: [1], outputClusters: [2], profileID: 99}, - }, - attributes: { - 1: { - modelId: 'myDevice9123', - manufacturerName: 'Boef', - zclVersion: 1, - appVersion: 2, - hwVersion: 3, - dateCode: '201901', - swBuildId: '1.01', - powerSource: 1, - stackVersion: 101, - }, - 4: {}, - }, - }, - 162: { - nodeDescriptor: {type: 'Router', manufacturerCode: 1213}, - activeEndpoints: {endpoints: [2, 1]}, - simpleDescriptor: { - 1: {endpointID: 1, deviceID: 5, inputClusters: [1], outputClusters: [2], profileID: 99}, - 2: {endpointID: 2, deviceID: 5, inputClusters: [0, 1], outputClusters: [2], profileID: 99}, - }, - attributes: { - 2: { - modelId: 'myDevice9124', - manufacturerName: 'Boef', - zclVersion: 1, - appVersion: 2, - hwVersion: 3, - dateCode: '201901', - swBuildId: '1.01', - powerSource: 1, - stackVersion: 101, - }, - 1: {}, - }, - }, - 170: { - nodeDescriptor: {type: 'EndDevice', manufacturerCode: 4619}, - activeEndpoints: {endpoints: [1]}, - simpleDescriptor: {1: {endpointID: 1, deviceID: 5, inputClusters: [0, 1280], outputClusters: [2], profileID: 99}}, - attributes: { - 1: { - zoneState: 0, - iasCieAddr: '0x0000012300000000', - modelId: 'myIasDevice', - manufacturerName: 'KoenAndCoSecurity', - zclVersion: 1, - appVersion: 2, - hwVersion: 3, - dateCode: '201901', - swBuildId: '1.01', - powerSource: 1, - stackVersion: 101, - }, - }, - }, - 171: { - // Xiaomi WXCJKG11LM - https://github.com/koenkk/zigbee2mqtt/issues/2844 - nodeDescriptor: {type: 'EndDevice', manufacturerCode: 1212}, - activeEndpoints: {endpoints: [1, 2, 3, 4, 5, 6]}, - simpleDescriptor: {1: {endpointID: 1, deviceID: 5, inputClusters: [0, 1, 2], outputClusters: [2], profileID: 99}}, - attributes: { - 1: { - modelId: 'lumi.remote.b286opcn01', - manufacturerName: 'Xioami', - zclVersion: 1, - appVersion: 2, - hwVersion: 3, - dateCode: '201901', - swBuildId: '1.01', - powerSource: 1, - stackVersion: 101, - }, - }, - }, - 172: { - // Gledopto GL-C-007/GL-C-008 - https://github.com/Koenkk/zigbee2mqtt/issues/2872 - // All endpoints announce to support genBasic but only endpoint 12 really responds - nodeDescriptor: {type: 'Router', manufacturerCode: 1212}, - activeEndpoints: {endpoints: [12, 11, 13]}, - simpleDescriptor: { - 11: {endpointID: 11, deviceID: 0x0210, inputClusters: [0, 3, 4, 5, 6, 8, 768, 912301], outputClusters: [2], profileID: 99}, - 12: {endpointID: 12, deviceID: 0xe15e, inputClusters: [0, 3, 4, 5, 6, 8, 768], outputClusters: [2], profileID: 99}, - 13: {endpointID: 13, deviceID: 0x0100, inputClusters: [0, 3, 4, 5, 6, 8, 768], outputClusters: [2], profileID: 99}, - }, - attributes: { - 12: { - modelId: 'GL-C-008', - manufacturerName: 'Gledopto', - zclVersion: 1, - appVersion: 2, - hwVersion: 3, - dateCode: '201901', - swBuildId: '1.01', - powerSource: 1, - stackVersion: 101, - }, - }, - }, - 173: { - nodeDescriptor: {type: 'EndDevice', manufacturerCode: 0}, - activeEndpoints: {endpoints: [1]}, - simpleDescriptor: 'error', - attributes: {}, - }, - 174: { - nodeDescriptor: {type: 'EndDevice', manufacturerCode: 1213}, - activeEndpoints: {endpoints: [1]}, - simpleDescriptor: {1: {endpointID: 1, deviceID: 5, inputClusters: [0, 32], outputClusters: [2], profileID: 99}}, - attributes: { - 1: {checkinInterval: default174CheckinInterval}, - }, - }, - 175: { - nodeDescriptor: {type: 'Router', manufacturerCode: 1212}, - activeEndpoints: {endpoints: [1, 2, 3, 4, 5, 6]}, - simpleDescriptor: {1: {endpointID: 1, deviceID: 5, inputClusters: [0, 1, 2], outputClusters: [2], profileID: 99}}, - attributes: { - 1: { - modelId: 'lumi.plug', - manufacturerName: 'LUMI', - zclVersion: 1, - appVersion: 2, - hwVersion: 3, - dateCode: '201901', - swBuildId: '1.01', - powerSource: 1, - stackVersion: 101, - }, - }, - }, - 176: { - nodeDescriptor: {type: 'Router', manufacturerCode: 1212}, - activeEndpoints: {endpoints: [1, 2, 3, 4, 5, 6]}, - simpleDescriptor: {1: {endpointID: 1, deviceID: 5, inputClusters: [1, 2], outputClusters: [2], profileID: 99}}, - attributes: { - 1: { - modelId: 'lumi.plug', - manufacturerName: 'LUMI', - zclVersion: 1, - appVersion: 2, - hwVersion: 3, - dateCode: '201901', - swBuildId: '1.01', - powerSource: 1, - stackVersion: 101, - }, - }, - }, - 177: { - nodeDescriptor: {type: 'Router', manufacturerCode: 4129}, - activeEndpoints: {endpoints: [1]}, - simpleDescriptor: { - 1: {endpointID: 1, deviceID: 514, inputClusters: [0, 3, 258, 4, 5, 15, 64513], outputClusters: [258, 0, 64513, 5, 25], profileID: 260}, - }, - attributes: { - 1: { - modelId: ' Shutter switch with neutral', - manufacturerName: 'Legrand', - zclVersion: 2, - appVersion: 0, - hwVersion: 8, - dateCode: '231030', - swBuildId: '0038', - powerSource: 1, - stackVersion: 67, - }, - }, - }, -}; - // Mock realPathSync jest.mock('../src/utils/realpathSync', () => { return jest.fn().mockImplementation((path) => { @@ -485,15 +322,19 @@ jest.mock('../src/utils/realpathSync', () => { jest.mock('../src/utils/wait', () => { return jest.fn().mockImplementation(() => { - return new Promise((resolve, reject) => resolve()); + return new Promise((resolve, reject) => resolve()); }); }); const getCluster = (key) => { const cluster = Zcl.Utils.getCluster(key, undefined, {}); + // @ts-expect-error mock delete cluster.getAttribute; + // @ts-expect-error mock delete cluster.getCommand; + // @ts-expect-error mock delete cluster.hasAttribute; + // @ts-expect-error mock delete cluster.getCommandResponse; return cluster; }; @@ -521,10 +362,12 @@ const mockDummyBackup: Models.Backup = { { networkAddress: 1001, ieeeAddress: Buffer.from('c1c2c3c4c5c6c7c8', 'hex'), + isDirectChild: false, }, { networkAddress: 1002, ieeeAddress: Buffer.from('d1d2d3d4d5d6d7d8', 'hex'), + isDirectChild: false, linkKey: { key: Buffer.from('f8f7f6f5f4f3f2f1e1e2e3e4e5e6e7e8', 'hex'), rxCounter: 10000, @@ -539,11 +382,12 @@ let dummyBackup; jest.mock('../src/adapter/z-stack/adapter/zStackAdapter', () => { return jest.fn().mockImplementation(() => { return { - greenPowerGroup: 0x0b84, + hasZdoMessageOverhead: false, + manufacturerID: 0x0007, on: (event, handler) => (mockAdapterEvents[event] = handler), removeAllListeners: (event) => delete mockAdapterEvents[event], start: mockAdapterStart, - getCoordinator: mockAdapterGetCoordinator, + getCoordinatorIEEE: mockAdapterGetCoordinatorIEEE, reset: mockAdapterReset, supportsBackup: mockAdapterSupportsBackup, backup: () => { @@ -555,26 +399,6 @@ jest.mock('../src/adapter/z-stack/adapter/zStackAdapter', () => { getNetworkParameters: mockAdapterGetNetworkParameters, waitFor: mockAdapterWaitFor, setTransmitPower: mockAdapterSetTransmitPower, - changeChannel: mockAdapterChangeChannel, - nodeDescriptor: mockAdapterNodeDescriptor, - activeEndpoints: (networkAddress) => { - if (mockDevices[networkAddress].activeEndpoints === 'error') { - throw new Error('timeout'); - } else { - return mockDevices[networkAddress].activeEndpoints; - } - }, - simpleDescriptor: (networkAddress, endpoint) => { - if (mockDevices[networkAddress].simpleDescriptor === 'error') { - throw new Error('timeout'); - } - - if (mockDevices[networkAddress].simpleDescriptor[endpoint] === undefined) { - throw new Error('Simple descriptor failed'); - } - - return mockDevices[networkAddress].simpleDescriptor[endpoint]; - }, sendZclFrameToEndpoint: mocksendZclFrameToEndpoint, sendZclFrameToGroup: mocksendZclFrameToGroup, sendZclFrameToAll: mocksendZclFrameToAll, @@ -583,33 +407,11 @@ jest.mock('../src/adapter/z-stack/adapter/zStackAdapter', () => { supportsDiscoverRoute: mockAdapterSupportsDiscoverRoute, discoverRoute: mockDiscoverRoute, stop: mockAdapterStop, - removeDevice: mockAdapterRemoveDevice, - lqi: (networkAddress) => { - if (networkAddress === 140) { - return { - neighbors: [ - {ieeeAddr: '0x160', networkAddress: 160, linkquality: 20, relationship: 2, depth: 5}, - {ieeeAddr: '0x170', networkAddress: 170, linkquality: 21, relationship: 4, depth: 8}, - ], - }; - } - }, - routingTable: (networkAddress) => { - if (networkAddress === 140) { - return { - table: [ - {destinationAddress: 120, status: 'SUCCESS', nextHop: 1}, - {destinationAddress: 130, status: 'FAILED', nextHop: 2}, - ], - }; - } - }, - bind: mockAdapterBind, - unbind: mockAdapterUnbind, setChannelInterPAN: mockSetChannelInterPAN, sendZclFrameInterPANToIeeeAddr: mocksendZclFrameInterPANToIeeeAddr, sendZclFrameInterPANBroadcast: mocksendZclFrameInterPANBroadcast, restoreChannelInterPAN: mockRestoreChannelInterPAN, + sendZdo: mockAdapterSendZdo, }; }); }); @@ -655,7 +457,6 @@ ZiGateAdapter.autoDetectPath = mockZiGateAdapterAutoDetectPath; const mocksRestore = [ mockAdapterPermitJoin, mockAdapterStop, - mockAdapterRemoveDevice, mocksendZclFrameToAll, mockZStackAdapterIsValidPath, mockZStackAdapterAutoDetectPath, @@ -689,10 +490,13 @@ const options = { path: '/dummy/conbee', adapter: undefined, }, - databasePath: getTempFile('database'), - databaseBackupPath: undefined, + adapter: { + disableLED: false, + }, + databasePath: getTempFile('database.db'), + databaseBackupPath: getTempFile('database.db.backup'), backupPath, - acceptJoiningDeviceHandler: undefined, + acceptJoiningDeviceHandler: jest.fn().mockResolvedValue(true), }; const databaseContents = () => fs.readFileSync(options.databasePath).toString(); @@ -702,8 +506,7 @@ describe('Controller', () => { beforeAll(async () => { jest.useFakeTimers({doNotFake: ['setTimeout']}); - Date.now = jest.fn(); - Date.now.mockReturnValue(150); + Date.now = jest.fn().mockReturnValue(150); setLogger(mockLogger); dummyBackup = await Utils.BackupUtils.toUnifiedBackup(mockDummyBackup); }); @@ -714,10 +517,11 @@ describe('Controller', () => { }); beforeEach(async () => { + sendZdoResponseStatus = Zdo.Status.SUCCESS; mocksRestore.forEach((m) => m.mockRestore()); mocksClear.forEach((m) => m.mockClear()); - // @ts-ignore - mockDevices[174].attributes[1].checkinInterval = default174CheckinInterval; + MOCK_DEVICES[174]!.attributes![1].checkinInterval = DEFAULT_184_CHECKIN_INTERVAL; + // @ts-expect-error mock zclTransactionSequenceNumber.number = 1; iasZoneReadState170Count = 0; configureReportStatus = 0; @@ -899,7 +703,7 @@ describe('Controller', () => { meta: {}, clusters: {}, deviceIeeeAddress: '0x0000012300000000', - deviceNetworkAddress: 123, + deviceNetworkAddress: 0, _binds: [], _configuredReportings: [], }, @@ -919,7 +723,7 @@ describe('Controller', () => { ID: 2, clusters: {}, deviceIeeeAddress: '0x0000012300000000', - deviceNetworkAddress: 123, + deviceNetworkAddress: 0, _binds: [], _configuredReportings: [], }, @@ -928,8 +732,8 @@ describe('Controller', () => { _interviewCompleted: true, _interviewing: false, _skipDefaultResponse: false, - _manufacturerID: 100, - _networkAddress: 123, + _manufacturerID: 0x0007, + _networkAddress: 0, _type: 'Coordinator', meta: {}, }); @@ -941,15 +745,7 @@ describe('Controller', () => { await controller.start(); expect(controller.getDevicesByType('Coordinator')[0].ieeeAddr).toStrictEqual('0x0000012300000000'); await controller.stop(); - mockAdapterGetCoordinator.mockReturnValueOnce({ - ieeeAddr: '0x123444', - networkAddress: 123, - manufacturerID: 100, - endpoints: [ - {ID: 1, profileID: 2, deviceID: 3, inputClusters: [10], outputClusters: [11]}, - {ID: 2, profileID: 3, deviceID: 5, inputClusters: [1], outputClusters: [0]}, - ], - }); + mockAdapterGetCoordinatorIEEE.mockReturnValueOnce('0x123444'); await controller.start(); expect(controller.getDevicesByType('Coordinator')[0].ieeeAddr).toStrictEqual('0x123444'); }); @@ -1785,7 +1581,7 @@ describe('Controller', () => { const frame = Zcl.Frame.create(0, 1, true, undefined, 10, 'readRsp', 0, [{attrId: 5, status: 0, dataType: 66, attrData: 'new.model.id'}], {}); await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - expect(Device.byIeeeAddr('0x129').modelID).toBe('myModelID'); + expect(Device.byIeeeAddr('0x129')!.modelID).toBe('myModelID'); await mockAdapterEvents['zclPayload']({ wasBroadcast: false, address: 129, @@ -1797,44 +1593,35 @@ describe('Controller', () => { groupID: 1, }); - expect(Device.byIeeeAddr('0x129').modelID).toBe('new.model.id'); - }); - - it('Change channel', async () => { - await controller.start(); - await controller.changeChannel(10, 20); - expect(mockAdapterChangeChannel).toHaveBeenCalledWith(20); - mockAdapterGetNetworkParameters.mockReturnValueOnce({panID: 1, extendedPanID: 3, channel: 20}); - expect(await controller.getNetworkParameters()).toEqual({panID: 1, channel: 20, extendedPanID: 3}); + expect(Device.byIeeeAddr('0x129')!.modelID).toBe('new.model.id'); }); it('Change channel on start', async () => { mockAdapterStart.mockReturnValueOnce('resumed'); mockAdapterGetNetworkParameters.mockReturnValueOnce({panID: 1, extendedPanID: 3, channel: 25}); + // @ts-expect-error private + const changeChannelSpy = jest.spyOn(controller, 'changeChannel'); await controller.start(); expect(mockAdapterGetNetworkParameters).toHaveBeenCalledTimes(1); - expect(mockAdapterChangeChannel).toHaveBeenCalledWith(15); + const zdoPayload = Zdo.Buffalo.buildRequest(false, Zdo.ClusterId.NWK_UPDATE_REQUEST, [15], 0xfe, undefined, 0, undefined); + expect(mockAdapterSendZdo).toHaveBeenCalledWith( + ZSpec.BLANK_EUI64, + ZSpec.BroadcastAddress.SLEEPY, + Zdo.ClusterId.NWK_UPDATE_REQUEST, + zdoPayload, + true, + ); expect(await controller.getNetworkParameters()).toEqual({panID: 1, channel: 15, extendedPanID: 3}); + expect(changeChannelSpy).toHaveBeenCalledTimes(1); }); it('Does not change channel on start if not changed', async () => { mockAdapterStart.mockReturnValueOnce('resumed'); + // @ts-expect-error private + const changeChannelSpy = jest.spyOn(controller, 'changeChannel'); await controller.start(); expect(mockAdapterGetNetworkParameters).toHaveBeenCalledTimes(1); - expect(mockAdapterChangeChannel).toHaveBeenCalledTimes(0); - }); - - it('Does not change channel on start if not supported', async () => { - mockAdapterStart.mockReturnValueOnce('resumed'); - mockAdapterChangeChannel.mockImplementationOnce(() => { - throw new Error('Not supported'); - }); - mockAdapterGetNetworkParameters.mockReturnValueOnce({panID: 1, extendedPanID: 3, channel: 25}); - await expect(controller.start()).rejects.toThrow(`Not supported`); - expect(mockAdapterGetNetworkParameters).toHaveBeenCalledTimes(1); - expect(mockAdapterChangeChannel).toHaveBeenCalledTimes(1); - // get rid of the mockReturnValueOnce that was never called - mockAdapterGetNetworkParameters(); + expect(changeChannelSpy).toHaveBeenCalledTimes(0); }); it('Set transmit power', async () => { @@ -1946,7 +1733,7 @@ describe('Controller', () => { _pendingRequestTimeout: 0, _skipDefaultResponse: false, _lastSeen: deepClone(Date.now()), - _type: 'Unknown', + _type: 'Router', _ieeeAddr: '0x129', _networkAddress: 129, meta: {}, @@ -1969,7 +1756,6 @@ describe('Controller', () => { profileID: 99, }, ], - _type: 'Router', _manufacturerID: 1212, _manufacturerName: 'KoenAndCo', _powerSource: 'Mains (single phase)', @@ -1978,8 +1764,6 @@ describe('Controller', () => { _stackVersion: 101, _zclVersion: 1, _hardwareVersion: 3, - _events: {}, - _eventsCount: 0, _dateCode: '201901', _softwareBuildID: '1.01', _interviewCompleted: true, @@ -1989,9 +1773,9 @@ describe('Controller', () => { expect(deepClone(controller.getDeviceByNetworkAddress(129))).toStrictEqual(device); expect(events.deviceInterview.length).toBe(2); expect(databaseContents()).toStrictEqual( - `{"id":1,"type":"Coordinator","ieeeAddr":"0x0000012300000000","nwkAddr":123,"manufId":100,"epList":[1,2],"endpoints":{"1":{"profId":2,"epId":1,"devId":3,"inClusterList":[10],"outClusterList":[11],"clusters":{},"binds":[],"configuredReportings":[],"meta":{}},"2":{"profId":3,"epId":2,"devId":5,"inClusterList":[1],"outClusterList":[0],"clusters":{},"binds":[],"configuredReportings":[],"meta":{}}},"interviewCompleted":true,"meta":{}}\n{"id":2,"type":"Router","ieeeAddr":"0x129","nwkAddr":129,"manufId":1212,"manufName":"KoenAndCo","powerSource":"Mains (single phase)","modelId":"myModelID","epList":[1],"endpoints":{"1":{"profId":99,"epId":1,"devId":5,"inClusterList":[0,1],"outClusterList":[2],"clusters":{},"binds":[],"configuredReportings":[],"meta":{}}},"appVersion":2,"stackVersion":101,"hwVersion":3,"dateCode":"201901","swBuildId":"1.01","zclVersion":1,"interviewCompleted":true,"meta":{},"lastSeen":150}`, + `{"id":1,"type":"Coordinator","ieeeAddr":"0x0000012300000000","nwkAddr":0,"manufId":7,"epList":[1,2],"endpoints":{"1":{"profId":2,"epId":1,"devId":3,"inClusterList":[10],"outClusterList":[11],"clusters":{},"binds":[],"configuredReportings":[],"meta":{}},"2":{"profId":3,"epId":2,"devId":5,"inClusterList":[1],"outClusterList":[0],"clusters":{},"binds":[],"configuredReportings":[],"meta":{}}},"interviewCompleted":true,"meta":{}}\n{"id":2,"type":"Router","ieeeAddr":"0x129","nwkAddr":129,"manufId":1212,"manufName":"KoenAndCo","powerSource":"Mains (single phase)","modelId":"myModelID","epList":[1],"endpoints":{"1":{"profId":99,"epId":1,"devId":5,"inClusterList":[0,1],"outClusterList":[2],"clusters":{},"binds":[],"configuredReportings":[],"meta":{}}},"appVersion":2,"stackVersion":101,"hwVersion":3,"dateCode":"201901","swBuildId":"1.01","zclVersion":1,"interviewCompleted":true,"meta":{},"lastSeen":150}`, ); - expect(controller.getDeviceByNetworkAddress(129).lastSeen).toBe(Date.now()); + expect(controller.getDeviceByNetworkAddress(129)!.lastSeen).toBe(Date.now()); }); it('Join a device and explictly accept it', async () => { @@ -2029,7 +1813,7 @@ describe('Controller', () => { _pendingRequestTimeout: 0, _skipDefaultResponse: false, _lastSeen: deepClone(Date.now()), - _type: 'Unknown', + _type: 'Router', _ieeeAddr: '0x129', _networkAddress: 129, meta: {}, @@ -2052,7 +1836,6 @@ describe('Controller', () => { profileID: 99, }, ], - _type: 'Router', _manufacturerID: 1212, _manufacturerName: 'KoenAndCo', _powerSource: 'Mains (single phase)', @@ -2061,8 +1844,6 @@ describe('Controller', () => { _stackVersion: 101, _zclVersion: 1, _hardwareVersion: 3, - _events: {}, - _eventsCount: 0, _dateCode: '201901', _softwareBuildID: '1.01', _interviewCompleted: true, @@ -2072,29 +1853,49 @@ describe('Controller', () => { expect(deepClone(controller.getDeviceByIeeeAddr('0x129'))).toStrictEqual(device); expect(events.deviceInterview.length).toBe(2); expect(databaseContents().includes('0x129')).toBeTruthy(); - expect(controller.getDeviceByIeeeAddr('0x129').lastSeen).toBe(Date.now()); + expect(controller.getDeviceByIeeeAddr('0x129')!.lastSeen).toBe(Date.now()); + }); + + it('Join a device and explictly refuses it', async () => { + const mockAcceptJoiningDeviceHandler = jest.fn().mockReturnValue(false); + controller = new Controller({...options, acceptJoiningDeviceHandler: mockAcceptJoiningDeviceHandler}); + controller.on('deviceJoined', (device) => events.deviceJoined.push(device)); + controller.on('deviceInterview', (device) => events.deviceInterview.push(deepClone(device))); + await controller.start(); + mockAdapterSendZdo.mockClear(); + expect(databaseContents().includes('0x129')).toBeFalsy(); + await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); + expect(events.deviceJoined.length).toBe(0); + expect(events.deviceInterview.length).toBe(0); + expect(databaseContents().includes('0x129')).toBeFalsy(); + expect(controller.getDeviceByIeeeAddr('0x129')).toBeUndefined(); + const zdoPayload = Zdo.Buffalo.buildRequest(false, Zdo.ClusterId.LEAVE_REQUEST, '0x129', Zdo.LeaveRequestFlags.WITHOUT_REJOIN); + expect(mockAdapterSendZdo).toHaveBeenNthCalledWith(1, '0x129', 129, Zdo.ClusterId.LEAVE_REQUEST, zdoPayload, false); }); - it('Join a device and explictly refuse it', async () => { - mockAdapterRemoveDevice.mockImplementationOnce(() => new Promise((_, r) => r("I won't remove myself!"))); + it('Join a device and explictly refuses it but LEAVE request fails', async () => { const mockAcceptJoiningDeviceHandler = jest.fn().mockReturnValue(false); controller = new Controller({...options, acceptJoiningDeviceHandler: mockAcceptJoiningDeviceHandler}); controller.on('deviceJoined', (device) => events.deviceJoined.push(device)); controller.on('deviceInterview', (device) => events.deviceInterview.push(deepClone(device))); await controller.start(); + mockAdapterSendZdo.mockClear(); expect(databaseContents().includes('0x129')).toBeFalsy(); + sendZdoResponseStatus = Zdo.Status.NOT_SUPPORTED; await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); expect(events.deviceJoined.length).toBe(0); expect(events.deviceInterview.length).toBe(0); expect(databaseContents().includes('0x129')).toBeFalsy(); expect(controller.getDeviceByIeeeAddr('0x129')).toBeUndefined(); - expect(mockAdapterRemoveDevice).toHaveBeenNthCalledWith(1, 129, '0x129'); + const zdoPayload = Zdo.Buffalo.buildRequest(false, Zdo.ClusterId.LEAVE_REQUEST, '0x129', Zdo.LeaveRequestFlags.WITHOUT_REJOIN); + expect(mockAdapterSendZdo).toHaveBeenNthCalledWith(1, '0x129', 129, Zdo.ClusterId.LEAVE_REQUEST, zdoPayload, false); + expect(mockLogger.error).toHaveBeenCalledWith(`Failed to remove rejected device: Status 'NOT_SUPPORTED'`, 'zh:controller'); }); it('Set device powersource by string', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const device = controller.getDeviceByIeeeAddr('0x129'); + const device = controller.getDeviceByIeeeAddr('0x129')!; device.powerSource = 'test123'; expect(device.powerSource).toBe('test123'); }); @@ -2109,7 +1910,13 @@ describe('Controller', () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); expect(events.deviceAnnounce.length).toBe(0); - await mockAdapterEvents['deviceAnnounce']({networkAddress: 129, ieeeAddr: '0x129'}); + // @ts-expect-error private + const onDeviceAnnounceSpy = jest.spyOn(controller, 'onDeviceAnnounce'); + await mockAdapterEvents['zdoResponse'](Zdo.ClusterId.END_DEVICE_ANNOUNCE, [ + Zdo.Status.SUCCESS, + {nwkAddress: 129, eui64: '0x129', capabilities: Zdo.Utils.getMacCapFlags(0x10)}, + ]); + expect(onDeviceAnnounceSpy).toHaveBeenCalledTimes(1); expect(events.deviceAnnounce.length).toBe(1); expect(events.deviceAnnounce[0].device).toBeInstanceOf(Device); expect(events.deviceAnnounce[0].device.ieeeAddr).toBe('0x129'); @@ -2118,53 +1925,143 @@ describe('Controller', () => { it('Skip Device announce event from unknown device', async () => { await controller.start(); - await mockAdapterEvents['deviceAnnounce']({networkAddress: 12999, ieeeAddr: '0x12999'}); + // @ts-expect-error private + const onDeviceAnnounceSpy = jest.spyOn(controller, 'onDeviceAnnounce'); + await mockAdapterEvents['zdoResponse'](Zdo.ClusterId.END_DEVICE_ANNOUNCE, [ + Zdo.Status.SUCCESS, + {nwkAddress: 12999, eui64: '0x12999', capabilities: Zdo.Utils.getMacCapFlags(0x10)}, + ]); + expect(onDeviceAnnounceSpy).toHaveBeenCalledTimes(1); expect(events.deviceAnnounce.length).toBe(0); }); it('Device announce event should update network address when different', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - expect(controller.getDeviceByNetworkAddress(129)?.ieeeAddr).toStrictEqual('0x129'); + expect(controller.getDeviceByNetworkAddress(129)!.ieeeAddr).toStrictEqual('0x129'); expect(events.deviceAnnounce.length).toBe(0); - await mockAdapterEvents['deviceAnnounce']({networkAddress: 9999, ieeeAddr: '0x129'}); - expect(controller.getDeviceByIeeeAddr('0x129')?.networkAddress).toBe(9999); - expect(controller.getDeviceByIeeeAddr('0x129')?.getEndpoint(1)?.deviceNetworkAddress).toBe(9999); + // @ts-expect-error private + const onDeviceAnnounceSpy = jest.spyOn(controller, 'onDeviceAnnounce'); + await mockAdapterEvents['zdoResponse'](Zdo.ClusterId.END_DEVICE_ANNOUNCE, [ + Zdo.Status.SUCCESS, + {nwkAddress: 9999, eui64: '0x129', capabilities: Zdo.Utils.getMacCapFlags(0x10)}, + ]); + expect(onDeviceAnnounceSpy).toHaveBeenCalledTimes(1); + 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'); + expect(controller.getDeviceByNetworkAddress(9999)!.ieeeAddr).toStrictEqual('0x129'); }); it('Network address event 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['networkAddress']({networkAddress: 9999, ieeeAddr: '0x129'}); - expect(controller.getDeviceByIeeeAddr('0x129')?.networkAddress).toBe(9999); - expect(controller.getDeviceByIeeeAddr('0x129')?.getEndpoint(1)?.deviceNetworkAddress).toBe(9999); + expect(controller.getDeviceByNetworkAddress(129)!.ieeeAddr).toStrictEqual('0x129'); + // @ts-expect-error private + const onNetworkAddressSpy = jest.spyOn(controller, 'onNetworkAddress'); + await mockAdapterEvents['zdoResponse'](Zdo.ClusterId.NETWORK_ADDRESS_RESPONSE, [ + Zdo.Status.SUCCESS, + {nwkAddress: 9999, eui64: '0x129', startIndex: 0, assocDevList: []}, + ]); + expect(onNetworkAddressSpy).toHaveBeenCalledTimes(1); + 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'); + expect(controller.getDeviceByNetworkAddress(9999)!.ieeeAddr).toStrictEqual('0x129'); }); it('Network address event shouldnt update network address when the same', async () => { await controller.start(); + // @ts-expect-error private + const onNetworkAddressSpy = jest.spyOn(controller, 'onNetworkAddress'); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - await mockAdapterEvents['networkAddress']({networkAddress: 129, ieeeAddr: '0x129'}); + await mockAdapterEvents['zdoResponse'](Zdo.ClusterId.NETWORK_ADDRESS_RESPONSE, [ + Zdo.Status.SUCCESS, + {nwkAddress: 129, eui64: '0x129', startIndex: 0, assocDevList: []}, + ]); + expect(onNetworkAddressSpy).toHaveBeenCalledTimes(1); expect(controller.getDeviceByIeeeAddr('0x129')?.networkAddress).toBe(129); expect(controller.getDeviceByIeeeAddr('0x129')?.getEndpoint(1)?.deviceNetworkAddress).toBe(129); }); it('Network address event from unknown device', async () => { await controller.start(); - await mockAdapterEvents['networkAddress']({networkAddress: 19321, ieeeAddr: '0x19321'}); + // @ts-expect-error private + const onNetworkAddressSpy = jest.spyOn(controller, 'onNetworkAddress'); + await mockAdapterEvents['zdoResponse'](Zdo.ClusterId.NETWORK_ADDRESS_RESPONSE, [ + Zdo.Status.SUCCESS, + {nwkAddress: 19321, eui64: '0x19321', startIndex: 0, assocDevList: []}, + ]); + expect(onNetworkAddressSpy).toHaveBeenCalledTimes(1); }); it('Network address event 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['networkAddress']({networkAddress: 129, ieeeAddr: '0x129'}); + (Date.now as ReturnType).mockReturnValue(200); + // @ts-expect-error private + const onNetworkAddressSpy = jest.spyOn(controller, 'onNetworkAddress'); + await mockAdapterEvents['zdoResponse'](Zdo.ClusterId.NETWORK_ADDRESS_RESPONSE, [ + Zdo.Status.SUCCESS, + {nwkAddress: 129, eui64: '0x129', startIndex: 0, assocDevList: []}, + ]); + expect(onNetworkAddressSpy).toHaveBeenCalledTimes(1); + expect(events.lastSeenChanged[1].device.lastSeen).toBe(200); + }); + + it('IEEE address event should update network address when different', async () => { + await controller.start(); + await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); + expect(controller.getDeviceByNetworkAddress(129)!.ieeeAddr).toStrictEqual('0x129'); + // @ts-expect-error private + const onIEEEAddressSpy = jest.spyOn(controller, 'onIEEEAddress'); + await mockAdapterEvents['zdoResponse'](Zdo.ClusterId.IEEE_ADDRESS_RESPONSE, [ + Zdo.Status.SUCCESS, + {nwkAddress: 9999, eui64: '0x129', startIndex: 0, assocDevList: []}, + ]); + expect(onIEEEAddressSpy).toHaveBeenCalledTimes(1); + 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('IEEE address event shouldnt update network address when the same', async () => { + await controller.start(); + // @ts-expect-error private + const onIEEEAddressSpy = jest.spyOn(controller, 'onIEEEAddress'); + await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); + await mockAdapterEvents['zdoResponse'](Zdo.ClusterId.IEEE_ADDRESS_RESPONSE, [ + Zdo.Status.SUCCESS, + {nwkAddress: 129, eui64: '0x129', startIndex: 0, assocDevList: []}, + ]); + expect(onIEEEAddressSpy).toHaveBeenCalledTimes(1); + expect(controller.getDeviceByIeeeAddr('0x129')?.networkAddress).toBe(129); + expect(controller.getDeviceByIeeeAddr('0x129')?.getEndpoint(1)?.deviceNetworkAddress).toBe(129); + }); + + it('IEEE address event from unknown device', async () => { + await controller.start(); + // @ts-expect-error private + const onIEEEAddressSpy = jest.spyOn(controller, 'onIEEEAddress'); + await mockAdapterEvents['zdoResponse'](Zdo.ClusterId.IEEE_ADDRESS_RESPONSE, [ + Zdo.Status.SUCCESS, + {nwkAddress: 19321, eui64: '0x19321', startIndex: 0, assocDevList: []}, + ]); + expect(onIEEEAddressSpy).toHaveBeenCalledTimes(1); + }); + + it('IEEE address event should update the last seen value', async () => { + await controller.start(); + await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); + (Date.now as ReturnType).mockReturnValue(200); + // @ts-expect-error private + const onIEEEAddressSpy = jest.spyOn(controller, 'onIEEEAddress'); + await mockAdapterEvents['zdoResponse'](Zdo.ClusterId.IEEE_ADDRESS_RESPONSE, [ + Zdo.Status.SUCCESS, + {nwkAddress: 129, eui64: '0x129', startIndex: 0, assocDevList: []}, + ]); + expect(onIEEEAddressSpy).toHaveBeenCalledTimes(1); expect(events.lastSeenChanged[1].device.lastSeen).toBe(200); }); @@ -2204,9 +2101,7 @@ describe('Controller', () => { 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); + (Date.now as ReturnType).mockReturnValue(200); await mockAdapterEvents['zdoResponse'](Zdo.ClusterId.NETWORK_ADDRESS_RESPONSE, [ Zdo.Status.SUCCESS, {nwkAddress: 129, eui64: '0x129', assocDevList: [], startIndex: 0}, @@ -2385,7 +2280,7 @@ describe('Controller', () => { expect(fs.existsSync(databaseTmpPath)).toBeFalsy(); // There should still be a database.db.tmp. - const dbtmp = fs.readdirSync(TEMP_PATH).filter((value) => value.startsWith('database.tmp')); + const dbtmp = fs.readdirSync(TEMP_PATH).filter((value) => value.startsWith('database.db.tmp')); expect(dbtmp.length).toBe(1); // The database.db.tmp. should still have our "Hello, World!" @@ -2462,9 +2357,9 @@ describe('Controller', () => { // Green power expect(mocksendZclFrameToAll).toHaveBeenCalledTimes(1); const commisionFrameEnable = Zcl.Frame.create(1, 1, true, undefined, 2, 'commisioningMode', 33, {options: 0x0b, commisioningWindow: 254}, {}); - expect(mocksendZclFrameToAll.mock.calls[0][0]).toBe(242); + expect(mocksendZclFrameToAll.mock.calls[0][0]).toBe(ZSpec.GP_ENDPOINT); expect(deepClone(mocksendZclFrameToAll.mock.calls[0][1])).toStrictEqual(deepClone(commisionFrameEnable)); - expect(mocksendZclFrameToAll.mock.calls[0][2]).toBe(242); + expect(mocksendZclFrameToAll.mock.calls[0][2]).toBe(ZSpec.GP_ENDPOINT); // should call it again ever +- 200 seconds jest.advanceTimersByTime(210 * 1000); @@ -2493,9 +2388,9 @@ describe('Controller', () => { // Green power expect(mocksendZclFrameToAll).toHaveBeenCalledTimes(4); const commissionFrameDisable = Zcl.Frame.create(1, 1, true, undefined, 5, 'commisioningMode', 33, {options: 0x0a, commisioningWindow: 0}, {}); - expect(mocksendZclFrameToAll.mock.calls[3][0]).toBe(242); + expect(mocksendZclFrameToAll.mock.calls[3][0]).toBe(ZSpec.GP_ENDPOINT); expect(deepClone(mocksendZclFrameToAll.mock.calls[3][1])).toStrictEqual(deepClone(commissionFrameDisable)); - expect(mocksendZclFrameToAll.mock.calls[3][2]).toBe(242); + expect(mocksendZclFrameToAll.mock.calls[3][2]).toBe(ZSpec.GP_ENDPOINT); expect(mocksendZclFrameToAll).toHaveBeenCalledTimes(4); }); @@ -2515,7 +2410,7 @@ describe('Controller', () => { it('Controller permit joining for specific time', async () => { await controller.start(); - await controller.permitJoin(true, null, 10); + await controller.permitJoin(true, undefined, 10); expect(mockAdapterPermitJoin).toHaveBeenCalledTimes(1); expect(mockAdapterPermitJoin.mock.calls[0][0]).toBe(254); expect(events.permitJoinChanged.length).toBe(1); @@ -2614,7 +2509,7 @@ describe('Controller', () => { expect(events.deviceInterview[0].device._ieeeAddr).toBe('0x140'); expect(events.deviceInterview[1].status).toBe('failed'); expect(events.deviceInterview[1].device._ieeeAddr).toBe('0x140'); - expect(controller.getDeviceByIeeeAddr('0x140').type).toStrictEqual('Unknown'); + expect(controller.getDeviceByIeeeAddr('0x140')!.type).toStrictEqual('Unknown'); }); it('Device joins with endpoints [4,1], should read modelID from 1', async () => { @@ -2783,7 +2678,7 @@ describe('Controller', () => { }); it('Device joins and interview iAs enrollment fails', async () => { - mockDevices['170'].attributes['1'].zoneState = 0; + MOCK_DEVICES['170']!.attributes!['1'].zoneState = 0; enroll170 = false; await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 170, ieeeAddr: '0x170'}); @@ -2871,8 +2766,6 @@ describe('Controller', () => { _stackVersion: 101, _zclVersion: 1, _hardwareVersion: 3, - _events: {}, - _eventsCount: 0, _dateCode: '201901', _softwareBuildID: '1.01', _interviewCompleted: true, @@ -2917,7 +2810,7 @@ describe('Controller', () => { }, }; expect(deepClone(events.message[0])).toStrictEqual(expected); - expect(controller.getDeviceByIeeeAddr('0x129').linkquality).toEqual(50); + expect(controller.getDeviceByIeeeAddr('0x129')!.linkquality).toEqual(50); }); it('Receive raw data', async () => { @@ -2983,8 +2876,6 @@ describe('Controller', () => { _stackVersion: 101, _zclVersion: 1, _hardwareVersion: 3, - _events: {}, - _eventsCount: 0, _dateCode: '201901', _softwareBuildID: '1.01', _interviewCompleted: true, @@ -3074,8 +2965,6 @@ describe('Controller', () => { _stackVersion: 101, _zclVersion: 1, _hardwareVersion: 3, - _events: {}, - _eventsCount: 0, _dateCode: '201901', _softwareBuildID: '1.01', _interviewCompleted: true, @@ -3206,8 +3095,6 @@ describe('Controller', () => { _stackVersion: 101, _zclVersion: 1, _hardwareVersion: 3, - _events: {}, - _eventsCount: 0, _dateCode: '201901', _softwareBuildID: '1.01', _interviewCompleted: true, @@ -3250,7 +3137,7 @@ describe('Controller', () => { }, }; expect(deepClone(events.message[0])).toStrictEqual(expected); - expect(controller.getDeviceByIeeeAddr('0x129').endpoints.length).toBe(2); + expect(controller.getDeviceByIeeeAddr('0x129')!.endpoints.length).toBe(2); }); it('Receive cluster command', async () => { @@ -3312,8 +3199,6 @@ describe('Controller', () => { _stackVersion: 101, _zclVersion: 1, _hardwareVersion: 3, - _events: {}, - _eventsCount: 0, _dateCode: '201901', _softwareBuildID: '1.01', _interviewCompleted: true, @@ -3658,7 +3543,7 @@ describe('Controller', () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); mocksendZclFrameToEndpoint.mockClear(); - const device = controller.getDeviceByIeeeAddr('0x129'); + const device = controller.getDeviceByIeeeAddr('0x129')!; expect(device.skipDefaultResponse).toBeFalsy(); device.skipDefaultResponse = true; await mockAdapterEvents['zclPayload']({ @@ -3697,7 +3582,7 @@ describe('Controller', () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); mocksendZclFrameToEndpoint.mockClear(); - const device = controller.getDeviceByIeeeAddr('0x129'); + const device = controller.getDeviceByIeeeAddr('0x129')!; expect(device.skipDefaultResponse).toBeFalsy(); await mockAdapterEvents['zclPayload']({ wasBroadcast: true, @@ -3834,7 +3719,7 @@ describe('Controller', () => { await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); mocksendZclFrameToEndpoint.mockClear(); - const device = controller.getDeviceByIeeeAddr('0x129'); + const device = controller.getDeviceByIeeeAddr('0x129')!; device.customReadResponse = jest.fn().mockReturnValue(true); const frame = Zcl.Frame.create(0, 0, true, undefined, 40, 0, 10, [{attrId: 0}, {attrId: 1}, {attrId: 7}, {attrId: 9}], {}); @@ -3854,14 +3739,14 @@ describe('Controller', () => { expect(mocksendZclFrameToEndpoint).toHaveBeenCalledTimes(0); expect(device.customReadResponse).toHaveBeenCalledTimes(1); expect(device.customReadResponse).toHaveBeenCalledWith(expect.any(Zcl.Frame), device.getEndpoint(1)); - expect(device.customReadResponse.mock.calls[0][0].header).toBe(payload.header); + expect((device.customReadResponse as ReturnType).mock.calls[0][0].header).toBe(payload.header); }); it('Respond to read of attribute', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const device = controller.getDeviceByIeeeAddr('0x129'); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const endpoint = device.getEndpoint(1)!; endpoint.saveClusterAttributeKeyValue('hvacThermostat', {systemMode: 3}); mocksendZclFrameToEndpoint.mockClear(); const frame = Zcl.Frame.create(0, 0, true, undefined, 40, 0, 513, [{attrId: 28}, {attrId: 290}], {}); @@ -3934,13 +3819,16 @@ describe('Controller', () => { expect(events.deviceInterview[1].status).toBe('failed'); expect(events.deviceInterview[1].device._ieeeAddr).toBe('0x173'); expect(mocksendZclFrameToEndpoint).toHaveBeenCalledTimes(1); - expect(controller.getDeviceByIeeeAddr('0x173').modelID).toBe(undefined); - expect(controller.getDeviceByIeeeAddr('0x173').manufacturerName).toBe(undefined); + expect(controller.getDeviceByIeeeAddr('0x173')!.modelID).toBe(undefined); + expect(controller.getDeviceByIeeeAddr('0x173')!.manufacturerName).toBe(undefined); // Second pair attempt await mockAdapterEvents['deviceLeave']({networkAddress: 173, ieeeAddr: '0x173'}); - mockDevices[173].nodeDescriptor = 'error'; - mockDevices[173].attributes[1] = {modelId: 'TS0203', manufacturerName: '_TYZB01_xph99wvr'}; + // backup + const descriptor173 = MOCK_DEVICES[173]!.nodeDescriptor; + const attributes173 = MOCK_DEVICES[173]!.attributes; + MOCK_DEVICES[173]!.nodeDescriptor = undefined; + MOCK_DEVICES[173]!.attributes![1] = {modelId: 'TS0203', manufacturerName: '_TYZB01_xph99wvr'}; await mockAdapterEvents['deviceJoined']({networkAddress: 173, ieeeAddr: '0x173'}); expect(events.deviceInterview.length).toBe(4); @@ -3950,14 +3838,18 @@ describe('Controller', () => { expect(events.deviceInterview[3].device._ieeeAddr).toBe('0x173'); expect(mocksendZclFrameToEndpoint).toHaveBeenCalledTimes(2); - expect(controller.getDeviceByIeeeAddr('0x173').modelID).toBe('TS0203'); - expect(controller.getDeviceByIeeeAddr('0x173').manufacturerName).toBe('_TYZB01_xph99wvr'); - expect(controller.getDeviceByIeeeAddr('0x173').powerSource).toBe('Battery'); + expect(controller.getDeviceByIeeeAddr('0x173')!.modelID).toBe('TS0203'); + expect(controller.getDeviceByIeeeAddr('0x173')!.manufacturerName).toBe('_TYZB01_xph99wvr'); + expect(controller.getDeviceByIeeeAddr('0x173')!.powerSource).toBe('Battery'); + + // restore + MOCK_DEVICES[173]!.nodeDescriptor = descriptor173; + MOCK_DEVICES[173]!.attributes = attributes173; }); it('Xiaomi WXCJKG11LM join (get simple descriptor for endpoint 2 fails)', async () => { // https://github.com/koenkk/zigbee2mqtt/issues/2844 - Date.now.mockReturnValue(150); + (Date.now as ReturnType).mockReturnValue(150); await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 171, ieeeAddr: '0x171'}); expect(events.deviceInterview.length).toBe(2); @@ -3965,7 +3857,7 @@ describe('Controller', () => { expect(events.deviceInterview[0].device._ieeeAddr).toBe('0x171'); expect(events.deviceInterview[1].status).toBe('successful'); expect(events.deviceInterview[1].device._ieeeAddr).toBe('0x171'); - expect(controller.getDeviceByIeeeAddr('0x171').modelID).toBe('lumi.remote.b286opcn01'); + expect(controller.getDeviceByIeeeAddr('0x171')!.modelID).toBe('lumi.remote.b286opcn01'); }); it('Gledopto GL-C-007/GL-C-008 join (all endpoints support genBasic but only 12 responds)', async () => { @@ -3977,7 +3869,7 @@ describe('Controller', () => { expect(events.deviceInterview[0].device._ieeeAddr).toBe('0x172'); expect(events.deviceInterview[1].status).toBe('successful'); expect(events.deviceInterview[1].device._ieeeAddr).toBe('0x172'); - expect(controller.getDeviceByIeeeAddr('0x172').modelID).toBe('GL-C-008'); + expect(controller.getDeviceByIeeeAddr('0x172')!.modelID).toBe('GL-C-008'); }); it('Xiaomi end device joins (node descriptor fails)', async () => { @@ -4086,17 +3978,22 @@ describe('Controller', () => { it('Should use cached node descriptor when device is re-interviewed, but retrieve it when ignoreCache=true', async () => { await controller.start(); + mockAdapterSendZdo.mockClear(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - expect(mockAdapterNodeDescriptor).toHaveBeenCalledTimes(1); + expect(mockAdapterSendZdo).toHaveBeenCalledTimes(3); // nodeDesc + activeEp + simpleDesc x1 - // Re-join should use cached node descriptor - await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - expect(mockAdapterNodeDescriptor).toHaveBeenCalledTimes(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const deviceNodeDescSpy = jest.spyOn(device, 'updateNodeDescriptor'); + + // Interview with ignoreCache=false should use cached node descriptor + await device.interview(false); + expect(deviceNodeDescSpy).toHaveBeenCalledTimes(0); + expect(mockAdapterSendZdo).toHaveBeenCalledTimes(5); // activeEp + simpleDesc x1 // Interview with ignoreCache=true should read node descriptor - const device = controller.getDeviceByIeeeAddr('0x129'); - device.interview(true); - expect(mockAdapterNodeDescriptor).toHaveBeenCalledTimes(2); + await device.interview(true); + expect(deviceNodeDescSpy).toHaveBeenCalledTimes(1); + expect(mockAdapterSendZdo).toHaveBeenCalledTimes(8); // nodeDesc + activeEp + simpleDesc x1 }); it('Receive zclData report from unkown attribute', async () => { @@ -4177,8 +4074,6 @@ describe('Controller', () => { _stackVersion: 101, _zclVersion: 1, _hardwareVersion: 3, - _events: {}, - _eventsCount: 0, _dateCode: '201901', _pendingRequestTimeout: 0, _softwareBuildID: '1.01', @@ -4250,7 +4145,7 @@ describe('Controller', () => { it('Should allow to specify custom attributes for existing cluster', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const device = controller.getDeviceByIeeeAddr('0x129'); + const device = controller.getDeviceByIeeeAddr('0x129')!; device.addCustomCluster('genBasic', { ID: 0, commands: {}, @@ -4280,7 +4175,7 @@ describe('Controller', () => { it('Should allow to specify custom cluster', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const device = controller.getDeviceByIeeeAddr('0x129'); + const device = controller.getDeviceByIeeeAddr('0x129')!; device.addCustomCluster('myCustomCluster', { ID: 9123, commands: {}, @@ -4307,7 +4202,7 @@ describe('Controller', () => { it('Should allow to specify custom cluster as override for Zcl cluster', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const device = controller.getDeviceByIeeeAddr('0x129'); + const device = controller.getDeviceByIeeeAddr('0x129')!; device.addCustomCluster('myCustomCluster', { ID: Zcl.Clusters.genBasic.ID, commands: {}, @@ -4334,8 +4229,8 @@ describe('Controller', () => { it('Send zcl command to all no options', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const device = controller.getDeviceByIeeeAddr('0x129'); - device.getEndpoint(1).zclCommandBroadcast(255, BroadcastAddress.SLEEPY, Zcl.Clusters.ssIasZone.ID, 'initTestMode', {}); + const device = controller.getDeviceByIeeeAddr('0x129')!; + device.getEndpoint(1)!.zclCommandBroadcast(255, BroadcastAddress.SLEEPY, Zcl.Clusters.ssIasZone.ID, 'initTestMode', {}); const sentFrame = Zcl.Frame.create( Zcl.FrameType.SPECIFIC, Zcl.Direction.CLIENT_TO_SERVER, @@ -4358,7 +4253,7 @@ describe('Controller', () => { it('Send zcl command to all with manufacturer option', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const device = controller.getDeviceByIeeeAddr('0x129'); + const device = controller.getDeviceByIeeeAddr('0x129')!; device.addCustomCluster('ssIasZone', { ID: Zcl.Clusters.ssIasZone.ID, commands: {boschSmokeAlarmSiren: {ID: 0x80, parameters: [{name: 'data', type: Zcl.DataType.UINT16}]}}, @@ -4367,7 +4262,7 @@ describe('Controller', () => { }); const options = {manufacturerCode: Zcl.ManufacturerCode.ROBERT_BOSCH_GMBH}; device - .getEndpoint(1) + .getEndpoint(1)! .zclCommandBroadcast(255, BroadcastAddress.SLEEPY, Zcl.Clusters.ssIasZone.ID, 'boschSmokeAlarmSiren', {data: 0x0000}, options); const sentFrame = Zcl.Frame.create( Zcl.FrameType.SPECIFIC, @@ -4392,8 +4287,8 @@ describe('Controller', () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); mocksendZclFrameToEndpoint.mockClear(); - const device = controller.getDeviceByIeeeAddr('0x129'); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const endpoint = device.getEndpoint(1)!; expect(endpoint.supportsOutputCluster('genDeviceTempCfg')).toBeTruthy(); expect(endpoint.supportsOutputCluster('genBasic')).toBeFalsy(); for (let i = 0; i < 300; i++) { @@ -4401,7 +4296,7 @@ describe('Controller', () => { } expect(mocksendZclFrameToEndpoint).toHaveBeenCalledTimes(300); - const ids = []; + const ids: number[] = []; for (let i = 0; i < 300; i++) { ids.push(mocksendZclFrameToEndpoint.mock.calls[i][3].header.transactionSequenceNumber); } @@ -4413,7 +4308,7 @@ describe('Controller', () => { it('Throw error when creating endpoint which already exists', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const device = controller.getDeviceByIeeeAddr('0x129'); + const device = controller.getDeviceByIeeeAddr('0x129')!; let error; try { await device.createEndpoint(1); @@ -4428,7 +4323,7 @@ describe('Controller', () => { await mockAdapterEvents['deviceJoined']({networkAddress: 140, ieeeAddr: '0x129'}); let error; try { - await Device.create('Router', '0x129', 140, null, null, null, null, null); + Device.create('Router', '0x129', 140, undefined, undefined, undefined, undefined, false); } catch (e) { error = e; } @@ -4437,7 +4332,7 @@ describe('Controller', () => { it('Should allow to set type', async () => { await controller.start(); - const device = await Device.create('Router', '0x129', 140, null, null, null, null, null, []); + const device = Device.create('Router', '0x129', 140, undefined, undefined, undefined, undefined, false); device.type = 'EndDevice'; expect(device.type).toStrictEqual('EndDevice'); }); @@ -4452,7 +4347,7 @@ describe('Controller', () => { it('Throw error when interviewing and calling interview again', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 140, ieeeAddr: '0x129'}); - const device = controller.getDeviceByIeeeAddr('0x129'); + const device = controller.getDeviceByIeeeAddr('0x129')!; const firstInterview = device.interview(); let error; try { @@ -4469,10 +4364,10 @@ describe('Controller', () => { it('Remove device from network', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 140, ieeeAddr: '0x140'}); - const device = controller.getDeviceByIeeeAddr('0x140'); + const device = controller.getDeviceByIeeeAddr('0x140')!; await device.removeFromNetwork(); - expect(mockAdapterRemoveDevice).toHaveBeenCalledTimes(1); - expect(mockAdapterRemoveDevice).toHaveBeenCalledWith(140, '0x140'); + const zdoPayload = Zdo.Buffalo.buildRequest(false, Zdo.ClusterId.LEAVE_REQUEST, '0x140', Zdo.LeaveRequestFlags.WITHOUT_REJOIN); + expect(mockAdapterSendZdo).toHaveBeenCalledWith('0x140', 140, Zdo.ClusterId.LEAVE_REQUEST, zdoPayload, false); expect(controller.getDeviceByIeeeAddr('0x140')).toBeUndefined(); // shouldn't throw when removing from database when not in await device.removeFromDatabase(); @@ -4481,9 +4376,9 @@ describe('Controller', () => { it('Remove group from network', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const device = controller.getDeviceByIeeeAddr('0x129'); + const device = controller.getDeviceByIeeeAddr('0x129')!; const group = await controller.createGroup(4); - const endpoint = device.getEndpoint(1); + const endpoint = device.getEndpoint(1)!; await endpoint.addToGroup(group); mocksendZclFrameToEndpoint.mockClear(); @@ -4590,7 +4485,7 @@ describe('Controller', () => { it('Device lqi', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 140, ieeeAddr: '0x140'}); - const device = controller.getDeviceByIeeeAddr('0x140'); + const device = controller.getDeviceByIeeeAddr('0x140')!; const result = await device.lqi(); expect(result).toStrictEqual({ neighbors: [ @@ -4603,12 +4498,12 @@ describe('Controller', () => { it('Device routing table', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 140, ieeeAddr: '0x140'}); - const device = controller.getDeviceByIeeeAddr('0x140'); + const device = controller.getDeviceByIeeeAddr('0x140')!; const result = await device.routingTable(); expect(result).toStrictEqual({ table: [ - {destinationAddress: 120, status: 'SUCCESS', nextHop: 1}, - {destinationAddress: 130, status: 'FAILED', nextHop: 2}, + {destinationAddress: 120, status: 'ACTIVE', nextHop: 1}, + {destinationAddress: 130, status: 'DISCOVERY_FAILED', nextHop: 2}, ], }); }); @@ -4616,7 +4511,7 @@ describe('Controller', () => { it('Device ping', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 176, ieeeAddr: '0x176'}); - const device = controller.getDeviceByIeeeAddr('0x176'); + const device = controller.getDeviceByIeeeAddr('0x176')!; mocksendZclFrameToEndpoint.mockClear(); const result = await device.ping(); expect(mocksendZclFrameToEndpoint).toHaveBeenCalledTimes(1); @@ -4670,19 +4565,19 @@ describe('Controller', () => { it('Poll control supported', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 174, ieeeAddr: '0x174'}); - const device = controller.getDeviceByIeeeAddr('0x174'); + const device = controller.getDeviceByIeeeAddr('0x174')!; await device.interview(); - const endpoint = device.getEndpoint(1); + const endpoint = device.getEndpoint(1)!; const coordinator = Device.byType('Coordinator')[0]; const target = coordinator.getEndpoint(1); expect(deepClone(endpoint.binds)).toStrictEqual(deepClone([{cluster: Zcl.Utils.getCluster('genPollCtrl', undefined, {}), target}])); device.checkinInterval = undefined; - expect(device._checkinInterval).toBeUndefined(); - expect(device._pendingRequestTimeout).toStrictEqual(0); + expect(device.checkinInterval).toBeUndefined(); + expect(device.pendingRequestTimeout).toStrictEqual(0); mocksendZclFrameToEndpoint.mockClear(); mocksendZclFrameToEndpoint.mockReturnValueOnce(null); - mocksendZclFrameToEndpoint.mockImplementationOnce((ieeeAddr, networkAddress, endpoint, frame: ZclFrame) => { + mocksendZclFrameToEndpoint.mockImplementationOnce((ieeeAddr, networkAddress, endpoint, frame: Zcl.Frame) => { const payload = [{attrId: 0, status: 0, dataType: 35, attrData: 204}]; const responseFrame = Zcl.Frame.create(0, 1, true, undefined, 10, 'readRsp', frame.cluster.ID, payload, {}); return {header: responseFrame.header, data: responseFrame.toBuffer(), clusterID: frame.cluster.ID}; @@ -4710,10 +4605,10 @@ describe('Controller', () => { groupID: undefined, }); await flushPromises(); - expect(device._checkinInterval).toStrictEqual(51); + expect(device.checkinInterval).toStrictEqual(51); expect(device.pendingRequestTimeout).toStrictEqual(51000); expect(mocksendZclFrameToEndpoint).toHaveBeenCalledTimes(3); - device._checkinInterval = 50; + device.checkinInterval = 50; mocksendZclFrameToEndpoint.mockClear(); frame = Zcl.Frame.create( @@ -4749,54 +4644,54 @@ describe('Controller', () => { it('Poll control unsupported', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const device = controller.getDeviceByIeeeAddr('0x129'); + const device = controller.getDeviceByIeeeAddr('0x129')!; await device.interview(); - const endpoint = device.getEndpoint(1); - const coordinator = Device.byType('Coordinator')[0]; - const target = coordinator.getEndpoint(1); + const endpoint = device.getEndpoint(1)!; expect(deepClone(endpoint.binds)).toStrictEqual([]); }); it('Endpoint get id', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const device = controller.getDeviceByIeeeAddr('0x129'); - expect(device.getEndpoint(1).ID).toBe(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + expect(device.getEndpoint(1)!.ID).toBe(1); }); it('Endpoint get id by endpoint device type', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 172, ieeeAddr: '0x172'}); - const device = controller.getDeviceByIeeeAddr('0x172'); + const device = controller.getDeviceByIeeeAddr('0x172')!; expect(device.getEndpointByDeviceType('ZLLOnOffPluginUnit')).toBeUndefined(); - expect(device.getEndpointByDeviceType('ZLLExtendedColorLight').ID).toBe(11); + expect(device.getEndpointByDeviceType('ZLLExtendedColorLight')!.ID).toBe(11); }); it('Endpoint bind', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); await mockAdapterEvents['deviceJoined']({networkAddress: 170, ieeeAddr: '0x170'}); - const device = controller.getDeviceByIeeeAddr('0x129'); - const target = controller.getDeviceByIeeeAddr('0x170').getEndpoint(1); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const target = controller.getDeviceByIeeeAddr('0x170')!.getEndpoint(1)!; + const endpoint = device.getEndpoint(1)!; + mockAdapterSendZdo.mockClear(); await endpoint.bind('genBasic', target); expect(deepClone(endpoint.binds)).toStrictEqual(deepClone([{cluster: Zcl.Utils.getCluster(0, undefined, {}), target}])); - expect(mockAdapterBind).toHaveBeenCalledWith(129, '0x129', 1, 0, '0x170', 'endpoint', 1); + const zdoPayload = Zdo.Buffalo.buildRequest(false, Zdo.ClusterId.BIND_REQUEST, '0x129', 1, 0, Zdo.UNICAST_BINDING, '0x170', 0, 1); + expect(mockAdapterSendZdo).toHaveBeenCalledWith('0x129', 129, Zdo.ClusterId.BIND_REQUEST, zdoPayload, false); // Should bind another time but not add it to the binds - mockAdapterBind.mockClear(); + mockAdapterSendZdo.mockClear(); await endpoint.bind('genBasic', target); expect(deepClone(endpoint.binds)).toStrictEqual(deepClone([{cluster: Zcl.Utils.getCluster(0, undefined, {}), target}])); - expect(mockAdapterBind).toHaveBeenCalledWith(129, '0x129', 1, 0, '0x170', 'endpoint', 1); + expect(mockAdapterSendZdo).toHaveBeenCalledWith('0x129', 129, Zdo.ClusterId.BIND_REQUEST, zdoPayload, false); }); it('Endpoint addBinding', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); await mockAdapterEvents['deviceJoined']({networkAddress: 170, ieeeAddr: '0x170'}); - const device = controller.getDeviceByIeeeAddr('0x129'); - const target = controller.getDeviceByIeeeAddr('0x170').getEndpoint(1); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const target = controller.getDeviceByIeeeAddr('0x170')!.getEndpoint(1)!; + const endpoint = device.getEndpoint(1)!; endpoint.addBinding('genPowerCfg', target); expect(deepClone(endpoint.binds)).toStrictEqual(deepClone([{cluster: Zcl.Utils.getCluster(1, undefined, {}), target}])); @@ -4808,9 +4703,15 @@ describe('Controller', () => { it('Endpoint get binds non-existing device', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const device = controller.getDeviceByIeeeAddr('0x129'); - const endpoint = device.getEndpoint(1); - endpoint._binds.push({type: 'endpoint', deviceIeeeAddress: 'notexisting', endpointID: 1, cluster: 2}); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const endpoint = device.getEndpoint(1)!; + // @ts-expect-error private + endpoint._binds.push({ + type: 'endpoint', + deviceIeeeAddress: 'notexisting', + endpointID: 1, + cluster: 2, + }); expect(endpoint.binds).toStrictEqual([]); }); @@ -4818,19 +4719,30 @@ describe('Controller', () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); const group = await controller.createGroup(4); - const device = controller.getDeviceByIeeeAddr('0x129'); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const endpoint = device.getEndpoint(1)!; await endpoint.bind('genPowerCfg', group); expect(deepClone(endpoint.binds)).toStrictEqual(deepClone([{cluster: Zcl.Utils.getCluster(1, undefined, {}), target: group}])); - expect(mockAdapterBind).toHaveBeenCalledWith(129, '0x129', 1, 1, 4, 'group', undefined); + const zdoPayload = Zdo.Buffalo.buildRequest( + false, + Zdo.ClusterId.BIND_REQUEST, + '0x129', + 1, + 1, + Zdo.MULTICAST_BINDING, + ZSpec.BLANK_EUI64, + 4, + 0xff, + ); + expect(mockAdapterSendZdo).toHaveBeenCalledWith('0x129', 129, Zdo.ClusterId.BIND_REQUEST, zdoPayload, false); }); it('Group addBinding', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); const group = await controller.createGroup(4); - const device = controller.getDeviceByIeeeAddr('0x129'); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const endpoint = device.getEndpoint(1)!; endpoint.addBinding('genBasic', group); expect(deepClone(endpoint.binds)).toStrictEqual(deepClone([{cluster: Zcl.Utils.getCluster(0, undefined, {}), target: group}])); }); @@ -4839,20 +4751,31 @@ describe('Controller', () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); expect(Group.byGroupID(11)).toBeUndefined(); - const device = controller.getDeviceByIeeeAddr('0x129'); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const endpoint = device.getEndpoint(1)!; await endpoint.bind('genPowerCfg', 11); const group = Group.byGroupID(11); expect(deepClone(endpoint.binds)).toStrictEqual(deepClone([{cluster: Zcl.Utils.getCluster(1, undefined, {}), target: group}])); - expect(mockAdapterBind).toHaveBeenCalledWith(129, '0x129', 1, 1, 11, 'group', undefined); + const zdoPayload = Zdo.Buffalo.buildRequest( + false, + Zdo.ClusterId.BIND_REQUEST, + '0x129', + 1, + 1, + Zdo.MULTICAST_BINDING, + ZSpec.BLANK_EUI64, + 11, + 0xff, + ); + expect(mockAdapterSendZdo).toHaveBeenCalledWith('0x129', 129, Zdo.ClusterId.BIND_REQUEST, zdoPayload, false); }); it('Group addBinding by number (should create group)', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); expect(Group.byGroupID(11)).toBeUndefined(); - const device = controller.getDeviceByIeeeAddr('0x129'); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const endpoint = device.getEndpoint(1)!; endpoint.addBinding('genBasic', 11); const group = Group.byGroupID(11); expect(deepClone(endpoint.binds)).toStrictEqual(deepClone([{cluster: Zcl.Utils.getCluster(0, undefined, {}), target: group}])); @@ -4862,55 +4785,76 @@ describe('Controller', () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); await mockAdapterEvents['deviceJoined']({networkAddress: 170, ieeeAddr: '0x170'}); - const device = controller.getDeviceByIeeeAddr('0x129'); - const target = controller.getDeviceByIeeeAddr('0x170').getEndpoint(1); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const target = controller.getDeviceByIeeeAddr('0x170')!.getEndpoint(1)!; + const endpoint = device.getEndpoint(1)!; await endpoint.bind('genBasic', target); - mockAdapterBind.mockClear(); await endpoint.unbind('genBasic', target); expect(endpoint.binds).toStrictEqual([]); - expect(mockAdapterUnbind).toHaveBeenCalledWith(129, '0x129', 1, 0, '0x170', 'endpoint', 1); + const zdoPayload = Zdo.Buffalo.buildRequest(false, Zdo.ClusterId.UNBIND_REQUEST, '0x129', 1, 0, Zdo.UNICAST_BINDING, '0x170', 0, 1); + expect(mockAdapterSendZdo).toHaveBeenCalledWith('0x129', 129, Zdo.ClusterId.UNBIND_REQUEST, zdoPayload, false); // Should unbind another time when not in binds - mockAdapterBind.mockClear(); await endpoint.unbind('genBasic', target); expect(endpoint.binds).toStrictEqual([]); - expect(mockAdapterUnbind).toHaveBeenCalledWith(129, '0x129', 1, 0, '0x170', 'endpoint', 1); + expect(mockAdapterSendZdo).toHaveBeenCalledWith('0x129', 129, Zdo.ClusterId.UNBIND_REQUEST, zdoPayload, false); }); it('Group unbind', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); const group = await controller.createGroup(5); - const device = controller.getDeviceByIeeeAddr('0x129'); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const endpoint = device.getEndpoint(1)!; expect(endpoint.binds.length).toBe(0); await endpoint.bind('genPowerCfg', group); expect(endpoint.binds.length).toBe(1); await endpoint.unbind('genPowerCfg', group); - expect(mockAdapterUnbind).toHaveBeenCalledWith(129, '0x129', 1, 1, 5, 'group', undefined); - expect(endpoint.binds.length).toBe(0); - }); - - it('Group unbind by number', async () => { + const zdoPayload = Zdo.Buffalo.buildRequest( + false, + Zdo.ClusterId.UNBIND_REQUEST, + '0x129', + 1, + 1, + Zdo.MULTICAST_BINDING, + ZSpec.BLANK_EUI64, + 5, + 0xff, + ); + expect(mockAdapterSendZdo).toHaveBeenCalledWith('0x129', 129, Zdo.ClusterId.UNBIND_REQUEST, zdoPayload, false); + expect(endpoint.binds.length).toBe(0); + }); + + it('Group unbind by number', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const device = controller.getDeviceByIeeeAddr('0x129'); + const device = controller.getDeviceByIeeeAddr('0x129')!; const group = await controller.createGroup(5); - const endpoint = device.getEndpoint(1); + const endpoint = device.getEndpoint(1)!; expect(endpoint.binds.length).toBe(0); await endpoint.bind('genPowerCfg', group); expect(endpoint.binds.length).toBe(1); await endpoint.unbind('genPowerCfg', 5); - expect(mockAdapterUnbind).toHaveBeenCalledWith(129, '0x129', 1, 1, 5, 'group', undefined); + const zdoPayload = Zdo.Buffalo.buildRequest( + false, + Zdo.ClusterId.UNBIND_REQUEST, + '0x129', + 1, + 1, + Zdo.MULTICAST_BINDING, + ZSpec.BLANK_EUI64, + 5, + 0xff, + ); + expect(mockAdapterSendZdo).toHaveBeenCalledWith('0x129', 129, Zdo.ClusterId.UNBIND_REQUEST, zdoPayload, false); expect(endpoint.binds.length).toBe(0); }); it('Endpoint configure reporting', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const device = controller.getDeviceByIeeeAddr('0x129'); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const endpoint = device.getEndpoint(1)!; mocksendZclFrameToEndpoint.mockClear(); await endpoint.configureReporting('genPowerCfg', [ { @@ -4990,10 +4934,11 @@ describe('Controller', () => { it('Should replace legacy configured reportings without manufacturerCode', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const device = controller.getDeviceByIeeeAddr('0x129'); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const endpoint = device.getEndpoint(1)!; mocksendZclFrameToEndpoint.mockClear(); + // @ts-expect-error private endpoint._configuredReportings = [{cluster: 65382, attrId: 5, minRepIntval: 60, maxRepIntval: 900, repChange: 1}]; await endpoint.configureReporting('liXeePrivate', [ @@ -5013,9 +4958,10 @@ describe('Controller', () => { it('Endpoint configure reporting for manufacturer specific attribute', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const device = controller.getDeviceByIeeeAddr('0x129'); + const device = controller.getDeviceByIeeeAddr('0x129')!; + // @ts-expect-error private device._manufacturerID = 4641; - const endpoint = device.getEndpoint(1); + const endpoint = device.getEndpoint(1)!; mocksendZclFrameToEndpoint.mockClear(); await endpoint.configureReporting( 'hvacThermostat', @@ -5079,9 +5025,10 @@ describe('Controller', () => { it('Endpoint configure reporting for manufacturer specific attribute from definition', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const device = controller.getDeviceByIeeeAddr('0x129'); + const device = controller.getDeviceByIeeeAddr('0x129')!; + // @ts-expect-error private device._manufacturerID = 0x10f2; - const endpoint = device.getEndpoint(1); + const endpoint = device.getEndpoint(1)!; mocksendZclFrameToEndpoint.mockClear(); await endpoint.configureReporting('hvacThermostat', [ { @@ -5141,9 +5088,10 @@ describe('Controller', () => { it('Endpoint configure reporting with manufacturer attribute should throw exception', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const device = controller.getDeviceByIeeeAddr('0x129'); + const device = controller.getDeviceByIeeeAddr('0x129')!; + // @ts-expect-error private device._manufacturerID = 0x10f2; - const endpoint = device.getEndpoint(1); + const endpoint = device.getEndpoint(1)!; mocksendZclFrameToEndpoint.mockClear(); let error; try { @@ -5170,8 +5118,8 @@ describe('Controller', () => { it('Save endpoint configure reporting', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const device = controller.getDeviceByIeeeAddr('0x129'); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const endpoint = device.getEndpoint(1)!; const genPowerCfg = Zcl.Utils.getCluster('genPowerCfg', undefined, {}); const msOccupancySensing = Zcl.Utils.getCluster('msOccupancySensing', undefined, {}); @@ -5239,8 +5187,8 @@ describe('Controller', () => { configureReportStatus = 1; await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const device = controller.getDeviceByIeeeAddr('0x129'); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const endpoint = device.getEndpoint(1)!; mocksendZclFrameToEndpoint.mockClear(); let error; try { @@ -5267,8 +5215,8 @@ describe('Controller', () => { configureReportDefaultRsp = true; await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const device = controller.getDeviceByIeeeAddr('0x129'); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const endpoint = device.getEndpoint(1)!; mocksendZclFrameToEndpoint.mockClear(); let error; try { @@ -5294,8 +5242,8 @@ describe('Controller', () => { configureReportStatus = 1; await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const device = controller.getDeviceByIeeeAddr('0x129'); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const endpoint = device.getEndpoint(1)!; mocksendZclFrameToEndpoint.mockReturnValueOnce(null); let error; try { @@ -5339,8 +5287,8 @@ describe('Controller', () => { it('Add endpoint to group', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const device = controller.getDeviceByIeeeAddr('0x129'); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const endpoint = device.getEndpoint(1)!; const group = await controller.createGroup(2); mocksendZclFrameToEndpoint.mockClear(); await endpoint.addToGroup(group); @@ -5440,15 +5388,15 @@ describe('Controller', () => { }); expect(group.members).toContain(endpoint); expect(databaseContents()).toContain( - `{"id":1,"type":"Coordinator","ieeeAddr":"0x0000012300000000","nwkAddr":123,"manufId":100,"epList":[1,2],"endpoints":{"1":{"profId":2,"epId":1,"devId":3,"inClusterList":[10],"outClusterList":[11],"clusters":{},"binds":[],"configuredReportings":[],"meta":{}},"2":{"profId":3,"epId":2,"devId":5,"inClusterList":[1],"outClusterList":[0],"clusters":{},"binds":[],"configuredReportings":[],"meta":{}}},"interviewCompleted":true,"meta":{}}\n{"id":2,"type":"Router","ieeeAddr":"0x129","nwkAddr":129,"manufId":1212,"manufName":"KoenAndCo","powerSource":"Mains (single phase)","modelId":"myModelID","epList":[1],"endpoints":{"1":{"profId":99,"epId":1,"devId":5,"inClusterList":[0,1],"outClusterList":[2],"clusters":{},"binds":[],"configuredReportings":[],"meta":{}}},"appVersion":2,"stackVersion":101,"hwVersion":3,"dateCode":"201901","swBuildId":"1.01","zclVersion":1,"interviewCompleted":true,"meta":{},"lastSeen":150}\n{"id":3,"type":"Group","groupID":2,"members":[{"deviceIeeeAddr":"0x129","endpointID":1}],"meta":{}}`, + `{"id":1,"type":"Coordinator","ieeeAddr":"0x0000012300000000","nwkAddr":0,"manufId":7,"epList":[1,2],"endpoints":{"1":{"profId":2,"epId":1,"devId":3,"inClusterList":[10],"outClusterList":[11],"clusters":{},"binds":[],"configuredReportings":[],"meta":{}},"2":{"profId":3,"epId":2,"devId":5,"inClusterList":[1],"outClusterList":[0],"clusters":{},"binds":[],"configuredReportings":[],"meta":{}}},"interviewCompleted":true,"meta":{}}\n{"id":2,"type":"Router","ieeeAddr":"0x129","nwkAddr":129,"manufId":1212,"manufName":"KoenAndCo","powerSource":"Mains (single phase)","modelId":"myModelID","epList":[1],"endpoints":{"1":{"profId":99,"epId":1,"devId":5,"inClusterList":[0,1],"outClusterList":[2],"clusters":{},"binds":[],"configuredReportings":[],"meta":{}}},"appVersion":2,"stackVersion":101,"hwVersion":3,"dateCode":"201901","swBuildId":"1.01","zclVersion":1,"interviewCompleted":true,"meta":{},"lastSeen":150}\n{"id":3,"type":"Group","groupID":2,"members":[{"deviceIeeeAddr":"0x129","endpointID":1}],"meta":{}}`, ); }); it('Remove endpoint from group', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const device = controller.getDeviceByIeeeAddr('0x129'); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const endpoint = device.getEndpoint(1)!; const group = await controller.createGroup(2); await group.addMember(endpoint); mocksendZclFrameToEndpoint.mockClear(); @@ -5545,8 +5493,8 @@ describe('Controller', () => { it('Remove endpoint from group by number', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const device = controller.getDeviceByIeeeAddr('0x129'); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const endpoint = device.getEndpoint(1)!; mocksendZclFrameToEndpoint.mockClear(); await endpoint.removeFromGroup(4); const call = mocksendZclFrameToEndpoint.mock.calls[0]; @@ -5640,8 +5588,8 @@ describe('Controller', () => { it('Command response', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const device = controller.getDeviceByIeeeAddr('0x129'); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const endpoint = device.getEndpoint(1)!; const response = await endpoint.command('genGroups', 'add', {groupid: 1, groupname: ''}); expect(response).toStrictEqual({groupid: 1, status: 0}); }); @@ -5674,8 +5622,8 @@ describe('Controller', () => { it('Endpoint command with options', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const device = controller.getDeviceByIeeeAddr('0x129'); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const endpoint = device.getEndpoint(1)!; mocksendZclFrameToEndpoint.mockClear(); await endpoint.command('genOnOff', 'off', {}, {manufacturerCode: 100, disableDefaultResponse: true}); expect(mocksendZclFrameToEndpoint.mock.calls[0][0]).toBe('0x129'); @@ -5701,8 +5649,8 @@ describe('Controller', () => { it('Endpoint command with duplicate cluster ID', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const device = controller.getDeviceByIeeeAddr('0x129'); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const endpoint = device.getEndpoint(1)!; mocksendZclFrameToEndpoint.mockClear(); await endpoint.command('manuSpecificAssaDoorLock', 'getBatteryLevel', {}); expect(mocksendZclFrameToEndpoint.mock.calls[0][0]).toBe('0x129'); @@ -5714,8 +5662,8 @@ describe('Controller', () => { it('Endpoint command with duplicate identifier', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const device = controller.getDeviceByIeeeAddr('0x129'); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const endpoint = device.getEndpoint(1)!; mocksendZclFrameToEndpoint.mockClear(); await endpoint.command('lightingColorCtrl', 'tuyaMoveToHueAndSaturationBrightness', {hue: 1, saturation: 1, transtime: 0, brightness: 22}); expect(mocksendZclFrameToEndpoint.mock.calls[0][0]).toBe('0x129'); @@ -5727,10 +5675,10 @@ describe('Controller', () => { it('Endpoint commandResponse', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const device = controller.getDeviceByIeeeAddr('0x129'); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const endpoint = device.getEndpoint(1)!; mocksendZclFrameToEndpoint.mockClear(); - await endpoint.commandResponse('genOta', 'imageNotify', {payloadType: 0, queryJitter: 1}, null, null); + await endpoint.commandResponse('genOta', 'imageNotify', {payloadType: 0, queryJitter: 1}, undefined, undefined); expect(mocksendZclFrameToEndpoint.mock.calls[0][0]).toBe('0x129'); expect(mocksendZclFrameToEndpoint.mock.calls[0][1]).toBe(129); expect(mocksendZclFrameToEndpoint.mock.calls[0][2]).toBe(1); @@ -5871,8 +5819,8 @@ describe('Controller', () => { it('Endpoint waitForCommand', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const device = controller.getDeviceByIeeeAddr('0x129'); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const endpoint = device.getEndpoint(1)!; mocksendZclFrameToEndpoint.mockClear(); const buffer = Buffer.from([24, 169, 10, 0, 0, 24, 1]); const frame = Zcl.Frame.fromBuffer(Zcl.Utils.getCluster('msOccupancySensing', undefined, {}).ID, Zcl.Header.fromBuffer(buffer), buffer, {}); @@ -5895,8 +5843,8 @@ describe('Controller', () => { it('Endpoint waitForCommand error', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const device = controller.getDeviceByIeeeAddr('0x129'); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const endpoint = device.getEndpoint(1)!; mocksendZclFrameToEndpoint.mockClear(); const promise = new Promise((resolve, reject) => reject(new Error('whoops!'))); mockAdapterWaitFor.mockReturnValueOnce({promise, cancel: () => {}}); @@ -5973,8 +5921,6 @@ describe('Controller', () => { _hardwareVersion: 1, _ieeeAddr: '0x90fd9ffffe4b64ae', _interviewCompleted: true, - _events: {}, - _eventsCount: 0, _interviewing: false, _manufacturerID: 4476, _manufacturerName: 'IKEA of Sweden', @@ -6073,8 +6019,8 @@ describe('Controller', () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); mocksendZclFrameToEndpoint.mockClear(); - const device = controller.getDeviceByIeeeAddr('0x129'); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const endpoint = device.getEndpoint(1)!; const options = {manufacturerCode: 0x100b, disableDefaultResponse: true, timeout: 12, defaultResponseTimeout: 16}; await endpoint.write('genBasic', {0x0031: {value: 0x000b, type: 0x19}}, options); expect(mocksendZclFrameToEndpoint).toHaveBeenCalledTimes(1); @@ -6136,9 +6082,10 @@ describe('Controller', () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); mocksendZclFrameToEndpoint.mockClear(); - const device = controller.getDeviceByIeeeAddr('0x129'); + const device = controller.getDeviceByIeeeAddr('0x129')!; + // @ts-expect-error private device._manufacturerID = 0x10f2; - const endpoint = device.getEndpoint(1); + const endpoint = device.getEndpoint(1)!; await endpoint.write('hvacThermostat', {viessmannWindowOpenInternal: 1}); expect(mocksendZclFrameToEndpoint).toHaveBeenCalledTimes(1); const call = mocksendZclFrameToEndpoint.mock.calls[0]; @@ -6172,8 +6119,8 @@ describe('Controller', () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); mocksendZclFrameToEndpoint.mockClear(); - const device = controller.getDeviceByIeeeAddr('0x129'); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const endpoint = device.getEndpoint(1)!; const options = { manufacturerCode: 0x100b, disableDefaultResponse: true, @@ -6241,8 +6188,8 @@ describe('Controller', () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); mocksendZclFrameToEndpoint.mockClear(); - const device = controller.getDeviceByIeeeAddr('0x129'); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const endpoint = device.getEndpoint(1)!; let error; try { await endpoint.write('genBasic', {UNKNOWN: {value: 0x000b, type: 0x19}}); @@ -6257,9 +6204,10 @@ describe('Controller', () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); mocksendZclFrameToEndpoint.mockClear(); - const device = controller.getDeviceByIeeeAddr('0x129'); + const device = controller.getDeviceByIeeeAddr('0x129')!; + // @ts-expect-error private device._manufacturerID = 0x10f2; - const endpoint = device.getEndpoint(1); + const endpoint = device.getEndpoint(1)!; let error; try { await endpoint.write('hvacThermostat', {occupiedHeatingSetpoint: 2000, viessmannWindowOpenInternal: 1}); @@ -6273,8 +6221,8 @@ describe('Controller', () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); mocksendZclFrameToEndpoint.mockClear(); - const device = controller.getDeviceByIeeeAddr('0x129'); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const endpoint = device.getEndpoint(1)!; await endpoint.writeResponse('genBasic', 99, {0x55: {status: 0x01}}); expect(mocksendZclFrameToEndpoint).toHaveBeenCalledTimes(1); const call = mocksendZclFrameToEndpoint.mock.calls[0]; @@ -6332,8 +6280,8 @@ describe('Controller', () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); mocksendZclFrameToEndpoint.mockClear(); - const device = controller.getDeviceByIeeeAddr('0x129'); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const endpoint = device.getEndpoint(1)!; let error; try { await endpoint.writeResponse('genBasic', 99, {UNKNOWN: {status: 0x01}}); @@ -6348,8 +6296,8 @@ describe('Controller', () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); mocksendZclFrameToEndpoint.mockClear(); - const device = controller.getDeviceByIeeeAddr('0x129'); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const endpoint = device.getEndpoint(1)!; let error; try { await endpoint.writeResponse('genBasic', 99, {UNKNOWN: {status: 0x01}}, {transactionSequenceNumber: 5}); @@ -6364,8 +6312,8 @@ describe('Controller', () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); mocksendZclFrameToEndpoint.mockClear(); - const device = controller.getDeviceByIeeeAddr('0x129'); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const endpoint = device.getEndpoint(1)!; let error; try { await endpoint.writeResponse('genBasic', 99, {0x0001: {value: 0x55}}); @@ -6380,8 +6328,8 @@ describe('Controller', () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); mocksendZclFrameToEndpoint.mockClear(); - const device = controller.getDeviceByIeeeAddr('0x129'); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const endpoint = device.getEndpoint(1)!; await endpoint.writeResponse('genBasic', 99, {zclVersion: {status: 0x01}}); expect(mocksendZclFrameToEndpoint).toHaveBeenCalledTimes(1); const call = mocksendZclFrameToEndpoint.mock.calls[0]; @@ -6438,8 +6386,8 @@ describe('Controller', () => { it('WriteResponse error', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const device = controller.getDeviceByIeeeAddr('0x129'); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const endpoint = device.getEndpoint(1)!; mocksendZclFrameToEndpoint.mockRejectedValueOnce(new Error('timeout occurred')); let error; try { @@ -6458,8 +6406,8 @@ describe('Controller', () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); mocksendZclFrameToEndpoint.mockClear(); - const device = controller.getDeviceByIeeeAddr('0x129'); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const endpoint = device.getEndpoint(1)!; await endpoint.read('genBasic', ['stackVersion']); expect(mocksendZclFrameToEndpoint).toHaveBeenCalledTimes(1); const call = mocksendZclFrameToEndpoint.mock.calls[0]; @@ -6510,9 +6458,10 @@ describe('Controller', () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); mocksendZclFrameToEndpoint.mockClear(); - const device = controller.getDeviceByIeeeAddr('0x129'); + const device = controller.getDeviceByIeeeAddr('0x129')!; + // @ts-expect-error private device._manufacturerID = 0x10f2; - const endpoint = device.getEndpoint(1); + const endpoint = device.getEndpoint(1)!; await endpoint.read('hvacThermostat', ['viessmannWindowOpenInternal']); expect(mocksendZclFrameToEndpoint).toHaveBeenCalledTimes(1); const call = mocksendZclFrameToEndpoint.mock.calls[0]; @@ -6537,9 +6486,10 @@ describe('Controller', () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); mocksendZclFrameToEndpoint.mockClear(); - const device = controller.getDeviceByIeeeAddr('0x129'); + const device = controller.getDeviceByIeeeAddr('0x129')!; + // @ts-expect-error private device._manufacturerID = 0x10f2; - const endpoint = device.getEndpoint(1); + const endpoint = device.getEndpoint(1)!; let error; try { await endpoint.read('hvacThermostat', ['localTemp', 'viessmannWindowOpenInternal']); @@ -6553,8 +6503,8 @@ describe('Controller', () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); mocksendZclFrameToEndpoint.mockClear(); - const device = controller.getDeviceByIeeeAddr('0x129'); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const endpoint = device.getEndpoint(1)!; await endpoint.read('genBasic', [0xff22], {manufacturerCode: 0x115f, disableDefaultResponse: true}); expect(mocksendZclFrameToEndpoint).toHaveBeenCalledTimes(1); const call = mocksendZclFrameToEndpoint.mock.calls[0]; @@ -6606,8 +6556,8 @@ describe('Controller', () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); mocksendZclFrameToEndpoint.mockClear(); - const device = controller.getDeviceByIeeeAddr('0x129'); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const endpoint = device.getEndpoint(1)!; await endpoint.readResponse('genBasic', 99, {0x55: {value: 0x000b, type: 0x19}}); expect(mocksendZclFrameToEndpoint).toHaveBeenCalledTimes(1); const call = mocksendZclFrameToEndpoint.mock.calls[0]; @@ -6667,8 +6617,8 @@ describe('Controller', () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); mocksendZclFrameToEndpoint.mockClear(); - const device = controller.getDeviceByIeeeAddr('0x129'); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const endpoint = device.getEndpoint(1)!; let error; try { await endpoint.readResponse('genBasic', 99, {UNKNOWN: {value: 0x000b, type: 0x19}}); @@ -6683,8 +6633,8 @@ describe('Controller', () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); mocksendZclFrameToEndpoint.mockClear(); - const device = controller.getDeviceByIeeeAddr('0x129'); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const endpoint = device.getEndpoint(1)!; let error; try { await endpoint.readResponse('genBasic', 99, {UNKNOWN: {value: 0x000b, type: 0x19}}, {transactionSequenceNumber: 5}); @@ -6699,8 +6649,8 @@ describe('Controller', () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); mocksendZclFrameToEndpoint.mockClear(); - const device = controller.getDeviceByIeeeAddr('0x129'); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const endpoint = device.getEndpoint(1)!; await endpoint.configureReporting('hvacThermostat', [ { attribute: {ID: 0x4004, type: 41}, @@ -6762,17 +6712,17 @@ describe('Controller', () => { it('Remove endpoint from all groups', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const device1 = controller.getDeviceByIeeeAddr('0x129'); + const device1 = controller.getDeviceByIeeeAddr('0x129')!; await mockAdapterEvents['deviceJoined']({networkAddress: 170, ieeeAddr: '0x170'}); - const device2 = controller.getDeviceByIeeeAddr('0x170'); + const device2 = controller.getDeviceByIeeeAddr('0x170')!; const group1 = await controller.createGroup(1); const group6 = await controller.createGroup(6); const group7 = await controller.createGroup(7); - const endpoint1 = device1.getEndpoint(1); + const endpoint1 = device1.getEndpoint(1)!; await group1.addMember(endpoint1); await group6.addMember(endpoint1); - await group6.addMember(device2.getEndpoint(1)); - await group7.addMember(device2.getEndpoint(1)); + await group6.addMember(device2.getEndpoint(1)!); + await group7.addMember(device2.getEndpoint(1)!); mocksendZclFrameToEndpoint.mockClear(); await endpoint1.removeFromAllGroups(); const call = mocksendZclFrameToEndpoint.mock.calls[0]; @@ -6901,8 +6851,6 @@ describe('Controller', () => { deviceNetworkAddress: 0, _binds: [], _configuredReportings: [], - _events: {}, - _eventsCount: 0, meta: {}, pendingRequests: {ID: 1, deviceIeeeAddress: '0x0000012300000000', sendInProgress: false}, }, @@ -7042,8 +6990,6 @@ describe('Controller', () => { _hardwareVersion: 1, _ieeeAddr: '0x000b57fffec6a5b2', _interviewCompleted: true, - _events: {}, - _eventsCount: 0, _interviewing: false, _manufacturerID: 4476, _manufacturerName: 'IKEA of Sweden', @@ -7176,59 +7122,62 @@ describe('Controller', () => { }); expect((await controller.getGroups()).length).toBe(2); - const group1 = controller.getGroupByID(1); - group1._members = Array.from(group1._members); - expect(deepClone(group1)).toStrictEqual({_events: {}, _eventsCount: 0, databaseID: 2, groupID: 1, _members: [], meta: {}}); - const group2 = controller.getGroupByID(2); - group2._members = Array.from(group2._members); - expect(deepClone(group2)).toStrictEqual({ - _events: {}, - _eventsCount: 0, - databaseID: 5, - groupID: 2, - _members: [ - { - meta: {}, - _binds: [], - _configuredReportings: [], - clusters: {}, - ID: 1, - _events: {}, - _eventsCount: 0, - deviceID: 544, - deviceIeeeAddress: '0x000b57fffec6a5b2', - deviceNetworkAddress: 40369, - inputClusters: [0, 3, 4, 5, 6, 8, 768, 2821, 4096], - outputClusters: [5, 25, 32, 4096], - pendingRequests: {ID: 1, deviceIeeeAddress: '0x000b57fffec6a5b2', sendInProgress: false}, - profileID: 49246, - }, - ], - meta: {}, - }); + const group1 = controller.getGroupByID(1)!; + expect(deepClone(group1)).toStrictEqual(deepClone({_events: {}, _eventsCount: 0, databaseID: 2, groupID: 1, _members: new Set(), meta: {}})); + const group2 = controller.getGroupByID(2)!; + expect(deepClone(group2)).toStrictEqual( + deepClone({ + _events: {}, + _eventsCount: 0, + databaseID: 5, + groupID: 2, + _members: new Set([ + { + meta: {}, + _binds: [], + _configuredReportings: [], + clusters: {}, + ID: 1, + _events: {}, + _eventsCount: 0, + deviceID: 544, + deviceIeeeAddress: '0x000b57fffec6a5b2', + deviceNetworkAddress: 40369, + inputClusters: [0, 3, 4, 5, 6, 8, 768, 2821, 4096], + outputClusters: [5, 25, 32, 4096], + pendingRequests: {ID: 1, deviceIeeeAddress: '0x000b57fffec6a5b2', sendInProgress: false}, + profileID: 49246, + }, + ]), + meta: {}, + }), + ); }); it('Shouldnt load device from group databaseentry', async () => { expect(() => { // @ts-ignore Device.fromDatabaseEntry({type: 'Group', endpoints: []}); - }).toThrowError('Cannot load device from group'); + }).toThrow('Cannot load device from group'); }); it('Should throw datbase basic crud errors', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); expect(() => { + // @ts-expect-error mock controller.database.insert({id: 2}); - }).toThrowError(`DatabaseEntry with ID '2' already exists`); + }).toThrow(`DatabaseEntry with ID '2' already exists`); expect(() => { + // @ts-expect-error mock controller.database.remove(3); - }).toThrowError(`DatabaseEntry with ID '3' does not exist`); + }).toThrow(`DatabaseEntry with ID '3' does not exist`); expect(() => { + // @ts-expect-error mock controller.database.update({id: 3}); - }).toThrowError(`DatabaseEntry with ID '3' does not exist`); + }).toThrow(`DatabaseEntry with ID '3' does not exist`); }); it('Should save received attributes', async () => { @@ -7246,7 +7195,7 @@ describe('Controller', () => { linkquality: 50, groupID: 1, }); - const device = controller.getDeviceByIeeeAddr('0x129'); + const device = controller.getDeviceByIeeeAddr('0x129')!; const endpoint = device.endpoints[0]; expect(endpoint.getClusterAttributeValue('msOccupancySensing', 'occupancy')).toBe(1); expect(endpoint.getClusterAttributeValue('genBasic', 'modelId')).toBeUndefined(); @@ -7269,32 +7218,102 @@ describe('Controller', () => { it('Adapter create', async () => { mockZStackAdapterIsValidPath.mockReturnValueOnce(true); - await Adapter.create(null, {path: '/dev/bla', baudRate: 100, rtscts: false, adapter: undefined}, null, null); + await Adapter.create( + { + panID: 0, + channelList: [], + }, + {path: '/dev/bla', baudRate: 100, rtscts: false, adapter: undefined}, + 'test.db', + { + disableLED: false, + }, + ); expect(mockZStackAdapterIsValidPath).toHaveBeenCalledWith('/dev/bla'); - expect(ZStackAdapter).toHaveBeenCalledWith(null, {baudRate: 100, path: '/dev/bla', rtscts: false, adapter: undefined}, null, null); + expect(ZStackAdapter).toHaveBeenCalledWith( + { + panID: 0, + channelList: [], + }, + {baudRate: 100, path: '/dev/bla', rtscts: false, adapter: undefined}, + 'test.db', + { + disableLED: false, + }, + ); }); it('Adapter create continue when is valid path fails', async () => { mockZStackAdapterIsValidPath.mockImplementationOnce(() => { throw new Error('failed'); }); - await Adapter.create(null, {path: '/dev/bla', baudRate: 100, rtscts: false, adapter: undefined}, null, null); + await Adapter.create( + { + panID: 0, + channelList: [], + }, + {path: '/dev/bla', baudRate: 100, rtscts: false, adapter: undefined}, + 'test.db', + { + disableLED: false, + }, + ); expect(mockZStackAdapterIsValidPath).toHaveBeenCalledWith('/dev/bla'); - expect(ZStackAdapter).toHaveBeenCalledWith(null, {baudRate: 100, path: '/dev/bla', rtscts: false, adapter: undefined}, null, null); + expect(ZStackAdapter).toHaveBeenCalledWith( + { + panID: 0, + channelList: [], + }, + {baudRate: 100, path: '/dev/bla', rtscts: false, adapter: undefined}, + 'test.db', + { + disableLED: false, + }, + ); }); it('Adapter create auto detect', async () => { mockZStackAdapterIsValidPath.mockReturnValueOnce(true); mockZStackAdapterAutoDetectPath.mockReturnValueOnce('/dev/test'); - await Adapter.create(null, {path: undefined, baudRate: 100, rtscts: false, adapter: undefined}, null, null); - expect(ZStackAdapter).toHaveBeenCalledWith(null, {baudRate: 100, path: '/dev/test', rtscts: false, adapter: undefined}, null, null); + await Adapter.create( + { + panID: 0, + channelList: [], + }, + {path: undefined, baudRate: 100, rtscts: false, adapter: undefined}, + 'test.db', + { + disableLED: false, + }, + ); + expect(ZStackAdapter).toHaveBeenCalledWith( + { + panID: 0, + channelList: [], + }, + {baudRate: 100, path: '/dev/test', rtscts: false, adapter: undefined}, + 'test.db', + { + disableLED: false, + }, + ); }); it('Adapter mdns timeout test', async () => { const fakeAdapterName = 'mdns_test_device'; try { - await Adapter.create(null, {path: `mdns://${fakeAdapterName}`, baudRate: 100, rtscts: false, adapter: undefined}, null, null); + await Adapter.create( + { + panID: 0, + channelList: [], + }, + {path: `mdns://${fakeAdapterName}`, baudRate: 100, rtscts: false, adapter: undefined}, + 'test.db', + { + disableLED: false, + }, + ); } catch (e) { expect(e).toStrictEqual(new Error(`Coordinator [${fakeAdapterName}] not found after timeout of 2000ms!`)); } @@ -7304,7 +7323,17 @@ describe('Controller', () => { const fakeAdapterName = ''; try { - await Adapter.create(null, {path: `mdns://${fakeAdapterName}`, baudRate: 100, rtscts: false, adapter: undefined}, null, null); + await Adapter.create( + { + panID: 0, + channelList: [], + }, + {path: `mdns://${fakeAdapterName}`, baudRate: 100, rtscts: false, adapter: undefined}, + 'test.db', + { + disableLED: false, + }, + ); } catch (e) { expect(e).toStrictEqual( new Error(`No mdns device specified. You must specify the coordinator mdns service type after mdns://, e.g. mdns://my-adapter`), @@ -7318,14 +7347,25 @@ describe('Controller', () => { const fakePort = 6638; const fakeBaud = '115200'; - Bonjour.prototype.findOne = function (opts?: BrowserConfig | undefined, timeout?: number, callback?: CallableFunction) { + // @ts-expect-error mock + Bonjour.prototype.findOne = function (opts?: BrowserConfig | undefined, timeout?: number, callback?: CallableFunction): Browser { setTimeout(() => { - callback({name: 'fakeAdapter', type: fakeAdapterName, port: fakePort, addresses: [fakeIp], txt: {baud_rate: fakeBaud}}); + callback?.({name: 'fakeAdapter', type: fakeAdapterName, port: fakePort, addresses: [fakeIp], txt: {baud_rate: fakeBaud}}); }, 200); }; try { - await Adapter.create(null, {path: `mdns://${fakeAdapterName}`, baudRate: 100, rtscts: false, adapter: undefined}, null, null); + await Adapter.create( + { + panID: 0, + channelList: [], + }, + {path: `mdns://${fakeAdapterName}`, baudRate: 100, rtscts: false, adapter: undefined}, + 'test.db', + { + disableLED: false, + }, + ); } catch (e) { expect(e).toStrictEqual( new Error( @@ -7346,9 +7386,10 @@ describe('Controller', () => { const fakeRadio = 'ezsp'; const fakeBaud = '115200'; - Bonjour.prototype.findOne = function (opts?: BrowserConfig | undefined, timeout?: number, callback?: CallableFunction) { + // @ts-expect-error mock + Bonjour.prototype.findOne = function (opts?: BrowserConfig | undefined, timeout?: number, callback?: CallableFunction): Browser { setTimeout(() => { - callback({ + callback?.({ name: 'fakeAdapter', type: fakeAdapterName, port: fakePort, @@ -7358,7 +7399,17 @@ describe('Controller', () => { }, 200); }; - await Adapter.create(null, {path: `mdns://${fakeAdapterName}`, baudRate: 100, rtscts: false, adapter: undefined}, null, null); + await Adapter.create( + { + panID: 0, + channelList: [], + }, + {path: `mdns://${fakeAdapterName}`, baudRate: 100, rtscts: false, adapter: undefined}, + 'test.db', + { + disableLED: false, + }, + ); expect(mockLogger.info.mock.calls[0][0]).toBe(`Starting mdns discovery for coordinator: ${fakeAdapterName}`); expect(mockLogger.info.mock.calls[1][0]).toBe(`Coordinator Ip: ${fakeIp}`); @@ -7374,9 +7425,10 @@ describe('Controller', () => { const fakeRadio = 'auto'; const fakeBaud = '115200'; - Bonjour.prototype.findOne = function (opts?: BrowserConfig | undefined, timeout?: number, callback?: CallableFunction) { + // @ts-expect-error mock + Bonjour.prototype.findOne = function (opts?: BrowserConfig | undefined, timeout?: number, callback?: CallableFunction): Browser { setTimeout(() => { - callback({ + callback?.({ name: 'fakeAdapter', type: fakeAdapterName, port: fakePort, @@ -7387,7 +7439,17 @@ describe('Controller', () => { }; try { - await Adapter.create(null, {path: `mdns://${fakeAdapterName}`, baudRate: 100, rtscts: false, adapter: undefined}, null, null); + await Adapter.create( + { + panID: 0, + channelList: [], + }, + {path: `mdns://${fakeAdapterName}`, baudRate: 100, rtscts: false, adapter: undefined}, + 'test.db', + { + disableLED: false, + }, + ); } catch (e) { expect(e).toStrictEqual(new Error(`Adapter ${fakeRadio} is not supported.`)); } @@ -7400,9 +7462,10 @@ describe('Controller', () => { const fakeRadio = 'znp'; const fakeBaud = '115200'; - Bonjour.prototype.findOne = function (opts?: BrowserConfig | undefined, timeout?: number, callback?: CallableFunction) { + // @ts-expect-error mock + Bonjour.prototype.findOne = function (opts?: BrowserConfig | undefined, timeout?: number, callback?: CallableFunction): Browser { setTimeout(() => { - callback({ + callback?.({ name: 'fakeAdapter', type: fakeAdapterName, port: fakePort, @@ -7412,7 +7475,17 @@ describe('Controller', () => { }, 200); }; - await Adapter.create(null, {path: `mdns://${fakeAdapterName}`, baudRate: 100, rtscts: false, adapter: undefined}, null, null); + await Adapter.create( + { + panID: 0, + channelList: [], + }, + {path: `mdns://${fakeAdapterName}`, baudRate: 100, rtscts: false, adapter: undefined}, + 'test.db', + { + disableLED: false, + }, + ); expect(mockLogger.info.mock.calls[0][0]).toBe(`Starting mdns discovery for coordinator: ${fakeAdapterName}`); expect(mockLogger.info.mock.calls[1][0]).toBe(`Coordinator Ip: ${fakeIp}`); @@ -7426,7 +7499,17 @@ describe('Controller', () => { mockZStackAdapterAutoDetectPath.mockReturnValueOnce(null); try { - await Adapter.create(null, {path: undefined, baudRate: 100, rtscts: false, adapter: undefined}, null, null); + await Adapter.create( + { + panID: 0, + channelList: [], + }, + {path: undefined, baudRate: 100, rtscts: false, adapter: undefined}, + 'test.db', + { + disableLED: false, + }, + ); } catch (e) { expect(e).toStrictEqual(new Error('No path provided and failed to auto detect path')); } @@ -7435,8 +7518,28 @@ describe('Controller', () => { it('Adapter create with unknown path should take ZStackAdapter by default', async () => { mockZStackAdapterIsValidPath.mockReturnValueOnce(false); mockZStackAdapterAutoDetectPath.mockReturnValueOnce('/dev/test'); - await Adapter.create(null, {path: undefined, baudRate: 100, rtscts: false, adapter: undefined}, null, null); - expect(ZStackAdapter).toHaveBeenCalledWith(null, {baudRate: 100, path: '/dev/test', rtscts: false, adapter: undefined}, null, null); + await Adapter.create( + { + panID: 0, + channelList: [], + }, + {path: undefined, baudRate: 100, rtscts: false, adapter: undefined}, + 'test.db', + { + disableLED: false, + }, + ); + expect(ZStackAdapter).toHaveBeenCalledWith( + { + panID: 0, + channelList: [], + }, + {baudRate: 100, path: '/dev/test', rtscts: false, adapter: undefined}, + 'test.db', + { + disableLED: false, + }, + ); }); it('Adapter create should be able to specify adapter', async () => { @@ -7446,10 +7549,50 @@ describe('Controller', () => { mockDeconzAdapterAutoDetectPath.mockReturnValueOnce('/dev/test'); mockZiGateAdapterIsValidPath.mockReturnValueOnce(false); mockZiGateAdapterAutoDetectPath.mockReturnValueOnce('/dev/test'); - await Adapter.create(null, {path: undefined, baudRate: 100, rtscts: false, adapter: 'deconz'}, null, null); - expect(DeconzAdapter).toHaveBeenCalledWith(null, {baudRate: 100, path: '/dev/test', rtscts: false, adapter: 'deconz'}, null, null); - await Adapter.create(null, {path: undefined, baudRate: 100, rtscts: false, adapter: 'zigate'}, null, null); - expect(ZiGateAdapter).toHaveBeenCalledWith(null, {baudRate: 100, path: '/dev/test', rtscts: false, adapter: 'zigate'}, null, null); + await Adapter.create( + { + panID: 0, + channelList: [], + }, + {path: undefined, baudRate: 100, rtscts: false, adapter: 'deconz'}, + 'test.db', + { + disableLED: false, + }, + ); + expect(DeconzAdapter).toHaveBeenCalledWith( + { + panID: 0, + channelList: [], + }, + {baudRate: 100, path: '/dev/test', rtscts: false, adapter: 'deconz'}, + 'test.db', + { + disableLED: false, + }, + ); + await Adapter.create( + { + panID: 0, + channelList: [], + }, + {path: undefined, baudRate: 100, rtscts: false, adapter: 'zigate'}, + 'test.db', + { + disableLED: false, + }, + ); + expect(ZiGateAdapter).toHaveBeenCalledWith( + { + panID: 0, + channelList: [], + }, + {baudRate: 100, path: '/dev/test', rtscts: false, adapter: 'zigate'}, + 'test.db', + { + disableLED: false, + }, + ); }); it('Adapter create should throw on uknown adapter', async () => { @@ -7459,7 +7602,23 @@ describe('Controller', () => { mockDeconzAdapterAutoDetectPath.mockReturnValueOnce('/dev/test'); try { - await Adapter.create(null, {path: undefined, baudRate: 100, rtscts: false, adapter: 'efr'}, null, null); + await Adapter.create( + { + panID: 0, + channelList: [], + }, + { + path: undefined, + baudRate: 100, + rtscts: false, + // @ts-expect-error bad on purpose + adapter: 'efr', + }, + 'test.db', + { + disableLED: false, + }, + ); } catch (e) { expect(e).toStrictEqual(new Error(`Adapter 'efr' does not exists, possible options: zstack, deconz, zigate, ezsp, ember, zboss`)); } @@ -7666,8 +7825,8 @@ describe('Controller', () => { it('Endpoint command error', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const device = controller.getDeviceByIeeeAddr('0x129'); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const endpoint = device.getEndpoint(1)!; mocksendZclFrameToEndpoint.mockRejectedValueOnce(new Error('timeout occurred')); let error; try { @@ -7685,12 +7844,12 @@ describe('Controller', () => { it('Endpoint commandResponse error', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const device = controller.getDeviceByIeeeAddr('0x129'); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const endpoint = device.getEndpoint(1)!; mocksendZclFrameToEndpoint.mockRejectedValueOnce(new Error('timeout occurred')); let error; try { - await endpoint.commandResponse('genOta', 'imageNotify', {payloadType: 0, queryJitter: 1}, null, null); + await endpoint.commandResponse('genOta', 'imageNotify', {payloadType: 0, queryJitter: 1}, undefined, undefined); } catch (e) { error = e; } @@ -7704,12 +7863,12 @@ describe('Controller', () => { it('Endpoint commandResponse error when transactionSequenceNumber provided through options', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const device = controller.getDeviceByIeeeAddr('0x129'); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const endpoint = device.getEndpoint(1)!; mocksendZclFrameToEndpoint.mockRejectedValueOnce(new Error('timeout occurred')); let error; try { - await endpoint.commandResponse('genOta', 'imageNotify', {payloadType: 0, queryJitter: 1}, {transactionSequenceNumber: 10}, null); + await endpoint.commandResponse('genOta', 'imageNotify', {payloadType: 0, queryJitter: 1}, {transactionSequenceNumber: 10}, undefined); } catch (e) { error = e; } @@ -7719,8 +7878,8 @@ describe('Controller', () => { it('ConfigureReporting error', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const device = controller.getDeviceByIeeeAddr('0x129'); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const endpoint = device.getEndpoint(1)!; mocksendZclFrameToEndpoint.mockRejectedValueOnce(new Error('timeout occurred')); let error; try { @@ -7740,8 +7899,8 @@ describe('Controller', () => { it('DefaultResponse error', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const device = controller.getDeviceByIeeeAddr('0x129'); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const endpoint = device.getEndpoint(1)!; mocksendZclFrameToEndpoint.mockRejectedValueOnce(new Error('timeout occurred')); let error; try { @@ -7759,8 +7918,8 @@ describe('Controller', () => { it('DefaultResponse error when transactionSequenceNumber provided through options', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const device = controller.getDeviceByIeeeAddr('0x129'); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const endpoint = device.getEndpoint(1)!; mocksendZclFrameToEndpoint.mockRejectedValueOnce(new Error('timeout occurred')); let error; try { @@ -7775,17 +7934,17 @@ describe('Controller', () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); await mockAdapterEvents['deviceJoined']({networkAddress: 170, ieeeAddr: '0x170'}); - const endpoint = controller.getDeviceByIeeeAddr('0x129').getEndpoint(1); - const target = controller.getDeviceByIeeeAddr('0x170').getEndpoint(1); - mockAdapterUnbind.mockClear(); + const endpoint = controller.getDeviceByIeeeAddr('0x129')?.getEndpoint(1)!; + const target = controller.getDeviceByIeeeAddr('0x170')?.getEndpoint(1)!; + mockAdapterSendZdo.mockClear(); await endpoint.unbind('genOnOff', target); - expect(mockAdapterUnbind).toHaveBeenCalledTimes(0); + expect(mockAdapterSendZdo).toHaveBeenCalledTimes(0); }); it('Handle unbind with number not matching any group', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const endpoint = controller.getDeviceByIeeeAddr('0x129').getEndpoint(1); + const endpoint = controller.getDeviceByIeeeAddr('0x129')!.getEndpoint(1)!; let error; try { await endpoint.unbind('genOnOff', 1); @@ -7799,40 +7958,64 @@ describe('Controller', () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); await mockAdapterEvents['deviceJoined']({networkAddress: 170, ieeeAddr: '0x170'}); - const endpoint = controller.getDeviceByIeeeAddr('0x129').getEndpoint(1); - const target = controller.getDeviceByIeeeAddr('0x170').getEndpoint(1); + const endpoint = controller.getDeviceByIeeeAddr('0x129')!.getEndpoint(1)!; + const target = controller.getDeviceByIeeeAddr('0x170')!.getEndpoint(1)!; await endpoint.bind('genOnOff', target); - mockAdapterUnbind.mockRejectedValueOnce(new Error('timeout occurred')); - let error; - try { + mockAdapterSendZdo.mockClear(); + + sendZdoResponseStatus = Zdo.Status.INVALID_INDEX; + + expect(async () => { await endpoint.unbind('genOnOff', target); - } catch (e) { - error = e; - } - expect(error).toStrictEqual(new Error(`Unbind 0x129/1 genOnOff from '0x170/1' failed (timeout occurred)`)); + }).rejects.toThrow(`Unbind 0x129/1 genOnOff from '0x170/1' failed (Status 'INVALID_INDEX')`); + + const zdoPayload = Zdo.Buffalo.buildRequest( + false, + Zdo.ClusterId.UNBIND_REQUEST, + '0x129', + 1, + Zcl.Clusters.genOnOff.ID, + Zdo.UNICAST_BINDING, + '0x170', + 0, + 1, + ); + expect(mockAdapterSendZdo).toHaveBeenCalledWith('0x129', 129, Zdo.ClusterId.UNBIND_REQUEST, zdoPayload, false); }); it('Bind error', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); await mockAdapterEvents['deviceJoined']({networkAddress: 170, ieeeAddr: '0x170'}); - const endpoint = controller.getDeviceByIeeeAddr('0x129').getEndpoint(1); - const target = controller.getDeviceByIeeeAddr('0x170').getEndpoint(1); - mockAdapterBind.mockRejectedValueOnce(new Error('timeout occurred')); - let error; - try { + const endpoint = controller.getDeviceByIeeeAddr('0x129')!.getEndpoint(1)!; + const target = controller.getDeviceByIeeeAddr('0x170')!.getEndpoint(1)!; + mockAdapterSendZdo.mockClear(); + + sendZdoResponseStatus = Zdo.Status.INVALID_INDEX; + + expect(async () => { await endpoint.bind('genOnOff', target); - } catch (e) { - error = e; - } - expect(error).toStrictEqual(new Error(`Bind 0x129/1 genOnOff from '0x170/1' failed (timeout occurred)`)); + }).rejects.toThrow(`Bind 0x129/1 genOnOff from '0x170/1' failed (Status 'INVALID_INDEX')`); + + const zdoPayload = Zdo.Buffalo.buildRequest( + false, + Zdo.ClusterId.BIND_REQUEST, + '0x129', + 1, + Zcl.Clusters.genOnOff.ID, + Zdo.UNICAST_BINDING, + '0x170', + 0, + 1, + ); + expect(mockAdapterSendZdo).toHaveBeenCalledWith('0x129', 129, Zdo.ClusterId.BIND_REQUEST, zdoPayload, false); }); it('ReadResponse error', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const device = controller.getDeviceByIeeeAddr('0x129'); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const endpoint = device.getEndpoint(1)!; mocksendZclFrameToEndpoint.mockRejectedValueOnce(new Error('timeout occurred')); let error; try { @@ -7850,8 +8033,8 @@ describe('Controller', () => { it('Read error', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const device = controller.getDeviceByIeeeAddr('0x129'); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const endpoint = device.getEndpoint(1)!; mocksendZclFrameToEndpoint.mockRejectedValueOnce(new Error('timeout occurred')); let error; try { @@ -7869,8 +8052,8 @@ describe('Controller', () => { it('Read with disable response', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const device = controller.getDeviceByIeeeAddr('0x129'); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const endpoint = device.getEndpoint(1)!; mocksendZclFrameToEndpoint.mockReturnValueOnce(null); let error; try { @@ -7884,8 +8067,8 @@ describe('Controller', () => { it('Write error', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const device = controller.getDeviceByIeeeAddr('0x129'); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const endpoint = device.getEndpoint(1)!; mocksendZclFrameToEndpoint.mockRejectedValueOnce(new Error('timeout occurred')); let error; try { @@ -7903,8 +8086,8 @@ describe('Controller', () => { it('Write with disable response', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const device = controller.getDeviceByIeeeAddr('0x129'); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const endpoint = device.getEndpoint(1)!; mocksendZclFrameToEndpoint.mockReturnValueOnce(null); let error; try { @@ -7932,8 +8115,8 @@ describe('Controller', () => { await controller.start(); await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const device = controller.getDeviceByIeeeAddr('0x129'); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const endpoint = device.getEndpoint(1)!; mocksendZclFrameToEndpoint.mockReturnValueOnce(null); let error; try { @@ -7948,8 +8131,8 @@ describe('Controller', () => { await controller.start(); await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const device = controller.getDeviceByIeeeAddr('0x129'); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const endpoint = device.getEndpoint(1)!; mocksendZclFrameToEndpoint.mockReturnValueOnce(null); let error; try { @@ -7963,8 +8146,8 @@ describe('Controller', () => { it('Write structured error', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const device = controller.getDeviceByIeeeAddr('0x129'); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const endpoint = device.getEndpoint(1)!; mocksendZclFrameToEndpoint.mockRejectedValueOnce(new Error('timeout occurred')); let error; try { @@ -8004,7 +8187,7 @@ describe('Controller', () => { clusterID: frame.cluster.ID, data: frame.toBuffer(), header: frame.header, - endpoint: 242, + endpoint: ZSpec.GP_ENDPOINT, linkquality: 50, groupID: 1, }); @@ -8019,9 +8202,9 @@ describe('Controller', () => { }; const frameResponse = Zcl.Frame.create(1, 1, true, undefined, 2, 'pairing', 33, dataResponse, {}); - expect(mocksendZclFrameToAll.mock.calls[0][0]).toBe(242); + expect(mocksendZclFrameToAll.mock.calls[0][0]).toBe(ZSpec.GP_ENDPOINT); expect(deepClone(mocksendZclFrameToAll.mock.calls[0][1])).toStrictEqual(deepClone(frameResponse)); - expect(mocksendZclFrameToAll.mock.calls[0][2]).toBe(242); + expect(mocksendZclFrameToAll.mock.calls[0][2]).toBe(ZSpec.GP_ENDPOINT); expect(mocksendZclFrameToAll).toHaveBeenCalledTimes(1); // When joins again, shouldnt emit duplicate event @@ -8032,7 +8215,7 @@ describe('Controller', () => { clusterID: frame.cluster.ID, data: frame.toBuffer(), header: frame.header, - endpoint: 242, + endpoint: ZSpec.GP_ENDPOINT, linkquality: 50, groupID: 1, }); @@ -8048,8 +8231,8 @@ describe('Controller', () => { { inputClusters: [], outputClusters: [], - pendingRequests: {ID: 242, deviceIeeeAddress: '0x000000000046f4fe', sendInProgress: false}, - ID: 242, + pendingRequests: {ID: ZSpec.GP_ENDPOINT, deviceIeeeAddress: '0x000000000046f4fe', sendInProgress: false}, + ID: ZSpec.GP_ENDPOINT, clusters: {}, deviceIeeeAddress: '0x000000000046f4fe', deviceNetworkAddress: 0xf4fe, @@ -8093,7 +8276,7 @@ describe('Controller', () => { meta: {}, }, }); - expect(controller.getDeviceByIeeeAddr('0x000000000046f4fe').networkAddress).toBe(0xf4fe); + expect(controller.getDeviceByIeeeAddr('0x000000000046f4fe')!.networkAddress).toBe(0xf4fe); expect(events.message.length).toBe(2); // Green power device send message @@ -8115,7 +8298,7 @@ describe('Controller', () => { clusterID: frameToggle.cluster.ID, data: frameToggle.toBuffer(), header: frameToggle.header, - endpoint: 242, + endpoint: ZSpec.GP_ENDPOINT, linkquality: 50, groupID: 1, }); @@ -8134,8 +8317,8 @@ describe('Controller', () => { inputClusters: [], meta: {}, outputClusters: [], - pendingRequests: {ID: 242, deviceIeeeAddress: '0x000000000046f4fe', sendInProgress: false}, - ID: 242, + pendingRequests: {ID: ZSpec.GP_ENDPOINT, deviceIeeeAddress: '0x000000000046f4fe', sendInProgress: false}, + ID: ZSpec.GP_ENDPOINT, _events: {}, _eventsCount: 0, clusters: {}, @@ -8160,8 +8343,8 @@ describe('Controller', () => { inputClusters: [], meta: {}, outputClusters: [], - pendingRequests: {ID: 242, deviceIeeeAddress: '0x000000000046f4fe', sendInProgress: false}, - ID: 242, + pendingRequests: {ID: ZSpec.GP_ENDPOINT, deviceIeeeAddress: '0x000000000046f4fe', sendInProgress: false}, + ID: ZSpec.GP_ENDPOINT, _events: {}, _eventsCount: 0, clusters: {}, @@ -8194,7 +8377,7 @@ describe('Controller', () => { clusterID: frame.cluster.ID, data: buffer, header: frame.header, - endpoint: 242, + endpoint: ZSpec.GP_ENDPOINT, linkquality: 50, groupID: 1, }); @@ -8206,7 +8389,7 @@ describe('Controller', () => { it('Should ignore invalid green power frame', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const device = controller.getDeviceByIeeeAddr('0x129'); + const device = controller.getDeviceByIeeeAddr('0x129')!; device.addCustomCluster('myCustomCluster', { ID: 9123, commands: {}, @@ -8230,11 +8413,14 @@ describe('Controller', () => { it('Green power channel request', async () => { await controller.start(); + const srcID = 0x0046f4fe; + // mock device as already joined to avoid zclPayload triggering unknown device identification + await mockAdapterEvents['deviceJoined']({ieeeAddr: '0x000000000046f4fe', networkAddress: srcID & 0xffff}); // Channel Request const data = { options: 0, - srcID: 0x0046f4fe, + srcID, frameCounter: 228, commandID: 0xe3, gppNwkAddr: 0x1234, @@ -8248,11 +8434,11 @@ describe('Controller', () => { jest.spyOn(Zcl.Frame, 'fromBuffer').mockReturnValueOnce(frame); // Mock because no Buffalo write for 0xe3 is implemented await mockAdapterEvents['zclPayload']({ wasBroadcast: true, - address: 0x46f4fe, + address: srcID & 0xffff, clusterID: frame.cluster.ID, data: frame.toBuffer(), header: frame.header, - endpoint: 242, + endpoint: ZSpec.GP_ENDPOINT, linkquality: 50, groupID: 1, }); @@ -8261,7 +8447,7 @@ describe('Controller', () => { options: 0, tempMaster: 0x1234, tempMasterTx: 10, - srcID: 0x0046f4fe, + srcID, gpdCmd: 0xf3, gpdPayload: { commandID: 0xf3, @@ -8272,9 +8458,9 @@ describe('Controller', () => { const frameResponse = Zcl.Frame.create(1, 1, true, undefined, 2, 'response', 33, commissioningReply, {}); expect(mocksendZclFrameToAll).toHaveBeenCalledTimes(1); - expect(mocksendZclFrameToAll.mock.calls[0][0]).toBe(242); + expect(mocksendZclFrameToAll.mock.calls[0][0]).toBe(ZSpec.GP_ENDPOINT); expect(deepClone(mocksendZclFrameToAll.mock.calls[0][1])).toStrictEqual(deepClone(frameResponse)); - expect(mocksendZclFrameToAll.mock.calls[0][2]).toBe(242); + expect(mocksendZclFrameToAll.mock.calls[0][2]).toBe(ZSpec.GP_ENDPOINT); }); it('Green power rxOnCap', async () => { @@ -8303,12 +8489,12 @@ describe('Controller', () => { clusterID: frame.cluster.ID, data: frame.toBuffer(), header: frame.header, - endpoint: 242, + endpoint: ZSpec.GP_ENDPOINT, linkquality: 50, groupID: 1, }); - const device = controller.getDeviceByIeeeAddr('0x000000000046f4fe'); + const device = controller.getDeviceByIeeeAddr('0x000000000046f4fe')!; const networkParameters = await controller.getNetworkParameters(); const commissioningReply = { @@ -8326,9 +8512,9 @@ describe('Controller', () => { const frameResponse = Zcl.Frame.create(1, 1, true, undefined, 2, 'response', 33, commissioningReply, {}); expect(mocksendZclFrameToAll).toHaveBeenCalledTimes(2); - expect(mocksendZclFrameToAll.mock.calls[0][0]).toBe(242); + expect(mocksendZclFrameToAll.mock.calls[0][0]).toBe(ZSpec.GP_ENDPOINT); expect(deepClone(mocksendZclFrameToAll.mock.calls[0][1])).toStrictEqual(deepClone(frameResponse)); - expect(mocksendZclFrameToAll.mock.calls[0][2]).toBe(242); + expect(mocksendZclFrameToAll.mock.calls[0][2]).toBe(ZSpec.GP_ENDPOINT); const pairingData = { options: 424, @@ -8338,9 +8524,9 @@ describe('Controller', () => { }; const pairing = Zcl.Frame.create(1, 1, true, undefined, 3, 'pairing', 33, pairingData, {}); - expect(mocksendZclFrameToAll.mock.calls[1][0]).toBe(242); + expect(mocksendZclFrameToAll.mock.calls[1][0]).toBe(ZSpec.GP_ENDPOINT); expect(deepClone(mocksendZclFrameToAll.mock.calls[1][1])).toStrictEqual(deepClone(pairing)); - expect(mocksendZclFrameToAll.mock.calls[1][2]).toBe(242); + expect(mocksendZclFrameToAll.mock.calls[1][2]).toBe(ZSpec.GP_ENDPOINT); mocksendZclFrameToAll.mockClear(); // Test green power response on endpoint object @@ -8356,26 +8542,27 @@ describe('Controller', () => { }, }; - await device.getEndpoint(242).commandResponse('greenPower', 'response', payload, { - srcEndpoint: 242, + await device.getEndpoint(ZSpec.GP_ENDPOINT)!.commandResponse('greenPower', 'response', payload, { + srcEndpoint: ZSpec.GP_ENDPOINT, disableDefaultResponse: true, }); const response = Zcl.Frame.create(1, 1, true, undefined, 4, 'response', 33, payload, {}); expect(mocksendZclFrameToAll).toHaveBeenCalledTimes(1); - expect(mocksendZclFrameToAll.mock.calls[0][0]).toBe(242); + expect(mocksendZclFrameToAll.mock.calls[0][0]).toBe(ZSpec.GP_ENDPOINT); expect(deepClone(mocksendZclFrameToAll.mock.calls[0][1])).toStrictEqual(deepClone(response)); - expect(mocksendZclFrameToAll.mock.calls[0][2]).toBe(242); + expect(mocksendZclFrameToAll.mock.calls[0][2]).toBe(ZSpec.GP_ENDPOINT); }); it('Green power unicast', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); + const srcID = 0x017171f8; const gppDevice = controller.getDeviceByIeeeAddr('0x129')!; const data = { options: 0x800, // Proxy info present - srcID: 0x017171f8, + srcID, frameCounter: 248, commandID: 0xe0, payloadSize: 46, @@ -8417,16 +8604,16 @@ describe('Controller', () => { clusterID: expectedFrame.cluster.ID, data: expectedFrame.toBuffer(), header: expectedFrame.header, - endpoint: 242, + endpoint: ZSpec.GP_ENDPOINT, linkquality: 50, groupID: 0, }); const dataResponse = { options: 0x00e568, - srcID: 0x017171f8, + srcID, sinkIEEEAddr: '0x0000012300000000', - sinkNwkAddr: 123, + sinkNwkAddr: 0, deviceID: 2, frameCounter: 4600, gpdKey: [0x09, 0x3c, 0xed, 0x1d, 0xbf, 0x25, 0x63, 0xf9, 0x29, 0x5c, 0x0d, 0x3d, 0x9f, 0xc5, 0x76, 0xe1], @@ -8436,12 +8623,12 @@ describe('Controller', () => { expect(mocksendZclFrameToEndpoint).toHaveBeenLastCalledWith( gppDevice.ieeeAddr, gppDevice.networkAddress, - 242, + ZSpec.GP_ENDPOINT, expect.any(Object), 10000, false, false, - 242, + ZSpec.GP_ENDPOINT, ); expect(deepClone(mocksendZclFrameToEndpoint.mock.calls[mocksendZclFrameToEndpoint.mock.calls.length - 1][3])).toStrictEqual( deepClone(frameResponse), @@ -8455,7 +8642,7 @@ describe('Controller', () => { clusterID: expectedFrame.cluster.ID, data: expectedFrame.toBuffer(), header: expectedFrame.header, - endpoint: 242, + endpoint: ZSpec.GP_ENDPOINT, linkquality: 50, groupID: 0, }); @@ -8471,7 +8658,7 @@ describe('Controller', () => { _customClusters: {}, _endpoints: [ { - ID: 242, + ID: ZSpec.GP_ENDPOINT, _binds: [], _configuredReportings: [], _events: {}, @@ -8482,7 +8669,7 @@ describe('Controller', () => { inputClusters: [], meta: {}, outputClusters: [], - pendingRequests: {ID: 242, deviceIeeeAddress: '0x00000000017171f8', sendInProgress: false}, + pendingRequests: {ID: ZSpec.GP_ENDPOINT, deviceIeeeAddress: '0x00000000017171f8', sendInProgress: false}, }, ], _ieeeAddr: '0x00000000017171f8', @@ -8525,7 +8712,7 @@ describe('Controller', () => { expect(events.message.length).toBe(0); const dataScene = { options: 0x5488, - srcID: 0x017171f8, + srcID, frameCounter: 4601, commandID: 0x13, payloadSize: 0, @@ -8536,11 +8723,11 @@ describe('Controller', () => { const frameScene = Zcl.Frame.create(1, 0, true, undefined, 10, 'notification', 33, dataScene, {}); await mockAdapterEvents['zclPayload']({ wasBroadcast: false, - address: 0x017171f8, + address: srcID & 0xffff, clusterID: frameScene.cluster.ID, data: frameScene.toBuffer(), header: frameScene.header, - endpoint: 242, + endpoint: ZSpec.GP_ENDPOINT, linkquality: 50, groupID: 0, }); @@ -8560,7 +8747,7 @@ describe('Controller', () => { { _events: {}, _eventsCount: 0, - ID: 242, + ID: ZSpec.GP_ENDPOINT, inputClusters: [], outputClusters: [], deviceNetworkAddress: 29176, @@ -8569,7 +8756,7 @@ describe('Controller', () => { _binds: [], _configuredReportings: [], meta: {}, - pendingRequests: {sendInProgress: false, ID: 242, deviceIeeeAddress: '0x00000000017171f8'}, + pendingRequests: {sendInProgress: false, ID: ZSpec.GP_ENDPOINT, deviceIeeeAddress: '0x00000000017171f8'}, }, ], _modelID: 'GreenPower_2', @@ -8584,7 +8771,7 @@ describe('Controller', () => { endpoint: { _events: {}, _eventsCount: 0, - ID: 242, + ID: ZSpec.GP_ENDPOINT, inputClusters: [], outputClusters: [], deviceNetworkAddress: 29176, @@ -8593,7 +8780,7 @@ describe('Controller', () => { _binds: [], _configuredReportings: [], meta: {}, - pendingRequests: {sendInProgress: false, ID: 242, deviceIeeeAddress: '0x00000000017171f8'}, + pendingRequests: {sendInProgress: false, ID: ZSpec.GP_ENDPOINT, deviceIeeeAddress: '0x00000000017171f8'}, }, data: { options: 21640, @@ -8618,16 +8805,16 @@ describe('Controller', () => { // Remove green power device from network const removeCommand = { options: 0x002550, - srcID: 0x017171f8, + srcID, }; const removeFrame = Zcl.Frame.create(1, 1, true, undefined, 13, 'pairing', 33, removeCommand, {}); events.message = []; - const device = controller.getDeviceByIeeeAddr('0x00000000017171f8')!; + const device = controller.getDeviceByIeeeAddr('0x00000000017171f8')!!; await device.removeFromNetwork(); - expect(mocksendZclFrameToAll.mock.calls[0][0]).toBe(242); + expect(mocksendZclFrameToAll.mock.calls[0][0]).toBe(ZSpec.GP_ENDPOINT); expect(deepClone(mocksendZclFrameToAll.mock.calls[0][1])).toStrictEqual(deepClone(removeFrame)); - expect(mocksendZclFrameToAll.mock.calls[0][2]).toBe(242); + expect(mocksendZclFrameToAll.mock.calls[0][2]).toBe(ZSpec.GP_ENDPOINT); expect(mocksendZclFrameToAll).toHaveBeenCalledTimes(1); expect(controller.getDeviceByIeeeAddr('0x00000000017171f8')).toBeUndefined(); @@ -8641,7 +8828,7 @@ describe('Controller', () => { _customClusters: {}, _endpoints: [ { - ID: 242, + ID: ZSpec.GP_ENDPOINT, _binds: [], _configuredReportings: [], _events: {}, @@ -8652,7 +8839,7 @@ describe('Controller', () => { inputClusters: [], meta: {}, outputClusters: [], - pendingRequests: {ID: 242, deviceIeeeAddress: '0x00000000017171f8', sendInProgress: false}, + pendingRequests: {ID: ZSpec.GP_ENDPOINT, deviceIeeeAddress: '0x00000000017171f8', sendInProgress: false}, }, ], _ieeeAddr: '0x00000000017171f8', @@ -8674,7 +8861,7 @@ describe('Controller', () => { clusterID: expectedFrame.cluster.ID, data: expectedFrame.toBuffer(), header: expectedFrame.header, - endpoint: 242, + endpoint: ZSpec.GP_ENDPOINT, linkquality: 50, groupID: 0, }); @@ -8688,7 +8875,7 @@ describe('Controller', () => { _customClusters: {}, _endpoints: [ { - ID: 242, + ID: ZSpec.GP_ENDPOINT, _binds: [], _configuredReportings: [], _events: {}, @@ -8699,7 +8886,7 @@ describe('Controller', () => { inputClusters: [], meta: {}, outputClusters: [], - pendingRequests: {ID: 242, deviceIeeeAddress: '0x00000000017171f8', sendInProgress: false}, + pendingRequests: {ID: ZSpec.GP_ENDPOINT, deviceIeeeAddress: '0x00000000017171f8', sendInProgress: false}, }, ], _ieeeAddr: '0x00000000017171f8', @@ -8717,8 +8904,8 @@ describe('Controller', () => { it('Get input/ouptut clusters', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 172, ieeeAddr: '0x172'}); - const device = controller.getDeviceByIeeeAddr('0x172'); - const endpoint = device.getEndpoint(11); + const device = controller.getDeviceByIeeeAddr('0x172')!; + const endpoint = device.getEndpoint(11)!; expect(endpoint.getInputClusters().map((c) => c.name)).toStrictEqual([ 'genBasic', 'genIdentify', @@ -8727,7 +8914,7 @@ describe('Controller', () => { 'genOnOff', 'genLevelCtrl', 'lightingColorCtrl', - '912301', + '62301', ]); expect(endpoint.getOutputClusters().map((c) => c.name)).toStrictEqual(['genDeviceTempCfg']); }); @@ -8736,8 +8923,8 @@ describe('Controller', () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); mocksendZclFrameToEndpoint.mockClear(); - const device = controller.getDeviceByIeeeAddr('0x129'); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const endpoint = device.getEndpoint(1)!; const options = {manufacturerCode: 0x100b, disableDefaultResponse: true, timeout: 12, defaultResponseTimeout: 16}; await endpoint.report('genBasic', {0x0031: {value: 0x000b, type: 0x19}}, options); expect(mocksendZclFrameToEndpoint).toHaveBeenCalledTimes(1); @@ -8798,8 +8985,8 @@ describe('Controller', () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); mocksendZclFrameToEndpoint.mockClear(); - const device = controller.getDeviceByIeeeAddr('0x129'); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const endpoint = device.getEndpoint(1)!; let error; try { await endpoint.report('genBasic', {UNKNOWN: {value: 0x000b, type: 0x19}}); @@ -8813,8 +9000,8 @@ describe('Controller', () => { it('Report error', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const device = controller.getDeviceByIeeeAddr('0x129'); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const endpoint = device.getEndpoint(1)!; mocksendZclFrameToEndpoint.mockRejectedValueOnce(new Error('timeout occurred')); let error; try { @@ -8832,17 +9019,33 @@ describe('Controller', () => { it('Write to device with pendingRequestTimeout > 0', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 174, ieeeAddr: '0x129'}); - const device = controller.getDeviceByIeeeAddr('0x129'); + const device = controller.getDeviceByIeeeAddr('0x129')!; device.pendingRequestTimeout = 10000; - const endpoint = device.getEndpoint(1); + const endpoint = device.getEndpoint(1)!; + const buffer = Buffer.from([24, 169, 10, 0, 0, 24, 1]); + const frame = Zcl.Frame.fromBuffer(Zcl.Utils.getCluster('msOccupancySensing', undefined, {}).ID, Zcl.Header.fromBuffer(buffer), buffer, {}); + const data = { + wasBroadcast: false, + address: '0x129', + clusterID: frame.cluster.ID, + data: frame.toBuffer(), + header: frame.header, + endpoint: 1, + linkquality: 50, + groupID: 1, + }; // We need to wait for the data to be queued + // @ts-expect-error private const origQueueRequest = endpoint.pendingRequests.queue; + // @ts-expect-error private endpoint.pendingRequests.queue = async (req) => { + // @ts-expect-error private const f = origQueueRequest.call(endpoint.pendingRequests, req); jest.advanceTimersByTime(10); return f; }; - endpoint.pendingRequests.add(new Request(async () => {}, [], 100)); + // @ts-expect-error private + endpoint.pendingRequests.add(new Request(async () => {}, frame, 100)); mocksendZclFrameToEndpoint.mockClear(); mocksendZclFrameToEndpoint.mockImplementationOnce(async () => { throw new Error('Dogs barking too hard'); @@ -8852,18 +9055,6 @@ describe('Controller', () => { const result = endpoint.write('genOnOff', {onOff: 1}, {disableResponse: true}); expect(mocksendZclFrameToEndpoint).toHaveBeenCalledTimes(1); - const buffer = Buffer.from([24, 169, 10, 0, 0, 24, 1]); - const frame = Zcl.Frame.fromBuffer(Zcl.Utils.getCluster('msOccupancySensing', undefined, {}).ID, Zcl.Header.fromBuffer(buffer), buffer, {}); - const data = { - wasBroadcast: false, - address: '0x129', - clusterID: frame.cluster.ID, - data: frame.toBuffer(), - header: frame.header, - endpoint: 1, - linkquality: 50, - groupID: 1, - }; await nextTick; await mockAdapterEvents['zclPayload'](data); await result; @@ -8876,11 +9067,15 @@ describe('Controller', () => { it('Write to device with pendingRequestTimeout > 0, override default sendPolicy', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 174, ieeeAddr: '0x129'}); - const device = controller.getDeviceByIeeeAddr('0x129'); + const device = controller.getDeviceByIeeeAddr('0x129')!; device.pendingRequestTimeout = 10000; - const endpoint = device.getEndpoint(1); + const endpoint = device.getEndpoint(1)!; - endpoint.pendingRequests.add(new Request(async () => {}, [], 100)); + // @ts-expect-error private + endpoint.pendingRequests.add( + // @ts-expect-error mock + new Request(async () => {}, {}, 100), + ); mocksendZclFrameToEndpoint.mockClear(); mocksendZclFrameToEndpoint.mockImplementationOnce(async () => { throw new Error('Dogs barking too hard'); @@ -8896,19 +9091,35 @@ describe('Controller', () => { ); } expect(mocksendZclFrameToEndpoint).toHaveBeenCalledTimes(1); + // @ts-expect-error private expect(endpoint.pendingRequests.size).toStrictEqual(1); }); it('Write to device with pendingRequestTimeout > 0, error', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const device = controller.getDeviceByIeeeAddr('0x129'); + const device = controller.getDeviceByIeeeAddr('0x129')!; device.checkinInterval = 10; + const buffer = Buffer.from([24, 169, 10, 0, 0, 24, 1]); + const frame = Zcl.Frame.fromBuffer(Zcl.Utils.getCluster('msOccupancySensing', undefined, {}).ID, Zcl.Header.fromBuffer(buffer), buffer, {}); + const data = { + wasBroadcast: false, + address: '0x129', + clusterID: frame.cluster.ID, + data: frame.toBuffer(), + header: frame.header, + endpoint: 1, + linkquality: 50, + groupID: 1, + }; expect(device.pendingRequestTimeout).toStrictEqual(10000); - const endpoint = device.getEndpoint(1); + const endpoint = device.getEndpoint(1)!; // We need to wait for the data to be queued + // @ts-expect-error private const origQueueRequest = endpoint.pendingRequests.queue; + // @ts-expect-error private endpoint.pendingRequests.queue = async (req) => { + // @ts-expect-error private const f = origQueueRequest.call(endpoint.pendingRequests, req); jest.advanceTimersByTime(10); return f; @@ -8919,10 +9130,11 @@ describe('Controller', () => { jest.advanceTimersByTime(10); return f; }; + // @ts-expect-error private endpoint.pendingRequests.add( new Request( async () => {}, - [], + frame, 100, undefined, undefined, @@ -8942,25 +9154,13 @@ describe('Controller', () => { const result = endpoint.write('genOnOff', {onOff: 1}, {disableResponse: true}); await nextTick; expect(mocksendZclFrameToEndpoint).toHaveBeenCalledTimes(1); - const buffer = Buffer.from([24, 169, 10, 0, 0, 24, 1]); - const frame = Zcl.Frame.fromBuffer(Zcl.Utils.getCluster('msOccupancySensing', undefined, {}).ID, Zcl.Header.fromBuffer(buffer), buffer, {}); - const data = { - wasBroadcast: false, - address: '0x129', - clusterID: frame.cluster.ID, - data: frame.toBuffer(), - header: frame.header, - endpoint: 1, - linkquality: 50, - groupID: 1, - }; nextTick = new Promise(process.nextTick); await mockAdapterEvents['zclPayload'](data); await nextTick; expect(mocksendZclFrameToEndpoint).toHaveBeenCalledTimes(2); - Date.now.mockReturnValue(100000); - let error = null; + (Date.now as ReturnType).mockReturnValue(100000); + let error; try { await mockAdapterEvents['zclPayload'](data); await result; @@ -8976,20 +9176,27 @@ describe('Controller', () => { it('Write to device with pendingRequestTimeout > 0, replace queued messages', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const device = controller.getDeviceByIeeeAddr('0x129'); + const device = controller.getDeviceByIeeeAddr('0x129')!; device.pendingRequestTimeout = 10000; - const endpoint = device.getEndpoint(1); + const endpoint = device.getEndpoint(1)!; // We need to wait for the data to be queued, but not for the promise to resolve + // @ts-expect-error private const origQueueRequest = endpoint.pendingRequests.queue; + // @ts-expect-error private endpoint.pendingRequests.queue = async (req) => { + // @ts-expect-error private const f = origQueueRequest.call(endpoint.pendingRequests, req); jest.advanceTimersByTime(10); return f; }; //add a request with empty data and a ZclFrame to the queue - endpoint.pendingRequests.add(new Request(async () => {}, [], 100)); + // @ts-expect-error private + endpoint.pendingRequests.add( + // @ts-expect-error mock + new Request(async () => {}, {}, 100), + ); // Queue content: // 1. empty request mocksendZclFrameToEndpoint.mockClear(); @@ -9048,6 +9255,7 @@ describe('Controller', () => { // Queue content: // 1. empty // 2. ZCL write 'genOnOff' {onOff: 0, startUpOnOff: 0} + // @ts-expect-error private expect(endpoint.pendingRequests.size).toStrictEqual(2); result1 = endpoint.write('genOnOff', {onOff: 0}, {disableResponse: true}); await new Promise(process.nextTick); @@ -9055,19 +9263,27 @@ describe('Controller', () => { // 1. empty // 2. ZCL write 'genOnOff' {startUpOnOff: 0} // 3. ZCL write 'genOnOff' {onOff: 0} --> result1 + // @ts-expect-error private expect(endpoint.pendingRequests.size).toStrictEqual(3); expect(mocksendZclFrameToEndpoint).toHaveBeenCalledTimes(2); //add another non-ZCL request, should go directly to queue without errors - const result6 = endpoint.sendRequest(5, [], (d) => { - throw new Error(d + 1); - }); + // @ts-expect-error private + const result6 = endpoint.sendRequest( + // @ts-expect-error mock + 5, + [], + () => { + throw new Error(`1`); + }, + ); await new Promise(process.nextTick); // Queue content: // 1. empty // 2. ZCL write 'genOnOff' {startUpOnOff: 0} // 3. ZCL write 'genOnOff' {onOff: 0} // 4. add 1 + // @ts-expect-error private expect(endpoint.pendingRequests.size).toStrictEqual(4); expect(mocksendZclFrameToEndpoint).toHaveBeenCalledTimes(2); @@ -9083,6 +9299,7 @@ describe('Controller', () => { // 2. ZCL write 'genOnOff' {startUpOnOff: 0} // 3. add 1 // 4. ZCL write 'genOnOff' {onOff: 1} --> result2 + // @ts-expect-error private expect(endpoint.pendingRequests.size).toStrictEqual(4); } // Now add the same ZCL request with same payload again. The previous one should *not* be rejected but removed from the queue @@ -9093,6 +9310,7 @@ describe('Controller', () => { // 2. ZCL write 'genOnOff' {startUpOnOff: 0} // 3. add 1 // 4. ZCL write 'genOnOff' {onOff: 1} --> result2, result3 + // @ts-expect-error private expect(endpoint.pendingRequests.size).toStrictEqual(4); // writeUndiv request should not be divided, so both should go to the queue @@ -9107,6 +9325,7 @@ describe('Controller', () => { // 4. ZCL write 'genOnOff' {onOff: 1} --> result2, result3 // 5. ZCL writeUndiv 'genOnOff' {onOff: 0, startUpOnOff: 0} // 6. ZCL writeUndiv 'genOnOff' {startUpOnOff: 1} + // @ts-expect-error private expect(endpoint.pendingRequests.size).toStrictEqual(6); // read requests should be combined to one @@ -9122,6 +9341,7 @@ describe('Controller', () => { // 5. ZCL writeUndiv 'genOnOff' {onOff: 0, startUpOnOff: 0} // 6. ZCL writeUndiv 'genOnOff' {startUpOnOff: 1} // 7. ZCL read 'genOnOff' --> result4, result5 + // @ts-expect-error private expect(endpoint.pendingRequests.size).toStrictEqual(7); expect(mocksendZclFrameToEndpoint).toHaveBeenCalledTimes(8); @@ -9153,14 +9373,14 @@ describe('Controller', () => { }); it('Write to device with pendingRequestTimeout > 0, discard messages after expiration', async () => { - Date.now.mockReturnValue(1000); + (Date.now as ReturnType).mockReturnValue(1000); await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 174, ieeeAddr: '0x174'}); - const device = controller.getDeviceByIeeeAddr('0x174'); - mockDevices[174].attributes[1].checkinInterval = 3996; //999 seconds + const device = controller.getDeviceByIeeeAddr('0x174')!; + MOCK_DEVICES[174]!.attributes![1].checkinInterval = 3996; //999 seconds await device.interview(); - const endpoint = device.getEndpoint(1); + const endpoint = device.getEndpoint(1)!; expect(device.checkinInterval).toBe(999); expect(device.pendingRequestTimeout).toBe(999000); mocksendZclFrameToEndpoint.mockClear(); @@ -9170,8 +9390,11 @@ describe('Controller', () => { // We need to send the data after it's been queued, but before we await // the promise. Hijacking queueRequest seems easiest. + // @ts-expect-error private const origQueueRequest = endpoint.pendingRequests.queue; + // @ts-expect-error private endpoint.pendingRequests.queue = async (req) => { + // @ts-expect-error private const f = origQueueRequest.call(endpoint.pendingRequests, req); const buffer = Buffer.from([24, 169, 10, 0, 0, 24, 1]); const frame = Zcl.Frame.fromBuffer( @@ -9193,11 +9416,15 @@ describe('Controller', () => { return f; }; - endpoint.pendingRequests.add(new Request(async () => {}, [], 100)); + // @ts-expect-error private + endpoint.pendingRequests.add( + // @ts-expect-error mock + new Request(async () => {}, {}, 100), + ); const result = endpoint.write('genOnOff', {onOff: 10}, {disableResponse: true}); expect(mocksendZclFrameToEndpoint).toHaveBeenCalledTimes(1); - Date.now.mockReturnValue(1001000); + (Date.now as ReturnType).mockReturnValue(1001000); let error = null; try { await result; @@ -9205,32 +9432,38 @@ describe('Controller', () => { error = e; } expect(mocksendZclFrameToEndpoint).toHaveBeenCalledTimes(1); + // @ts-expect-error private expect(endpoint.pendingRequests.size).toBe(0); - Date.now.mockReturnValue(150); + (Date.now as ReturnType).mockReturnValue(150); }); it('Implicit checkin while send already in progress', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 174, ieeeAddr: '0x174'}); - const device = controller.getDeviceByIeeeAddr('0x174'); + const device = controller.getDeviceByIeeeAddr('0x174')!; await device.interview(); mocksendZclFrameToEndpoint.mockClear(); mocksendZclFrameToEndpoint.mockImplementationOnce(() => { throw new Error('dogs barking too hard'); }); - const endpoint = device.getEndpoint(1); + const endpoint = device.getEndpoint(1)!; + // @ts-expect-error private const origQueueRequest = endpoint.pendingRequests.queue; + // @ts-expect-error private endpoint.pendingRequests.queue = async (req) => { + // @ts-expect-error private const f = origQueueRequest.call(endpoint.pendingRequests, req); jest.advanceTimersByTime(10); return f; }; + // @ts-expect-error private endpoint.pendingRequests.add( new Request( async () => { await endpoint.sendPendingRequests(false); }, - [], + // @ts-expect-error mock + {}, 100, ), ); @@ -9240,6 +9473,7 @@ describe('Controller', () => { await endpoint.sendPendingRequests(false); await result; expect(mocksendZclFrameToEndpoint).toHaveBeenCalledTimes(2); + // @ts-expect-error private expect(endpoint.pendingRequests.size).toBe(0); }); @@ -9247,9 +9481,9 @@ describe('Controller', () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 174, ieeeAddr: '0x174'}); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const device = controller.getDeviceByIeeeAddr('0x174'); + const device = controller.getDeviceByIeeeAddr('0x174')!; await device.interview(); - const endpoint = device.getEndpoint(1); + const endpoint = device.getEndpoint(1)!; mocksendZclFrameToEndpoint.mockClear(); mocksendZclFrameToEndpoint.mockReturnValueOnce(null); @@ -9448,8 +9682,6 @@ describe('Controller', () => { _events: {}, _eventsCount: 0, _hardwareVersion: 3, - _events: {}, - _eventsCount: 0, _ieeeAddr: '0x171', _interviewCompleted: true, _interviewing: false, @@ -9622,11 +9854,10 @@ describe('Controller', () => { }); it('zclCommand', async () => { - await controller.start(); await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const device = controller.getDeviceByIeeeAddr('0x129'); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const endpoint = device.getEndpoint(1)!; mocksendZclFrameToEndpoint.mockReturnValueOnce(null); let error; try { @@ -9638,11 +9869,11 @@ describe('Controller', () => { }); it('zclCommand with error', async () => { - await controller.start(); await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const device = controller.getDeviceByIeeeAddr('0x129'); - const endpoint = device.getEndpoint(1); + const device = controller.getDeviceByIeeeAddr('0x129')!; + const endpoint = device.getEndpoint(1)!; + console.log(endpoint); mocksendZclFrameToEndpoint.mockRejectedValueOnce(new Error('timeout occurred')); let error; try { @@ -9656,4 +9887,569 @@ describe('Controller', () => { ), ); }); + + it('Interview on coordinator', async () => { + await controller.start(); + mockAdapterSendZdo.mockClear(); + const device = controller.getDeviceByNetworkAddress(ZSpec.COORDINATOR_ADDRESS)!; + const deviceNodeDescSpy = jest.spyOn(device, 'updateNodeDescriptor'); + + await device.interview(true); + + expect(deviceNodeDescSpy).toHaveBeenCalledTimes(1); + expect(mockAdapterSendZdo).toHaveBeenCalledTimes(4); // nodeDesc + activeEp + simpleDesc x2 + }); + + it('Device node descriptor fails', async () => { + await controller.start(); + mockAdapterSendZdo.mockClear(); + const device = controller.getDeviceByNetworkAddress(ZSpec.COORDINATOR_ADDRESS)!; + sendZdoResponseStatus = Zdo.Status.INSUFFICIENT_SPACE; + + expect(async () => { + await device.updateNodeDescriptor(); + }).rejects.toThrow(`Status 'INSUFFICIENT_SPACE'`); + expect(mockAdapterSendZdo).toHaveBeenCalledTimes(1); + }); + + it('Device active endpoints fails', async () => { + await controller.start(); + mockAdapterSendZdo.mockClear(); + const device = controller.getDeviceByNetworkAddress(ZSpec.COORDINATOR_ADDRESS)!; + sendZdoResponseStatus = Zdo.Status.INSUFFICIENT_SPACE; + + expect(async () => { + await device.updateActiveEndpoints(); + }).rejects.toThrow(`Status 'INSUFFICIENT_SPACE'`); + expect(mockAdapterSendZdo).toHaveBeenCalledTimes(1); + }); + + it('Endpoint simple descriptor fails', async () => { + await controller.start(); + mockAdapterSendZdo.mockClear(); + const device = controller.getDeviceByNetworkAddress(ZSpec.COORDINATOR_ADDRESS)!; + const endpoint = device.getEndpoint(1)!; + sendZdoResponseStatus = Zdo.Status.INSUFFICIENT_SPACE; + + expect(async () => { + await endpoint.updateSimpleDescriptor(); + }).rejects.toThrow(`Status 'INSUFFICIENT_SPACE'`); + expect(mockAdapterSendZdo).toHaveBeenCalledTimes(1); + }); + + it('Node Descriptor on R21 device', async () => { + await controller.start(); + await mockAdapterEvents['deviceJoined']({networkAddress: 162, ieeeAddr: '0x162'}); + + expect(mockLogger.info).toHaveBeenCalledWith( + `Device '0x162' is only compliant to revision '21' of the ZigBee specification (current revision: ${ZSpec.ZIGBEE_REVISION}).`, + `zh:controller:device`, + ); + }); + + it('Node Descriptor on R pre-21 device', async () => { + await controller.start(); + await mockAdapterEvents['deviceJoined']({networkAddress: 161, ieeeAddr: '0x161'}); + + expect(mockLogger.info).toHaveBeenCalledWith( + `Device '0x161' is only compliant to revision 'pre-21' of the ZigBee specification (current revision: ${ZSpec.ZIGBEE_REVISION}).`, + `zh:controller:device`, + ); + }); + + it('Device update network address - unchanged', async () => { + await controller.start(); + await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); + mockAdapterSendZdo.mockClear(); + const device = controller.getDeviceByNetworkAddress(129)!; + expect(device.ieeeAddr).toStrictEqual('0x129'); + + mockAdapterSendZdo.mockImplementationOnce(async () => { + const zdoResponse = [ + Zdo.Status.SUCCESS, + { + eui64: '0x129', + nwkAddress: 129, + startIndex: 0, + assocDevList: [], + } as NetworkAddressResponse, + ]; + + await mockAdapterEvents['zdoResponse'](Zdo.ClusterId.NETWORK_ADDRESS_RESPONSE, zdoResponse); + return zdoResponse; + }); + + await device.updateNetworkAddress(); + + expect(mockAdapterSendZdo).toHaveBeenCalledTimes(1); + const zdoPayload = Zdo.Buffalo.buildRequest(false, Zdo.ClusterId.NETWORK_ADDRESS_REQUEST, '0x129', false, 0); + expect(mockAdapterSendZdo).toHaveBeenCalledWith( + '0x129', + ZSpec.BroadcastAddress.RX_ON_WHEN_IDLE, + Zdo.ClusterId.NETWORK_ADDRESS_REQUEST, + zdoPayload, + false, + ); + + expect(controller.getDeviceByIeeeAddr('0x129')!.networkAddress).toBe(129); + }); + + it('Device update network address - changed', async () => { + await controller.start(); + await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); + mockAdapterSendZdo.mockClear(); + const device = controller.getDeviceByNetworkAddress(129)!; + expect(device.ieeeAddr).toStrictEqual('0x129'); + + mockAdapterSendZdo.mockImplementationOnce(async () => { + const zdoResponse = [ + Zdo.Status.SUCCESS, + { + eui64: '0x129', + nwkAddress: 9999, + startIndex: 0, + assocDevList: [], + } as NetworkAddressResponse, + ]; + + await mockAdapterEvents['zdoResponse'](Zdo.ClusterId.NETWORK_ADDRESS_RESPONSE, zdoResponse); + return zdoResponse; + }); + + await device.updateNetworkAddress(); + + expect(mockAdapterSendZdo).toHaveBeenCalledTimes(1); + const zdoPayload = Zdo.Buffalo.buildRequest(false, Zdo.ClusterId.NETWORK_ADDRESS_REQUEST, '0x129', false, 0); + expect(mockAdapterSendZdo).toHaveBeenCalledWith( + '0x129', + ZSpec.BroadcastAddress.RX_ON_WHEN_IDLE, + Zdo.ClusterId.NETWORK_ADDRESS_REQUEST, + zdoPayload, + false, + ); + + 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('Device update network address fails', async () => { + await controller.start(); + await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); + mockAdapterSendZdo.mockClear(); + const device = controller.getDeviceByNetworkAddress(129)!; + + sendZdoResponseStatus = Zdo.Status.INSUFFICIENT_SPACE; + + expect(async () => { + await device.updateNetworkAddress(); + }).rejects.toThrow(`Status 'INSUFFICIENT_SPACE'`); + + expect(mockAdapterSendZdo).toHaveBeenCalledTimes(1); + const zdoPayload = Zdo.Buffalo.buildRequest(false, Zdo.ClusterId.NETWORK_ADDRESS_REQUEST, '0x129', false, 0); + expect(mockAdapterSendZdo).toHaveBeenCalledWith( + '0x129', + ZSpec.BroadcastAddress.RX_ON_WHEN_IDLE, + Zdo.ClusterId.NETWORK_ADDRESS_REQUEST, + zdoPayload, + false, + ); + }); + + it('Device remove from network fails', async () => { + await controller.start(); + await mockAdapterEvents['deviceJoined']({networkAddress: 140, ieeeAddr: '0x140'}); + const device = controller.getDeviceByIeeeAddr('0x140')!; + sendZdoResponseStatus = Zdo.Status.INVALID_INDEX; + + expect(async () => { + await device.removeFromNetwork(); + }).rejects.toThrow(`Status 'INVALID_INDEX'`); + + const zdoPayload = Zdo.Buffalo.buildRequest(false, Zdo.ClusterId.LEAVE_REQUEST, '0x140', Zdo.LeaveRequestFlags.WITHOUT_REJOIN); + expect(mockAdapterSendZdo).toHaveBeenCalledWith('0x140', 140, Zdo.ClusterId.LEAVE_REQUEST, zdoPayload, false); + expect(controller.getDeviceByIeeeAddr('0x140')).toBeDefined(); + }); + + it('Device LQI table fails', async () => { + await controller.start(); + await mockAdapterEvents['deviceJoined']({networkAddress: 140, ieeeAddr: '0x140'}); + const device = controller.getDeviceByIeeeAddr('0x140')!; + sendZdoResponseStatus = Zdo.Status.INVALID_INDEX; + + expect(async () => { + await device.lqi(); + }).rejects.toThrow(`Status 'INVALID_INDEX'`); + + const zdoPayload = Zdo.Buffalo.buildRequest(false, Zdo.ClusterId.LQI_TABLE_REQUEST, 0); + expect(mockAdapterSendZdo).toHaveBeenCalledWith('0x140', 140, Zdo.ClusterId.LQI_TABLE_REQUEST, zdoPayload, false); + expect(controller.getDeviceByIeeeAddr('0x140')).toBeDefined(); + }); + + it('Device routing table fails', async () => { + await controller.start(); + await mockAdapterEvents['deviceJoined']({networkAddress: 140, ieeeAddr: '0x140'}); + const device = controller.getDeviceByIeeeAddr('0x140')!; + sendZdoResponseStatus = Zdo.Status.INVALID_INDEX; + + expect(async () => { + await device.routingTable(); + }).rejects.toThrow(`Status 'INVALID_INDEX'`); + + const zdoPayload = Zdo.Buffalo.buildRequest(false, Zdo.ClusterId.ROUTING_TABLE_REQUEST, 0); + expect(mockAdapterSendZdo).toHaveBeenCalledWith('0x140', 140, Zdo.ClusterId.ROUTING_TABLE_REQUEST, zdoPayload, false); + expect(controller.getDeviceByIeeeAddr('0x140')).toBeDefined(); + }); + + it('Device LQI table with more than 1 request', async () => { + await controller.start(); + await mockAdapterEvents['deviceJoined']({networkAddress: 140, ieeeAddr: '0x140'}); + const device = controller.getDeviceByIeeeAddr('0x140')!; + mockAdapterSendZdo + .mockImplementationOnce(() => { + return [ + Zdo.Status.SUCCESS, + { + neighborTableEntries: 3, + startIndex: 0, + entryList: [ + {...LQI_TABLE_ENTRY_DEFAULTS, eui64: '0x160', nwkAddress: 160, lqi: 20, relationship: 2, depth: 5}, + {...LQI_TABLE_ENTRY_DEFAULTS, eui64: '0x170', nwkAddress: 170, lqi: 21, relationship: 4, depth: 8}, + ], + }, + ]; + }) + .mockImplementationOnce(() => { + return [ + Zdo.Status.SUCCESS, + { + neighborTableEntries: 3, + startIndex: 2, + entryList: [{...LQI_TABLE_ENTRY_DEFAULTS, eui64: '0x180', nwkAddress: 180, lqi: 200, relationship: 4, depth: 2}], + }, + ]; + }); + + const result = await device.lqi(); + expect(result).toStrictEqual({ + neighbors: [ + {ieeeAddr: '0x160', networkAddress: 160, linkquality: 20, relationship: 2, depth: 5}, + {ieeeAddr: '0x170', networkAddress: 170, linkquality: 21, relationship: 4, depth: 8}, + {ieeeAddr: '0x180', networkAddress: 180, linkquality: 200, relationship: 4, depth: 2}, + ], + }); + }); + + it('Device routing table with more than 1 request', async () => { + await controller.start(); + await mockAdapterEvents['deviceJoined']({networkAddress: 140, ieeeAddr: '0x140'}); + const device = controller.getDeviceByIeeeAddr('0x140')!; + mockAdapterSendZdo + .mockImplementationOnce(() => { + return [ + Zdo.Status.SUCCESS, + { + routingTableEntries: 3, + startIndex: 0, + entryList: [ + {...ROUTING_TABLE_ENTRY_DEFAULTS, destinationAddress: 120, status: 'ACTIVE', nextHopAddress: 1}, + {...ROUTING_TABLE_ENTRY_DEFAULTS, destinationAddress: 130, status: 'DISCOVERY_FAILED', nextHopAddress: 2}, + ], + }, + ]; + }) + .mockImplementationOnce(() => { + return [ + Zdo.Status.SUCCESS, + { + routingTableEntries: 3, + startIndex: 2, + entryList: [{...ROUTING_TABLE_ENTRY_DEFAULTS, destinationAddress: 140, status: 'INACTIVE', nextHopAddress: 3}], + }, + ]; + }); + + const result = await device.routingTable(); + expect(result).toStrictEqual({ + table: [ + {destinationAddress: 120, status: 'ACTIVE', nextHop: 1}, + {destinationAddress: 130, status: 'DISCOVERY_FAILED', nextHop: 2}, + {destinationAddress: 140, status: 'INACTIVE', nextHop: 3}, + ], + }); + }); + + it('Adapter permitJoin fails to keep alive', async () => { + await controller.start(); + mockAdapterPermitJoin.mockResolvedValueOnce(undefined).mockRejectedValueOnce('timeout'); + await controller.permitJoin(true); + await jest.advanceTimersByTimeAsync(240 * 1000); + + expect(mockLogger.error).toHaveBeenCalledWith(`Failed to keep permit join alive: timeout`, 'zh:controller'); + }); + + it('Adapter permitJoin fails during stop', async () => { + await controller.start(); + mockAdapterPermitJoin.mockRejectedValueOnce('timeout'); + await controller.stop(); + + expect(mockLogger.error).toHaveBeenCalledWith(`Failed to disable join on stop: timeout`, 'zh:controller'); + }); + + it('Adapter stop fails after adapter disconnected', async () => { + await controller.start(); + mockAdapterStop.mockRejectedValueOnce('timeout'); + await mockAdapterEvents['disconnected'](); + + expect(mockLogger.error).toHaveBeenCalledWith(`Failed to stop adapter on disconnect: timeout`, 'zh:controller'); + }); + + it('Device network address changed while Z2M was offline, received no notification on start', async () => { + const oldNwkAddress = 40369; + const newNwkAddress = 12345; + const database = ` + {"id":1,"type":"Coordinator","ieeeAddr":"0x0000012300000000","nwkAddr":0,"manufId":0,"epList":[11,6,5,4,3,2,1],"endpoints":{"1":{"profId":260,"epId":1,"devId":5,"inClusterList":[],"meta":{},"outClusterList":[],"clusters":{}},"2":{"profId":257,"epId":2,"devId":5,"inClusterList":[],"meta":{},"outClusterList":[],"clusters":{}},"3":{"profId":261,"epId":3,"devId":5,"inClusterList":[],"meta":{},"outClusterList":[],"clusters":{}},"4":{"profId":263,"epId":4,"devId":5,"inClusterList":[],"meta":{},"outClusterList":[],"clusters":{}},"5":{"profId":264,"epId":5,"devId":5,"inClusterList":[],"meta":{},"outClusterList":[],"clusters":{}},"6":{"profId":265,"epId":6,"devId":5,"inClusterList":[],"meta":{},"outClusterList":[],"clusters":{}},"11":{"profId":260,"epId":11,"devId":1024,"inClusterList":[],"meta":{},"outClusterList":[1280],"clusters":{}}},"interviewCompleted":false,"meta":{},"_id":"aM341ldunExFmJ3u"} + {"id":3,"type":"Router","ieeeAddr":"0x000b57fffec6a5b2","nwkAddr":${oldNwkAddress},"manufId":4476,"manufName":"IKEA of Sweden","powerSource":"Mains (single phase)","modelId":"TRADFRI bulb E27 WS opal 980lm","epList":[1],"endpoints":{"1":{"profId":49246,"epId":1,"devId":544,"inClusterList":[0,3,4,5,6,8,768,2821,4096],"meta":{},"outClusterList":[5,25,32,4096],"clusters":{}}},"appVersion":17,"stackVersion":87,"hwVersion":1,"dateCode":"20170331","swBuildId":"1.2.217","zclVersion":1,"interviewCompleted":true,"meta":{"reporting":1},"_id":"pagvP2f9Bbj3o9TM"} + `; + fs.writeFileSync(options.databasePath, database); + await controller.start(); + + const device = controller.getDeviceByIeeeAddr('0x000b57fffec6a5b2')!; + expect(device.networkAddress).toStrictEqual(oldNwkAddress); + + mockAdapterSendZdo.mockImplementationOnce(async () => { + const zdoResponse = [ + Zdo.Status.SUCCESS, + { + eui64: '0x000b57fffec6a5b2', + nwkAddress: newNwkAddress, + startIndex: 0, + assocDevList: [], + } as IEEEAddressResponse, + ]; + + await mockAdapterEvents['zdoResponse'](Zdo.ClusterId.IEEE_ADDRESS_RESPONSE, zdoResponse); + return zdoResponse; + }); + + const frame = Zcl.Frame.create(0, 1, true, undefined, 10, 'readRsp', 0, [{attrId: 5, status: 0, dataType: 66, attrData: 'new.model.id'}], {}); + await mockAdapterEvents['zclPayload']({ + wasBroadcast: false, + address: newNwkAddress, + clusterID: frame.cluster.ID, + data: frame.toBuffer(), + header: frame.header, + endpoint: 1, + linkquality: 50, + groupID: 1, + }); + + expect(device.networkAddress).toStrictEqual(newNwkAddress); + expect(device.modelID).toBe('new.model.id'); + }); + + it('Device network address changed while Z2M was running, received no notification', async () => { + const oldNwkAddress = 40369; + const newNwkAddress = 12345; + const database = ` + {"id":1,"type":"Coordinator","ieeeAddr":"0x0000012300000000","nwkAddr":0,"manufId":0,"epList":[11,6,5,4,3,2,1],"endpoints":{"1":{"profId":260,"epId":1,"devId":5,"inClusterList":[],"meta":{},"outClusterList":[],"clusters":{}},"2":{"profId":257,"epId":2,"devId":5,"inClusterList":[],"meta":{},"outClusterList":[],"clusters":{}},"3":{"profId":261,"epId":3,"devId":5,"inClusterList":[],"meta":{},"outClusterList":[],"clusters":{}},"4":{"profId":263,"epId":4,"devId":5,"inClusterList":[],"meta":{},"outClusterList":[],"clusters":{}},"5":{"profId":264,"epId":5,"devId":5,"inClusterList":[],"meta":{},"outClusterList":[],"clusters":{}},"6":{"profId":265,"epId":6,"devId":5,"inClusterList":[],"meta":{},"outClusterList":[],"clusters":{}},"11":{"profId":260,"epId":11,"devId":1024,"inClusterList":[],"meta":{},"outClusterList":[1280],"clusters":{}}},"interviewCompleted":false,"meta":{},"_id":"aM341ldunExFmJ3u"} + {"id":4,"type":"EndDevice","ieeeAddr":"0x0017880104e45517","lastSeen":123,"nwkAddr":${oldNwkAddress},"manufId":4107,"manufName":"Philips","powerSource":"Battery","modelId":"RWL021","epList":[1,2],"endpoints":{"1":{"profId":49246,"epId":1,"devId":2096,"inClusterList":[0],"meta":{},"outClusterList":[0,3,4,6,8,5],"binds":[{"type":"endpoint","endpointID":1,"deviceIeeeAddr":"0x000b57fffec6a5b2"}],"configuredReportings":[{"cluster":1,"attrId":0,"minRepIntval":1,"maxRepIntval":20,"repChange":2}],"clusters":{"genBasic":{"dir":{"value":3},"attrs":{"modelId":"RWL021"}}}},"2":{"profId":260,"epId":2,"devId":12,"inClusterList":[0,1,3,15,64512],"meta":{},"outClusterList":[25],"clusters":{}}},"appVersion":2,"stackVersion":1,"hwVersion":1,"dateCode":"20160302","swBuildId":"5.45.1.17846","zclVersion":1,"interviewCompleted":true,"meta":{"configured":1},"_id":"qxhymbX6H2GXDw8Z"} + `; + fs.writeFileSync(options.databasePath, database); + await controller.start(); + + const device = controller.getDeviceByIeeeAddr('0x0017880104e45517')!; + expect(device.networkAddress).toStrictEqual(oldNwkAddress); + + mockAdapterSendZdo.mockImplementationOnce(async () => { + const zdoResponse = [ + Zdo.Status.SUCCESS, + { + eui64: '0x0017880104e45517', + nwkAddress: newNwkAddress, + startIndex: 0, + assocDevList: [], + } as IEEEAddressResponse, + ]; + + await mockAdapterEvents['zdoResponse'](Zdo.ClusterId.IEEE_ADDRESS_RESPONSE, zdoResponse); + return zdoResponse; + }); + + const frame = Zcl.Frame.create(0, 1, true, undefined, 10, 'readRsp', 0, [{attrId: 5, status: 0, dataType: 66, attrData: 'new.model.id'}], {}); + await mockAdapterEvents['zclPayload']({ + wasBroadcast: false, + address: oldNwkAddress, + clusterID: frame.cluster.ID, + data: frame.toBuffer(), + header: frame.header, + endpoint: 1, + linkquality: 50, + groupID: 1, + }); + + expect(device.networkAddress).toStrictEqual(oldNwkAddress); + expect(device.modelID).toBe('new.model.id'); + + const frame2 = Zcl.Frame.create( + 0, + 1, + true, + undefined, + 10, + 'readRsp', + 0, + [{attrId: 5, status: 0, dataType: 66, attrData: 'new.model.id2'}], + {}, + ); + await mockAdapterEvents['zclPayload']({ + wasBroadcast: false, + address: newNwkAddress, + clusterID: frame2.cluster.ID, + data: frame2.toBuffer(), + header: frame2.header, + endpoint: 1, + linkquality: 50, + groupID: 1, + }); + + expect(device.networkAddress).toStrictEqual(newNwkAddress); + expect(device.modelID).toBe('new.model.id2'); + }); + + it('Device network address changed while Z2M was offline - fails to retrieve new one', async () => { + const oldNwkAddress = 40369; + const newNwkAddress = 12345; + const database = ` + {"id":1,"type":"Coordinator","ieeeAddr":"0x0000012300000000","nwkAddr":0,"manufId":0,"epList":[11,6,5,4,3,2,1],"endpoints":{"1":{"profId":260,"epId":1,"devId":5,"inClusterList":[],"meta":{},"outClusterList":[],"clusters":{}},"2":{"profId":257,"epId":2,"devId":5,"inClusterList":[],"meta":{},"outClusterList":[],"clusters":{}},"3":{"profId":261,"epId":3,"devId":5,"inClusterList":[],"meta":{},"outClusterList":[],"clusters":{}},"4":{"profId":263,"epId":4,"devId":5,"inClusterList":[],"meta":{},"outClusterList":[],"clusters":{}},"5":{"profId":264,"epId":5,"devId":5,"inClusterList":[],"meta":{},"outClusterList":[],"clusters":{}},"6":{"profId":265,"epId":6,"devId":5,"inClusterList":[],"meta":{},"outClusterList":[],"clusters":{}},"11":{"profId":260,"epId":11,"devId":1024,"inClusterList":[],"meta":{},"outClusterList":[1280],"clusters":{}}},"interviewCompleted":false,"meta":{},"_id":"aM341ldunExFmJ3u"} + {"id":3,"type":"Router","ieeeAddr":"0x000b57fffec6a5b2","nwkAddr":${oldNwkAddress},"manufId":4476,"manufName":"IKEA of Sweden","powerSource":"Mains (single phase)","modelId":"TRADFRI bulb E27 WS opal 980lm","epList":[1],"endpoints":{"1":{"profId":49246,"epId":1,"devId":544,"inClusterList":[0,3,4,5,6,8,768,2821,4096],"meta":{},"outClusterList":[5,25,32,4096],"clusters":{}}},"appVersion":17,"stackVersion":87,"hwVersion":1,"dateCode":"20170331","swBuildId":"1.2.217","zclVersion":1,"interviewCompleted":true,"meta":{"reporting":1},"_id":"pagvP2f9Bbj3o9TM"} + `; + fs.writeFileSync(options.databasePath, database); + await controller.start(); + + const device = controller.getDeviceByIeeeAddr('0x000b57fffec6a5b2')!; + expect(device.networkAddress).toStrictEqual(oldNwkAddress); + + mockAdapterSendZdo.mockImplementationOnce(async () => { + const zdoResponse = [Zdo.Status.INV_REQUESTTYPE, undefined]; + + await mockAdapterEvents['zdoResponse'](Zdo.ClusterId.IEEE_ADDRESS_RESPONSE, zdoResponse); + return zdoResponse; + }); + + const frame = Zcl.Frame.create(0, 1, true, undefined, 10, 'readRsp', 0, [{attrId: 5, status: 0, dataType: 66, attrData: 'new.model.id'}], {}); + await mockAdapterEvents['zclPayload']({ + wasBroadcast: false, + address: newNwkAddress, + clusterID: frame.cluster.ID, + data: frame.toBuffer(), + header: frame.header, + endpoint: 1, + linkquality: 50, + groupID: 1, + }); + + expect(device.networkAddress).toStrictEqual(oldNwkAddress); + expect(device.modelID).toBe('TRADFRI bulb E27 WS opal 980lm'); + expect(mockLogger.debug).toHaveBeenCalledWith( + `Failed to retrieve IEEE address for device '${newNwkAddress}': INV_REQUESTTYPE`, + 'zh:controller', + ); + }); + + it('Device network address changed while Z2M was offline, no duplicate triggering of IEEE request', async () => { + const oldNwkAddress = 40369; + const newNwkAddress = 12345; + const database = ` + {"id":1,"type":"Coordinator","ieeeAddr":"0x0000012300000000","nwkAddr":0,"manufId":0,"epList":[11,6,5,4,3,2,1],"endpoints":{"1":{"profId":260,"epId":1,"devId":5,"inClusterList":[],"meta":{},"outClusterList":[],"clusters":{}},"2":{"profId":257,"epId":2,"devId":5,"inClusterList":[],"meta":{},"outClusterList":[],"clusters":{}},"3":{"profId":261,"epId":3,"devId":5,"inClusterList":[],"meta":{},"outClusterList":[],"clusters":{}},"4":{"profId":263,"epId":4,"devId":5,"inClusterList":[],"meta":{},"outClusterList":[],"clusters":{}},"5":{"profId":264,"epId":5,"devId":5,"inClusterList":[],"meta":{},"outClusterList":[],"clusters":{}},"6":{"profId":265,"epId":6,"devId":5,"inClusterList":[],"meta":{},"outClusterList":[],"clusters":{}},"11":{"profId":260,"epId":11,"devId":1024,"inClusterList":[],"meta":{},"outClusterList":[1280],"clusters":{}}},"interviewCompleted":false,"meta":{},"_id":"aM341ldunExFmJ3u"} + {"id":3,"type":"Router","ieeeAddr":"0x000b57fffec6a5b2","nwkAddr":${oldNwkAddress},"manufId":4476,"manufName":"IKEA of Sweden","powerSource":"Mains (single phase)","modelId":"TRADFRI bulb E27 WS opal 980lm","epList":[1],"endpoints":{"1":{"profId":49246,"epId":1,"devId":544,"inClusterList":[0,3,4,5,6,8,768,2821,4096],"meta":{},"outClusterList":[5,25,32,4096],"clusters":{}}},"appVersion":17,"stackVersion":87,"hwVersion":1,"dateCode":"20170331","swBuildId":"1.2.217","zclVersion":1,"interviewCompleted":true,"meta":{"reporting":1},"_id":"pagvP2f9Bbj3o9TM"} + `; + fs.writeFileSync(options.databasePath, database); + await controller.start(); + mockAdapterSendZdo.mockClear(); + const identifyUnknownDeviceSpy = jest.spyOn(controller, 'identifyUnknownDevice'); + + const device = controller.getDeviceByIeeeAddr('0x000b57fffec6a5b2')!; + expect(device.networkAddress).toStrictEqual(oldNwkAddress); + + const frame = Zcl.Frame.create(0, 1, true, undefined, 10, 'readRsp', 0, [{attrId: 5, status: 0, dataType: 66, attrData: 'new.model.id'}], {}); + const zclPayload = { + wasBroadcast: false, + address: newNwkAddress, + clusterID: frame.cluster.ID, + data: frame.toBuffer(), + header: frame.header, + endpoint: 1, + linkquality: 50, + groupID: 1, + }; + + mockAdapterSendZdo.mockImplementationOnce(async () => { + await mockAdapterEvents['zclPayload'](zclPayload); + + const zdoResponse = [ + Zdo.Status.SUCCESS, + { + eui64: '0x000b57fffec6a5b2', + nwkAddress: newNwkAddress, + startIndex: 0, + assocDevList: [], + } as IEEEAddressResponse, + ]; + + await mockAdapterEvents['zdoResponse'](Zdo.ClusterId.IEEE_ADDRESS_RESPONSE, zdoResponse); + return zdoResponse; + }); + + await mockAdapterEvents['zclPayload'](zclPayload); + + expect(device.networkAddress).toStrictEqual(newNwkAddress); + expect(device.modelID).toBe('new.model.id'); + expect(identifyUnknownDeviceSpy).toHaveBeenCalledTimes(2); + expect(mockAdapterSendZdo).toHaveBeenCalledTimes(1); + }); + + it('Device network address changed while Z2M was offline, no spamming of IEEE request when device doesnt respond', async () => { + const nwkAddress = 40369; + await controller.start(); + mockAdapterSendZdo.mockClear(); + mockAdapterSendZdo.mockImplementationOnce(async () => { + const zdoResponse = [Zdo.Status.NOT_SUPPORTED, undefined]; + + await mockAdapterEvents['zdoResponse'](Zdo.ClusterId.IEEE_ADDRESS_RESPONSE, zdoResponse); + return zdoResponse; + }); + const identifyUnknownDeviceSpy = jest.spyOn(controller, 'identifyUnknownDevice'); + + const frame = Zcl.Frame.create(0, 1, true, undefined, 10, 'readRsp', 0, [{attrId: 5, status: 0, dataType: 66, attrData: 'new.model.id'}], {}); + await mockAdapterEvents['zclPayload']({ + wasBroadcast: false, + address: nwkAddress, + clusterID: frame.cluster.ID, + data: frame.toBuffer(), + header: frame.header, + endpoint: 1, + linkquality: 50, + groupID: 1, + }); + + expect(mockAdapterSendZdo).toHaveBeenCalledTimes(1); + expect(identifyUnknownDeviceSpy).toHaveBeenCalledTimes(1); + expect(mockLogger.debug).toHaveBeenCalledWith(`Failed to retrieve IEEE address for device '${nwkAddress}': NOT_SUPPORTED`, 'zh:controller'); + + await mockAdapterEvents['zclPayload']({ + wasBroadcast: false, + address: nwkAddress, + clusterID: frame.cluster.ID, + data: frame.toBuffer(), + header: frame.header, + endpoint: 1, + linkquality: 50, + groupID: 1, + }); + + expect(mockAdapterSendZdo).toHaveBeenCalledTimes(1); + expect(identifyUnknownDeviceSpy).toHaveBeenCalledTimes(2); + }); }); diff --git a/test/mockDevices.ts b/test/mockDevices.ts new file mode 100644 index 0000000000..f1e84ad1fb --- /dev/null +++ b/test/mockDevices.ts @@ -0,0 +1,472 @@ +import * as Zdo from '../src/zspec/zdo'; +import * as ZdoTypes from '../src/zspec/zdo/definition/tstypes'; + +export const NODE_DESC_DEFAULTS = { + // nwkAddress: NodeId; + /** 000 == Zigbee Coordinator, 001 == Zigbee Router, 010 === Zigbee End Device, 011-111 === Reserved */ + // logicalType: number; + fragmentationSupported: undefined, + apsFlags: 0, + frequencyBand: 3, + capabilities: Zdo.Utils.getMacCapFlags(0x8e), + // manufacturerCode: number; + maxBufSize: 0xaa, + maxIncTxSize: 0xac, + serverMask: Zdo.Utils.getServerMask(0x2c43), + maxOutTxSize: 0xdc, + deprecated1: 0, + tlvs: [], +}; +export const LQI_TABLE_ENTRY_DEFAULTS = { + extendedPanId: [1, 2, 3, 4, 5, 6, 7, 8], + // eui64: EUI64; + // nwkAddress: NodeId; + /** + * The type of the neighbor device: + * 0x00 = ZigBee coordinator + * 0x01 = ZigBee router + * 0x02 = ZigBee end device + * 0x03 = Unknown + * + * 2-bit + */ + deviceType: 0x01, + /** + * 0x00 = Receiver is off + * 0x01 = Receiver is on + * 0x02 = unknown + */ + rxOnWhenIdle: 0x00, + /** + * 0x00 = neighbor is the parent + * 0x01 = neighbor is a child + * 0x02 = neighbor is a sibling + * 0x03 = None of the above + * 0x04 = previous child + */ + // relationship: number; + reserved1: 0, + /** + * 0x00 = neighbor is not accepting join requests + * 0x01 = neighbor is accepting join requests + * 0x02 = unknown + */ + permitJoining: 0x00, + /** Each of these reserved bits shall be set to 0. 6-bit */ + reserved2: 0, + // depth: number; + // lqi: number; +}; +export const ROUTING_TABLE_ENTRY_DEFAULTS = { + // destinationAddress: NodeId; + /** + * Status of the route + * 0x0=ACTIVE. + * 0x1=DISCOVERY_UNDERWAY. + * 0x2=DISCOVERY_FAILED. + * 0x3=INACTIVE. + * 0x4=VALIDATION_UNDERWAY + * 0x5-0x7=RESERVED + * + * 3-bit + */ + // status: keyof typeof RoutingTableStatus | 'UNKNOWN'; + memoryConstrained: 0, + manyToOne: 0, + routeRecordRequired: 0, + reserved1: 0, + // nextHopAddress: number; +}; + +export const DEFAULT_184_CHECKIN_INTERVAL = 50; + +/** + * - undefined => should timeout + * - key => identifier for special behavior + */ +export const MOCK_DEVICES: { + [key: number]: { + nodeDescriptor?: ZdoTypes.ResponseMap[Zdo.ClusterId.NODE_DESCRIPTOR_RESPONSE]; + activeEndpoints?: ZdoTypes.ResponseMap[Zdo.ClusterId.ACTIVE_ENDPOINTS_RESPONSE]; + simpleDescriptor?: {[key: number]: ZdoTypes.ResponseMap[Zdo.ClusterId.SIMPLE_DESCRIPTOR_RESPONSE] | undefined}; + lqiTable?: ZdoTypes.ResponseMap[Zdo.ClusterId.LQI_TABLE_RESPONSE]; + routingTable?: ZdoTypes.ResponseMap[Zdo.ClusterId.ROUTING_TABLE_RESPONSE]; + attributes?: {[key: number]: {[key: string]: unknown}}; + key?: 'xiaomi'; + }; +} = { + // coordinator + 0: { + nodeDescriptor: [Zdo.Status.SUCCESS, {...NODE_DESC_DEFAULTS, nwkAddress: 0, logicalType: 0b000, manufacturerCode: 0x0007}], + activeEndpoints: [Zdo.Status.SUCCESS, {nwkAddress: 0, endpointList: [1, 2]}], + simpleDescriptor: { + 1: [ + Zdo.Status.SUCCESS, + {nwkAddress: 0, length: 14, endpoint: 1, profileId: 2, deviceId: 3, deviceVersion: 1, inClusterList: [10], outClusterList: [11]}, + ], + 2: [ + Zdo.Status.SUCCESS, + {nwkAddress: 0, length: 14, endpoint: 2, profileId: 3, deviceId: 5, deviceVersion: 1, inClusterList: [1], outClusterList: [0]}, + ], + }, + }, + 129: { + nodeDescriptor: [Zdo.Status.SUCCESS, {...NODE_DESC_DEFAULTS, nwkAddress: 129, logicalType: 0b001, manufacturerCode: 1212}], // {type: 'Router', manufacturerCode: 1212}, + activeEndpoints: [Zdo.Status.SUCCESS, {nwkAddress: 129, endpointList: [1]}], // {endpoints: [1]}, + simpleDescriptor: { + 1: [ + Zdo.Status.SUCCESS, + {nwkAddress: 129, length: 14, endpoint: 1, profileId: 99, deviceId: 5, deviceVersion: 1, inClusterList: [0, 1], outClusterList: [2]}, + ], // {1: {endpointID: 1, deviceID: 5, inputClusters: [0, 1], outputClusters: [2], profileID: 99}}, + }, + attributes: { + 1: { + modelId: 'myModelID', + manufacturerName: 'KoenAndCo', + zclVersion: 1, + appVersion: 2, + hwVersion: 3, + dateCode: '201901', + swBuildId: '1.01', + powerSource: 1, + stackVersion: 101, + }, + }, + }, + 140: { + nodeDescriptor: undefined, + lqiTable: [ + Zdo.Status.SUCCESS, + { + neighborTableEntries: 2, + startIndex: 0, + entryList: [ + {...LQI_TABLE_ENTRY_DEFAULTS, eui64: '0x160', nwkAddress: 160, lqi: 20, relationship: 2, depth: 5}, + {...LQI_TABLE_ENTRY_DEFAULTS, eui64: '0x170', nwkAddress: 170, lqi: 21, relationship: 4, depth: 8}, + ], + }, + ], + routingTable: [ + Zdo.Status.SUCCESS, + { + routingTableEntries: 2, + startIndex: 0, + entryList: [ + {...ROUTING_TABLE_ENTRY_DEFAULTS, destinationAddress: 120, status: 'ACTIVE', nextHopAddress: 1}, + {...ROUTING_TABLE_ENTRY_DEFAULTS, destinationAddress: 130, status: 'DISCOVERY_FAILED', nextHopAddress: 2}, + ], + }, + ], + }, + 150: { + nodeDescriptor: undefined, + key: 'xiaomi', + }, + 151: { + nodeDescriptor: [Zdo.Status.SUCCESS, {...NODE_DESC_DEFAULTS, nwkAddress: 151, logicalType: 0b010, manufacturerCode: 1219}], + activeEndpoints: undefined, + key: 'xiaomi', + }, + 160: { + nodeDescriptor: [Zdo.Status.SUCCESS, {...NODE_DESC_DEFAULTS, nwkAddress: 160, logicalType: 0b001, manufacturerCode: 1212}], + activeEndpoints: [Zdo.Status.SUCCESS, {nwkAddress: 160, endpointList: []}], + attributes: {}, + }, + 161: { + // stackComplianceRevision 0 + nodeDescriptor: [ + Zdo.Status.SUCCESS, + {...NODE_DESC_DEFAULTS, nwkAddress: 161, logicalType: 0b001, manufacturerCode: 1213, serverMask: Zdo.Utils.getServerMask(0)}, + ], + activeEndpoints: [Zdo.Status.SUCCESS, {nwkAddress: 161, endpointList: [4, 1]}], + simpleDescriptor: { + 1: [ + Zdo.Status.SUCCESS, + {nwkAddress: 161, length: 14, endpoint: 1, profileId: 99, deviceId: 5, deviceVersion: 1, inClusterList: [0, 1], outClusterList: [2]}, + ], + 4: [ + Zdo.Status.SUCCESS, + {nwkAddress: 161, length: 12, endpoint: 4, profileId: 99, deviceId: 5, deviceVersion: 1, inClusterList: [1], outClusterList: [2]}, + ], + }, + attributes: { + 1: { + modelId: 'myDevice9123', + manufacturerName: 'Boef', + zclVersion: 1, + appVersion: 2, + hwVersion: 3, + dateCode: '201901', + swBuildId: '1.01', + powerSource: 1, + stackVersion: 101, + }, + 4: {}, + }, + }, + 162: { + // stackComplianceRevision 21 + nodeDescriptor: [ + Zdo.Status.SUCCESS, + {...NODE_DESC_DEFAULTS, nwkAddress: 162, logicalType: 0b001, manufacturerCode: 1213, serverMask: Zdo.Utils.getServerMask(0x2a00)}, + ], + activeEndpoints: [Zdo.Status.SUCCESS, {nwkAddress: 162, endpointList: [2, 1]}], + simpleDescriptor: { + 1: [ + Zdo.Status.SUCCESS, + {nwkAddress: 162, length: 12, endpoint: 1, profileId: 99, deviceId: 5, deviceVersion: 1, inClusterList: [1], outClusterList: [2]}, + ], + 2: [ + Zdo.Status.SUCCESS, + {nwkAddress: 162, length: 14, endpoint: 2, profileId: 99, deviceId: 5, deviceVersion: 1, inClusterList: [0, 1], outClusterList: [2]}, + ], + }, + attributes: { + 2: { + modelId: 'myDevice9124', + manufacturerName: 'Boef', + zclVersion: 1, + appVersion: 2, + hwVersion: 3, + dateCode: '201901', + swBuildId: '1.01', + powerSource: 1, + stackVersion: 101, + }, + 1: {}, + }, + }, + 170: { + nodeDescriptor: [Zdo.Status.SUCCESS, {...NODE_DESC_DEFAULTS, nwkAddress: 170, logicalType: 0b010, manufacturerCode: 4619}], + activeEndpoints: [Zdo.Status.SUCCESS, {nwkAddress: 170, endpointList: [1]}], + simpleDescriptor: { + 1: [ + Zdo.Status.SUCCESS, + { + nwkAddress: 170, + length: 14, + endpoint: 1, + profileId: 99, + deviceId: 5, + deviceVersion: 1, + inClusterList: [0, 1280], + outClusterList: [2], + }, + ], + }, + attributes: { + 1: { + zoneState: 0, + iasCieAddr: '0x0000012300000000', + modelId: 'myIasDevice', + manufacturerName: 'KoenAndCoSecurity', + zclVersion: 1, + appVersion: 2, + hwVersion: 3, + dateCode: '201901', + swBuildId: '1.01', + powerSource: 1, + stackVersion: 101, + }, + }, + }, + 171: { + // Xiaomi WXCJKG11LM - https://github.com/koenkk/zigbee2mqtt/issues/2844 + nodeDescriptor: [Zdo.Status.SUCCESS, {...NODE_DESC_DEFAULTS, nwkAddress: 171, logicalType: 0b010, manufacturerCode: 1212}], + activeEndpoints: [Zdo.Status.SUCCESS, {nwkAddress: 171, endpointList: [1, 2, 3, 4, 5, 6]}], + simpleDescriptor: { + 1: [ + Zdo.Status.SUCCESS, + { + nwkAddress: 171, + length: 16, + endpoint: 1, + profileId: 99, + deviceId: 5, + deviceVersion: 1, + inClusterList: [0, 1, 2], + outClusterList: [2], + }, + ], + }, + attributes: { + 1: { + modelId: 'lumi.remote.b286opcn01', + manufacturerName: 'Xioami', + zclVersion: 1, + appVersion: 2, + hwVersion: 3, + dateCode: '201901', + swBuildId: '1.01', + powerSource: 1, + stackVersion: 101, + }, + }, + }, + 172: { + // Gledopto GL-C-007/GL-C-008 - https://github.com/Koenkk/zigbee2mqtt/issues/2872 + // All endpoints announce to support genBasic but only endpoint 12 really responds + nodeDescriptor: [Zdo.Status.SUCCESS, {...NODE_DESC_DEFAULTS, nwkAddress: 172, logicalType: 0b001, manufacturerCode: 1212}], + activeEndpoints: [Zdo.Status.SUCCESS, {nwkAddress: 172, endpointList: [12, 11, 13]}], + simpleDescriptor: { + 11: [ + Zdo.Status.SUCCESS, + { + nwkAddress: 172, + length: 26, + endpoint: 11, + profileId: 99, + deviceId: 0x0210, + deviceVersion: 1, + inClusterList: [0, 3, 4, 5, 6, 8, 768, 62301], + outClusterList: [2], + }, + ], + 12: [ + Zdo.Status.SUCCESS, + { + nwkAddress: 172, + length: 24, + endpoint: 12, + profileId: 99, + deviceId: 0xe15e, + deviceVersion: 1, + inClusterList: [0, 3, 4, 5, 6, 8, 768], + outClusterList: [2], + }, + ], + 13: [ + Zdo.Status.SUCCESS, + { + nwkAddress: 172, + length: 24, + endpoint: 13, + profileId: 99, + deviceId: 0x0100, + deviceVersion: 1, + inClusterList: [0, 3, 4, 5, 6, 8, 768], + outClusterList: [2], + }, + ], + }, + attributes: { + 12: { + modelId: 'GL-C-008', + manufacturerName: 'Gledopto', + zclVersion: 1, + appVersion: 2, + hwVersion: 3, + dateCode: '201901', + swBuildId: '1.01', + powerSource: 1, + stackVersion: 101, + }, + }, + }, + 173: { + nodeDescriptor: [Zdo.Status.SUCCESS, {...NODE_DESC_DEFAULTS, nwkAddress: 173, logicalType: 0b010, manufacturerCode: 0}], + activeEndpoints: [Zdo.Status.SUCCESS, {nwkAddress: 173, endpointList: [1]}], + simpleDescriptor: undefined, + attributes: {}, + }, + 174: { + nodeDescriptor: [Zdo.Status.SUCCESS, {...NODE_DESC_DEFAULTS, nwkAddress: 174, logicalType: 0b010, manufacturerCode: 1213}], + activeEndpoints: [Zdo.Status.SUCCESS, {nwkAddress: 174, endpointList: [1]}], + simpleDescriptor: { + 1: [ + Zdo.Status.SUCCESS, + {nwkAddress: 174, length: 14, endpoint: 1, profileId: 99, deviceId: 5, deviceVersion: 1, inClusterList: [0, 32], outClusterList: [2]}, + ], + }, + attributes: { + 1: {checkinInterval: DEFAULT_184_CHECKIN_INTERVAL}, + }, + }, + 175: { + nodeDescriptor: [Zdo.Status.SUCCESS, {...NODE_DESC_DEFAULTS, nwkAddress: 175, logicalType: 0b001, manufacturerCode: 1212}], + activeEndpoints: [Zdo.Status.SUCCESS, {nwkAddress: 175, endpointList: [1, 2, 3, 4, 5, 6]}], + simpleDescriptor: { + 1: [ + Zdo.Status.SUCCESS, + { + nwkAddress: 175, + length: 16, + endpoint: 1, + profileId: 99, + deviceId: 5, + deviceVersion: 1, + inClusterList: [0, 1, 2], + outClusterList: [2], + }, + ], + }, + attributes: { + 1: { + modelId: 'lumi.plug', + manufacturerName: 'LUMI', + zclVersion: 1, + appVersion: 2, + hwVersion: 3, + dateCode: '201901', + swBuildId: '1.01', + powerSource: 1, + stackVersion: 101, + }, + }, + }, + 176: { + nodeDescriptor: [Zdo.Status.SUCCESS, {...NODE_DESC_DEFAULTS, nwkAddress: 176, logicalType: 0b001, manufacturerCode: 1212}], + activeEndpoints: [Zdo.Status.SUCCESS, {nwkAddress: 176, endpointList: [1, 2, 3, 4, 5, 6]}], + simpleDescriptor: { + 1: [ + Zdo.Status.SUCCESS, + {nwkAddress: 176, length: 14, endpoint: 1, profileId: 99, deviceId: 5, deviceVersion: 1, inClusterList: [1, 2], outClusterList: [2]}, + ], + }, + attributes: { + 1: { + modelId: 'lumi.plug', + manufacturerName: 'LUMI', + zclVersion: 1, + appVersion: 2, + hwVersion: 3, + dateCode: '201901', + swBuildId: '1.01', + powerSource: 1, + stackVersion: 101, + }, + }, + }, + 177: { + nodeDescriptor: [Zdo.Status.SUCCESS, {...NODE_DESC_DEFAULTS, nwkAddress: 177, logicalType: 0b001, manufacturerCode: 4129}], + activeEndpoints: [Zdo.Status.SUCCESS, {nwkAddress: 177, endpointList: [1]}], + simpleDescriptor: { + 1: [ + Zdo.Status.SUCCESS, + { + nwkAddress: 177, + length: 32, + endpoint: 1, + profileId: 260, + deviceId: 514, + deviceVersion: 1, + inClusterList: [0, 3, 258, 4, 5, 15, 64513], + outClusterList: [258, 0, 64513, 5, 25], + }, + ], + }, + attributes: { + 1: { + modelId: ' Shutter switch with neutral', + manufacturerName: 'Legrand', + zclVersion: 2, + appVersion: 0, + hwVersion: 8, + dateCode: '231030', + swBuildId: '0038', + powerSource: 1, + stackVersion: 67, + }, + }, + }, +};