diff --git a/src/adapter/adapter.ts b/src/adapter/adapter.ts index 9bea1b551d..e5484384ca 100644 --- a/src/adapter/adapter.ts +++ b/src/adapter/adapter.ts @@ -1,7 +1,7 @@ import * as TsType from './tstype'; import {ZclPayload} from './events'; import events from 'events'; -import {ZclFrame, FrameType, Direction} from '../zcl'; +import * as Zcl from '../zspec/zcl'; import * as Models from "../models"; import Bonjour, {Service} from 'bonjour-service'; import {logger} from '../utils/logger'; @@ -175,7 +175,7 @@ abstract class Adapter extends events.EventEmitter { public abstract addInstallCode(ieeeAddress: string, key: Buffer): Promise; public abstract waitFor( - networkAddress: number, endpoint: number, frameType: FrameType, direction: Direction, + networkAddress: number, endpoint: number, frameType: Zcl.FrameType, direction: Zcl.Direction, transactionSequenceNumber: number, clusterID: number, commandIdentifier: number, timeout: number, ): {promise: Promise; cancel: () => void}; @@ -214,13 +214,13 @@ abstract class Adapter extends events.EventEmitter { */ public abstract sendZclFrameToEndpoint( - ieeeAddr: string, networkAddress: number, endpoint: number, zclFrame: ZclFrame, timeout: number, + ieeeAddr: string, networkAddress: number, endpoint: number, zclFrame: Zcl.Frame, timeout: number, disableResponse: boolean, disableRecovery: boolean, sourceEndpoint?: number, ): Promise; - public abstract sendZclFrameToGroup(groupID: number, zclFrame: ZclFrame, sourceEndpoint?: number): Promise; + public abstract sendZclFrameToGroup(groupID: number, zclFrame: Zcl.Frame, sourceEndpoint?: number): Promise; - public abstract sendZclFrameToAll(endpoint: number, zclFrame: ZclFrame, sourceEndpoint: number, destination: BroadcastAddress): Promise; + public abstract sendZclFrameToAll(endpoint: number, zclFrame: Zcl.Frame, sourceEndpoint: number, destination: BroadcastAddress): Promise; /** * InterPAN @@ -228,10 +228,10 @@ abstract class Adapter extends events.EventEmitter { public abstract setChannelInterPAN(channel: number): Promise; - public abstract sendZclFrameInterPANToIeeeAddr(zclFrame: ZclFrame, ieeeAddress: string): Promise; + public abstract sendZclFrameInterPANToIeeeAddr(zclFrame: Zcl.Frame, ieeeAddress: string): Promise; public abstract sendZclFrameInterPANBroadcast( - zclFrame: ZclFrame, timeout: number + zclFrame: Zcl.Frame, timeout: number ): Promise; public abstract restoreChannelInterPAN(): Promise; diff --git a/src/adapter/deconz/adapter/deconzAdapter.ts b/src/adapter/deconz/adapter/deconzAdapter.ts index 9e42241e47..8bb57ff094 100644 --- a/src/adapter/deconz/adapter/deconzAdapter.ts +++ b/src/adapter/deconz/adapter/deconzAdapter.ts @@ -7,7 +7,7 @@ import { } from '../../tstype'; import Adapter from '../../adapter'; import Driver from '../driver/driver'; -import {ZclFrame, FrameType, Direction, ZclHeader} from '../../../zcl'; +import * as Zcl from '../../../zspec/zcl'; import * as Events from '../../events'; import processFrame from '../driver/frameParser'; import {Queue, Waitress, Wait} from '../../../utils'; @@ -24,7 +24,7 @@ interface WaitressMatcher { address: number | string; endpoint: number; transactionSequenceNumber?: number; - frameType: FrameType; + frameType: Zcl.FrameType; clusterID: number; commandIdentifier: number; direction: number; @@ -580,7 +580,7 @@ class DeconzAdapter extends Adapter { } public waitFor( - networkAddress: number, endpoint: number, frameType: FrameType, direction: Direction, + networkAddress: number, endpoint: number, frameType: Zcl.FrameType, direction: Zcl.Direction, transactionSequenceNumber: number, clusterID: number, commandIdentifier: number, timeout: number, ): {promise: Promise; cancel: () => void} { const payload = { @@ -593,7 +593,7 @@ class DeconzAdapter extends Adapter { } public async sendZclFrameToEndpoint( - ieeeAddr: string, networkAddress: number, endpoint: number, zclFrame: ZclFrame, timeout: number, + ieeeAddr: string, networkAddress: number, endpoint: number, zclFrame: Zcl.Frame, timeout: number, disableResponse: boolean, disableRecovery: boolean, sourceEndpoint?: number, ): Promise { @@ -646,7 +646,7 @@ class DeconzAdapter extends Adapter { address: (data.srcAddrMode === 0x02) ? data.srcAddr16 : null, data: buffer, clusterID: zclFrame.cluster.ID, - header: ZclHeader.fromBuffer(buffer), + header: Zcl.Header.fromBuffer(buffer), endpoint: data.srcEndpoint, linkquality: data.lqi, groupID: (data.srcAddrMode === 0x01) ? data.srcAddr16 : null, @@ -665,7 +665,7 @@ class DeconzAdapter extends Adapter { } } - public async sendZclFrameToGroup(groupID: number, zclFrame: ZclFrame): Promise { + public async sendZclFrameToGroup(groupID: number, zclFrame: Zcl.Frame): Promise { const transactionID = this.nextTransactionID(); const request: ApsDataRequest = {}; let pay = zclFrame.toBuffer(); @@ -695,7 +695,7 @@ class DeconzAdapter extends Adapter { } } - public async sendZclFrameToAll(endpoint: number, zclFrame: ZclFrame, sourceEndpoint: number, destination: BroadcastAddress): Promise { + public async sendZclFrameToAll(endpoint: number, zclFrame: Zcl.Frame, sourceEndpoint: number, destination: BroadcastAddress): Promise { const transactionID = this.nextTransactionID(); const request: ApsDataRequest = {}; let pay = zclFrame.toBuffer(); @@ -1020,18 +1020,18 @@ class DeconzAdapter extends Adapter { throw new Error("not supported"); } - public async sendZclFrameInterPANToIeeeAddr(zclFrame: ZclFrame, ieeeAddr: string): Promise { + public async sendZclFrameInterPANToIeeeAddr(zclFrame: Zcl.Frame, ieeeAddr: string): Promise { throw new Error("not supported"); } public async sendZclFrameInterPANBroadcast( - zclFrame: ZclFrame, timeout: number + zclFrame: Zcl.Frame, timeout: number ): Promise { throw new Error("not supported"); } public async sendZclFrameInterPANBroadcastWithResponse( - zclFrame: ZclFrame, timeout: number + zclFrame: Zcl.Frame, timeout: number ): Promise { throw new Error("not supported"); } @@ -1052,7 +1052,7 @@ class DeconzAdapter extends Adapter { throw new Error("not supported"); } - public async sendZclFrameInterPANIeeeAddr(zclFrame: ZclFrame, ieeeAddr: any): Promise { + public async sendZclFrameInterPANIeeeAddr(zclFrame: Zcl.Frame, ieeeAddr: any): Promise { throw new Error("not supported"); } @@ -1084,7 +1084,7 @@ class DeconzAdapter extends Adapter { const payBuf = Buffer.from(gpFrame); const payload: Events.ZclPayload = { - header: ZclHeader.fromBuffer(payBuf), + header: Zcl.Header.fromBuffer(payBuf), data: payBuf, clusterID: ind.clusterId, address: ind.srcId, @@ -1101,13 +1101,13 @@ class DeconzAdapter extends Adapter { private checkReceivedDataPayload(resp: ReceivedDataResponse) { let srcAddr: any = null; - let header: ZclHeader = null; + let header: Zcl.Header = null; const payBuf = resp != null ? Buffer.from(resp.asduPayload) : null; if (resp != null) { srcAddr = (resp.srcAddr16 != null) ? resp.srcAddr16 : resp.srcAddr64; if (resp.profileId != 0x00) { - header = ZclHeader.fromBuffer(payBuf); + header = Zcl.Header.fromBuffer(payBuf); } } diff --git a/src/adapter/deconz/driver/frameParser.ts b/src/adapter/deconz/driver/frameParser.ts index 5edf5b21b3..3da7fb4268 100644 --- a/src/adapter/deconz/driver/frameParser.ts +++ b/src/adapter/deconz/driver/frameParser.ts @@ -6,7 +6,6 @@ import PARAM from './constants'; import { busyQueue, apsBusyQueue, readyToSend, enableRTS, disableRTS, enableRtsTimeout } from './driver'; import { Request, ReceivedDataResponse, DataStateResponse, Command, ParamMac, ParamPanId, ParamNwkAddr, ParamExtPanId, ParamChannel, ParamChannelMask, ParamPermitJoin, ParamNetworkKey, gpDataInd } from './constants'; import * as Events from '../../events'; -import {ZclFrame} from '../../../zcl'; import {logger} from '../../../utils/logger'; const NS = 'zh:deconz:frameparser'; diff --git a/src/adapter/ember/adapter/emberAdapter.ts b/src/adapter/ember/adapter/emberAdapter.ts index 400c63142e..20bacb6b67 100644 --- a/src/adapter/ember/adapter/emberAdapter.ts +++ b/src/adapter/ember/adapter/emberAdapter.ts @@ -6,8 +6,7 @@ import SocketPortUtils from '../../socketPortUtils'; import {BackupUtils, RealpathSync, Wait} from "../../../utils"; import {Adapter, TsType} from "../.."; import {Backup, UnifiedBackupStorage} from "../../../models"; -import {FrameType, Direction, ZclFrame, ZclHeader, Foundation, ManufacturerCode} from "../../../zcl"; -import Clusters from "../../../zcl/definition/cluster"; +import * as Zcl from "../../../zspec/zcl"; import { DeviceAnnouncePayload, DeviceJoinedPayload, @@ -391,15 +390,15 @@ const DEFAULT_NETWORK_REQUEST_TIMEOUT = 10000;// nothing on the network to bothe /** Time between watchdog counters reading/clearing */ const WATCHDOG_COUNTERS_FEED_INTERVAL = 3600000;// every hour... /** Default manufacturer code reported by coordinator. */ -const DEFAULT_MANUFACTURER_CODE = ManufacturerCode.SILICON_LABORATORIES; +const DEFAULT_MANUFACTURER_CODE = Zcl.ManufacturerCode.SILICON_LABORATORIES; /** * Workaround for devices that require a specific manufacturer code to be reported by coordinator while interviewing... * - Lumi/Aqara devices do not work properly otherwise (missing features): https://github.com/Koenkk/zigbee2mqtt/issues/9274 */ -const WORKAROUND_JOIN_MANUF_IEEE_PREFIX_TO_CODE: {[ieeePrefix: string]: ManufacturerCode} = { +const WORKAROUND_JOIN_MANUF_IEEE_PREFIX_TO_CODE: {[ieeePrefix: string]: Zcl.ManufacturerCode} = { // NOTE: Lumi has a new prefix registered since 2021, in case they start using that one with new devices, it might need to be added here too... // "0x18c23c" https://maclookup.app/vendors/lumi-united-technology-co-ltd - "0x54ef44": ManufacturerCode.LUMI_UNITED_TECHOLOGY_LTD_SHENZHEN, + "0x54ef44": Zcl.ManufacturerCode.LUMI_UNITED_TECHOLOGY_LTD_SHENZHEN, }; /** @@ -409,7 +408,7 @@ const WORKAROUND_JOIN_MANUF_IEEE_PREFIX_TO_CODE: {[ieeePrefix: string]: Manufact */ export class EmberAdapter extends Adapter { /** Current manufacturer code assigned to the coordinator. Used for join workarounds... */ - private manufacturerCode: ManufacturerCode; + private manufacturerCode: Zcl.ManufacturerCode; /** Key in STACK_CONFIGS */ public readonly stackConfig: 'default' | 'zigbeed'; /** EMBER_LOW_RAM_CONCENTRATOR or EMBER_HIGH_RAM_CONCENTRATOR. */ @@ -585,7 +584,7 @@ export class EmberAdapter extends Adapter { messageContents: Buffer): Promise { const payload: ZclPayload = { clusterID: apsFrame.clusterId, - header: ZclHeader.fromBuffer(messageContents), + header: Zcl.Header.fromBuffer(messageContents), address: sender, data: messageContents, endpoint: apsFrame.sourceEndpoint, @@ -611,9 +610,9 @@ export class EmberAdapter extends Adapter { private async onTouchlinkMessage(sourcePanId: EmberPanId, sourceAddress: EmberEUI64, groupId: number | null, lastHopLqi: number, messageContents: Buffer): Promise { const payload: ZclPayload = { - clusterID: Clusters.touchlink.ID, + clusterID: Zcl.Clusters.touchlink.ID, data: messageContents, - header: ZclHeader.fromBuffer(messageContents), + header: Zcl.Header.fromBuffer(messageContents), address: sourceAddress, endpoint: 1,// arbitrary since not sent over-the-air linkquality: lastHopLqi, @@ -654,9 +653,9 @@ export class EmberAdapter extends Adapter { const data = Buffer.concat([gpdHeader, gpdCommandPayload]); const payload: ZclPayload = { - header: ZclHeader.fromBuffer(data), + header: Zcl.Header.fromBuffer(data), data, - clusterID: Clusters.greenPower.ID, + clusterID: Zcl.Clusters.greenPower.ID, address: sourceId, endpoint: GP_ENDPOINT, linkquality: gpdLink, @@ -706,7 +705,7 @@ export class EmberAdapter extends Adapter { await new Promise((resolve, reject): void => { this.requestQueue.enqueue( async (): Promise => { - logger.debug(`[WORKAROUND] Setting coordinator manufacturer code to ${ManufacturerCode[joinManufCode]}.`, NS); + logger.debug(`[WORKAROUND] Setting coordinator manufacturer code to ${Zcl.ManufacturerCode[joinManufCode]}.`, NS); await this.ezsp.ezspSetManufacturerCode(joinManufCode); this.manufacturerCode = joinManufCode; @@ -2954,7 +2953,7 @@ export class EmberAdapter extends Adapter { } /** WARNING: Adapter impl. Starts timer immediately upon returning */ - public waitFor(networkAddress: number, endpoint: number, frameType: FrameType, direction: Direction, transactionSequenceNumber: number, + public waitFor(networkAddress: number, endpoint: number, frameType: Zcl.FrameType, direction: Zcl.Direction, transactionSequenceNumber: number, clusterID: number, commandIdentifier: number, timeout: number): {promise: Promise; cancel: () => void;} { const sourceEndpointInfo = FIXED_ENDPOINTS[0]; const waiter = this.oneWaitress.waitFor({ @@ -3566,7 +3565,7 @@ export class EmberAdapter extends Adapter { //---- ZCL // queued, non-InterPAN - public async sendZclFrameToEndpoint(ieeeAddr: string, networkAddress: number, endpoint: number, zclFrame: ZclFrame, timeout: number, + public async sendZclFrameToEndpoint(ieeeAddr: string, networkAddress: number, endpoint: number, zclFrame: Zcl.Frame, timeout: number, disableResponse: boolean, disableRecovery: boolean, sourceEndpoint?: number): Promise { const sourceEndpointInfo = typeof sourceEndpoint === 'number' ? FIXED_ENDPOINTS.find((epi) => (epi.endpoint === sourceEndpoint)) : FIXED_ENDPOINTS[0]; @@ -3576,7 +3575,7 @@ export class EmberAdapter extends Adapter { if (command.hasOwnProperty('response') && disableResponse === false) { commandResponseId = command.response; } else if (!zclFrame.header.frameControl.disableDefaultResponse) { - commandResponseId = Foundation.defaultRsp.ID; + commandResponseId = Zcl.Foundation.defaultRsp.ID; } const apsFrame: EmberApsFrame = { @@ -3651,7 +3650,7 @@ export class EmberAdapter extends Adapter { } // queued, non-InterPAN - public async sendZclFrameToGroup(groupID: number, zclFrame: ZclFrame, sourceEndpoint?: number): Promise { + public async sendZclFrameToGroup(groupID: number, zclFrame: Zcl.Frame, sourceEndpoint?: number): Promise { const sourceEndpointInfo = typeof sourceEndpoint === 'number' ? FIXED_ENDPOINTS.find((epi) => (epi.endpoint === sourceEndpoint)) : FIXED_ENDPOINTS[0]; const apsFrame: EmberApsFrame = { @@ -3707,7 +3706,7 @@ export class EmberAdapter extends Adapter { } // queued, non-InterPAN - public async sendZclFrameToAll(endpoint: number, zclFrame: ZclFrame, sourceEndpoint: number, destination: BroadcastAddress): Promise { + public async sendZclFrameToAll(endpoint: number, zclFrame: Zcl.Frame, sourceEndpoint: number, destination: BroadcastAddress): Promise { const sourceEndpointInfo = typeof sourceEndpoint === 'number' ? FIXED_ENDPOINTS.find((epi) => (epi.endpoint === sourceEndpoint)) : FIXED_ENDPOINTS[0]; const apsFrame: EmberApsFrame = { @@ -3794,7 +3793,7 @@ export class EmberAdapter extends Adapter { } // queued - public async sendZclFrameInterPANToIeeeAddr(zclFrame: ZclFrame, ieeeAddress: string): Promise { + public async sendZclFrameInterPANToIeeeAddr(zclFrame: Zcl.Frame, ieeeAddress: string): Promise { return new Promise((resolve, reject): void => { this.requestQueue.enqueue( async (): Promise => { @@ -3834,7 +3833,7 @@ export class EmberAdapter extends Adapter { } // queued - public async sendZclFrameInterPANBroadcast(zclFrame: ZclFrame, timeout: number): Promise { + public async sendZclFrameInterPANBroadcast(zclFrame: Zcl.Frame, timeout: number): Promise { const command = zclFrame.command; if (!command.hasOwnProperty('response')) { diff --git a/src/adapter/ember/adapter/endpoints.ts b/src/adapter/ember/adapter/endpoints.ts index d1f78515e7..e60f37c13e 100644 --- a/src/adapter/ember/adapter/endpoints.ts +++ b/src/adapter/ember/adapter/endpoints.ts @@ -1,4 +1,4 @@ -import Clusters from '../../../zcl/definition/cluster'; +import {Clusters} from '../../../zspec/zcl/definition/cluster'; import {GP_ENDPOINT, GP_PROFILE_ID, HA_PROFILE_ID} from '../consts'; import {ClusterId, EmberMulticastId, ProfileId} from '../types'; diff --git a/src/adapter/ember/ezsp/ezsp.ts b/src/adapter/ember/ezsp/ezsp.ts index 7af4b0610f..09e9143c4f 100644 --- a/src/adapter/ember/ezsp/ezsp.ts +++ b/src/adapter/ember/ezsp/ezsp.ts @@ -1,7 +1,7 @@ /* istanbul ignore file */ import EventEmitter from "events"; import {SerialPortOptions} from "../../tstype"; -import Clusters from "../../../zcl/definition/cluster"; +import {Clusters} from "../../../zspec/zcl/definition/cluster"; import {byteToBits, getMacCapFlags, highByte, highLowToInt, lowByte, lowHighBits} from "../utils/math"; import { EmberOutgoingMessageType, diff --git a/src/adapter/events.ts b/src/adapter/events.ts index 5fa22117d1..2a235588ae 100644 --- a/src/adapter/events.ts +++ b/src/adapter/events.ts @@ -1,4 +1,4 @@ -import {ZclHeader} from '../zcl'; +import {Header as ZclHeader} from '../zspec/zcl'; enum Events { networkAddress = "networkAddress", @@ -36,7 +36,7 @@ interface ZclPayload { clusterID: number; address: number | string; header: ZclHeader | undefined; - // This buffer contains the whole ZclFrame (including the ZclHeader) + // This buffer contains the whole Zcl.Frame (including the ZclHeader) data: Buffer; endpoint: number; linkquality: number; diff --git a/src/adapter/ezsp/adapter/ezspAdapter.ts b/src/adapter/ezsp/adapter/ezspAdapter.ts index aa72ade2eb..aa708f76cd 100644 --- a/src/adapter/ezsp/adapter/ezspAdapter.ts +++ b/src/adapter/ezsp/adapter/ezspAdapter.ts @@ -8,7 +8,7 @@ import Adapter from '../../adapter'; import {Driver, EmberIncomingMessage} from '../driver'; import {EmberZDOCmd, uint16_t, EmberEUI64, EmberStatus} from '../driver/types'; -import {ZclFrame, FrameType, Direction, Foundation, ZclHeader} from '../../../zcl'; +import * as Zcl from '../../../zspec/zcl'; import * as Events from '../../events'; import {Queue, Waitress, Wait, RealpathSync} from '../../../utils'; import * as Models from "../../../models"; @@ -81,7 +81,7 @@ class EZSPAdapter extends Adapter { } else if (frame.apsFrame.profileId == 260 || frame.apsFrame.profileId == 0xFFFF) { const payload: Events.ZclPayload = { clusterID: frame.apsFrame.clusterId, - header: ZclHeader.fromBuffer(frame.message), + header: Zcl.Header.fromBuffer(frame.message), data: frame.message, address: frame.sender, endpoint: frame.apsFrame.sourceEndpoint, @@ -96,7 +96,7 @@ class EZSPAdapter extends Adapter { } else if (frame.apsFrame.profileId == 0xc05e && frame.senderEui64) { // ZLL Frame const payload: Events.ZclPayload = { clusterID: frame.apsFrame.clusterId, - header: ZclHeader.fromBuffer(frame.message), + header: Zcl.Header.fromBuffer(frame.message), data: frame.message, address: `0x${frame.senderEui64.toString()}`, endpoint: 0xFE, @@ -114,7 +114,7 @@ class EZSPAdapter extends Adapter { // https://github.com/Koenkk/zigbee2mqtt/issues/20838 if (frame.apsFrame.clusterId === 33) { const payload: Events.ZclPayload = { - header: ZclHeader.fromBuffer(frame.message), + header: Zcl.Header.fromBuffer(frame.message), clusterID: frame.apsFrame.clusterId, data: frame.message, address: frame.sender, @@ -428,7 +428,7 @@ class EZSPAdapter extends Adapter { } public async sendZclFrameToEndpoint( - ieeeAddr: string, networkAddress: number, endpoint: number, zclFrame: ZclFrame, timeout: number, + ieeeAddr: string, networkAddress: number, endpoint: number, zclFrame: Zcl.Frame, timeout: number, disableResponse: boolean, disableRecovery: boolean, sourceEndpoint?: number, ): Promise { return this.queue.execute(async () => { @@ -441,7 +441,7 @@ class EZSPAdapter extends Adapter { } private async sendZclFrameToEndpointInternal( - ieeeAddr: string, networkAddress: number, endpoint: number, sourceEndpoint: number, zclFrame: ZclFrame, + ieeeAddr: string, networkAddress: number, endpoint: number, sourceEndpoint: number, zclFrame: Zcl.Frame, timeout: number, disableResponse: boolean, disableRecovery: boolean, responseAttempt: number, dataRequestAttempt: number, checkedNetworkAddress: boolean, discoveredRoute: boolean, assocRemove: boolean, assocRestore: { ieeeadr: string, nwkaddr: number, noderelation: number } @@ -461,7 +461,7 @@ class EZSPAdapter extends Adapter { } else if (!zclFrame.header.frameControl.disableDefaultResponse) { response = this.waitForInternal( networkAddress, endpoint, - zclFrame.header.transactionSequenceNumber, zclFrame.cluster.ID, Foundation.defaultRsp.ID, + zclFrame.header.transactionSequenceNumber, zclFrame.cluster.ID, Zcl.Foundation.defaultRsp.ID, timeout, ); } @@ -501,7 +501,7 @@ class EZSPAdapter extends Adapter { } } - public async sendZclFrameToGroup(groupID: number, zclFrame: ZclFrame): Promise { + public async sendZclFrameToGroup(groupID: number, zclFrame: Zcl.Frame): Promise { return this.queue.execute(async () => { this.checkInterpanLock(); const frame = this.driver.makeApsFrame(zclFrame.cluster.ID, false); @@ -520,7 +520,7 @@ class EZSPAdapter extends Adapter { }); } - public async sendZclFrameToAll(endpoint: number, zclFrame: ZclFrame, sourceEndpoint: number, destination: BroadcastAddress): Promise { + public async sendZclFrameToAll(endpoint: number, zclFrame: Zcl.Frame, sourceEndpoint: number, destination: BroadcastAddress): Promise { return this.queue.execute(async () => { this.checkInterpanLock(); const frame = this.driver.makeApsFrame(zclFrame.cluster.ID, false); @@ -648,7 +648,7 @@ class EZSPAdapter extends Adapter { } } - public async sendZclFrameInterPANToIeeeAddr(zclFrame: ZclFrame, ieeeAddr: string): Promise { + public async sendZclFrameInterPANToIeeeAddr(zclFrame: Zcl.Frame, ieeeAddr: string): Promise { return this.queue.execute(async () => { logger.debug(`sendZclFrameInterPANToIeeeAddr to ${ieeeAddr}`, NS); try { @@ -670,7 +670,7 @@ class EZSPAdapter extends Adapter { }); } - public async sendZclFrameInterPANBroadcast(zclFrame: ZclFrame, timeout: number): Promise { + public async sendZclFrameInterPANBroadcast(zclFrame: Zcl.Frame, timeout: number): Promise { return this.queue.execute(async () => { logger.debug(`sendZclFrameInterPANBroadcast`, NS); const command = zclFrame.command; @@ -741,7 +741,7 @@ class EZSPAdapter extends Adapter { } public waitFor( - networkAddress: number, endpoint: number, frameType: FrameType, direction: Direction, + networkAddress: number, endpoint: number, frameType: Zcl.FrameType, direction: Zcl.Direction, transactionSequenceNumber: number, clusterID: number, commandIdentifier: number, timeout: number, ): { promise: Promise; cancel: () => void } { const waiter = this.waitForInternal( diff --git a/src/adapter/ezsp/driver/driver.ts b/src/adapter/ezsp/driver/driver.ts index c80c338a58..8f54464e2b 100644 --- a/src/adapter/ezsp/driver/driver.ts +++ b/src/adapter/ezsp/driver/driver.ts @@ -28,7 +28,7 @@ import equals from 'fast-deep-equal/es6'; import {ParamsDesc} from './commands'; import {EZSPAdapterBackup} from '../adapter/backup'; import {logger} from '../../../utils/logger'; -import Clusters from '../../../zcl/definition/cluster'; +import {Clusters} from '../../../zspec/zcl/definition/cluster'; const NS = 'zh:ezsp:driv'; diff --git a/src/adapter/z-stack/adapter/endpoints.ts b/src/adapter/z-stack/adapter/endpoints.ts index 8b957883c6..0c8c97e844 100644 --- a/src/adapter/z-stack/adapter/endpoints.ts +++ b/src/adapter/z-stack/adapter/endpoints.ts @@ -1,5 +1,5 @@ import * as Constants from '../constants'; -import {Clusters} from '../../../zcl/index'; +import {Clusters} from '../../../zspec/zcl/definition/cluster'; const EndpointDefaults: { appdeviceid: number; diff --git a/src/adapter/z-stack/adapter/zStackAdapter.ts b/src/adapter/z-stack/adapter/zStackAdapter.ts index 39c0452546..163272f97b 100644 --- a/src/adapter/z-stack/adapter/zStackAdapter.ts +++ b/src/adapter/z-stack/adapter/zStackAdapter.ts @@ -8,7 +8,7 @@ import * as Events from '../../events'; import Adapter from '../../adapter'; import {Znp, ZpiObject} from '../znp'; import {Constants as UnpiConstants} from '../unpi'; -import {ZclFrame, FrameType, Direction, Foundation, ZclHeader} from '../../../zcl'; +import * as Zcl from '../../../zspec/zcl'; import {Queue, Waitress, Wait} from '../../../utils'; import * as Constants from '../constants'; import debounce from 'debounce'; @@ -38,7 +38,7 @@ interface WaitressMatcher { address: number | string; endpoint: number; transactionSequenceNumber?: number; - frameType: FrameType; + frameType: Zcl.FrameType; clusterID: number; commandIdentifier: number; direction: number; @@ -346,7 +346,7 @@ class ZStackAdapter extends Adapter { } public async sendZclFrameToEndpoint( - ieeeAddr: string, networkAddress: number, endpoint: number, zclFrame: ZclFrame, timeout: number, + ieeeAddr: string, networkAddress: number, endpoint: number, zclFrame: Zcl.Frame, timeout: number, disableResponse: boolean, disableRecovery: boolean, sourceEndpoint?: number, ): Promise { return this.queue.execute(async () => { @@ -359,7 +359,7 @@ class ZStackAdapter extends Adapter { } private async sendZclFrameToEndpointInternal( - ieeeAddr: string, networkAddress: number, endpoint: number, sourceEndpoint: number, zclFrame: ZclFrame, + ieeeAddr: string, networkAddress: number, endpoint: number, sourceEndpoint: number, zclFrame: Zcl.Frame, timeout: number, disableResponse: boolean, disableRecovery: boolean, responseAttempt: number, dataRequestAttempt: number, checkedNetworkAddress: boolean, discoveredRoute: boolean, assocRemove: boolean, assocRestore: {ieeeadr: string, nwkaddr: number, noderelation: number} @@ -370,13 +370,13 @@ class ZStackAdapter extends Adapter { const command = zclFrame.command; if (command.hasOwnProperty('response') && disableResponse === false) { response = this.waitForInternal( - networkAddress, endpoint, zclFrame.header.frameControl.frameType, Direction.SERVER_TO_CLIENT, + networkAddress, endpoint, zclFrame.header.frameControl.frameType, Zcl.Direction.SERVER_TO_CLIENT, zclFrame.header.transactionSequenceNumber, zclFrame.cluster.ID, command.response, timeout ); } else if (!zclFrame.header.frameControl.disableDefaultResponse) { response = this.waitForInternal( - networkAddress, endpoint, FrameType.GLOBAL, Direction.SERVER_TO_CLIENT, - zclFrame.header.transactionSequenceNumber, zclFrame.cluster.ID, Foundation.defaultRsp.ID, + networkAddress, endpoint, Zcl.FrameType.GLOBAL, Zcl.Direction.SERVER_TO_CLIENT, + zclFrame.header.transactionSequenceNumber, zclFrame.cluster.ID, Zcl.Foundation.defaultRsp.ID, timeout, ); } @@ -533,7 +533,7 @@ class ZStackAdapter extends Adapter { } } - public async sendZclFrameToGroup(groupID: number, zclFrame: ZclFrame, sourceEndpoint?: number): Promise { + public async sendZclFrameToGroup(groupID: number, zclFrame: Zcl.Frame, sourceEndpoint?: number): Promise { return this.queue.execute(async () => { this.checkInterpanLock(); await this.dataRequestExtended( @@ -550,7 +550,7 @@ class ZStackAdapter extends Adapter { }); } - public async sendZclFrameToAll(endpoint: number, zclFrame: ZclFrame, sourceEndpoint: number, destination: BroadcastAddress): Promise { + public async sendZclFrameToAll(endpoint: number, zclFrame: Zcl.Frame, sourceEndpoint: number, destination: BroadcastAddress): Promise { return this.queue.execute(async () => { this.checkInterpanLock(); await this.dataRequestExtended( @@ -832,7 +832,7 @@ class ZStackAdapter extends Adapter { const payload: Events.ZclPayload = { clusterID: object.payload.clusterid, data: object.payload.data, - header: ZclHeader.fromBuffer(object.payload.data), + header: Zcl.Header.fromBuffer(object.payload.data), address: object.payload.srcaddr, endpoint: object.payload.srcendpoint, linkquality: object.payload.linkquality, @@ -877,7 +877,7 @@ class ZStackAdapter extends Adapter { }); } - public async sendZclFrameInterPANToIeeeAddr(zclFrame: ZclFrame, ieeeAddr: string): Promise { + public async sendZclFrameInterPANToIeeeAddr(zclFrame: Zcl.Frame, ieeeAddr: string): Promise { return this.queue.execute(async () => { await this.dataRequestExtended( AddressMode.ADDR_64BIT, ieeeAddr, 0xFE, 0xFFFF, @@ -886,7 +886,7 @@ class ZStackAdapter extends Adapter { }); } - public async sendZclFrameInterPANBroadcast(zclFrame: ZclFrame, timeout: number): Promise { + public async sendZclFrameInterPANBroadcast(zclFrame: Zcl.Frame, timeout: number): Promise { return this.queue.execute(async () => { const command = zclFrame.command; if (!command.hasOwnProperty('response')) { @@ -894,7 +894,7 @@ class ZStackAdapter extends Adapter { } const response = this.waitForInternal( - null, 0xFE, zclFrame.header.frameControl.frameType, Direction.SERVER_TO_CLIENT, null, + null, 0xFE, zclFrame.header.frameControl.frameType, Zcl.Direction.SERVER_TO_CLIENT, null, zclFrame.cluster.ID, command.response, timeout ); @@ -951,7 +951,7 @@ class ZStackAdapter extends Adapter { } private waitForInternal( - networkAddress: number, endpoint: number, frameType: FrameType, direction: Direction, + networkAddress: number, endpoint: number, frameType: Zcl.FrameType, direction: Zcl.Direction, transactionSequenceNumber: number, clusterID: number, commandIdentifier: number, timeout: number, ): {start: () => {promise: Promise}; cancel: () => void} { const payload = { @@ -965,7 +965,7 @@ class ZStackAdapter extends Adapter { } public waitFor( - networkAddress: number, endpoint: number, frameType: FrameType, direction: Direction, + networkAddress: number, endpoint: number, frameType: Zcl.FrameType, direction: Zcl.Direction, transactionSequenceNumber: number, clusterID: number, commandIdentifier: number, timeout: number, ): {promise: Promise; cancel: () => void} { const waiter = this.waitForInternal( diff --git a/src/adapter/zigate/adapter/zigateAdapter.ts b/src/adapter/zigate/adapter/zigateAdapter.ts index 35afad188b..cec95fa3c0 100644 --- a/src/adapter/zigate/adapter/zigateAdapter.ts +++ b/src/adapter/zigate/adapter/zigateAdapter.ts @@ -4,7 +4,7 @@ import * as TsType from '../../tstype'; import {ActiveEndpoints, DeviceType, LQI, LQINeighbor, NodeDescriptor, SimpleDescriptor} from '../../tstype'; import * as Events from '../../events'; import Adapter from '../../adapter'; -import {Direction, Foundation, FrameType, ZclFrame, ZclHeader} from '../../../zcl'; +import * as Zcl from '../../../zspec/zcl'; import {Queue, Wait, Waitress} from '../../../utils'; import Driver from '../driver/zigate'; import { @@ -28,7 +28,7 @@ interface WaitressMatcher { address: number | string; endpoint: number; transactionSequenceNumber?: number; - frameType: FrameType; + frameType: Zcl.FrameType; clusterID: number; commandIdentifier: number; direction: number; @@ -507,7 +507,7 @@ class ZiGateAdapter extends Adapter { }; public async sendZclFrameToEndpoint( - ieeeAddr: string, networkAddress: number, endpoint: number, zclFrame: ZclFrame, timeout: number, + ieeeAddr: string, networkAddress: number, endpoint: number, zclFrame: Zcl.Frame, timeout: number, disableResponse: boolean, disableRecovery: boolean, sourceEndpoint?: number, ): Promise { return this.queue.execute(async () => { @@ -519,7 +519,7 @@ class ZiGateAdapter extends Adapter { }; private async sendZclFrameToEndpointInternal( - ieeeAddr: string, networkAddress: number, endpoint: number, sourceEndpoint: number, zclFrame: ZclFrame, timeout: number, + ieeeAddr: string, networkAddress: number, endpoint: number, sourceEndpoint: number, zclFrame: Zcl.Frame, timeout: number, disableResponse: boolean, disableRecovery: boolean, responseAttempt: number, dataRequestAttempt: number, checkedNetworkAddress: boolean, discoveredRoute: boolean, ): Promise { @@ -543,13 +543,13 @@ class ZiGateAdapter extends Adapter { if (command.hasOwnProperty('response') && disableResponse === false) { response = this.waitFor( - networkAddress, endpoint, zclFrame.header.frameControl.frameType, Direction.SERVER_TO_CLIENT, + networkAddress, endpoint, zclFrame.header.frameControl.frameType, Zcl.Direction.SERVER_TO_CLIENT, zclFrame.header.transactionSequenceNumber, zclFrame.cluster.ID, command.response, timeout ); } else if (!zclFrame.header.frameControl.disableDefaultResponse) { response = this.waitFor( - networkAddress, endpoint, FrameType.GLOBAL, Direction.SERVER_TO_CLIENT, - zclFrame.header.transactionSequenceNumber, zclFrame.cluster.ID, Foundation.defaultRsp.ID, + networkAddress, endpoint, Zcl.FrameType.GLOBAL, Zcl.Direction.SERVER_TO_CLIENT, + zclFrame.header.transactionSequenceNumber, zclFrame.cluster.ID, Zcl.Foundation.defaultRsp.ID, timeout, ); } @@ -593,7 +593,7 @@ class ZiGateAdapter extends Adapter { } } - public async sendZclFrameToAll(endpoint: number, zclFrame: ZclFrame, sourceEndpoint: number, destination: BroadcastAddress): Promise { + public async sendZclFrameToAll(endpoint: number, zclFrame: Zcl.Frame, sourceEndpoint: number, destination: BroadcastAddress): Promise { return this.queue.execute(async () => { if (sourceEndpoint !== 0x01 /*&& sourceEndpoint !== 242*/) { // @todo on zigate firmware without gp causes hang logger.error(`source endpoint ${sourceEndpoint}, not supported`, NS); @@ -620,7 +620,7 @@ class ZiGateAdapter extends Adapter { }); }; - public async sendZclFrameToGroup(groupID: number, zclFrame: ZclFrame, sourceEndpoint?: number): Promise { + public async sendZclFrameToGroup(groupID: number, zclFrame: Zcl.Frame, sourceEndpoint?: number): Promise { return this.queue.execute(async () => { logger.debug(`sendZclFrameToGroup ${JSON.stringify(arguments)}`, NS); const data = zclFrame.toBuffer(); @@ -680,7 +680,7 @@ class ZiGateAdapter extends Adapter { } public waitFor( - networkAddress: number, endpoint: number, frameType: FrameType, direction: Direction, + networkAddress: number, endpoint: number, frameType: Zcl.FrameType, direction: Zcl.Direction, transactionSequenceNumber: number, clusterID: number, commandIdentifier: number, timeout: number, ): { promise: Promise; cancel: () => void } { logger.debug(`waitForInternal ${JSON.stringify(arguments)}`, NS); @@ -715,13 +715,13 @@ class ZiGateAdapter extends Adapter { return Promise.reject("Not supported"); }; - public async sendZclFrameInterPANToIeeeAddr(zclFrame: ZclFrame, ieeeAddress: string): Promise { + public async sendZclFrameInterPANToIeeeAddr(zclFrame: Zcl.Frame, ieeeAddress: string): Promise { logger.debug(`sendZclFrameInterPANToIeeeAddr ${JSON.stringify(arguments)}`, NS); return Promise.reject("Not supported"); }; public async sendZclFrameInterPANBroadcast( - zclFrame: ZclFrame, timeout: number + zclFrame: Zcl.Frame, timeout: number ): Promise { logger.debug(`sendZclFrameInterPANBroadcast ${JSON.stringify(arguments)}`, NS); return Promise.reject("Not supported"); @@ -748,7 +748,7 @@ class ZiGateAdapter extends Adapter { address: data.ziGateObject.payload.sourceAddress, clusterID: data.ziGateObject.payload.clusterID, data: data.ziGateObject.payload.payload, - header: ZclHeader.fromBuffer(data.ziGateObject.payload.payload), + header: Zcl.Header.fromBuffer(data.ziGateObject.payload.payload), endpoint: data.ziGateObject.payload.sourceEndpoint, linkquality: data.ziGateObject.frame.readRSSI(), groupID: null, // @todo diff --git a/src/adapter/zigate/driver/buffaloZiGate.ts b/src/adapter/zigate/driver/buffaloZiGate.ts index 4728b8b594..7c65bfbeea 100644 --- a/src/adapter/zigate/driver/buffaloZiGate.ts +++ b/src/adapter/zigate/driver/buffaloZiGate.ts @@ -1,7 +1,7 @@ /* istanbul ignore file */ /* eslint-disable */ import {Buffalo} from '../../../buffalo'; -import {BuffaloZclOptions} from '../../../zcl/tstype'; +import {BuffaloZclOptions} from '../../../zspec/zcl/definition/tstype'; import {LOG_LEVEL} from "./constants"; import ParameterType from './parameterType'; diff --git a/src/adapter/zigate/driver/zigate.ts b/src/adapter/zigate/driver/zigate.ts index 167b1218b9..113abcac3d 100644 --- a/src/adapter/zigate/driver/zigate.ts +++ b/src/adapter/zigate/driver/zigate.ts @@ -11,7 +11,6 @@ import {Queue, Wait} from "../../../utils"; import {SerialPortOptions} from "../../tstype"; import {STATUS, ZiGateCommandCode, ZiGateMessageCode, ZiGateObjectPayload} from "./constants"; import ZiGateObject from "./ziGateObject"; -import {ZclFrame} from "../../../zcl"; import Waitress from "../../../utils/waitress"; import {equal, ZiGateResponseMatcher, ZiGateResponseMatcherRule} from "./commandType"; import ZiGateFrame from "./frame"; diff --git a/src/buffalo/buffalo.ts b/src/buffalo/buffalo.ts index b7028acdbc..43980a072a 100644 --- a/src/buffalo/buffalo.ts +++ b/src/buffalo/buffalo.ts @@ -1,3 +1,5 @@ +import {EUI64} from "../zspec/tstypes"; + class Buffalo { protected position: number; protected buffer: Buffer; @@ -177,14 +179,14 @@ class Buffalo { return value; } - public writeIeeeAddr(value: string): void { + public writeIeeeAddr(value: string/*TODO: EUI64*/): void { this.writeUInt32(parseInt(value.slice(10), 16)); this.writeUInt32(parseInt(value.slice(2, 10), 16)); } - public readIeeeAddr(): string { + public readIeeeAddr(): EUI64 { const octets = Array.from(this.readBuffer(8).reverse()); - return '0x' + octets.map(octet => octet.toString(16).padStart(2, '0')).join(""); + return `0x${octets.map(octet => octet.toString(16).padStart(2, '0')).join("")}`; } public writeBuffer(values: Buffer | number[], length: number): void { diff --git a/src/controller/controller.ts b/src/controller/controller.ts index 2d3a46dc90..b77f60d2c4 100644 --- a/src/controller/controller.ts +++ b/src/controller/controller.ts @@ -6,7 +6,8 @@ import {ZclFrameConverter} from './helpers'; import * as Events from './events'; import {KeyValue, DeviceType, GreenPowerEvents, GreenPowerDeviceJoinedPayload} from './tstype'; import fs from 'fs'; -import {Utils as ZclUtils, FrameControl, ZclFrame, Clusters} from '../zcl'; +import * as Zcl from '../zspec/zcl'; +import {FrameControl} from '../zspec/zcl/definition/tstype'; import Touchlink from './touchlink'; import GreenPower from './greenPower'; import {BackupUtils} from "../utils"; @@ -612,15 +613,15 @@ class Controller extends events.EventEmitter { } private async onZclPayload(payload: AdapterEvents.ZclPayload): Promise { - let frame: ZclFrame | undefined = undefined; + let frame: Zcl.Frame | undefined = undefined; let device: Device = undefined; - if (payload.clusterID === Clusters.touchlink.ID) { + if (payload.clusterID === Zcl.Clusters.touchlink.ID) { // This is handled by touchlink return; - } else if (payload.clusterID === Clusters.greenPower.ID) { + } else if (payload.clusterID === Zcl.Clusters.greenPower.ID) { try { // Custom clusters are not supported for Green Power since we need to parse the frame to get the device. - frame = ZclFrame.fromBuffer(payload.clusterID, payload.header, payload.data, {}); + frame = Zcl.Frame.fromBuffer(payload.clusterID, payload.header, payload.data, {}); } catch (error) { logger.debug(`Failed to parse frame green power frame, ignoring it: ${error}`, NS); return; @@ -648,7 +649,7 @@ class Controller extends events.EventEmitter { device = Device.byNetworkAddress(payload.groupID); } try { - frame = ZclFrame.fromBuffer(payload.clusterID, payload.header, payload.data, device?.customClusters); + frame = Zcl.Frame.fromBuffer(payload.clusterID, payload.header, payload.data, device?.customClusters); } catch (error) { logger.debug(`Failed to parse frame: ${error}`, NS); } @@ -740,7 +741,7 @@ class Controller extends events.EventEmitter { } else { type = 'raw'; data = payload.data; - const name = ZclUtils.getCluster(payload.clusterID, device.manufacturerID, device.customClusters).name; + const name = Zcl.Utils.getCluster(payload.clusterID, device.manufacturerID, device.customClusters).name; clusterName = Number.isNaN(Number(name)) ? name : Number(name); } diff --git a/src/controller/events.ts b/src/controller/events.ts index c0644c8bff..d5a74b5652 100644 --- a/src/controller/events.ts +++ b/src/controller/events.ts @@ -1,4 +1,4 @@ -import {FrameControl} from "../zcl"; +import {FrameControl} from "../zspec/zcl/definition/tstype"; import {Device, Endpoint} from "./model"; import {KeyValue} from "./tstype"; diff --git a/src/controller/greenPower.ts b/src/controller/greenPower.ts index 0545583556..61468b4b85 100644 --- a/src/controller/greenPower.ts +++ b/src/controller/greenPower.ts @@ -1,11 +1,10 @@ import {Adapter, Events as AdapterEvents} from '../adapter'; -import * as Zcl from '../zcl'; +import * as Zcl from '../zspec/zcl'; import crypto from 'crypto'; import ZclTransactionSequenceNumber from './helpers/zclTransactionSequenceNumber'; import events from 'events'; import {GreenPowerEvents, GreenPowerDeviceJoinedPayload} from './tstype'; import {logger} from '../utils/logger'; -import {Clusters} from '../zcl'; import {BroadcastAddress} from '../zspec/enums'; const NS = 'zh:controller:greenpower'; @@ -47,7 +46,7 @@ class GreenPower extends events.EventEmitter { } /* eslint-disable-next-line @typescript-eslint/no-explicit-any*/ - private async sendPairingCommand(payload: any, dataPayload: AdapterEvents.ZclPayload, frame: Zcl.ZclFrame): Promise { + private async sendPairingCommand(payload: any, dataPayload: AdapterEvents.ZclPayload, frame: Zcl.Frame): Promise { logger.debug(`Payload.Options: ${payload.options} wasBroadcast: ${dataPayload.wasBroadcast}`, NS); // Set sink address based on communication mode @@ -69,9 +68,9 @@ class GreenPower extends events.EventEmitter { return; } - const replyFrame = Zcl.ZclFrame.create( + const replyFrame = Zcl.Frame.create( Zcl.FrameType.SPECIFIC, Zcl.Direction.SERVER_TO_CLIENT, true, - null, ZclTransactionSequenceNumber.next(), 'pairing', Clusters.greenPower.ID, payload, + null, ZclTransactionSequenceNumber.next(), 'pairing', Zcl.Clusters.greenPower.ID, payload, {}, ); @@ -87,7 +86,7 @@ class GreenPower extends events.EventEmitter { } } - public async onZclGreenPowerData(dataPayload: AdapterEvents.ZclPayload, frame: Zcl.ZclFrame): Promise { + public async onZclGreenPowerData(dataPayload: AdapterEvents.ZclPayload, frame: Zcl.Frame): Promise { try { const commandID = frame.payload.commandID ?? frame.header.commandIdentifier; switch(commandID) { @@ -131,9 +130,9 @@ class GreenPower extends events.EventEmitter { } }; - const replyFrame = Zcl.ZclFrame.create( + const replyFrame = Zcl.Frame.create( Zcl.FrameType.SPECIFIC, Zcl.Direction.SERVER_TO_CLIENT, true, - null, ZclTransactionSequenceNumber.next(), 'response', Clusters.greenPower.ID, payloadReply, + null, ZclTransactionSequenceNumber.next(), 'response', Zcl.Clusters.greenPower.ID, payloadReply, {}, ); await this.adapter.sendZclFrameToAll(242, replyFrame, 242, BroadcastAddress.RX_ON_WHEN_IDLE); @@ -195,9 +194,9 @@ class GreenPower extends events.EventEmitter { } }; - const replyFrame = Zcl.ZclFrame.create( + const replyFrame = Zcl.Frame.create( Zcl.FrameType.SPECIFIC, Zcl.Direction.SERVER_TO_CLIENT, true, - null, ZclTransactionSequenceNumber.next(), 'response', Clusters.greenPower.ID, payload, + null, ZclTransactionSequenceNumber.next(), 'response', Zcl.Clusters.greenPower.ID, payload, {}, ); @@ -222,9 +221,9 @@ class GreenPower extends events.EventEmitter { commisioningWindow: time, }; - const frame = Zcl.ZclFrame.create( + const frame = Zcl.Frame.create( Zcl.FrameType.SPECIFIC, Zcl.Direction.SERVER_TO_CLIENT, true, - null, ZclTransactionSequenceNumber.next(), 'commisioningMode', Clusters.greenPower.ID, payload, + null, ZclTransactionSequenceNumber.next(), 'commisioningMode', Zcl.Clusters.greenPower.ID, payload, {}, ); diff --git a/src/controller/helpers/request.ts b/src/controller/helpers/request.ts index 45018561db..ea61df55b7 100644 --- a/src/controller/helpers/request.ts +++ b/src/controller/helpers/request.ts @@ -1,5 +1,5 @@ import {SendPolicy} from '../tstype'; -import * as Zcl from '../../zcl'; +import * as Zcl from '../../zspec/zcl'; /* eslint-disable-next-line @typescript-eslint/no-explicit-any*/ class Request { @@ -30,14 +30,14 @@ class Request { 0x16: 'immediate', // Discover Attributes Extended Response }; - private func: (frame: Zcl.ZclFrame) => Promise; - frame: Zcl.ZclFrame; + private func: (frame: Zcl.Frame) => Promise; + frame: Zcl.Frame; expires: number; sendPolicy: SendPolicy; private resolveQueue: Array<(value: Type) => void>; private rejectQueue: Array <(error: Error) => void>; private lastError: Error; - constructor (func: (frame: Zcl.ZclFrame) => Promise, frame: Zcl.ZclFrame, timeout: number, + constructor (func: (frame: Zcl.Frame) => Promise, frame: Zcl.Frame, timeout: number, sendPolicy?: SendPolicy, lastError?: Error, resolve?:(value: Type) => void, reject?: (error: Error) => void) { this.func = func; diff --git a/src/controller/helpers/requestQueue.ts b/src/controller/helpers/requestQueue.ts index 5801168eb3..af2e44278e 100755 --- a/src/controller/helpers/requestQueue.ts +++ b/src/controller/helpers/requestQueue.ts @@ -1,5 +1,5 @@ import {logger} from '../../utils/logger'; -import * as Zcl from '../../zcl'; +import * as Zcl from '../../zspec/zcl'; import {Endpoint} from '../model'; import Request from './request'; @@ -110,7 +110,7 @@ class RequestQueue extends Set { this.delete(request); } else if (newRequest.sendPolicy !== 'keep-cmd-undiv') { // remove all duplicate attributes if we shall not write undivided - (request.frame as Mutable).payload = filteredPayload; + (request.frame as Mutable).payload = filteredPayload; logger.debug(`Request Queue (${this.deviceIeeeAddress}/${this.ID}): Remove commands from request`, NS); } } diff --git a/src/controller/helpers/zclFrameConverter.ts b/src/controller/helpers/zclFrameConverter.ts index 08cc52a3a0..b6b4b8bfa2 100644 --- a/src/controller/helpers/zclFrameConverter.ts +++ b/src/controller/helpers/zclFrameConverter.ts @@ -1,7 +1,5 @@ -import {ZclFrame, Utils as ZclUtils} from '../../zcl'; -import {Cluster} from '../../zcl/tstype'; -import ManufacturerCode from '../../zcl/definition/manufacturerCode'; -import {CustomClusters} from '../../zcl/definition/tstype'; +import * as Zcl from '../../zspec/zcl'; +import {Cluster, CustomClusters} from '../../zspec/zcl/definition/tstype'; interface KeyValue {[s: string]: number | string} @@ -10,15 +8,15 @@ interface KeyValue {[s: string]: number | string} // This leads to incorrect reported attribute names. // Remap the attributes using the target device's manufacturer ID // if the header is lacking the information. -function getCluster(frame: ZclFrame, deviceManufacturerID: number, customClusters: CustomClusters): Cluster { +function getCluster(frame: Zcl.Frame, deviceManufacturerID: number, customClusters: CustomClusters): Cluster { let cluster = frame.cluster; - if (!frame?.header?.manufacturerCode && frame?.cluster && deviceManufacturerID == ManufacturerCode.LEGRAND_GROUP) { - cluster = ZclUtils.getCluster(frame.cluster.ID, deviceManufacturerID, customClusters); + if (!frame?.header?.manufacturerCode && frame?.cluster && deviceManufacturerID == Zcl.ManufacturerCode.LEGRAND_GROUP) { + cluster = Zcl.Utils.getCluster(frame.cluster.ID, deviceManufacturerID, customClusters); } return cluster; } -function attributeKeyValue(frame: ZclFrame, deviceManufacturerID: number, customClusters: CustomClusters): KeyValue { +function attributeKeyValue(frame: Zcl.Frame, deviceManufacturerID: number, customClusters: CustomClusters): KeyValue { const payload: KeyValue = {}; const cluster = getCluster(frame, deviceManufacturerID, customClusters); @@ -33,7 +31,7 @@ function attributeKeyValue(frame: ZclFrame, deviceManufacturerID: number, custom return payload; } -function attributeList(frame: ZclFrame, deviceManufacturerID: number, customClusters: CustomClusters): Array { +function attributeList(frame: Zcl.Frame, deviceManufacturerID: number, customClusters: CustomClusters): Array { const payload: Array = []; const cluster = getCluster(frame, deviceManufacturerID, customClusters); diff --git a/src/controller/model/device.ts b/src/controller/model/device.ts index 4341cbf41d..15677eee91 100755 --- a/src/controller/model/device.ts +++ b/src/controller/model/device.ts @@ -4,13 +4,11 @@ import ZclTransactionSequenceNumber from '../helpers/zclTransactionSequenceNumbe import Endpoint from './endpoint'; import Entity from './entity'; import {Wait} from '../../utils'; -import * as Zcl from '../../zcl'; +import * as Zcl from '../../zspec/zcl'; +import {ClusterDefinition, CustomClusters} from '../../zspec/zcl/definition/tstype'; import assert from 'assert'; import {ZclFrameConverter} from '../helpers'; import {logger} from '../../utils/logger'; -import {ClusterDefinition, CustomClusters} from '../../zcl/definition/tstype'; -import {Clusters} from '../../zcl'; -import {isClusterName} from '../../zcl/utils'; import {BroadcastAddress} from '../../zspec/enums'; /** @@ -31,7 +29,7 @@ interface RoutingTable { table: {destinationAddress: number; status: string; nextHop: number}[]; } -type CustomReadResponse = (frame: Zcl.ZclFrame, endpoint: Endpoint) => boolean; +type CustomReadResponse = (frame: Zcl.Frame, endpoint: Endpoint) => boolean; class Device extends Entity { private readonly ID: number; @@ -91,7 +89,7 @@ class Device extends Entity { } get powerSource(): string {return this._powerSource;} set powerSource(powerSource: string) { - this._powerSource = typeof powerSource === 'number' ? Zcl.PowerSource[powerSource & ~(1 << 7)] : powerSource; + this._powerSource = typeof powerSource === 'number' ? Zcl.POWER_SOURCES[powerSource & ~(1 << 7)] : powerSource; } get softwareBuildID(): string {return this._softwareBuildID;} set softwareBuildID(softwareBuildID: string) {this._softwareBuildID = softwareBuildID;} @@ -194,7 +192,7 @@ class Device extends Entity { // There might be multiple endpoints with same DeviceId but it is not supported and first endpoint is returned public getEndpointByDeviceType(deviceType: string): Endpoint { - const deviceID = Zcl.EndpointDeviceType[deviceType]; + const deviceID = Zcl.ENDPOINT_DEVICE_TYPE[deviceType]; return this.endpoints.find((d): boolean => d.deviceID === deviceID); } @@ -216,7 +214,7 @@ class Device extends Entity { return this.endpoints.find(e => e.hasPendingRequests()) !== undefined; } - public async onZclData(dataPayload: AdapterEvents.ZclPayload, frame: Zcl.ZclFrame, endpoint: Endpoint): Promise { + public async onZclData(dataPayload: AdapterEvents.ZclPayload, frame: Zcl.Frame, endpoint: Endpoint): Promise { // Update reportable properties if (frame.isCluster('genBasic') && (frame.isCommand('readRsp') || frame.isCommand('report'))) { for (const [key, val] of Object.entries(ZclFrameConverter.attributeKeyValue(frame, this.manufacturerID, this.customClusters))) { @@ -355,7 +353,7 @@ class Device extends Entity { // default: no timeout (messages expire immediately after first send attempt) let pendingRequestTimeout = 0; - if((endpoints.filter((e): boolean => e.inputClusters.includes(Clusters.genPollCtrl.ID))).length > 0) { + if((endpoints.filter((e): boolean => e.inputClusters.includes(Zcl.Clusters.genPollCtrl.ID))).length > 0) { // default for devices that support genPollCtrl cluster (RX off when idle): 1 day pendingRequestTimeout = 86400000; /* istanbul ignore else */ @@ -760,7 +758,7 @@ class Device extends Entity { srcID: Number(this.ieeeAddr), }; - const frame = Zcl.ZclFrame.create( + const frame = Zcl.Frame.create( Zcl.FrameType.SPECIFIC, Zcl.Direction.SERVER_TO_CLIENT, true, null, ZclTransactionSequenceNumber.next(), 'pairing', 33, payload, this.customClusters, ); @@ -812,10 +810,10 @@ class Device extends Entity { } public addCustomCluster(name: string, cluster: ClusterDefinition): void { - assert(!([Clusters.touchlink.ID, Clusters.greenPower.ID].includes(cluster.ID)), + assert(!([Zcl.Clusters.touchlink.ID, Zcl.Clusters.greenPower.ID].includes(cluster.ID)), 'Overriding of greenPower or touchlink cluster is not supported'); - if (isClusterName(name)) { - const existingCluster = Clusters[name]; + if (Zcl.Utils.isClusterName(name)) { + const existingCluster = Zcl.Clusters[name]; // Extend existing cluster assert(existingCluster.ID === cluster.ID, `Custom cluster ID (${cluster.ID}) should match existing cluster ID (${existingCluster.ID})`); diff --git a/src/controller/model/endpoint.ts b/src/controller/model/endpoint.ts index 6ccebd814b..13edda6f15 100644 --- a/src/controller/model/endpoint.ts +++ b/src/controller/model/endpoint.ts @@ -1,6 +1,7 @@ import Entity from './entity'; import {KeyValue, SendPolicy} from '../tstype'; -import * as Zcl from '../../zcl'; +import * as Zcl from '../../zspec/zcl'; +import * as ZclTypes from '../../zspec/zcl/definition/tstype'; import ZclTransactionSequenceNumber from '../helpers/zclTransactionSequenceNumber'; import * as ZclFrameConverter from '../helpers/zclFrameConverter'; import Request from '../helpers/request'; @@ -50,7 +51,7 @@ interface BindInternal { } interface Bind { - cluster: Zcl.TsType.Cluster; + cluster: ZclTypes.Cluster; target: Endpoint | Group; } @@ -64,8 +65,8 @@ interface ConfiguredReportingInternal { } interface ConfiguredReporting { - cluster: Zcl.TsType.Cluster; - attribute: Zcl.TsType.Attribute, + cluster: ZclTypes.Cluster; + attribute: ZclTypes.Attribute, minimumReportInterval: number, maximumReportInterval: number, reportableChange: number, @@ -109,7 +110,7 @@ class Endpoint extends Entity { get configuredReportings(): ConfiguredReporting[] { return this._configuredReportings.map((entry) => { const cluster = Zcl.Utils.getCluster(entry.cluster, entry.manufacturerCode, this.getDevice().customClusters); - let attribute : Zcl.TsType.Attribute; + let attribute : ZclTypes.Attribute; if (cluster.hasAttribute(entry.attrId)) { attribute = cluster.getAttribute(entry.attrId); @@ -178,20 +179,20 @@ class Endpoint extends Entity { } /** - * @returns {Zcl.TsType.Cluster[]} + * @returns {ZclTypes.Cluster[]} */ - public getInputClusters(): Zcl.TsType.Cluster[] { + public getInputClusters(): ZclTypes.Cluster[] { return this.clusterNumbersToClusters(this.inputClusters); } /** - * @returns {Zcl.TsType.Cluster[]} + * @returns {ZclTypes.Cluster[]} */ - public getOutputClusters(): Zcl.TsType.Cluster[] { + public getOutputClusters(): ZclTypes.Cluster[] { return this.clusterNumbersToClusters(this.outputClusters); } - private clusterNumbersToClusters(clusterNumbers: number[]): Zcl.TsType.Cluster[] { + private clusterNumbersToClusters(clusterNumbers: number[]): ZclTypes.Cluster[] { return clusterNumbers.map((c) => this.getCluster(c)); } @@ -264,11 +265,11 @@ class Endpoint extends Entity { return this.pendingRequests.send(fastPolling); } - private async sendRequest(frame: Zcl.ZclFrame, options: Options): Promise; - private async sendRequest(frame: Zcl.ZclFrame, options: Options, - func: (frame: Zcl.ZclFrame) => Promise): Promise; - private async sendRequest(frame: Zcl.ZclFrame, options: Options, - func: (d: Zcl.ZclFrame) => Promise = (d: Zcl.ZclFrame): Promise => { + private async sendRequest(frame: Zcl.Frame, options: Options): Promise; + private async sendRequest(frame: Zcl.Frame, options: Options, + func: (frame: Zcl.Frame) => Promise): Promise; + private async sendRequest(frame: Zcl.Frame, options: Options, + func: (d: Zcl.Frame) => Promise = (d: Zcl.Frame): Promise => { return Entity.adapter.sendZclFrameToEndpoint( this.deviceIeeeAddress, this.deviceNetworkAddress, this.ID, d, options.timeout, options.disableResponse, options.disableRecovery, options.srcEndpoint) as Promise; @@ -312,7 +313,7 @@ class Endpoint extends Entity { private checkStatus(payload: [{status: Zcl.Status}] | {cmdId: number; statusCode: number}): void { const codes = Array.isArray(payload) ? payload.map((i) => i.status) : [payload.statusCode]; const invalid = codes.find((c) => c !== Zcl.Status.SUCCESS); - if (invalid) throw new Zcl.ZclStatusError(invalid); + if (invalid) throw new Zcl.StatusError(invalid); } public async report( @@ -600,7 +601,7 @@ class Endpoint extends Entity { transactionSequenceNumber = transactionSequenceNumber || ZclTransactionSequenceNumber.next(); options = this.getOptionsWithDefaults(options, true, Zcl.Direction.SERVER_TO_CLIENT, cluster.manufacturerCode); - const frame = Zcl.ZclFrame.create( + const frame = Zcl.Frame.create( Zcl.FrameType.SPECIFIC, options.direction, options.disableDefaultResponse, options.manufacturerCode, transactionSequenceNumber, command.name, cluster.name, payload, device.customClusters, options.reservedBits ); @@ -630,7 +631,7 @@ class Endpoint extends Entity { public waitForCommand( clusterKey: number | string, commandKey: number | string, transactionSequenceNumber: number, timeout: number, - ): {promise: Promise<{header: Zcl.ZclHeader; payload: KeyValue}>; cancel: () => void} { + ): {promise: Promise<{header: Zcl.Header; payload: KeyValue}>; cancel: () => void} { const device = this.getDevice(); const cluster = this.getCluster(clusterKey, device); const command = cluster.getCommand(commandKey); @@ -639,10 +640,10 @@ class Endpoint extends Entity { transactionSequenceNumber, cluster.ID, command.ID, timeout ); - const promise = new Promise<{header: Zcl.ZclHeader; payload: KeyValue}>((resolve, reject) => { + const promise = new Promise<{header: Zcl.Header; payload: KeyValue}>((resolve, reject) => { waiter.promise.then( (payload) => { - const frame = Zcl.ZclFrame.fromBuffer(payload.clusterID, payload.header, payload.data, device.customClusters); + const frame = Zcl.Frame.fromBuffer(payload.clusterID, payload.header, payload.data, device.customClusters); resolve({header: frame.header, payload: frame.payload}); }, (error) => reject(error), @@ -672,7 +673,7 @@ class Endpoint extends Entity { } private ensureManufacturerCodeIsUniqueAndGet( - cluster: Zcl.TsType.Cluster, attributes: (string|number)[]|ConfigureReportingItem[], + cluster: ZclTypes.Cluster, attributes: (string|number)[]|ConfigureReportingItem[], fallbackManufacturerCode: number, caller: string, ): number { const manufacturerCodes = new Set(attributes.map((nameOrID): number => { @@ -712,7 +713,7 @@ class Endpoint extends Entity { group.addMember(this); } - private getCluster(clusterKey: number | string, device: Device | undefined = undefined): Zcl.TsType.Cluster { + private getCluster(clusterKey: number | string, device: Device | undefined = undefined): ZclTypes.Cluster { device = device ?? this.getDevice(); return Zcl.Utils.getCluster(clusterKey, device.manufacturerID, device.customClusters); } @@ -745,14 +746,14 @@ class Endpoint extends Entity { public async zclCommand( clusterKey: number | string, commandKey: number | string, payload: KeyValue, options?: Options, logPayload?: KeyValue, checkStatus: boolean = false, frameType: Zcl.FrameType = Zcl.FrameType.GLOBAL - ): Promise { + ): Promise { const device = this.getDevice(); const cluster = this.getCluster(clusterKey, device); const command = (frameType == Zcl.FrameType.GLOBAL) ? Zcl.Utils.getGlobalCommand(commandKey) : cluster.getCommand(commandKey); const hasResponse = (frameType == Zcl.FrameType.GLOBAL) ? true : command.hasOwnProperty('response'); options = this.getOptionsWithDefaults(options, hasResponse, Zcl.Direction.CLIENT_TO_SERVER, cluster.manufacturerCode); - const frame = Zcl.ZclFrame.create( + const frame = Zcl.Frame.create( frameType, options.direction, options.disableDefaultResponse, options.manufacturerCode, options.transactionSequenceNumber ?? ZclTransactionSequenceNumber.next(), command.name, cluster.name, payload, device.customClusters, options.reservedBits @@ -765,7 +766,7 @@ class Endpoint extends Entity { try { const result = await this.sendRequest(frame, options); if (result) { - const resultFrame = Zcl.ZclFrame.fromBuffer(result.clusterID, result.header, result.data, device.customClusters); + const resultFrame = Zcl.Frame.fromBuffer(result.clusterID, result.header, result.data, device.customClusters); if (result && checkStatus && !options.disableResponse) { this.checkStatus(resultFrame.payload); } @@ -786,7 +787,7 @@ class Endpoint extends Entity { options = this.getOptionsWithDefaults(options, true, Zcl.Direction.CLIENT_TO_SERVER, cluster.manufacturerCode); const sourceEndpoint = options.srcEndpoint ?? this.ID; - const frame = Zcl.ZclFrame.create( + const frame = Zcl.Frame.create( Zcl.FrameType.SPECIFIC, options.direction, true, options.manufacturerCode, options.transactionSequenceNumber ?? ZclTransactionSequenceNumber.next(), command.name, cluster.name, payload, device.customClusters, options.reservedBits diff --git a/src/controller/model/group.ts b/src/controller/model/group.ts index 143278f448..c69c322192 100644 --- a/src/controller/model/group.ts +++ b/src/controller/model/group.ts @@ -1,7 +1,7 @@ import {DatabaseEntry, KeyValue} from '../tstype'; import Entity from './entity'; import ZclTransactionSequenceNumber from '../helpers/zclTransactionSequenceNumber'; -import * as Zcl from '../../zcl'; +import * as Zcl from '../../zspec/zcl'; import Endpoint from './endpoint'; import Device from './device'; import assert from 'assert'; @@ -162,7 +162,7 @@ class Group extends Entity { logger.debug(log, NS); try { - const frame = Zcl.ZclFrame.create( + const frame = Zcl.Frame.create( Zcl.FrameType.GLOBAL, options.direction, true, options.manufacturerCode, options.transactionSequenceNumber ?? ZclTransactionSequenceNumber.next(), 'write', cluster.ID, payload, {}, options.reservedBits @@ -185,7 +185,7 @@ class Group extends Entity { payload.push({attrId: typeof attribute === 'number' ? attribute : cluster.getAttribute(attribute).ID}); } - const frame = Zcl.ZclFrame.create( + const frame = Zcl.Frame.create( Zcl.FrameType.GLOBAL, options.direction, true, options.manufacturerCode, options.transactionSequenceNumber ?? ZclTransactionSequenceNumber.next(), 'read', cluster.ID, payload, {}, options.reservedBits @@ -214,7 +214,7 @@ class Group extends Entity { logger.debug(log, NS); try { - const frame = Zcl.ZclFrame.create( + const frame = Zcl.Frame.create( Zcl.FrameType.SPECIFIC, options.direction, true, options.manufacturerCode, options.transactionSequenceNumber || ZclTransactionSequenceNumber.next(), command.ID, cluster.ID, payload, {}, options.reservedBits diff --git a/src/controller/touchlink.ts b/src/controller/touchlink.ts index 1c6ea169ca..75874ee53d 100644 --- a/src/controller/touchlink.ts +++ b/src/controller/touchlink.ts @@ -1,5 +1,5 @@ import {Adapter} from '../adapter'; -import * as Zcl from '../zcl'; +import * as Zcl from '../zspec/zcl'; import {Wait, AssertString} from '../utils'; import {logger} from '../utils/logger'; @@ -152,24 +152,24 @@ class Touchlink { return done; } - private createScanRequestFrame(transaction: number): Zcl.ZclFrame { - return Zcl.ZclFrame.create( + private createScanRequestFrame(transaction: number): Zcl.Frame { + return Zcl.Frame.create( Zcl.FrameType.SPECIFIC, Zcl.Direction.CLIENT_TO_SERVER, true, null, 0, 'scanRequest', Zcl.Clusters.touchlink.ID, {transactionID: transaction, zigbeeInformation: 4, touchlinkInformation: 18}, {}, ); } - private createIdentifyRequestFrame(transaction: number): Zcl.ZclFrame { - return Zcl.ZclFrame.create( + private createIdentifyRequestFrame(transaction: number): Zcl.Frame { + return Zcl.Frame.create( Zcl.FrameType.SPECIFIC, Zcl.Direction.CLIENT_TO_SERVER, true, null, 0, 'identifyRequest', Zcl.Clusters.touchlink.ID, {transactionID: transaction, duration: 65535}, {}, ); } - private createResetFactoryNewRequestFrame(transaction: number): Zcl.ZclFrame { - return Zcl.ZclFrame.create( + private createResetFactoryNewRequestFrame(transaction: number): Zcl.Frame { + return Zcl.Frame.create( Zcl.FrameType.SPECIFIC, Zcl.Direction.CLIENT_TO_SERVER, true, null, 0, 'resetToFactoryNew', Zcl.Clusters.touchlink.ID, {transactionID: transaction}, {}, diff --git a/src/index.ts b/src/index.ts index 5a15fe9bb3..6e81866239 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,5 @@ -import Controller from './controller/controller'; -import * as Zcl from './zcl'; -import * as ZSpec from './zspec'; -import * as logger from './utils/logger'; - -export {Zcl, Controller, ZSpec}; - -/* istanbul ignore next */ -export const setLogger = logger.setLogger; +export {default as Controller} from './controller/controller'; +export * as Zcl from './zspec/zcl'; +export * as Zdo from './zspec/zdo'; +export * as ZSpec from './zspec'; +export {setLogger} from './utils/logger'; diff --git a/src/zcl/definition/buffaloZclDataType.ts b/src/zcl/definition/buffaloZclDataType.ts deleted file mode 100644 index 12c4647ed8..0000000000 --- a/src/zcl/definition/buffaloZclDataType.ts +++ /dev/null @@ -1,18 +0,0 @@ -enum BuffaloZclDataType { - USE_DATA_TYPE = 1000, - LIST_UINT8 = 1001, - LIST_UINT16 = 1002, - LIST_UINT24 = 1003, - LIST_UINT32 = 1004, - LIST_ZONEINFO = 1005, - EXTENSION_FIELD_SETS = 1006, - LIST_THERMO_TRANSITIONS = 1007, - BUFFER = 1008, - GDP_FRAME = 1009, - STRUCTURED_SELECTOR = 1010, - LIST_TUYA_DATAPOINT_VALUES = 1011, - LIST_MIBOXER_ZONES = 1012, - BIG_ENDIAN_UINT24 = 1013, -} - -export default BuffaloZclDataType; \ No newline at end of file diff --git a/src/zcl/definition/direction.ts b/src/zcl/definition/direction.ts deleted file mode 100755 index c160716a1c..0000000000 --- a/src/zcl/definition/direction.ts +++ /dev/null @@ -1,6 +0,0 @@ -enum Direction { - CLIENT_TO_SERVER = 0, - SERVER_TO_CLIENT = 1, -} - -export default Direction; \ No newline at end of file diff --git a/src/zcl/definition/endpointDeviceType.ts b/src/zcl/definition/endpointDeviceType.ts deleted file mode 100644 index c3154aa0cd..0000000000 --- a/src/zcl/definition/endpointDeviceType.ts +++ /dev/null @@ -1,14 +0,0 @@ -const EndpointDeviceType: {[s: string]: number} = { - 'ZLLOnOffLight' : 0x0000, - 'ZLLOnOffPluginUnit' : 0x0010, - 'ZLLDimmableLight' : 0x0100, - 'ZLLDimmablePluginUnit' : 0x0110, - 'ZLLColorLight' : 0x0200, - 'ZLLExtendedColorLight' : 0x0210, - 'ZLLColorTemperatureLight' : 0x0220, - 'HAOnOffLight' : 0x0100, - 'HADimmableLight' : 0x0101, - 'HAColorLight' : 0x0102, -}; - -export default EndpointDeviceType; \ No newline at end of file diff --git a/src/zcl/definition/frameControl.ts b/src/zcl/definition/frameControl.ts deleted file mode 100644 index e01a5228a5..0000000000 --- a/src/zcl/definition/frameControl.ts +++ /dev/null @@ -1,12 +0,0 @@ -import Direction from './direction'; -import FrameType from './frameType'; - -interface FrameControl { - reservedBits: number; - frameType: FrameType; - manufacturerSpecific: boolean; - direction: Direction; - disableDefaultResponse: boolean; -} - -export default FrameControl; \ No newline at end of file diff --git a/src/zcl/definition/frameType.ts b/src/zcl/definition/frameType.ts deleted file mode 100644 index d3b95dc62d..0000000000 --- a/src/zcl/definition/frameType.ts +++ /dev/null @@ -1,6 +0,0 @@ -enum FrameType { - GLOBAL = 0, - SPECIFIC = 1, -} - -export default FrameType; \ No newline at end of file diff --git a/src/zcl/definition/index.ts b/src/zcl/definition/index.ts deleted file mode 100644 index bf497012fd..0000000000 --- a/src/zcl/definition/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -import BuffaloZclDataType from './buffaloZclDataType'; -import Clusters from './cluster'; -import Direction from './direction'; -import DataType from './dataType'; -import Foundation from './foundation'; -import Status from './status'; -import * as TsType from './tstype'; -import FrameType from './frameType'; -import PowerSource from './powerSource'; -import ManufacturerCode from './manufacturerCode'; -import FrameControl from './frameControl'; -import EndpointDeviceType from './endpointDeviceType'; - -export { - BuffaloZclDataType, Clusters, Direction, Foundation, Status, DataType, TsType, FrameType, PowerSource, - ManufacturerCode, FrameControl, EndpointDeviceType, -}; \ No newline at end of file diff --git a/src/zcl/definition/powerSource.ts b/src/zcl/definition/powerSource.ts deleted file mode 100644 index d726b5ecfe..0000000000 --- a/src/zcl/definition/powerSource.ts +++ /dev/null @@ -1,11 +0,0 @@ -const powerSource: {[s: number]: string} = { - 0: 'Unknown', - 1: 'Mains (single phase)', - 2: 'Mains (3 phase)', - 3: 'Battery', - 4: 'DC Source', - 5: 'Emergency mains constantly powered', - 6: 'Emergency mains and transfer switch' -}; - -export default powerSource; \ No newline at end of file diff --git a/src/zcl/definition/tstype.ts b/src/zcl/definition/tstype.ts deleted file mode 100644 index ad750db2e1..0000000000 --- a/src/zcl/definition/tstype.ts +++ /dev/null @@ -1,74 +0,0 @@ -import DataType from './dataType'; -import * as TsType from '../tstype'; - -interface ParameterDefinition extends TsType.Parameter { - conditions?: { - type: 'statusEquals' | - 'statusNotEquals' | - 'minimumRemainingBufferBytes' | - 'directionEquals' | - 'bitMaskSet' | - 'bitFieldEnum' | - 'dataTypeValueTypeEquals'; - param?: string; - mask?: number; - offset?: number; - size?: number; - value?: number | string; - }[]; -} - -interface AttributeDefinition { - ID: number; - type: DataType; - manufacturerCode?: number; -} - -interface ClusterDefinition { - ID: number; - manufacturerCode?: number; - attributes: Readonly>>; - commands: Readonly>>; - commandsResponse: Readonly>>; -} - -interface CustomClusters {[k: string]: ClusterDefinition}; - -interface CommandDefinition { - ID: number; - parameters: readonly ParameterDefinition[]; - response?: number; -} - -type ClusterName = ( - | 'genBasic' | 'genPowerCfg' | 'genDeviceTempCfg' | 'genIdentify' | 'genGroups' | 'genScenes' | 'genOnOff' - | 'genOnOffSwitchCfg' | 'genLevelCtrl' | 'genAlarms' | 'genTime' | 'genRssiLocation' | 'genAnalogInput' | 'genAnalogOutput' - | 'genAnalogValue' | 'genBinaryInput' | 'genBinaryOutput' | 'genBinaryValue' | 'genMultistateInput' | 'genMultistateOutput' - | 'genMultistateValue' | 'genCommissioning' | 'genOta' | 'genPollCtrl' | 'greenPower' | 'mobileDeviceCfg' | 'neighborCleaning' - | 'nearestGateway' | 'closuresShadeCfg' | 'closuresDoorLock' | 'closuresWindowCovering' | 'barrierControl' | 'hvacPumpCfgCtrl' - | 'hvacThermostat' | 'hvacFanCtrl' | 'hvacDehumidificationCtrl' | 'hvacUserInterfaceCfg' | 'lightingColorCtrl' | 'lightingBallastCfg' - | 'msIlluminanceMeasurement' | 'msIlluminanceLevelSensing' | 'msTemperatureMeasurement' | 'msPressureMeasurement' | 'msFlowMeasurement' - | 'msRelativeHumidity' | 'msOccupancySensing' | 'msSoilMoisture' | 'pHMeasurement' | 'msCO2' | 'pm25Measurement' | 'ssIasZone' - | 'ssIasAce' | 'ssIasWd' | 'piGenericTunnel' | 'piBacnetProtocolTunnel' | 'piAnalogInputReg' | 'piAnalogInputExt' | 'piAnalogOutputReg' - | 'piAnalogOutputExt' | 'piAnalogValueReg' | 'piAnalogValueExt' | 'piBinaryInputReg' | 'piBinaryInputExt' | 'piBinaryOutputReg' - | 'piBinaryOutputExt' | 'piBinaryValueReg' | 'piBinaryValueExt' | 'piMultistateInputReg' | 'piMultistateInputExt' | 'piMultistateOutputReg' - | 'piMultistateOutputExt' | 'piMultistateValueReg' | 'piMultistateValueExt' | 'pi11073ProtocolTunnel' | 'piIso7818ProtocolTunnel' - | 'piRetailTunnel' | 'seMetering' | 'tunneling' | 'telecommunicationsInformation' | 'telecommunicationsVoiceOverZigbee' - | 'telecommunicationsChatting' | 'haApplianceIdentification' | 'haMeterIdentification' | 'haApplianceEventsAlerts' | 'haApplianceStatistics' - | 'haElectricalMeasurement' | 'haDiagnostic' | 'touchlink' | 'manuSpecificIkeaAirPurifier' | 'msIkeaVocIndexMeasurement' - | 'manuSpecificClusterAduroSmart' | 'manuSpecificOsram' | 'manuSpecificPhilips' | 'manuSpecificPhilips2' | 'manuSpecificSinope' - | 'manuSpecificLegrandDevices' | 'manuSpecificLegrandDevices2' - | 'manuSpecificLegrandDevices3' | 'manuSpecificNiko1' | 'manuSpecificNiko2' | 'wiserDeviceInfo' | 'manuSpecificTuya' | 'manuSpecificLumi' - | 'liXeePrivate' | 'manuSpecificTuya_2' | 'manuSpecificTuya_3' | 'manuSpecificCentraliteHumidity' | 'manuSpecificSmartThingsArrivalSensor' - | 'manuSpecificSamsungAccelerometer' | 'heimanSpecificFormaldehydeMeasurement' | 'heimanSpecificAirQuality' | 'heimanSpecificScenes' - | 'tradfriButton' | 'heimanSpecificInfraRedRemote' | 'develcoSpecificAirQuality' | 'schneiderSpecificPilotMode' - | 'elkoOccupancySettingClusterServer' | 'elkoSwitchConfigurationClusterServer' | 'manuSpecificSchneiderLightSwitchConfiguration' - | 'manuSpecificSchneiderFanSwitchConfiguration' | 'sprutDevice' | 'sprutVoc' | 'sprutNoise' | 'sprutIrBlaster' | 'manuSpecificSiglisZigfred' - | 'manuSpecificInovelli' | 'owonClearMetering' | 'zosungIRTransmit' | 'zosungIRControl' | 'manuSpecificBosch' | 'manuSpecificBosch3' - | 'manuSpecificBosch5' | 'manuSpecificBosch7' | 'manuSpecificBosch8' | 'manuSpecificBosch9' | 'manuSpecificBosch10' | 'manuSpecificBosch11' - | 'manuSpecificAssaDoorLock' | 'manuSpecificDoorman' | 'manuSpecificNodOnPilotWire' | 'manuSpecificProfalux1' | 'manuSpecificAmazonWWAH' -); - -export { - ParameterDefinition, ClusterDefinition, AttributeDefinition, CommandDefinition, ClusterName, CustomClusters, -}; diff --git a/src/zcl/index.ts b/src/zcl/index.ts deleted file mode 100644 index 79e14583b3..0000000000 --- a/src/zcl/index.ts +++ /dev/null @@ -1,33 +0,0 @@ -import * as Utils from './utils'; -import Clusters from './definition/cluster'; -import Status from './definition/status'; -import Direction from './definition/direction'; -import FrameType from './definition/frameType'; -import FrameControl from './definition/frameControl'; -import DataType from './definition/dataType'; -import Foundation from './definition/foundation'; -import PowerSource from './definition/powerSource'; -import EndpointDeviceType from './definition/endpointDeviceType'; -import ManufacturerCode from './definition/manufacturerCode'; -import ZclFrame from './zclFrame'; -import ZclHeader from './zclHeader'; -import ZclStatusError from './zclStatusError'; -import * as TsType from './tstype'; - -export { - Clusters, - Direction, - FrameType, - Status, - DataType, - Foundation, - ZclFrame, - Utils, - PowerSource, - TsType, - ManufacturerCode, - FrameControl, - EndpointDeviceType, - ZclStatusError, - ZclHeader, -}; \ No newline at end of file diff --git a/src/zcl/tstype.ts b/src/zcl/tstype.ts deleted file mode 100644 index 84020df54a..0000000000 --- a/src/zcl/tstype.ts +++ /dev/null @@ -1,83 +0,0 @@ -import DataType from './definition/dataType'; -import BuffaloZclDataType from './definition/buffaloZclDataType'; - -interface Attribute { - ID: number; - name: string; - type: DataType; - manufacturerCode?: number; -} - -interface Parameter { - name: string; - type: DataType | BuffaloZclDataType; -} - -interface Command { - ID: number; - name: string; - parameters: readonly Parameter[]; - response?: number; -} - -interface Cluster { - ID: number; - name: string; - manufacturerCode?: number; - attributes: {[s: string]: Attribute}; - commands: { - [s: string]: Command; - }; - commandsResponse: { - [s: string]: Command; - }; - getAttribute: (key: number | string) => Attribute; - hasAttribute: (key: number | string) => boolean; - getCommand: (key: number | string) => Command; - getCommandResponse: (key: number | string) => Command; -} - -interface BuffaloZclOptions { - length?: number; - payload?: { - mode?: number;// used to read ListThermoTransitions - numoftrans?: number;// used to read ListThermoTransitions - commandID?: number;// used to read GdpFrame - payloadSize?: number;// used to read GdpFrame - } & {[key: string]: unknown}; - dataType?: DataType; - attrId?: number; -} - -interface ZclArray { - elementType: DataType | keyof typeof DataType; - elements: unknown[]; -} - -type DataTypeValueType = 'ANALOG' | 'DISCRETE'; - -/** - * The upper 4 bits of the Indicator subfield for Attributes Structured commands. - */ -enum StructuredIndicatorType { - /** - * Write: Only for attributes of type other than array, structure, set or bag - * - * Read: Only for attributes of type other than array or structure - */ - Whole = 0x00, - /** Add element to the set/bag */ - WriteAdd = 0x10, - /** Remove element from the set/bag */ - WriteRemove = 0x20, -} - -interface StructuredSelector { - indexes?: number[], - indicatorType?: StructuredIndicatorType, -} - -export { - Cluster, Attribute, Command, Parameter, DataTypeValueType, BuffaloZclOptions, ZclArray, StructuredIndicatorType, - StructuredSelector, -}; \ No newline at end of file diff --git a/src/zcl/utils.ts b/src/zcl/utils.ts deleted file mode 100644 index 62bad98366..0000000000 --- a/src/zcl/utils.ts +++ /dev/null @@ -1,224 +0,0 @@ -import {DataType, Clusters, Foundation} from './definition'; -import {FoundationCommandName} from './definition/foundation'; -import {ClusterDefinition, ClusterName, CustomClusters} from './definition/tstype'; -import * as TsType from './tstype'; - -const DataTypeValueType = { - discrete: [ - DataType.DATA8, DataType.DATA16, DataType.DATA24, DataType.DATA32, DataType.DATA40, - DataType.DATA48, DataType.DATA56, DataType.DATA64, DataType.BOOLEAN, - DataType.BITMAP8, DataType.BITMAP16, DataType.BITMAP24, DataType.BITMAP32, DataType.BITMAP40, - DataType.BITMAP48, DataType.BITMAP56, DataType.BITMAP64, DataType.ENUM8, DataType.ENUM16, - DataType.OCTET_STR, DataType.CHAR_STR, DataType.LONG_OCTET_STR, DataType.LONG_CHAR_STR, DataType.ARRAY, - DataType.STRUCT, DataType.SET, DataType.BAG, DataType.CLUSTER_ID, DataType.ATTR_ID, DataType.BAC_OID, - DataType.IEEE_ADDR, DataType.SEC_KEY, - ], - analog:[ - DataType.UINT8, DataType.UINT16, DataType.UINT24, DataType.UINT32, DataType.UINT40, - DataType.UINT48, DataType.UINT56, - DataType.INT8, DataType.INT16, DataType.INT24, DataType.INT32, DataType.INT40, - DataType.INT48, DataType.INT56, DataType.SEMI_PREC, DataType.SINGLE_PREC, DataType.DOUBLE_PREC, - DataType.TOD, DataType.DATE, DataType.UTC, - ], -}; - -function IsDataTypeAnalogOrDiscrete(dataType: DataType): 'ANALOG' | 'DISCRETE' { - if (DataTypeValueType.discrete.includes(dataType)) { - return 'DISCRETE'; - } else if (DataTypeValueType.analog.includes(dataType)) { - return 'ANALOG'; - } else { - throw new Error(`Don't know value type for '${DataType[dataType]}'`); - } -} - -function getClusterDefinition( - key: string | number, - manufacturerCode: number | null, - customClusters: CustomClusters, -): {name: string, cluster: ClusterDefinition} { - let name: string; - - const clusters: CustomClusters = { - ...customClusters, // Custom clusters have a higher priority than Zcl clusters (custom cluster has a DIFFERENT name than Zcl clusters) - ...Clusters, - ...customClusters, // Custom clusters should override Zcl clusters (custom cluster has the SAME name than Zcl clusters) - }; - - if (typeof key === 'number') { - if (manufacturerCode) { - name = Object.entries(clusters) - .find((e) => e[1].ID === key && e[1].manufacturerCode === manufacturerCode)?.[0]; - } - - if (!name) { - name = Object.entries(clusters).find((e) => e[1].ID === key && !e[1].manufacturerCode)?.[0]; - } - - if (!name) { - name = Object.entries(clusters).find((e) => e[1].ID === key)?.[0]; - } - } else { - name = key; - } - - let cluster = clusters[name]; - - if (!cluster) { - if (typeof key === 'number') { - name = key.toString(); - cluster = {attributes: {}, commands: {}, commandsResponse: {}, manufacturerCode: null, ID: key}; - } else { - throw new Error(`Cluster with name '${key}' does not exist`); - } - } - - return {name, cluster}; -} - -function createCluster(name: string, cluster: ClusterDefinition, manufacturerCode: number = null): TsType.Cluster { - const attributes: {[s: string]: TsType.Attribute} = Object.assign( - {}, - ...Object.entries(cluster.attributes).map(([k, v]) => ({[k]: {...v, name: k}})) - ); - const commands: {[s: string]: TsType.Command} = Object.assign( - {}, - ...Object.entries(cluster.commands).map(([k, v]) => ({[k]: {...v, name: k}})) - ); - const commandsResponse: {[s: string]: TsType.Command} = Object.assign( - {}, - ...Object.entries(cluster.commandsResponse).map(([k, v]) => ({[k]: {...v, name: k}})) - ); - - const getAttributeInternal = (key: number | string): TsType.Attribute => { - let result: TsType.Attribute = null; - if (typeof key === 'number') { - if (manufacturerCode) { - result = Object.values(attributes).find((a): boolean => { - return a.ID === key && a.manufacturerCode === manufacturerCode; - }); - } - - if (!result) { - result = Object.values(attributes).find((a): boolean => a.ID === key && a.manufacturerCode == null); - } - } else { - result = Object.values(attributes).find((a): boolean => a.name === key); - } - return result; - }; - - const getAttribute = (key: number | string): TsType.Attribute => { - const result = getAttributeInternal(key); - if (!result) { - throw new Error(`Cluster '${name}' has no attribute '${key}'`); - } - - return result; - }; - - const hasAttribute = (key: number | string): boolean => { - const result = getAttributeInternal(key); - return !!result; - }; - - const getCommand = (key: number | string): TsType.Command => { - let result: TsType.Command = null; - - if (typeof key === 'number') { - result = Object.values(commands).find((a): boolean => a.ID === key); - } else { - result = Object.values(commands).find((a): boolean => a.name === key); - } - - if (!result) { - throw new Error(`Cluster '${name}' has no command '${key}'`); - } - - return result; - }; - - const getCommandResponse = (key: number | string): TsType.Command => { - let result: TsType.Command = null; - - if (typeof key === 'number') { - result = Object.values(commandsResponse).find((a): boolean => a.ID === key); - } else { - result = Object.values(commandsResponse).find((a): boolean => a.name === key); - } - - if (!result) { - throw new Error(`Cluster '${name}' has no command response '${key}'`); - } - - return result; - }; - - return { - ID: cluster.ID, - attributes, - manufacturerCode: cluster.manufacturerCode, - name, - commands, - // eslint-disable-next-line - commandsResponse: Object.assign({}, ...Object.entries(cluster.commandsResponse).map(([k, v]): any => ({[k]: {...v, name: k}}))), - getAttribute, - hasAttribute, - getCommand, - getCommandResponse, - }; -} - -function getCluster( - key: string | number, - manufacturerCode: number | null, - customClusters: CustomClusters, -): TsType.Cluster { - const {name, cluster} = getClusterDefinition(key, manufacturerCode, customClusters); - return createCluster(name, cluster, manufacturerCode); -} - -function getGlobalCommand(key: number | string): TsType.Command { - let name: FoundationCommandName; - - if (typeof key === 'number') { - for (const commandName in Foundation) { - if (Foundation[commandName as FoundationCommandName].ID === key) { - name = commandName as FoundationCommandName; - break; - } - } - } else { - name = key as FoundationCommandName; - } - - const command = Foundation[name]; - - if (!command) { - throw new Error(`Global command with key '${key}' does not exist`); - } - - const result: TsType.Command = { - ID: command.ID, - name, - parameters: command.parameters, - }; - - if (command.response != undefined) { - result.response = command.response; - } - - return result; -} - -function isClusterName(name: string): name is ClusterName { - return (name in Clusters); -} - -export { - getCluster, - getGlobalCommand, - IsDataTypeAnalogOrDiscrete, - createCluster, - isClusterName, -}; \ No newline at end of file diff --git a/src/zcl/zclStatusError.ts b/src/zcl/zclStatusError.ts deleted file mode 100644 index b4a4f43a52..0000000000 --- a/src/zcl/zclStatusError.ts +++ /dev/null @@ -1,12 +0,0 @@ -import Status from './definition/status'; - -class ZclStatusError extends Error { - public code: number; - - constructor (code: number) { - super(`Status '${Status[code]}'`); - this.code = code; - } -} - -export default ZclStatusError; diff --git a/src/zspec/consts.ts b/src/zspec/consts.ts index e69de29bb2..e06d66cd2c 100644 --- a/src/zspec/consts.ts +++ b/src/zspec/consts.ts @@ -0,0 +1,72 @@ +import {ExtendedPanId} from "./tstypes"; + +/** Current supported Zigbee revision: https://csa-iot.org/wp-content/uploads/2023/04/05-3474-23-csg-zigbee-specification-compressed.pdf */ +export const ZIGBEE_REVISION = 23; + +/** The network ID of the coordinator in a ZigBee network is 0x0000. */ +export const COORDINATOR_ADDRESS = 0x0000; + +/** Endpoint profile ID for Zigbee 3.0. "Home Automation" */ +export const HA_PROFILE_ID = 0x0104; +/** Endpoint profile ID for Smart Energy */ +export const SE_PROFILE_ID = 0x0109; +/** Endpoint profile ID for Green Power */ +export const GP_PROFILE_ID = 0xA1E0; +/** The touchlink (ZigBee Light Link/ZLL) Profile ID. */ +export const TOUCHLINK_PROFILE_ID = 0xC05E; +/** The profile ID used to address all the public profiles. */ +export const WILDCARD_PROFILE_ID = 0xFFFF; + +/** The default HA endpoint. */ +export const HA_ENDPOINT = 0x01; +/** The GP endpoint, as defined in the ZigBee spec. */ +export const GP_ENDPOINT = 0xF2; + +/** The maximum 802.15.4 channel number is 26. */ +export const MAX_802_15_4_CHANNEL_NUMBER = 26; +/** The minimum 2.4GHz 802.15.4 channel number is 11. */ +export const MIN_802_15_4_CHANNEL_NUMBER = 11; +/** There are sixteen 802.15.4 channels. */ +export const NUM_802_15_4_CHANNELS = (MAX_802_15_4_CHANNEL_NUMBER - MIN_802_15_4_CHANNEL_NUMBER + 1); +/** A bitmask to scan all 2.4 GHz 802.15.4 channels. */ +export const ALL_802_15_4_CHANNELS_MASK = 0x07FFF800; +/** A bitmask of the preferred 2.4 GHz 802.15.4 channels to scan. */ +export const PREFERRED_802_15_4_CHANNELS_MASK = 0x0318C800; +/** List of all Zigbee channels */ +export const ALL_802_15_4_CHANNELS = [11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26]; +/** List of preferred Zigbee channels */ +export const PREFERRED_802_15_4_CHANNELS = [11, 14, 15, 19, 20, 24, 25]; + +/** A blank (also used as "wildcard") EUI64 hex string prefixed with 0x */ +export const BLANK_EUI64 = "0xFFFFFFFFFFFFFFFF"; +/** A blank extended PAN ID. (null/not present) */ +export const BLANK_EXTENDED_PAN_ID: Readonly = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; + +/** An invalid profile ID. This is a reserved profileId. */ +export const INVALID_PROFILE_ID = 0xFFFF; +/** An invalid cluster ID. */ +export const INVALID_CLUSTER_ID = 0xFFFF; +/** An invalid PAN ID. */ +export const INVALID_PAN_ID = 0xFFFF; + +/** A distinguished network ID that will never be assigned to any node. It is used to indicate the absence of a node ID. */ +export const NULL_NODE_ID = 0xFFFF; +/** A distinguished binding index used to indicate the absence of a binding. */ +export const NULL_BINDING = 0xFF; + +/** This key is "ZigBeeAlliance09" */ +export const INTEROPERABILITY_LINK_KEY: readonly number[] = [ + 0x5A, 0x69, 0x67, 0x42, 0x65, 0x65, 0x41, 0x6C, 0x6C, 0x69, 0x61, 0x6E, 0x63, 0x65, 0x30, 0x39 +]; + +export const PERMIT_JOIN_FOREVER = 0xFF; +export const PERMIT_JOIN_MAX_TIMEOUT = 0xFE; + +/** Size of EUI64 (an IEEE address) in bytes. */ +export const EUI64_SIZE = 8; +/** Size of an PAN identifier in bytes. */ +export const PAN_ID_SIZE = 2; +/** Size of an extended PAN identifier in bytes. */ +export const EXTENDED_PAN_ID_SIZE = 8; +/** Size of an encryption key in bytes. */ +export const DEFAULT_ENCRYPTION_KEY_SIZE = 16; diff --git a/src/zspec/enums.ts b/src/zspec/enums.ts index 679c5e2d92..f30f51484d 100644 --- a/src/zspec/enums.ts +++ b/src/zspec/enums.ts @@ -7,10 +7,16 @@ * Broadcasting to end devices is both significantly more resource-intensive and significantly less reliable than broadcasting to routers. */ export enum BroadcastAddress { - /** Broadcast to all routers. */ + // Reserved = 0xfff8, + // Reserved = 0xfff9, + // Reserved = 0xfffa, + /** Low power routers only */ + LOW_POWER_ROUTERS = 0xFFFB, + /** All routers and coordinator */ DEFAULT = 0xFFFC, - /** Broadcast to all non-sleepy devices. */ + /** macRxOnWhenIdle = TRUE (all non-sleepy devices) */ RX_ON_WHEN_IDLE = 0xFFFD, - /** Broadcast to all devices, including sleepy end devices. */ + // Reserved = 0xFFFE, + /** All devices in PAN (including sleepy end devices) */ SLEEPY = 0xFFFF, }; diff --git a/src/zspec/index.ts b/src/zspec/index.ts index 15b408f727..fd8a4e5c94 100644 --- a/src/zspec/index.ts +++ b/src/zspec/index.ts @@ -1,5 +1,15 @@ -import {BroadcastAddress} from './enums'; +export * from './consts'; +export * from './enums'; +export * as Utils from './utils'; -export { - BroadcastAddress, -}; \ No newline at end of file +// TODO: TBD: +// export Zcl/Zdo nested here, or just in root index.ts? +// export * as Zcl from './zcl'; +// export * as Zdo from './zdo'; + +// resulting in: +// import {ZSpec, Zcl, Zdo} from 'zigbee-herdsman'; +// vs +// import {ZSpec} from 'zigbee-herdsman'; +// ZSpec.Zcl; +// ZSpec.Zdo; \ No newline at end of file diff --git a/src/zspec/tstypes.ts b/src/zspec/tstypes.ts new file mode 100644 index 0000000000..5214d0c4d6 --- /dev/null +++ b/src/zspec/tstypes.ts @@ -0,0 +1,18 @@ +/** + * EUI 64-bit ID (IEEE 802.15.4 long address). uint8[EUI64_SIZE] + * + * NOTE: Expected to contain `0x` prefix + */ +export type EUI64 = `0x${string}`; +/** IEEE 802.15.4 node ID. Also known as short address. uint16 */ +export type NodeId = number; +/** IEEE 802.15.4 PAN ID. uint16 */ +export type PanId = number; +/** PAN 64-bit ID (IEEE 802.15.4 long address). uint8[EXTENDED_PAN_ID_SIZE] */ +export type ExtendedPanId = number[]; +/** 16-bit ZigBee multicast group identifier. uint16 */ +export type MulticastId = number; +/** Refer to the Zigbee application profile ID. uint16 */ +export type ProfileId = number; +/** Refer to the ZCL cluster ID. uint16 */ +export type ClusterId = number; diff --git a/src/zspec/utils.ts b/src/zspec/utils.ts new file mode 100644 index 0000000000..30cb8c8726 --- /dev/null +++ b/src/zspec/utils.ts @@ -0,0 +1,25 @@ +import {ALL_802_15_4_CHANNELS} from "./consts"; +import {BroadcastAddress} from "./enums"; + +/** + * Convert a channels array to a uint32 channel mask. + * @param channels + * @returns + */ +export const channelsToUInt32Mask = (channels: number[]): number => { + return channels.reduce((a, c) => a + (1 << c), 0); +}; + +/** + * Convert a uint32 channel mask to a channels array. + * @param mask + * @returns + */ +export const uint32MaskToChannels = (mask: number): number[] => { + return ALL_802_15_4_CHANNELS.map((c: number) => ((2 ** c) & mask) ? c : null).filter((x) => x); +}; + +export const isBroadcastAddress = (address: number): boolean => { + return address === BroadcastAddress.DEFAULT || address === BroadcastAddress.RX_ON_WHEN_IDLE + || address === BroadcastAddress.SLEEPY || address === BroadcastAddress.LOW_POWER_ROUTERS; +}; diff --git a/src/zspec/zcl/.gitkeep b/src/zspec/zcl/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/zcl/buffaloZcl.ts b/src/zspec/zcl/buffaloZcl.ts similarity index 89% rename from src/zcl/buffaloZcl.ts rename to src/zspec/zcl/buffaloZcl.ts index 49614f5a4f..081307df1c 100644 --- a/src/zcl/buffaloZcl.ts +++ b/src/zspec/zcl/buffaloZcl.ts @@ -1,23 +1,28 @@ -import {Buffalo} from '../buffalo'; -import {IsNumberArray} from '../utils'; -import {logger} from '../utils/logger'; -import {BuffaloZclDataType, DataType} from './definition'; -import {BuffaloZclOptions, StructuredIndicatorType, StructuredSelector, ZclArray} from './tstype'; +import {BuffaloZclOptions, StructuredSelector, ZclArray} from './definition/tstype'; +import {BuffaloZclDataType, DataType, StructuredIndicatorType} from './definition/enums'; +import {Buffalo} from '../../buffalo'; import * as Utils from './utils'; +import {IsNumberArray} from '../../utils'; +import {logger} from '../../utils/logger'; -const NS = 'zh:controller:buffalozcl'; +const NS = 'zh:zcl:buffalo'; interface KeyValue {[s: string | number]: number | string} const SEC_KEY_LENGTH = 16; -const extensionFieldSetsDateTypeLookup: {[key: number]: DataType[]} = { +const EXTENSION_FIELD_SETS_DATA_TYPE: {[key: number]: DataType[]} = { 6: [DataType.UINT8], 8: [DataType.UINT8], 258: [DataType.UINT8, DataType.UINT8], 768: [DataType.UINT16, DataType.UINT16, DataType.UINT16, DataType.UINT8, DataType.UINT8, DataType.UINT8, DataType.UINT16, DataType.UINT16], }; +interface Struct { + elmType: DataType; + elmVal: unknown; +} + interface ZclTimeOfDay { /** [0-23] */ hours?: number, @@ -40,18 +45,24 @@ interface ZclDate { dayOfWeek?: number, } +interface ZoneInfo { + zoneID: number; + zoneStatus: number; +} + +interface ExtensionFieldSet { + clstId: number; + len: number; + extField: unknown[]; +} + interface ThermoTransition { transitionTime: number; heatSetpoint?: number; coolSetpoint?: number; } -interface Struct { - elmType: DataType; - elmVal: unknown; -} - -interface Gdp { +interface GPD { deviceID: number; options: number; extendedOptions: number; @@ -69,18 +80,18 @@ interface Gdp { gpdClientClusters: Buffer; } -interface GdpChannelRequest { +interface GPDChannelRequest { nextChannel: number; nextNextChannel: number; } -interface GdpChannelConfiguration { +interface GPDChannelConfiguration { commandID: number; operationalChannel: number; basic: boolean; } -interface GdpCommissioningReply { +interface GPDCommissioningReply { commandID: number; options: number; panID: number; @@ -89,23 +100,17 @@ interface GdpCommissioningReply { frameCounter: number; } -interface GdpCustomReply { +interface GPDCustomReply { commandID: number; buffer: Buffer; } -interface GdpAttributeReport { +interface GPDAttributeReport { manufacturerCode: number; clusterID: number; attributes: KeyValue; } -interface ExtensionFieldSet { - clstId: number; - len: number; - extField: unknown[]; -} - interface TuyaDataPointValue { dp: number; datatype: number; @@ -117,12 +122,7 @@ interface MiboxerZone { groupId: number; } -interface ZoneInfo { - zoneID: number; - zoneStatus: number; -} - -class BuffaloZcl extends Buffalo { +export class BuffaloZcl extends Buffalo { // TODO: remove read/write int "SB" versions in favor of plain numbers, implemented in buffalo.ts @@ -149,6 +149,7 @@ class BuffaloZcl extends Buffalo { } private writeUInt56SB(value: number[]): void { + // XXX: [uint32, uint32] param not following read return pattern [uint32, uint16, uint8] const temp = Buffer.alloc(8); temp.writeUInt32LE(value[1], 0); temp.writeUInt32LE(value[0], 4); @@ -168,7 +169,7 @@ class BuffaloZcl extends Buffalo { } private writeUInt64SB(value: string): void { - // XXX: not following pattern, should pass as number[] + // XXX: not following pattern, should pass as number[uint32, uint32] const msb = parseInt(value.slice(2,10), 16); const lsb = parseInt(value.slice(10), 16); this.writeUInt32(lsb); @@ -228,36 +229,15 @@ class BuffaloZcl extends Buffalo { this.writeUInt8(value.length); this.writeUtf8String(value); } else { + // XXX: value.length not written? this.writeBuffer(value, value.length); } } - private readCharStr(options: BuffaloZclOptions): Record | string { + private readCharStr(): string { const length = this.readUInt8(); - // TODO: this workaround should be moved to a custom type - if (options.attrId === 65281) { - const value: Record = {}; - - if (length === 0xFF) { - return value; - } - - // Xiaomi struct parsing - for (let i = 0; i < length; i++) { - const index = this.readUInt8(); - const dataType = this.readUInt8(); - value[index] = this.read(dataType, {}); - - if (this.position === this.buffer.length) { - break; - } - } - - return value; - } else { - return (length < 0xFF) ? this.readUtf8String(length) : ''; - } + return (length < 0xFF) ? this.readUtf8String(length) : ''; } private writeLongOctetStr(value: number[]): void { @@ -349,7 +329,12 @@ class BuffaloZcl extends Buffalo { const seconds = this.readUInt8(); const hundredths = this.readUInt8(); - return {hours, minutes, seconds, hundredths}; + return { + hours: hours < 0xFF ? hours : null, + minutes: minutes < 0xFF ? minutes : null, + seconds: seconds < 0xFF ? seconds : null, + hundredths: hundredths < 0xFF ? hundredths : null, + }; } private writeDate(value: ZclDate): void { @@ -360,12 +345,17 @@ class BuffaloZcl extends Buffalo { } private readDate(): ZclDate { - const year = this.readUInt8() + 1900; + const year = this.readUInt8(); const month = this.readUInt8(); const dayOfMonth = this.readUInt8(); const dayOfWeek = this.readUInt8(); - return {year, month, dayOfMonth, dayOfWeek}; + return { + year: year < 0xFF ? year + 1900 : null, + month: month < 0xFF ? month : null, + dayOfMonth: dayOfMonth < 0xFF ? dayOfMonth : null, + dayOfWeek: dayOfWeek < 0xFF ? dayOfWeek : null, + }; } //--- BuffaloZclDataType @@ -393,24 +383,28 @@ class BuffaloZcl extends Buffalo { for (const value of values) { this.writeUInt16(value.clstId); this.writeUInt8(value.len); - value.extField.forEach((entry, index) => { - this.write(extensionFieldSetsDateTypeLookup[value.clstId][index], entry, {}); - }); + let index = 0; + + for (const entry of value.extField) { + this.write(EXTENSION_FIELD_SETS_DATA_TYPE[value.clstId][index], entry, {}); + index++; + } } } private readExtensionFieldSets(): ExtensionFieldSet[] { const value: ExtensionFieldSet[] = []; + // XXX: doesn't work if buffer has more unrelated fields after this one while (this.isMore()) { const clstId = this.readUInt16(); const len = this.readUInt8(); const end = this.getPosition() + len; - let index = 0; const extField: unknown[] = []; + while (this.getPosition() < end) { - extField.push(this.read(extensionFieldSetsDateTypeLookup[clstId][index], {})); + extField.push(this.read(EXTENSION_FIELD_SETS_DATA_TYPE[clstId][index], {})); index++; } @@ -462,9 +456,9 @@ class BuffaloZcl extends Buffalo { return result; } - private writeGdpFrame(value: GdpCommissioningReply | GdpChannelConfiguration | GdpCustomReply): void { + private writeGdpFrame(value: GPDCommissioningReply | GPDChannelConfiguration | GPDCustomReply): void { if (value.commandID == 0xF0) { // Commissioning Reply - const v = value; + const v = value; const panIDPresent = v.options & (1 << 0); const gpdSecurityKeyPresent = v.options & (1 << 1); @@ -499,20 +493,20 @@ class BuffaloZcl extends Buffalo { this.writeUInt32(v.frameCounter); } } else if (value.commandID == 0xF3) { // Channel configuration - const v = value; + const v = value; this.writeUInt8(1); this.writeUInt8(v.operationalChannel & 0xF | ((v.basic ? 1 : 0) << 4)); } else if (value.commandID == 0xF4 || value.commandID == 0xF5 || (value.commandID >= 0xF7 && value.commandID <= 0xFF)) { // Other commands sent to GPD - const v = value; + const v = value; this.writeUInt8(v.buffer.length); this.writeBuffer(v.buffer, v.buffer.length); } } - private readGdpFrame(options: BuffaloZclOptions): Gdp | GdpChannelRequest | GdpAttributeReport | {raw: Buffer} | Record { + private readGdpFrame(options: BuffaloZclOptions): GPD | GPDChannelRequest | GPDAttributeReport | {raw: Buffer} | Record { // Commisioning if (options.payload?.commandID === 0xE0) { const frame = { @@ -611,6 +605,7 @@ class BuffaloZcl extends Buffalo { try { attribute = cluster.getAttribute(attributeID).name; } catch { + // this is spammy because of the many manufacturer-specific attributes not currently used logger.debug("Unknown attribute " + attributeID + " in cluster " + cluster.name, NS); } @@ -618,7 +613,7 @@ class BuffaloZcl extends Buffalo { } return frame; - } else if (this.position != this.buffer.length) { + } else if (this.isMore()) { return {raw: this.buffer.subarray(this.position)}; } else { return {}; @@ -630,7 +625,9 @@ class BuffaloZcl extends Buffalo { const indexes = value.indexes || []; const indicatorType = value.indicatorType || StructuredIndicatorType.Whole; const indicator = indexes.length + indicatorType; + this.writeUInt8(indicator); + for (const index of indexes) { this.writeUInt16(index); } @@ -644,15 +641,17 @@ class BuffaloZcl extends Buffalo { if (indicator === 0) { // no indexes, whole attribute value is to be read return {indicatorType: StructuredIndicatorType.Whole}; - } else { + } else if (indicator < StructuredIndicatorType.WriteAdd) { const indexes: StructuredSelector['indexes'] = []; - + for (let i = 0; i < indicator; i++) { const index = this.readUInt16(); indexes.push(index); } return {indexes}; + } else { + throw new Error(`Read structured selector was outside [0-15] range.`); } } @@ -661,6 +660,7 @@ class BuffaloZcl extends Buffalo { this.writeUInt8(dpValue.dp); this.writeUInt8(dpValue.datatype); const dataLen = dpValue.data.length; + // UInt16BE this.writeUInt8((dataLen >> 8) & 0xFF); this.writeUInt8(dataLen & 0xFF); this.writeBuffer(dpValue.data, dataLen); @@ -670,6 +670,7 @@ class BuffaloZcl extends Buffalo { private readListTuyaDataPointValues(): TuyaDataPointValue[] { const value: TuyaDataPointValue[] = []; + // XXX: doesn't work if buffer has more unrelated fields after this one while (this.isMore()) { try { const dp = this.readUInt8(); @@ -682,11 +683,13 @@ class BuffaloZcl extends Buffalo { break; } } + return value; } private writeListMiboxerZones(values: MiboxerZone[]): void { this.writeUInt8(values.length); + for (const value of values) { this.writeUInt16(value.groupId); this.writeUInt8(value.zoneNum); @@ -696,12 +699,14 @@ class BuffaloZcl extends Buffalo { private readListMiboxerZones(): MiboxerZone[] { const value: MiboxerZone[] = []; const len = this.readUInt8(); + for (let i = 0; i < len; i++) { - value.push({ - groupId: this.readUInt16(), - zoneNum: this.readUInt8(), - }); + const groupId = this.readUInt16(); + const zoneNum = this.readUInt8(); + + value.push({groupId, zoneNum}); } + return value; } @@ -716,6 +721,31 @@ class BuffaloZcl extends Buffalo { return value; } + // private writeMiStruct(value: Record): { + // XXX: read only? + // } + + private readMiStruct(): Record { + const length = this.readUInt8(); + const value: Record = {}; + + if (length === 0xFF) { + return value; + } + + for (let i = 0; i < length; i++) { + const index = this.readUInt8(); + const dataType = this.readUInt8(); + value[index] = this.read(dataType, {}); + + if (this.position === this.buffer.length) { + break; + } + } + + return value; + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any public write(type: DataType | BuffaloZclDataType, value: any, options: BuffaloZclOptions): void { switch (type) { @@ -870,6 +900,7 @@ class BuffaloZcl extends Buffalo { return this.writeListThermoTransitions(value); } case BuffaloZclDataType.BUFFER: { + // XXX: inconsistent with read that allows partial with options.length, here always "whole" return this.writeBuffer(value, value.length); } case BuffaloZclDataType.GDP_FRAME: { @@ -992,7 +1023,7 @@ class BuffaloZcl extends Buffalo { return this.readOctetStr(); } case DataType.CHAR_STR: { - return this.readCharStr(options); + return this.readCharStr(); } case DataType.LONG_OCTET_STR: { return this.readLongOctetStr(); @@ -1087,10 +1118,11 @@ class BuffaloZcl extends Buffalo { case BuffaloZclDataType.BIG_ENDIAN_UINT24: { return this.readBigEndianUInt24(); } + case BuffaloZclDataType.MI_STRUCT: { + return this.readMiStruct(); + } } throw new Error(`Read for '${type}' not available`); } -} - -export default BuffaloZcl; +} \ No newline at end of file diff --git a/src/zcl/definition/cluster.ts b/src/zspec/zcl/definition/cluster.ts similarity index 98% rename from src/zcl/definition/cluster.ts rename to src/zspec/zcl/definition/cluster.ts index 5b1a1ec00d..0d559d9a4e 100644 --- a/src/zcl/definition/cluster.ts +++ b/src/zspec/zcl/definition/cluster.ts @@ -1,10 +1,10 @@ /* eslint max-len: 0 */ -import DataType from './dataType'; import {ClusterDefinition, ClusterName} from './tstype'; -import BuffaloZclDataType from './buffaloZclDataType'; -import ManufacturerCode from './manufacturerCode'; +import {DataType, BuffaloZclDataType, ParameterCondition} from './enums'; +import {ManufacturerCode} from './manufacturerCode'; +import {Status} from './status'; -const Clusters: Readonly>> = { +export const Clusters: Readonly>> = { genBasic: { ID: 0, attributes: { @@ -1113,10 +1113,10 @@ const Clusters: Readonly>> = { ID: 2, parameters: [ {name: 'status', type: DataType.UINT8}, - {name: 'manufacturerCode', type: DataType.UINT16, conditions: [{type: 'statusEquals', value: 0}]}, - {name: 'imageType', type: DataType.UINT16, conditions: [{type: 'statusEquals', value: 0}]}, - {name: 'fileVersion', type: DataType.UINT32, conditions: [{type: 'statusEquals', value: 0}]}, - {name: 'imageSize', type: DataType.UINT32, conditions: [{type: 'statusEquals', value: 0}]}, + {name: 'manufacturerCode', type: DataType.UINT16, conditions: [{type: ParameterCondition.STATUS_EQUAL, value: Status.SUCCESS}]}, + {name: 'imageType', type: DataType.UINT16, conditions: [{type: ParameterCondition.STATUS_EQUAL, value: Status.SUCCESS}]}, + {name: 'fileVersion', type: DataType.UINT32, conditions: [{type: ParameterCondition.STATUS_EQUAL, value: Status.SUCCESS}]}, + {name: 'imageSize', type: DataType.UINT32, conditions: [{type: ParameterCondition.STATUS_EQUAL, value: Status.SUCCESS}]}, ], }, imageBlockResponse: { @@ -1197,30 +1197,30 @@ const Clusters: Readonly>> = { ID: 0, parameters: [ {name: 'options', type: DataType.UINT16}, - {name: 'srcID', type: DataType.UINT32, conditions: [{type: 'bitFieldEnum', param:'options', offset: 0, size: 3, value: 0b000}]}, - {name: 'gpdIEEEAddr', type: DataType.IEEE_ADDR, conditions: [{type: 'bitFieldEnum', param:'options', offset: 0, size: 3, value: 0b010}]}, - {name: 'gpdEndpoint', type: DataType.UINT8, conditions: [{type: 'bitFieldEnum', param:'options', offset: 0, size: 3, value: 0b010}]}, + {name: 'srcID', type: DataType.UINT32, conditions: [{type: ParameterCondition.BITFIELD_ENUM, param:'options', offset: 0, size: 3, value: 0b000}]}, + {name: 'gpdIEEEAddr', type: DataType.IEEE_ADDR, conditions: [{type: ParameterCondition.BITFIELD_ENUM, param:'options', offset: 0, size: 3, value: 0b010}]}, + {name: 'gpdEndpoint', type: DataType.UINT8, conditions: [{type: ParameterCondition.BITFIELD_ENUM, param:'options', offset: 0, size: 3, value: 0b010}]}, {name: 'frameCounter', type: DataType.UINT32}, {name: 'commandID', type: DataType.UINT8}, {name: 'payloadSize', type: DataType.UINT8}, {name: 'commandFrame', type: BuffaloZclDataType.GDP_FRAME}, - {name: 'gppNwkAddr', type: DataType.UINT16,conditions: [{type: 'bitMaskSet', param:'options', mask: 0x4000}]}, - {name: 'gppGddLink', type: DataType.UINT8,conditions: [{type: 'bitMaskSet', param:'options', mask: 0x4000}]}, + {name: 'gppNwkAddr', type: DataType.UINT16,conditions: [{type: ParameterCondition.BITMASK_SET, param:'options', mask: 0x4000}]}, + {name: 'gppGddLink', type: DataType.UINT8,conditions: [{type: ParameterCondition.BITMASK_SET, param:'options', mask: 0x4000}]}, ], }, commissioningNotification: { ID: 4, parameters: [ {name: 'options', type: DataType.UINT16}, - {name: 'srcID', type: DataType.UINT32, conditions: [{type: 'bitFieldEnum', param:'options', offset: 0, size: 3, value: 0b000}]}, - {name: 'gpdIEEEAddr', type: DataType.IEEE_ADDR, conditions: [{type: 'bitFieldEnum', param:'options', offset: 0, size: 3, value: 0b010}]}, - {name: 'gpdEndpoint', type: DataType.UINT8, conditions: [{type: 'bitFieldEnum', param:'options', offset: 0, size: 3, value: 0b010}]}, + {name: 'srcID', type: DataType.UINT32, conditions: [{type: ParameterCondition.BITFIELD_ENUM, param:'options', offset: 0, size: 3, value: 0b000}]}, + {name: 'gpdIEEEAddr', type: DataType.IEEE_ADDR, conditions: [{type: ParameterCondition.BITFIELD_ENUM, param:'options', offset: 0, size: 3, value: 0b010}]}, + {name: 'gpdEndpoint', type: DataType.UINT8, conditions: [{type: ParameterCondition.BITFIELD_ENUM, param:'options', offset: 0, size: 3, value: 0b010}]}, {name: 'frameCounter', type: DataType.UINT32}, {name: 'commandID', type: DataType.UINT8}, {name: 'payloadSize', type: DataType.UINT8}, {name: 'commandFrame', type: BuffaloZclDataType.GDP_FRAME}, - {name: 'gppNwkAddr', type: DataType.UINT16,conditions: [{type: 'bitMaskSet', param:'options', mask: 0x800}]}, - {name: 'gppGddLink', type: DataType.UINT8,conditions: [{type: 'bitMaskSet', param:'options', mask: 0x800}]}, + {name: 'gppNwkAddr', type: DataType.UINT16,conditions: [{type: ParameterCondition.BITMASK_SET, param:'options', mask: 0x800}]}, + {name: 'gppGddLink', type: DataType.UINT8,conditions: [{type: ParameterCondition.BITMASK_SET, param:'options', mask: 0x800}]}, ], }, }, @@ -1231,9 +1231,9 @@ const Clusters: Readonly>> = { {name: 'options', type: DataType.UINT8}, {name: 'tempMaster', type: DataType.UINT16}, {name: 'tempMasterTx', type: DataType.UINT8}, - {name: 'srcID', type: DataType.UINT32, conditions: [{type: 'bitFieldEnum', param:'options', offset: 0, size: 3, value: 0b000}]}, - {name: 'gpdIEEEAddr', type: DataType.IEEE_ADDR, conditions: [{type: 'bitFieldEnum', param:'options', offset: 0, size: 3, value: 0b010}]}, - {name: 'gpdEndpoint', type: DataType.UINT8, conditions: [{type: 'bitFieldEnum', param:'options', offset: 0, size: 3, value: 0b010}]}, + {name: 'srcID', type: DataType.UINT32, conditions: [{type: ParameterCondition.BITFIELD_ENUM, param:'options', offset: 0, size: 3, value: 0b000}]}, + {name: 'gpdIEEEAddr', type: DataType.IEEE_ADDR, conditions: [{type: ParameterCondition.BITFIELD_ENUM, param:'options', offset: 0, size: 3, value: 0b010}]}, + {name: 'gpdEndpoint', type: DataType.UINT8, conditions: [{type: ParameterCondition.BITFIELD_ENUM, param:'options', offset: 0, size: 3, value: 0b010}]}, {name: 'gpdCmd', type: DataType.UINT8}, {name: 'gpdPayload', type: BuffaloZclDataType.GDP_FRAME}, ], @@ -1242,18 +1242,18 @@ const Clusters: Readonly>> = { ID: 1, parameters: [ {name: 'options', type: DataType.UINT24}, - {name: 'srcID', type: DataType.UINT32, conditions: [{type: 'bitFieldEnum', param:'options', offset: 0, size: 3, value: 0b000}]}, - {name: 'gpdIEEEAddr', type: DataType.IEEE_ADDR, conditions: [{type: 'bitFieldEnum', param:'options', offset: 0, size: 3, value: 0b010}]}, - {name: 'gpdEndpoint', type: DataType.UINT8, conditions: [{type: 'bitFieldEnum', param:'options', offset: 0, size: 3, value: 0b010}]}, - {name: 'sinkIEEEAddr', type: DataType.IEEE_ADDR, conditions: [{type: 'bitFieldEnum', param:'options', offset: 4, size: 3, value: 0b110}]}, - {name: 'sinkIEEEAddr', type: DataType.IEEE_ADDR, conditions: [{type: 'bitFieldEnum', param:'options', offset: 4, size: 3, value: 0b000}]}, - {name: 'sinkNwkAddr', type: DataType.UINT16, conditions: [{type: 'bitFieldEnum', param:'options', offset: 4, size: 3, value: 0b110}]}, - {name: 'sinkNwkAddr', type: DataType.UINT16, conditions: [{type: 'bitFieldEnum', param:'options', offset: 4, size: 3, value: 0b000}]}, - {name: 'sinkGroupID', type: DataType.UINT16, conditions: [{type: 'bitFieldEnum', param:'options', offset: 4, size: 3, value: 0b100}]}, - {name: 'sinkGroupID', type: DataType.UINT16, conditions: [{type: 'bitFieldEnum', param:'options', offset: 4, size: 3, value: 0b010}]}, - {name: 'deviceID', type: DataType.UINT8, conditions: [{type: 'bitMaskSet', param:'options', mask: 0x0008}]}, - {name: 'frameCounter', type: DataType.UINT32, conditions: [{type: 'bitMaskSet', param:'options', mask: 0x4000}]}, - {name: 'gpdKey', type: DataType.SEC_KEY, conditions: [{type: 'bitMaskSet', param:'options', mask: 0x8000}]}, + {name: 'srcID', type: DataType.UINT32, conditions: [{type: ParameterCondition.BITFIELD_ENUM, param:'options', offset: 0, size: 3, value: 0b000}]}, + {name: 'gpdIEEEAddr', type: DataType.IEEE_ADDR, conditions: [{type: ParameterCondition.BITFIELD_ENUM, param:'options', offset: 0, size: 3, value: 0b010}]}, + {name: 'gpdEndpoint', type: DataType.UINT8, conditions: [{type: ParameterCondition.BITFIELD_ENUM, param:'options', offset: 0, size: 3, value: 0b010}]}, + {name: 'sinkIEEEAddr', type: DataType.IEEE_ADDR, conditions: [{type: ParameterCondition.BITFIELD_ENUM, param:'options', offset: 4, size: 3, value: 0b110}]}, + {name: 'sinkIEEEAddr', type: DataType.IEEE_ADDR, conditions: [{type: ParameterCondition.BITFIELD_ENUM, param:'options', offset: 4, size: 3, value: 0b000}]}, + {name: 'sinkNwkAddr', type: DataType.UINT16, conditions: [{type: ParameterCondition.BITFIELD_ENUM, param:'options', offset: 4, size: 3, value: 0b110}]}, + {name: 'sinkNwkAddr', type: DataType.UINT16, conditions: [{type: ParameterCondition.BITFIELD_ENUM, param:'options', offset: 4, size: 3, value: 0b000}]}, + {name: 'sinkGroupID', type: DataType.UINT16, conditions: [{type: ParameterCondition.BITFIELD_ENUM, param:'options', offset: 4, size: 3, value: 0b100}]}, + {name: 'sinkGroupID', type: DataType.UINT16, conditions: [{type: ParameterCondition.BITFIELD_ENUM, param:'options', offset: 4, size: 3, value: 0b010}]}, + {name: 'deviceID', type: DataType.UINT8, conditions: [{type: ParameterCondition.BITMASK_SET, param:'options', mask: 0x0008}]}, + {name: 'frameCounter', type: DataType.UINT32, conditions: [{type: ParameterCondition.BITMASK_SET, param:'options', mask: 0x4000}]}, + {name: 'gpdKey', type: DataType.SEC_KEY, conditions: [{type: ParameterCondition.BITMASK_SET, param:'options', mask: 0x8000}]}, ], }, commisioningMode: { @@ -5724,5 +5724,3 @@ const Clusters: Readonly>> = { commandsResponse: {}, }, }; - -export default Clusters; diff --git a/src/zspec/zcl/definition/consts.ts b/src/zspec/zcl/definition/consts.ts new file mode 100644 index 0000000000..d80e24e0da --- /dev/null +++ b/src/zspec/zcl/definition/consts.ts @@ -0,0 +1,24 @@ +/** Mapping of power source bits to descriptive string. */ +export const POWER_SOURCES: Readonly<{[s: number]: string}> = { + 0: 'Unknown', + 1: 'Mains (single phase)', + 2: 'Mains (3 phase)', + 3: 'Battery', + 4: 'DC Source', + 5: 'Emergency mains constantly powered', + 6: 'Emergency mains and transfer switch', +}; + +/** Mapping of device type to ID */ +export const ENDPOINT_DEVICE_TYPE: Readonly<{[s: string]: number}> = { + 'ZLLOnOffLight' : 0x0000, + 'ZLLOnOffPluginUnit' : 0x0010, + 'ZLLDimmableLight' : 0x0100, + 'ZLLDimmablePluginUnit' : 0x0110, + 'ZLLColorLight' : 0x0200, + 'ZLLExtendedColorLight' : 0x0210, + 'ZLLColorTemperatureLight' : 0x0220, + 'HAOnOffLight' : 0x0100, + 'HADimmableLight' : 0x0101, + 'HAColorLight' : 0x0102, +}; diff --git a/src/zcl/definition/dataType.ts b/src/zspec/zcl/definition/enums.ts similarity index 73% rename from src/zcl/definition/dataType.ts rename to src/zspec/zcl/definition/enums.ts index 6884dfb156..d51738027d 100644 --- a/src/zcl/definition/dataType.ts +++ b/src/zspec/zcl/definition/enums.ts @@ -5,7 +5,7 @@ * used to measure the value of properties in the real world that vary continuously over a range. * - Values of discrete data types only have meaning as individual values and may not be added or subtracted. */ -enum DataType { +export enum DataType { /** length=0 */ NO_DATA = 0x00, @@ -131,4 +131,64 @@ enum DataType { UNKNOWN = 0xFF, } -export default DataType; \ No newline at end of file +/** @TODO strings for backwards compat in tests. Should be moved to numbers. */ +export enum DataTypeClass { + ANALOG = 'ANALOG', + DISCRETE = 'DISCRETE', +} + +export enum BuffaloZclDataType { + USE_DATA_TYPE = 1000, + LIST_UINT8 = 1001, + LIST_UINT16 = 1002, + LIST_UINT24 = 1003, + LIST_UINT32 = 1004, + LIST_ZONEINFO = 1005, + EXTENSION_FIELD_SETS = 1006, + LIST_THERMO_TRANSITIONS = 1007, + BUFFER = 1008, + GDP_FRAME = 1009, + STRUCTURED_SELECTOR = 1010, + LIST_TUYA_DATAPOINT_VALUES = 1011, + LIST_MIBOXER_ZONES = 1012, + BIG_ENDIAN_UINT24 = 1013, + MI_STRUCT = 1014, +} + +/** @TODO strings for backwards compat in tests. Should be moved to numbers. */ +export enum ParameterCondition { + STATUS_EQUAL = 'statusEquals', + STATUS_NOT_EQUAL = 'statusNotEquals', + MINIMUM_REMAINING_BUFFER_BYTES = 'minimumRemainingBufferBytes', + DIRECTION_EQUAL = 'directionEquals', + BITMASK_SET = 'bitMaskSet', + BITFIELD_ENUM = 'bitFieldEnum', + DATA_TYPE_CLASS_EQUAL = 'dataTypeValueTypeEquals', +} + + +export enum FrameType { + GLOBAL = 0, + SPECIFIC = 1, +} + +export enum Direction { + CLIENT_TO_SERVER = 0, + SERVER_TO_CLIENT = 1, +} + +/** + * The upper 4 bits of the Indicator subfield for Attributes Structured commands. + */ +export enum StructuredIndicatorType { + /** + * Write: Only for attributes of type other than array, structure, set or bag + * + * Read: Only for attributes of type other than array or structure + */ + Whole = 0x00, + /** Add element to the set/bag */ + WriteAdd = 0x10, + /** Remove element from the set/bag */ + WriteRemove = 0x20, +} diff --git a/src/zcl/definition/foundation.ts b/src/zspec/zcl/definition/foundation.ts similarity index 82% rename from src/zcl/definition/foundation.ts rename to src/zspec/zcl/definition/foundation.ts index f11aa02dc5..9041004458 100644 --- a/src/zcl/definition/foundation.ts +++ b/src/zspec/zcl/definition/foundation.ts @@ -1,10 +1,7 @@ /* eslint max-len: 0 */ - -import DataType from './dataType'; -import BuffaloZclDataType from './buffaloZclDataType'; -import Status from './status'; -import Direction from './direction'; import {ParameterDefinition} from './tstype'; +import {DataType, BuffaloZclDataType, Direction, ParameterCondition, DataTypeClass} from './enums'; +import {Status} from './status'; export type FoundationCommandName = ( 'read' | 'readRsp' | 'write' | 'writeUndiv' | 'writeRsp' | 'writeNoRsp' | 'configReport' | 'configReportRsp' | @@ -20,7 +17,7 @@ interface FoundationDefinition { response?: number; } -const Foundation: Readonly>> = { +export const Foundation: Readonly>> = { /** Read Attributes */ read: { ID: 0x00, @@ -37,8 +34,8 @@ const Foundation: Readonly Attribute; + hasAttribute: (key: number | string) => boolean; + getCommand: (key: number | string) => Command; + getCommandResponse: (key: number | string) => Command; +} + +export interface ClusterDefinition { + ID: number; + manufacturerCode?: number; + attributes: Readonly>>; + commands: Readonly>>; + commandsResponse: Readonly>>; +} + +export interface CustomClusters {[k: string]: ClusterDefinition}; + + +export type ClusterName = ( + 'genBasic' | 'genPowerCfg' | 'genDeviceTempCfg' | 'genIdentify' | 'genGroups' | 'genScenes' | 'genOnOff' | + 'genOnOffSwitchCfg' | 'genLevelCtrl' | 'genAlarms' | 'genTime' | 'genRssiLocation' | 'genAnalogInput' | 'genAnalogOutput' | + 'genAnalogValue' | 'genBinaryInput' | 'genBinaryOutput' | 'genBinaryValue' | 'genMultistateInput' | 'genMultistateOutput' | + 'genMultistateValue' | 'genCommissioning' | 'genOta' | 'genPollCtrl' | 'greenPower' | 'mobileDeviceCfg' | 'neighborCleaning' | + 'nearestGateway' | 'closuresShadeCfg' | 'closuresDoorLock' | 'closuresWindowCovering' | 'barrierControl' | 'hvacPumpCfgCtrl' | + 'hvacThermostat' | 'hvacFanCtrl' | 'hvacDehumidificationCtrl' | 'hvacUserInterfaceCfg' | 'lightingColorCtrl' | 'lightingBallastCfg' | + 'msIlluminanceMeasurement' | 'msIlluminanceLevelSensing' | 'msTemperatureMeasurement' | 'msPressureMeasurement' | 'msFlowMeasurement' | + 'msRelativeHumidity' | 'msOccupancySensing' | 'msSoilMoisture' | 'pHMeasurement' | 'msCO2' | 'pm25Measurement' | 'ssIasZone' | + 'ssIasAce' | 'ssIasWd' | 'piGenericTunnel' | 'piBacnetProtocolTunnel' | 'piAnalogInputReg' | 'piAnalogInputExt' | 'piAnalogOutputReg' | + 'piAnalogOutputExt' | 'piAnalogValueReg' | 'piAnalogValueExt' | 'piBinaryInputReg' | 'piBinaryInputExt' | 'piBinaryOutputReg' | + 'piBinaryOutputExt' | 'piBinaryValueReg' | 'piBinaryValueExt' | 'piMultistateInputReg' | 'piMultistateInputExt' | 'piMultistateOutputReg' | + 'piMultistateOutputExt' | 'piMultistateValueReg' | 'piMultistateValueExt' | 'pi11073ProtocolTunnel' | 'piIso7818ProtocolTunnel' | + 'piRetailTunnel' | 'seMetering' | 'tunneling' | 'telecommunicationsInformation' | 'telecommunicationsVoiceOverZigbee' | + 'telecommunicationsChatting' | 'haApplianceIdentification' | 'haMeterIdentification' | 'haApplianceEventsAlerts' | 'haApplianceStatistics' | + 'haElectricalMeasurement' | 'haDiagnostic' | 'touchlink' | 'manuSpecificIkeaAirPurifier' | 'msIkeaVocIndexMeasurement' | + 'manuSpecificClusterAduroSmart' | 'manuSpecificOsram' | 'manuSpecificPhilips' | 'manuSpecificPhilips2' | 'manuSpecificSinope' | + 'manuSpecificLegrandDevices' | 'manuSpecificLegrandDevices2' | + 'manuSpecificLegrandDevices3' | 'manuSpecificNiko1' | 'manuSpecificNiko2' | 'wiserDeviceInfo' | 'manuSpecificTuya' | 'manuSpecificLumi' | + 'liXeePrivate' | 'manuSpecificTuya_2' | 'manuSpecificTuya_3' | 'manuSpecificCentraliteHumidity' | 'manuSpecificSmartThingsArrivalSensor' | + 'manuSpecificSamsungAccelerometer' | 'heimanSpecificFormaldehydeMeasurement' | 'heimanSpecificAirQuality' | 'heimanSpecificScenes' | + 'tradfriButton' | 'heimanSpecificInfraRedRemote' | 'develcoSpecificAirQuality' | 'schneiderSpecificPilotMode' | + 'elkoOccupancySettingClusterServer' | 'elkoSwitchConfigurationClusterServer' | 'manuSpecificSchneiderLightSwitchConfiguration' | + 'manuSpecificSchneiderFanSwitchConfiguration' | 'sprutDevice' | 'sprutVoc' | 'sprutNoise' | 'sprutIrBlaster' | 'manuSpecificSiglisZigfred' | + 'manuSpecificInovelli' | 'owonClearMetering' | 'zosungIRTransmit' | 'zosungIRControl' | 'manuSpecificBosch' | 'manuSpecificBosch3' | + 'manuSpecificBosch5' | 'manuSpecificBosch7' | 'manuSpecificBosch8' | 'manuSpecificBosch9' | 'manuSpecificBosch10' | 'manuSpecificBosch11' | + 'manuSpecificAssaDoorLock' | 'manuSpecificDoorman' | 'manuSpecificNodOnPilotWire' | 'manuSpecificProfalux1' | 'manuSpecificAmazonWWAH' +); \ No newline at end of file diff --git a/src/zspec/zcl/index.ts b/src/zspec/zcl/index.ts new file mode 100644 index 0000000000..41661f820f --- /dev/null +++ b/src/zspec/zcl/index.ts @@ -0,0 +1,10 @@ +export * from './definition/consts'; +export * from './definition/enums'; +export {Clusters} from './definition/cluster'; +export {Status} from './definition/status'; +export {Foundation} from './definition/foundation'; +export {ManufacturerCode} from './definition/manufacturerCode'; +export {ZclFrame as Frame} from './zclFrame'; +export {ZclHeader as Header} from './zclHeader'; +export {ZclStatusError as StatusError} from './zclStatusError'; +export * as Utils from './utils'; diff --git a/src/zspec/zcl/utils.ts b/src/zspec/zcl/utils.ts new file mode 100644 index 0000000000..c5fc788dac --- /dev/null +++ b/src/zspec/zcl/utils.ts @@ -0,0 +1,276 @@ +import {Clusters} from './definition/cluster'; +import {Foundation} from './definition/foundation'; +import {DataType, DataTypeClass} from './definition/enums'; +import {FoundationCommandName} from './definition/foundation'; +import {Attribute, Cluster, ClusterDefinition, ClusterName, Command, CustomClusters} from './definition/tstype'; + +const DATA_TYPE_CLASS_DISCRETE = [ + DataType.DATA8, DataType.DATA16, DataType.DATA24, DataType.DATA32, DataType.DATA40, + DataType.DATA48, DataType.DATA56, DataType.DATA64, DataType.BOOLEAN, + DataType.BITMAP8, DataType.BITMAP16, DataType.BITMAP24, DataType.BITMAP32, DataType.BITMAP40, + DataType.BITMAP48, DataType.BITMAP56, DataType.BITMAP64, DataType.ENUM8, DataType.ENUM16, + DataType.OCTET_STR, DataType.CHAR_STR, DataType.LONG_OCTET_STR, DataType.LONG_CHAR_STR, DataType.ARRAY, + DataType.STRUCT, DataType.SET, DataType.BAG, DataType.CLUSTER_ID, DataType.ATTR_ID, DataType.BAC_OID, + DataType.IEEE_ADDR, DataType.SEC_KEY, +]; +const DATA_TYPE_CLASS_ANALOG = [ + DataType.UINT8, DataType.UINT16, DataType.UINT24, DataType.UINT32, DataType.UINT40, + DataType.UINT48, DataType.UINT56, + DataType.INT8, DataType.INT16, DataType.INT24, DataType.INT32, DataType.INT40, + DataType.INT48, DataType.INT56, DataType.SEMI_PREC, DataType.SINGLE_PREC, DataType.DOUBLE_PREC, + DataType.TOD, DataType.DATE, DataType.UTC, +]; + +export function getDataTypeClass(dataType: DataType): DataTypeClass { + if (DATA_TYPE_CLASS_DISCRETE.includes(dataType)) { + return DataTypeClass.DISCRETE; + } else if (DATA_TYPE_CLASS_ANALOG.includes(dataType)) { + return DataTypeClass.ANALOG; + } else { + throw new Error(`Don't know value type for '${DataType[dataType]}'`); + } +} + +function hasCustomClusters(customClusters: CustomClusters): boolean { + // XXX: was there a good reason to not set the parameter `customClusters` optional? it would allow simple undefined check + // below is twice faster than checking `Object.keys(customClusters).length` + for (const k in customClusters) return true; + return false; +} + +function findClusterNameByID(id: number, manufacturerCode: number | null, clusters: CustomClusters): [name: string, partialMatch: boolean] { + let name: string; + // if manufacturer code is given, consider partial match if didn't match against manufacturer code + let partialMatch: boolean = (manufacturerCode != null); + + for (const clusterName in clusters) { + const cluster = clusters[clusterName as ClusterName]; + + if (cluster.ID === id) { + // priority on first match when matching only ID + /* istanbul ignore else */ + if (name == undefined) { + name = clusterName; + } + + if (manufacturerCode && (cluster.manufacturerCode === manufacturerCode)) { + name = clusterName; + partialMatch = false; + break; + } else if (!cluster.manufacturerCode) { + name = clusterName; + break; + } + } + } + + return [name, partialMatch]; +} + +function getClusterDefinition(key: string | number, manufacturerCode: number | null, customClusters: CustomClusters) + : {name: string, cluster: ClusterDefinition} { + let name: string; + + if (typeof key === 'number') { + let partialMatch: boolean; + + // custom clusters have priority over Zcl clusters, except in case of better match (see below) + [name, partialMatch] = findClusterNameByID(key, manufacturerCode, customClusters); + + if (!name) { + [name, partialMatch] = findClusterNameByID(key, manufacturerCode, Clusters); + } else if (partialMatch) { + let zclName: string; + [zclName, partialMatch] = findClusterNameByID(key, manufacturerCode, Clusters); + + // Zcl clusters contain a better match, use that one + if (zclName != undefined && !partialMatch) { + name = zclName; + } + } + } else { + name = key; + } + + let cluster = (name != undefined) && hasCustomClusters(customClusters) ? + { + ...Clusters[name as ClusterName], + ...customClusters[name],// should override Zcl clusters + } + : Clusters[name as ClusterName]; + + if (!cluster) { + if (typeof key === 'number') { + name = key.toString(); + cluster = {attributes: {}, commands: {}, commandsResponse: {}, manufacturerCode: null, ID: key}; + } else { + throw new Error(`Cluster with name '${key}' does not exist`); + } + } + + return {name, cluster}; +} + +function createCluster(name: string, cluster: ClusterDefinition, manufacturerCode: number = null): Cluster { + const attributes: {[s: string]: Attribute} = Object.assign( + {}, + ...Object.entries(cluster.attributes).map(([k, v]) => ({[k]: {...v, name: k}})) + ); + const commands: {[s: string]: Command} = Object.assign( + {}, + ...Object.entries(cluster.commands).map(([k, v]) => ({[k]: {...v, name: k}})) + ); + const commandsResponse: {[s: string]: Command} = Object.assign( + {}, + ...Object.entries(cluster.commandsResponse).map(([k, v]) => ({[k]: {...v, name: k}})) + ); + + const getAttributeInternal = (key: number | string): Attribute => { + if (typeof key === 'number') { + let partialMatchAttr: Attribute = null; + + for (const attrKey in attributes) { + const attr = attributes[attrKey]; + + if (attr.ID === key) { + if (manufacturerCode && (attr.manufacturerCode === manufacturerCode)) { + return attr; + } + + if (attr.manufacturerCode == null) { + partialMatchAttr = attr; + } + } + } + + return partialMatchAttr; + } else { + for (const attrKey in attributes) { + const attr = attributes[attrKey]; + + if (attr.name === key) { + return attr; + } + } + } + + return null; + }; + + const getAttribute = (key: number | string): Attribute => { + const result = getAttributeInternal(key); + if (!result) { + throw new Error(`Cluster '${name}' has no attribute '${key}'`); + } + + return result; + }; + + const hasAttribute = (key: number | string): boolean => { + const result = getAttributeInternal(key); + return !!result; + }; + + const getCommand = (key: number | string): Command => { + if (typeof key === 'number') { + for (const cmdKey in commands) { + const cmd = commands[cmdKey]; + + if (cmd.ID === key) { + return cmd; + } + } + } else { + for (const cmdKey in commands) { + const cmd = commands[cmdKey]; + + if (cmd.name === key) { + return cmd; + } + } + } + + throw new Error(`Cluster '${name}' has no command '${key}'`); + }; + + const getCommandResponse = (key: number | string): Command => { + if (typeof key === 'number') { + for (const cmdKey in commandsResponse) { + const cmd = commandsResponse[cmdKey]; + + if (cmd.ID === key) { + return cmd; + } + } + } else { + for (const cmdKey in commandsResponse) { + const cmd = commandsResponse[cmdKey]; + + if (cmd.name === key) { + return cmd; + } + } + } + + throw new Error(`Cluster '${name}' has no command response '${key}'`); + }; + + return { + ID: cluster.ID, + attributes, + manufacturerCode: cluster.manufacturerCode, + name, + commands, + commandsResponse, + getAttribute, + hasAttribute, + getCommand, + getCommandResponse, + }; +} + +export function getCluster( + key: string | number, + manufacturerCode: number | null, + customClusters: CustomClusters, +): Cluster { + const {name, cluster} = getClusterDefinition(key, manufacturerCode, customClusters); + return createCluster(name, cluster, manufacturerCode); +} + +export function getGlobalCommand(key: number | string): Command { + let name: FoundationCommandName; + + if (typeof key === 'number') { + for (const commandName in Foundation) { + if (Foundation[commandName as FoundationCommandName].ID === key) { + name = commandName as FoundationCommandName; + break; + } + } + } else { + name = key as FoundationCommandName; + } + + const command = Foundation[name]; + + if (!command) { + throw new Error(`Global command with key '${key}' does not exist`); + } + + const result: Command = { + ID: command.ID, + name, + parameters: command.parameters, + }; + + if (command.response != undefined) { + result.response = command.response; + } + + return result; +} + +export function isClusterName(name: string): name is ClusterName { + return (name in Clusters); +} diff --git a/src/zcl/zclFrame.ts b/src/zspec/zcl/zclFrame.ts similarity index 76% rename from src/zcl/zclFrame.ts rename to src/zspec/zcl/zclFrame.ts index d935031d70..e1fa4db962 100644 --- a/src/zcl/zclFrame.ts +++ b/src/zspec/zcl/zclFrame.ts @@ -1,11 +1,10 @@ -import {Direction, Foundation, DataType, BuffaloZclDataType} from './definition'; -import ZclHeader from './zclHeader'; import * as Utils from './utils'; -import BuffaloZcl from './buffaloZcl'; -import * as TsType from './tstype'; -import {TsType as DefinitionTsType, FrameType} from './definition'; -import {ClusterName, CustomClusters} from './definition/tstype'; -import {FoundationCommandName} from './definition/foundation'; +import {BuffaloZclOptions, Cluster, Command, ClusterName, CustomClusters, ParameterDefinition} from './definition/tstype'; +import {Direction, DataType, BuffaloZclDataType, FrameType, ParameterCondition} from './definition/enums'; +import {FoundationCommandName, Foundation} from './definition/foundation'; +import {ZclHeader} from './zclHeader'; +import {BuffaloZcl} from './buffaloZcl'; +import {Status} from './definition/status'; // eslint-disable-next-line type ZclPayload = any; @@ -18,13 +17,13 @@ const ListTypes: number[] = [ BuffaloZclDataType.LIST_ZONEINFO, ]; -class ZclFrame { +export class ZclFrame { public readonly header: ZclHeader; public readonly payload: ZclPayload; - public readonly cluster: TsType.Cluster; - public readonly command: TsType.Command; + public readonly cluster: Cluster; + public readonly command: Command; - private constructor(header: ZclHeader, payload: ZclPayload, cluster: TsType.Cluster, command: TsType.Command) { + private constructor(header: ZclHeader, payload: ZclPayload, cluster: Cluster, command: Command) { this.header = header; this.payload = payload; this.cluster = cluster; @@ -44,7 +43,7 @@ class ZclFrame { payload: ZclPayload, customClusters: CustomClusters, reservedBits = 0 ): ZclFrame { const cluster = Utils.getCluster(clusterKey, manufacturerCode, customClusters); - let command: TsType.Command = null; + let command: Command = null; if (frameType === FrameType.GLOBAL) { command = Utils.getGlobalCommand(commandKey); } else { @@ -83,7 +82,7 @@ class ZclFrame { if (command.parseStrategy === 'repetitive') { for (const entry of this.payload) { for (const parameter of command.parameters) { - const options: TsType.BuffaloZclOptions = {}; + const options: BuffaloZclOptions = {}; if (!ZclFrame.conditionsValid(parameter, entry, null)) { continue; @@ -125,7 +124,7 @@ class ZclFrame { continue; } - if (!this.payload.hasOwnProperty(parameter.name)) { + if (this.payload[parameter.name] == undefined) { throw new Error(`Parameter '${parameter.name}' is missing`); } @@ -144,7 +143,7 @@ class ZclFrame { const buffalo = new BuffaloZcl(buffer, header.length); const cluster = Utils.getCluster(clusterID, header.manufacturerCode, customClusters); - let command: TsType.Command = null; + let command: Command = null; if (header.isGlobal) { command = Utils.getGlobalCommand(header.commandIdentifier); } else { @@ -157,7 +156,7 @@ class ZclFrame { return new ZclFrame(header, payload, cluster, command); } - private static parsePayload(header: ZclHeader, cluster: TsType.Cluster, buffalo: BuffaloZcl): ZclPayload { + private static parsePayload(header: ZclHeader, cluster: Cluster, buffalo: BuffaloZcl): ZclPayload { if (header.isGlobal) { return this.parsePayloadGlobal(header, buffalo); } else if (header.isSpecific) { @@ -167,13 +166,13 @@ class ZclFrame { } } - private static parsePayloadCluster(header: ZclHeader, cluster: TsType.Cluster, buffalo: BuffaloZcl): ZclPayload { + private static parsePayloadCluster(header: ZclHeader, cluster: Cluster, buffalo: BuffaloZcl): ZclPayload { const command = header.frameControl.direction === Direction.CLIENT_TO_SERVER ? cluster.getCommand(header.commandIdentifier) : cluster.getCommandResponse(header.commandIdentifier); const payload: ZclPayload = {}; for (const parameter of command.parameters) { - const options: TsType.BuffaloZclOptions = {payload}; + const options: BuffaloZclOptions = {payload}; if (!this.conditionsValid(parameter, payload, buffalo.getBuffer().length - buffalo.getPosition())) { continue; @@ -206,7 +205,7 @@ class ZclFrame { const entry: {[s: string]: any} = {}; for (const parameter of command.parameters) { - const options: TsType.BuffaloZclOptions = {}; + const options: BuffaloZclOptions = {}; if (!this.conditionsValid(parameter, entry, buffalo.getBuffer().length - buffalo.getPosition())) { continue; @@ -216,15 +215,16 @@ class ZclFrame { // We need to grab the dataType to parse useDataType options.dataType = entry.dataType; - if (entry.dataType === DataType.CHAR_STR && entry.hasOwnProperty('attrId')) { - // For Xiaomi struct parsing we need to pass the attributeID. - options.attrId = entry.attrId; + if (entry.dataType === DataType.CHAR_STR && entry.attrId === 65281) { + // [workaround] parse char str as Xiaomi struct + options.dataType = BuffaloZclDataType.MI_STRUCT; } } entry[parameter.name] = buffalo.read(parameter.type, options); // TODO: not needed, but temp workaroudn to make payload equal to that of zcl-packet + // XXX: is this still needed? if (parameter.type === BuffaloZclDataType.USE_DATA_TYPE && entry.dataType === DataType.STRUCT) { entry['structElms'] = entry.attrData; entry['numElms'] = entry.attrData.length; @@ -276,51 +276,57 @@ class ZclFrame { * Utils */ - private static conditionsValid( - parameter: DefinitionTsType.ParameterDefinition, + public static conditionsValid( + parameter: ParameterDefinition, entry: ZclPayload, - remainingBufferBytes: number + remainingBufferBytes: number | null ): boolean { if (parameter.conditions) { - const failedCondition = parameter.conditions.find((condition): boolean => { - if (condition.type === 'statusEquals') { - return entry.status !== condition.value; - } else if (condition.type == 'statusNotEquals') { - return entry.status === condition.value; - } else if (condition.type == 'directionEquals') { - return entry.direction !== condition.value; - } else if(condition.type == 'bitMaskSet') { - return (entry[condition.param] & condition.mask) !== condition.mask; - } else if(condition.type == 'bitFieldEnum') { - return ((entry[condition.param] >> condition.offset) & ((1<> condition.offset) & ((1 << condition.size) - 1)) !== condition.value) return false; + break; + } + case ParameterCondition.MINIMUM_REMAINING_BUFFER_BYTES: { + if (remainingBufferBytes != null && (remainingBufferBytes < (condition.value))) return false; + break; + } + case ParameterCondition.DATA_TYPE_CLASS_EQUAL: { + if (Utils.getDataTypeClass(entry.dataType) !== condition.value) return false; + break; + } + } + }; } return true; } - public isCluster(clusterName: ClusterName): boolean { + public isCluster(clusterName: FoundationCommandName | ClusterName): boolean { return this.cluster.name === clusterName; } // List of commands is not completed, feel free to add more. public isCommand( - commandName: FoundationCommandName | 'remove' | 'add' | 'write' | 'enrollReq' | 'checkin' + commandName: FoundationCommandName | 'remove' | 'add' | 'write' | 'enrollReq' | 'checkin' | 'getAlarm' ): boolean { return this.command.name === commandName; } } - -export default ZclFrame; diff --git a/src/zcl/zclHeader.ts b/src/zspec/zcl/zclHeader.ts similarity index 58% rename from src/zcl/zclHeader.ts rename to src/zspec/zcl/zclHeader.ts index fe90fa5c68..9397818772 100644 --- a/src/zcl/zclHeader.ts +++ b/src/zspec/zcl/zclHeader.ts @@ -1,12 +1,28 @@ -import {FrameControl, FrameType} from './definition'; -import BuffaloZcl from './buffaloZcl'; -import {logger} from '../utils/logger'; +import {FrameControl} from './definition/tstype'; +import {FrameType} from './definition/enums'; +import {BuffaloZcl} from './buffaloZcl'; +import {logger} from '../../utils/logger'; const NS = 'zh:zcl:header'; const HEADER_MINIMAL_LENGTH = 3; const HEADER_WITH_MANUF_LENGTH = HEADER_MINIMAL_LENGTH + 2; - -class ZclHeader { +/** ZCL Header frame control frame type */ +const HEADER_CTRL_FRAME_TYPE_MASK = 0x03; +const HEADER_CTRL_FRAME_TYPE_BIT = 0; +/** ZCL Header frame control manufacturer specific */ +const HEADER_CTRL_MANUF_SPE_MASK = 0x04; +const HEADER_CTRL_MANUF_SPE_BIT = 2; +/** ZCL Header frame control direction */ +const HEADER_CTRL_DIRECTION_MASK = 0x08; +const HEADER_CTRL_DIRECTION_BIT = 3; +/** ZCL Header frame control disable default response */ +const HEADER_CTRL_DISABLE_DEF_RESP_MASK = 0x10; +const HEADER_CTRL_DISABLE_DEF_RESP_BIT = 4; +/** ZCL Header frame control reserved */ +const HEADER_CTRL_RESERVED_MASK = 0xE0; +const HEADER_CTRL_RESERVED_BIT = 5; + +export class ZclHeader { public readonly frameControl: FrameControl; public readonly manufacturerCode: number | null; public readonly transactionSequenceNumber: number; @@ -34,11 +50,11 @@ class ZclHeader { public write(buffalo: BuffaloZcl): void { const frameControl = ( - (this.frameControl.frameType & 0x03) | - (((this.frameControl.manufacturerSpecific ? 1 : 0) << 2) & 0x04) | - ((this.frameControl.direction << 3) & 0x08) | - (((this.frameControl.disableDefaultResponse ? 1 : 0) << 4) & 0x10) | - ((this.frameControl.reservedBits << 5) & 0xE0) + ((this.frameControl.frameType << HEADER_CTRL_FRAME_TYPE_BIT) & HEADER_CTRL_FRAME_TYPE_MASK) | + (((this.frameControl.manufacturerSpecific ? 1 : 0) << HEADER_CTRL_MANUF_SPE_BIT) & HEADER_CTRL_MANUF_SPE_MASK) | + ((this.frameControl.direction << HEADER_CTRL_DIRECTION_BIT) & HEADER_CTRL_DIRECTION_MASK) | + (((this.frameControl.disableDefaultResponse ? 1 : 0) << HEADER_CTRL_DISABLE_DEF_RESP_BIT) & HEADER_CTRL_DISABLE_DEF_RESP_MASK) | + ((this.frameControl.reservedBits << HEADER_CTRL_RESERVED_BIT) & HEADER_CTRL_RESERVED_MASK) ); buffalo.writeUInt8(frameControl); @@ -61,11 +77,11 @@ class ZclHeader { const buffalo = new BuffaloZcl(buffer); const frameControlValue = buffalo.readUInt8(); const frameControl = { - frameType: frameControlValue & 0x03, - manufacturerSpecific: ((frameControlValue >> 2) & 0x01) === 1, - direction: (frameControlValue >> 3) & 0x01, - disableDefaultResponse: ((frameControlValue >> 4) & 0x01) === 1, - reservedBits: frameControlValue >> 5, + frameType: (frameControlValue & HEADER_CTRL_FRAME_TYPE_MASK) >> HEADER_CTRL_FRAME_TYPE_BIT, + manufacturerSpecific: ((frameControlValue & HEADER_CTRL_MANUF_SPE_MASK) >> HEADER_CTRL_MANUF_SPE_BIT) === 1, + direction: (frameControlValue & HEADER_CTRL_DIRECTION_MASK) >> HEADER_CTRL_DIRECTION_BIT, + disableDefaultResponse: ((frameControlValue & HEADER_CTRL_DISABLE_DEF_RESP_MASK) >> HEADER_CTRL_DISABLE_DEF_RESP_BIT) === 1, + reservedBits: (frameControlValue & HEADER_CTRL_RESERVED_MASK) >> HEADER_CTRL_RESERVED_BIT, }; let manufacturerCode: number | null = null; @@ -84,5 +100,3 @@ class ZclHeader { return new ZclHeader(frameControl, manufacturerCode, transactionSequenceNumber, commandIdentifier); } } - -export default ZclHeader; diff --git a/src/zspec/zcl/zclStatusError.ts b/src/zspec/zcl/zclStatusError.ts new file mode 100644 index 0000000000..f0c5a36e2c --- /dev/null +++ b/src/zspec/zcl/zclStatusError.ts @@ -0,0 +1,10 @@ +import {Status} from './definition/status'; + +export class ZclStatusError extends Error { + public code: Status; + + constructor (code: Status) { + super(`Status '${Status[code]}'`); + this.code = code; + } +} diff --git a/src/zspec/zdo/.gitkeep b/src/zspec/zdo/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/zspec/zdo/buffaloZdo.ts b/src/zspec/zdo/buffaloZdo.ts new file mode 100644 index 0000000000..1c156c5c7f --- /dev/null +++ b/src/zspec/zdo/buffaloZdo.ts @@ -0,0 +1,2248 @@ +import Buffalo from '../../buffalo/buffalo'; +import {Status} from './definition/status'; +import {ZdoStatusError} from './zdoStatusError'; +import { + ActiveEndpointsResponse, + BindingTableResponse, + EndDeviceAnnounce, + IEEEAddressResponse, + LQITableResponse, + MatchDescriptorsResponse, + NetworkAddressResponse, + NodeDescriptorResponse, + ParentAnnounceResponse, + PowerDescriptorResponse, + RoutingTableResponse, + SimpleDescriptorResponse, + SystemServerDiscoveryResponse, + LQITableEntry, + RoutingTableEntry, + BindingTableEntry, + NwkUpdateResponse, + NwkEnhancedUpdateResponse, + NwkIEEEJoiningListResponse, + NwkUnsolicitedEnhancedUpdateResponse, + NwkBeaconSurveyResponse, + StartKeyNegotiationResponse, + RetrieveAuthenticationTokenResponse, + GetAuthenticationLevelResponse, + SetConfigurationResponse, + GetConfigurationResponse, + ChallengeResponse, + APSFrameCounterChallengeTLV, + AuthenticationTokenIdTLV, + Curve25519PublicPointTLV, + FragmentationParametersGlobalTLV, + SelectedKeyNegotiationMethodTLV, + PotentialParentsTLV, + ClearAllBindingsReqEUI64TLV, + BeaconAppendixEncapsulationGlobalTLV, + TargetIEEEAddressTLV, + NextPanIdChangeGlobalTLV, + NextChannelChangeGlobalTLV, + ConfigurationParametersGlobalTLV, + DeviceEUI64ListTLV, + BeaconSurveyResultsTLV, + DeviceAuthenticationLevelTLV, + ProcessingStatusTLV, + APSFrameCounterResponseTLV, + BeaconSurveyConfigurationTLV, + ManufacturerSpecificGlobalTLV, + SupportedKeyNegotiationMethodsGlobalTLV, + PanIdConflictReportGlobalTLV, + SymmetricPassphraseGlobalTLV, + RouterInformationGlobalTLV, + JoinerEncapsulationGlobalTLV, + DeviceCapabilityExtensionGlobalTLV, + TLV, + LocalTLVReader, + ServerMask, +} from './definition/tstypes'; +import { + LeaveRequestFlags, + GlobalTLV, +} from './definition/enums'; +import { + ZDO_MESSAGE_OVERHEAD, + UNICAST_BINDING, + MULTICAST_BINDING, + CHALLENGE_VALUE_SIZE, + CURVE_PUBLIC_POINT_SIZE, +} from './definition/consts'; +import {logger} from '../../utils/logger'; +import {DEFAULT_ENCRYPTION_KEY_SIZE, EUI64_SIZE, EXTENDED_PAN_ID_SIZE, PAN_ID_SIZE} from '../consts'; +import * as Utils from './utils'; +import * as ZSpecUtils from '../utils'; +import {ClusterId as ZdoClusterId} from './definition/clusters'; +import {ClusterId, EUI64, NodeId, ProfileId} from '../tstypes'; + +const NS = 'zh:zdo:buffalo'; + +const MAX_BUFFER_SIZE = 255; + +export class BuffaloZdo extends Buffalo { + + /** + * Set the position of the internal position tracker. + * TODO: move to base `Buffalo` class + * @param position + */ + public setPosition(position: number): void { + this.position = position; + } + + /** + * Set the byte at given position without affecting the internal position tracker. + * TODO: move to base `Buffalo` class + * @param position + * @param value + */ + public setByte(position: number, value: number): void { + this.buffer.writeUInt8(value, position); + } + + /** + * Get the byte at given position without affecting the internal position tracker. + * TODO: move to base `Buffalo` class + * @param position + * @returns + */ + public getByte(position: number): number { + return this.buffer.readUInt8(position); + } + + /** + * Check if internal buffer has enough bytes to satisfy: (current position + given count). + * TODO: move to base `Buffalo` class + * @param count + * @returns True if has given more bytes + */ + public isMoreBy(count: number): boolean { + return (this.position + count) <= this.buffer.length; + } + + //-- GLOBAL TLVS + + private writeManufacturerSpecificGlobalTLV(tlv: ManufacturerSpecificGlobalTLV): void { + this.writeUInt16(tlv.zigbeeManufacturerId); + this.writeBuffer(tlv.additionalData, tlv.additionalData.length); + } + + private readManufacturerSpecificGlobalTLV(length: number): ManufacturerSpecificGlobalTLV { + logger.debug(`readManufacturerSpecificGlobalTLV with length=${length}`, NS); + if (length < 2) { + throw new Error(`Malformed TLV. Invalid length '${length}', expected at least 2.`); + } + + const zigbeeManufacturerId = this.readUInt16(); + const additionalData = this.readBuffer(length - 2); + + return { + zigbeeManufacturerId, + additionalData, + }; + } + + private writeSupportedKeyNegotiationMethodsGlobalTLV(tlv: SupportedKeyNegotiationMethodsGlobalTLV): void { + this.writeUInt8(tlv.keyNegotiationProtocolsBitmask); + this.writeUInt8(tlv.preSharedSecretsBitmask); + + if (tlv.sourceDeviceEui64) { + this.writeIeeeAddr(tlv.sourceDeviceEui64); + } + } + + private readSupportedKeyNegotiationMethodsGlobalTLV(length: number): SupportedKeyNegotiationMethodsGlobalTLV { + logger.debug(`readSupportedKeyNegotiationMethodsGlobalTLV with length=${length}`, NS); + if (length < 2) { + throw new Error(`Malformed TLV. Invalid length '${length}', expected at least 2.`); + } + + const keyNegotiationProtocolsBitmask = this.readUInt8(); + const preSharedSecretsBitmask = this.readUInt8(); + let sourceDeviceEui64: EUI64; + + if (length >= (2 + EUI64_SIZE)) { + sourceDeviceEui64 = this.readIeeeAddr(); + } + + return { + keyNegotiationProtocolsBitmask, + preSharedSecretsBitmask, + sourceDeviceEui64, + }; + } + + private writePanIdConflictReportGlobalTLV(tlv: PanIdConflictReportGlobalTLV): void { + this.writeUInt16(tlv.nwkPanIdConflictCount); + } + + private readPanIdConflictReportGlobalTLV(length: number): PanIdConflictReportGlobalTLV { + logger.debug(`readPanIdConflictReportGlobalTLV with length=${length}`, NS); + if (length < 2) { + throw new Error(`Malformed TLV. Invalid length '${length}', expected at least 2.`); + } + + const nwkPanIdConflictCount = this.readUInt16(); + + return { + nwkPanIdConflictCount, + }; + } + + private writeNextPanIdChangeGlobalTLV(tlv: NextPanIdChangeGlobalTLV): void { + this.writeUInt16(tlv.panId); + } + + private readNextPanIdChangeGlobalTLV(length: number): NextPanIdChangeGlobalTLV { + logger.debug(`readNextPanIdChangeGlobalTLV with length=${length}`, NS); + if (length < PAN_ID_SIZE) { + throw new Error(`Malformed TLV. Invalid length '${length}', expected at least ${PAN_ID_SIZE}.`); + } + + const panId = this.readUInt16(); + + return { + panId, + }; + } + + private writeNextChannelChangeGlobalTLV(tlv: NextChannelChangeGlobalTLV): void { + this.writeUInt32(tlv.channel); + } + + private readNextChannelChangeGlobalTLV(length: number): NextChannelChangeGlobalTLV { + logger.debug(`readNextChannelChangeGlobalTLV with length=${length}`, NS); + if (length < 4) { + throw new Error(`Malformed TLV. Invalid length '${length}', expected at least 4.`); + } + + const channel = this.readUInt32(); + + return { + channel, + }; + } + + private writeSymmetricPassphraseGlobalTLV(tlv: SymmetricPassphraseGlobalTLV): void { + this.writeBuffer(tlv.passphrase, DEFAULT_ENCRYPTION_KEY_SIZE); + } + + private readSymmetricPassphraseGlobalTLV(length: number): SymmetricPassphraseGlobalTLV { + logger.debug(`readSymmetricPassphraseGlobalTLV with length=${length}`, NS); + if (length < DEFAULT_ENCRYPTION_KEY_SIZE) { + throw new Error(`Malformed TLV. Invalid length '${length}', expected at least ${DEFAULT_ENCRYPTION_KEY_SIZE}.`); + } + + const passphrase = this.readBuffer(DEFAULT_ENCRYPTION_KEY_SIZE); + + return { + passphrase, + }; + } + + private writeRouterInformationGlobalTLV(tlv: RouterInformationGlobalTLV): void { + this.writeUInt16(tlv.bitmask); + } + + private readRouterInformationGlobalTLV(length: number): RouterInformationGlobalTLV { + logger.debug(`readRouterInformationGlobalTLV with length=${length}`, NS); + if (length < 2) { + throw new Error(`Malformed TLV. Invalid length '${length}', expected at least 2.`); + } + + const bitmask = this.readUInt16(); + + return { + bitmask, + }; + } + + private writeFragmentationParametersGlobalTLV(tlv: FragmentationParametersGlobalTLV): void { + this.writeUInt16(tlv.nwkAddress); + + if (tlv.fragmentationOptions != undefined) { + this.writeUInt8(tlv.fragmentationOptions); + } + + if (tlv.maxIncomingTransferUnit != undefined) { + this.writeUInt16(tlv.maxIncomingTransferUnit); + } + } + + private readFragmentationParametersGlobalTLV(length: number): FragmentationParametersGlobalTLV { + logger.debug(`readFragmentationParametersGlobalTLV with length=${length}`, NS); + if (length < 2) { + throw new Error(`Malformed TLV. Invalid length '${length}', expected at least 2.`); + } + + const nwkAddress = this.readUInt16(); + let fragmentationOptions: number; + let maxIncomingTransferUnit: number; + + if (length >= 3) { + fragmentationOptions = this.readUInt8(); + } + + if (length >= 5) { + maxIncomingTransferUnit = this.readUInt16(); + } + + return { + nwkAddress, + fragmentationOptions, + maxIncomingTransferUnit, + }; + } + + private writeJoinerEncapsulationGlobalTLV(encapsulationTLV: JoinerEncapsulationGlobalTLV): void { + this.writeGlobalTLVs(encapsulationTLV.additionalTLVs); + } + + private readJoinerEncapsulationGlobalTLV(length: number): JoinerEncapsulationGlobalTLV { + logger.debug(`readJoinerEncapsulationGlobalTLV with length=${length}`, NS); + // at least the length of tagId+length for first encapsulated tlv, doesn't make sense otherwise + if (length < 2) { + throw new Error(`Malformed TLV. Invalid length '${length}', expected at least 2.`); + } + + const encapsulationBuffalo = new BuffaloZdo(this.readBuffer(length)); + const additionalTLVs = encapsulationBuffalo.readTLVs(null, true); + + return { + additionalTLVs, + }; + } + + private writeBeaconAppendixEncapsulationGlobalTLV(encapsulationTLV: BeaconAppendixEncapsulationGlobalTLV): void { + this.writeGlobalTLVs(encapsulationTLV.additionalTLVs); + } + + private readBeaconAppendixEncapsulationGlobalTLV(length: number): BeaconAppendixEncapsulationGlobalTLV { + logger.debug(`readBeaconAppendixEncapsulationGlobalTLV with length=${length}`, NS); + // at least the length of tagId+length for first encapsulated tlv, doesn't make sense otherwise + if (length < 2) { + throw new Error(`Malformed TLV. Invalid length '${length}', expected at least 2.`); + } + + const encapsulationBuffalo = new BuffaloZdo(this.readBuffer(length)); + // Global: SupportedKeyNegotiationMethodsGlobalTLV + // Global: FragmentationParametersGlobalTLV + const additionalTLVs = encapsulationBuffalo.readTLVs(null, true); + + return { + additionalTLVs, + }; + } + + private writeConfigurationParametersGlobalTLV(configurationParameters: ConfigurationParametersGlobalTLV): void { + this.writeUInt16(configurationParameters.configurationParameters); + } + + private readConfigurationParametersGlobalTLV(length: number): ConfigurationParametersGlobalTLV { + logger.debug(`readConfigurationParametersGlobalTLV with length=${length}`, NS); + if (length < 2) { + throw new Error(`Malformed TLV. Invalid length '${length}', expected at least 2.`); + } + + const configurationParameters = this.readUInt16(); + + return { + configurationParameters, + }; + } + + private writeDeviceCapabilityExtensionGlobalTLV(tlv: DeviceCapabilityExtensionGlobalTLV): void { + this.writeBuffer(tlv.data, tlv.data.length); + } + + private readDeviceCapabilityExtensionGlobalTLV(length: number): DeviceCapabilityExtensionGlobalTLV { + logger.debug(`readDeviceCapabilityExtensionGlobalTLV with length=${length}`, NS); + const data = this.readBuffer(length); + + return { + data, + }; + } + + public writeGlobalTLV(tlv: TLV): void { + this.writeUInt8(tlv.tagId); + this.writeUInt8(tlv.length - 1);// remove offset (spec quirk...) + + switch (tlv.tagId) { + case GlobalTLV.MANUFACTURER_SPECIFIC: { + this.writeManufacturerSpecificGlobalTLV(tlv.tlv as ManufacturerSpecificGlobalTLV); + break; + } + case GlobalTLV.SUPPORTED_KEY_NEGOTIATION_METHODS: { + this.writeSupportedKeyNegotiationMethodsGlobalTLV(tlv.tlv as SupportedKeyNegotiationMethodsGlobalTLV); + break; + } + case GlobalTLV.PAN_ID_CONFLICT_REPORT: { + this.writePanIdConflictReportGlobalTLV(tlv.tlv as PanIdConflictReportGlobalTLV); + break; + } + case GlobalTLV.NEXT_PAN_ID_CHANGE: { + this.writeNextPanIdChangeGlobalTLV(tlv.tlv as NextPanIdChangeGlobalTLV); + break; + } + case GlobalTLV.NEXT_CHANNEL_CHANGE: { + this.writeNextChannelChangeGlobalTLV(tlv.tlv as NextChannelChangeGlobalTLV); + break; + } + case GlobalTLV.SYMMETRIC_PASSPHRASE: { + this.writeSymmetricPassphraseGlobalTLV(tlv.tlv as SymmetricPassphraseGlobalTLV); + break; + } + case GlobalTLV.ROUTER_INFORMATION: { + this.writeRouterInformationGlobalTLV(tlv.tlv as RouterInformationGlobalTLV); + break; + } + case GlobalTLV.FRAGMENTATION_PARAMETERS: { + this.writeFragmentationParametersGlobalTLV(tlv.tlv as FragmentationParametersGlobalTLV); + break; + } + case GlobalTLV.JOINER_ENCAPSULATION: { + this.writeJoinerEncapsulationGlobalTLV(tlv.tlv as JoinerEncapsulationGlobalTLV); + break; + } + case GlobalTLV.BEACON_APPENDIX_ENCAPSULATION: { + this.writeBeaconAppendixEncapsulationGlobalTLV(tlv.tlv as BeaconAppendixEncapsulationGlobalTLV); + break; + } + case GlobalTLV.CONFIGURATION_PARAMETERS: { + this.writeConfigurationParametersGlobalTLV(tlv.tlv as ConfigurationParametersGlobalTLV); + break; + } + case GlobalTLV.DEVICE_CAPABILITY_EXTENSION: { + this.writeDeviceCapabilityExtensionGlobalTLV(tlv.tlv as DeviceCapabilityExtensionGlobalTLV); + break; + } + default: { + throw new ZdoStatusError(Status.NOT_SUPPORTED); + } + } + } + + public readGlobalTLV(tagId: number, length: number): TLV['tlv'] { + switch (tagId) { + case GlobalTLV.MANUFACTURER_SPECIFIC: { + return this.readManufacturerSpecificGlobalTLV(length); + } + case GlobalTLV.SUPPORTED_KEY_NEGOTIATION_METHODS: { + return this.readSupportedKeyNegotiationMethodsGlobalTLV(length); + } + case GlobalTLV.PAN_ID_CONFLICT_REPORT: { + return this.readPanIdConflictReportGlobalTLV(length); + } + case GlobalTLV.NEXT_PAN_ID_CHANGE: { + return this.readNextPanIdChangeGlobalTLV(length); + } + case GlobalTLV.NEXT_CHANNEL_CHANGE: { + return this.readNextChannelChangeGlobalTLV(length); + } + case GlobalTLV.SYMMETRIC_PASSPHRASE: { + return this.readSymmetricPassphraseGlobalTLV(length); + } + case GlobalTLV.ROUTER_INFORMATION: { + return this.readRouterInformationGlobalTLV(length); + } + case GlobalTLV.FRAGMENTATION_PARAMETERS: { + return this.readFragmentationParametersGlobalTLV(length); + } + case GlobalTLV.JOINER_ENCAPSULATION: { + return this.readJoinerEncapsulationGlobalTLV(length); + } + case GlobalTLV.BEACON_APPENDIX_ENCAPSULATION: { + return this.readBeaconAppendixEncapsulationGlobalTLV(length); + } + case GlobalTLV.CONFIGURATION_PARAMETERS: { + return this.readConfigurationParametersGlobalTLV(length); + } + case GlobalTLV.DEVICE_CAPABILITY_EXTENSION: { + return this.readDeviceCapabilityExtensionGlobalTLV(length); + } + default: { + // validation: unknown tag shall be ignored + return null; + } + } + } + + public writeGlobalTLVs(tlvs: TLV[]): void { + for (const tlv of tlvs) { + this.writeGlobalTLV(tlv); + } + } + + //-- LOCAL TLVS + + // write only + // private readBeaconSurveyConfigurationTLV(length: number): BeaconSurveyConfigurationTLV { + // logger.debug(`readBeaconSurveyConfigurationTLV with length=${length}`, NS); + // const count = this.readUInt8(); + + // /* istanbul ignore else */ + // if (length !== (1 + (count * 4) + 1)) { + // throw new Error(`Malformed TLV. Invalid length '${length}', expected ${(1 + (count * 4) + 1)}.`); + // } + + // const scanChannelList = this.readListUInt32(count); + // const configurationBitmask = this.readUInt8(); + + // return { + // scanChannelList, + // configurationBitmask, + // }; + // } + + private readCurve25519PublicPointTLV(length: number): Curve25519PublicPointTLV { + logger.debug(`readCurve25519PublicPointTLV with length=${length}`, NS); + if (length !== (EUI64_SIZE + CURVE_PUBLIC_POINT_SIZE)) { + throw new Error(`Malformed TLV. Invalid length '${length}', expected ${(EUI64_SIZE + CURVE_PUBLIC_POINT_SIZE)}.`); + } + + const eui64 = this.readIeeeAddr(); + const publicPoint = this.readBuffer(CURVE_PUBLIC_POINT_SIZE); + + return { + eui64, + publicPoint, + }; + } + + // write only + // private readTargetIEEEAddressTLV(length: number): TargetIEEEAddressTLV { + // logger.debug(`readTargetIEEEAddressTLV with length=${length}`, NS); + // /* istanbul ignore else */ + // if (length !== EUI64_SIZE) { + // throw new Error(`Malformed TLV. Invalid length '${length}', expected ${EUI64_SIZE}.`); + // } + + // const ieee = this.readIeeeAddr(); + + // return { + // ieee, + // }; + // } + + // write only + // private readSelectedKeyNegotiationMethodTLV(length: number): SelectedKeyNegotiationMethodTLV { + // logger.debug(`readSelectedKeyNegotiationMethodTLV with length=${length}`, NS); + // /* istanbul ignore else */ + // if (length !== 10) { + // throw new Error(`Malformed TLV. Invalid length '${length}', expected 10.`); + // } + + // const protocol = this.readUInt8(); + // const presharedSecret = this.readUInt8(); + // const sendingDeviceEui64 = this.readIeeeAddr(); + + // return { + // protocol, + // presharedSecret, + // sendingDeviceEui64, + // }; + // } + + // write only + // private readDeviceEUI64ListTLV(length: number): DeviceEUI64ListTLV { + // logger.debug(`readDeviceEUI64ListTLV with length=${length}`, NS); + // const count = this.readUInt8(); + + // /* istanbul ignore else */ + // if (length !== (1 + (count * EUI64_SIZE))) { + // throw new Error(`Malformed TLV. Invalid length '${length}', expected ${(1 + (count * EUI64_SIZE))}.`); + // } + + // const eui64List: DeviceEUI64ListTLV['eui64List'] = []; + + // for (let i = 0; i < count; i++) { + // const eui64 = this.readIeeeAddr(); + + // eui64List.push(eui64); + // } + + // return { + // eui64List, + // }; + // } + + private readAPSFrameCounterResponseTLV(length: number): APSFrameCounterResponseTLV { + logger.debug(`readAPSFrameCounterResponseTLV with length=${length}`, NS); + if (length !== 32) { + throw new Error(`Malformed TLV. Invalid length '${length}', expected 32.`); + } + + const responderEui64 = this.readIeeeAddr(); + const receivedChallengeValue = this.readBuffer(CHALLENGE_VALUE_SIZE); + const apsFrameCounter = this.readUInt32(); + const challengeSecurityFrameCounter = this.readUInt32(); + const mic = this.readBuffer(8); + + return { + responderEui64, + receivedChallengeValue, + apsFrameCounter, + challengeSecurityFrameCounter, + mic, + }; + } + + private readBeaconSurveyResultsTLV(length: number): BeaconSurveyResultsTLV { + logger.debug(`readBeaconSurveyResultsTLV with length=${length}`, NS); + if (length !== 4) { + throw new Error(`Malformed TLV. Invalid length '${length}', expected 4.`); + } + + const totalBeaconsReceived = this.readUInt8(); + const onNetworkBeacons = this.readUInt8(); + const potentialParentBeacons = this.readUInt8(); + const otherNetworkBeacons = this.readUInt8(); + + return { + totalBeaconsReceived, + onNetworkBeacons, + potentialParentBeacons, + otherNetworkBeacons, + }; + } + + private readPotentialParentsTLV(length: number): PotentialParentsTLV { + logger.debug(`readPotentialParentsTLV with length=${length}`, NS); + if (length < 4) { + throw new Error(`Malformed TLV. Invalid length '${length}', expected at least 4.`); + } + + const currentParentNwkAddress = this.readUInt16(); + const currentParentLQA = this.readUInt8(); + // [0x00 - 0x05] + const entryCount = this.readUInt8(); + + if (length !== (4 + (entryCount * 3))) { + throw new Error(`Malformed TLV. Invalid length '${length}', expected ${(4 + (entryCount * 3))}.`); + } + + const potentialParents: PotentialParentsTLV['potentialParents'] = []; + + for (let i = 0; i < entryCount; i++) { + const nwkAddress = this.readUInt16(); + const lqa = this.readUInt8(); + + potentialParents.push({ + nwkAddress, + lqa, + }); + } + + return { + currentParentNwkAddress, + currentParentLQA, + entryCount, + potentialParents, + }; + } + + private readDeviceAuthenticationLevelTLV(length: number): DeviceAuthenticationLevelTLV { + logger.debug(`readDeviceAuthenticationLevelTLV with length=${length}`, NS); + if (length !== 10) { + throw new Error(`Malformed TLV. Invalid length '${length}', expected 10.`); + } + + const remoteNodeIeee = this.readIeeeAddr(); + const initialJoinMethod = this.readUInt8(); + const activeLinkKeyType = this.readUInt8(); + + return { + remoteNodeIeee, + initialJoinMethod, + activeLinkKeyType, + }; + } + + private readProcessingStatusTLV(length: number): ProcessingStatusTLV { + logger.debug(`readProcessingStatusTLV with length=${length}`, NS); + const count = this.readUInt8(); + + if (length !== (1 + (count * 2))) { + throw new Error(`Malformed TLV. Invalid length '${length}', expected ${(1 + (count * 2))}.`); + } + + const tlvs: ProcessingStatusTLV['tlvs'] = []; + + for (let i = 0; i < count; i++) { + const tagId = this.readUInt8(); + const processingStatus = this.readUInt8(); + + tlvs.push({ + tagId, + processingStatus, + }); + } + + return { + count, + tlvs, + }; + } + + /** + * ANNEX I ZIGBEE TLV DEFINITIONS AND FORMAT + * + * Unknown tags => TLV ignored + * Duplicate tags => reject message except for MANUFACTURER_SPECIFIC_GLOBAL_TLV + * Malformed TLVs => reject message + * + * @param localTLVReaders Mapping of tagID to local TLV reader function + * @param encapsulated Default false. If true, this is reading inside an encapsuled TLV (excludes further encapsulation) + * @returns + */ + public readTLVs(localTLVReaders: Map = null, encapsulated: boolean = false): TLV[] { + const tlvs: TLV[] = []; + + while (this.isMore()) { + const tagId = this.readUInt8(); + + // validation: cannot have duplicate tagId, except MANUFACTURER_SPECIFIC_GLOBAL_TLV + if ((tagId !== GlobalTLV.MANUFACTURER_SPECIFIC) && (tlvs.findIndex((tlv) => tlv.tagId === tagId) !== -1)) { + throw new Error(`Duplicate tag. Cannot have more than one of tagId=${tagId}.`); + } + + // validation: encapsulation TLV cannot contain another encapsulation TLV, outer considered malformed, reject message + if (encapsulated && ((tagId === GlobalTLV.BEACON_APPENDIX_ENCAPSULATION) || (tagId === GlobalTLV.JOINER_ENCAPSULATION))) { + throw new Error(`Invalid nested encapsulation for tagId=${tagId}.`); + } + + const length = this.readUInt8() + 1;// add offset (spec quirk...) + + console.log(this.position, length); + // validation: invalid if not at least ${length} bytes to read + if (!this.isMoreBy(length)) { + throw new Error(`Malformed TLV. Invalid data length for tagId=${tagId}, expected ${length}.`); + } + + const nextTLVStart = this.getPosition() + length; + // null == unknown tag + let tlv: TLV['tlv'] = null; + + if (tagId < GlobalTLV.MANUFACTURER_SPECIFIC) { + /* istanbul ignore else */ + if (localTLVReaders) { + const localTLVReader = localTLVReaders.get(tagId); + + /* istanbul ignore else */ + if (localTLVReader) { + tlv = localTLVReader.call(this, length); + } else { + logger.debug(`Local TLV found tagId=${tagId} but no reader given for it. Ignoring it.`, NS); + } + } else { + logger.debug(`Local TLV found tagId=${tagId} but no reader available. Ignoring it.`, NS); + } + } else { + tlv = this.readGlobalTLV(tagId, length); + } + + // validation: unknown tag shall be ignored + /* istanbul ignore else */ + if (tlv != null) { + tlvs.push({ + tagId, + length, + tlv, + }); + } else { + logger.debug(`Unknown TLV tagId=${tagId}. Ignoring it.`, NS); + } + + // ensure we're at the right position as dictated by the tlv length field, and not the tlv reader (should be the same if proper) + this.setPosition(nextTLVStart); + } + + return tlvs; + } + + //-- REQUESTS + + /** + * @see ClusterId.NETWORK_ADDRESS_REQUEST + * @param target IEEE address for the request + * @param reportKids True to request that the target list their children in the response. [request type = 0x01] + * @param childStartIndex The index of the first child to list in the response. Ignored if reportKids is false. + */ + public static buildNetworkAddressRequest(target: EUI64, reportKids: boolean, childStartIndex: number): Buffer { + const buffalo = new BuffaloZdo(Buffer.alloc(MAX_BUFFER_SIZE), ZDO_MESSAGE_OVERHEAD); + + buffalo.writeIeeeAddr(target); + buffalo.writeUInt8(reportKids ? 1 : 0); + buffalo.writeUInt8(childStartIndex); + + return buffalo.getWritten(); + } + + /** + * @see ClusterId.IEEE_ADDRESS_REQUEST + * Can be sent to target, or to another node that will send to target. + * @param target NWK address for the request + * @param reportKids True to request that the target list their children in the response. [request type = 0x01] + * @param childStartIndex The index of the first child to list in the response. Ignored if reportKids is false. + */ + public static buildIeeeAddressRequest(target: NodeId, reportKids: boolean, childStartIndex: number): Buffer { + const buffalo = new BuffaloZdo(Buffer.alloc(MAX_BUFFER_SIZE), ZDO_MESSAGE_OVERHEAD); + + buffalo.writeUInt16(target); + buffalo.writeUInt8(reportKids ? 1 : 0); + buffalo.writeUInt8(childStartIndex); + + return buffalo.getWritten(); + } + + /** + * @see ClusterId.NODE_DESCRIPTOR_REQUEST + * @param target NWK address for the request + */ + public static buildNodeDescriptorRequest(target: NodeId, fragmentationParameters?: FragmentationParametersGlobalTLV): Buffer { + const buffalo = new BuffaloZdo(Buffer.alloc(MAX_BUFFER_SIZE), ZDO_MESSAGE_OVERHEAD); + + buffalo.writeUInt16(target); + + if (fragmentationParameters) { + let length = 2; + + /* istanbul ignore else */ + if (fragmentationParameters.fragmentationOptions) { + length += 1; + } + + /* istanbul ignore else */ + if (fragmentationParameters.maxIncomingTransferUnit) { + length += 2; + } + + buffalo.writeGlobalTLV({tagId: GlobalTLV.FRAGMENTATION_PARAMETERS, length, tlv: fragmentationParameters}); + } + + return buffalo.getWritten(); + } + + /** + * @see ClusterId.POWER_DESCRIPTOR_REQUEST + * @param target NWK address for the request + */ + public static buildPowerDescriptorRequest(target: NodeId): Buffer { + const buffalo = new BuffaloZdo(Buffer.alloc(MAX_BUFFER_SIZE), ZDO_MESSAGE_OVERHEAD); + + buffalo.writeUInt16(target); + + return buffalo.getWritten(); + } + + /** + * @see ClusterId.SIMPLE_DESCRIPTOR_REQUEST + * @param target NWK address for the request + * @param targetEndpoint The endpoint on the destination + */ + public static buildSimpleDescriptorRequest(target: NodeId, targetEndpoint: number): Buffer { + const buffalo = new BuffaloZdo(Buffer.alloc(MAX_BUFFER_SIZE), ZDO_MESSAGE_OVERHEAD); + + buffalo.writeUInt16(target); + buffalo.writeUInt8(targetEndpoint); + + return buffalo.getWritten(); + } + + /** + * @see ClusterId.ACTIVE_ENDPOINTS_REQUEST + * @param target NWK address for the request + */ + public static buildActiveEndpointsRequest(target: NodeId): Buffer { + const buffalo = new BuffaloZdo(Buffer.alloc(MAX_BUFFER_SIZE), ZDO_MESSAGE_OVERHEAD); + + buffalo.writeUInt16(target); + + return buffalo.getWritten(); + } + + /** + * @see ClusterId.MATCH_DESCRIPTORS_REQUEST + * @param target NWK address for the request + * @param profileId Profile ID to be matched at the destination + * @param inClusterList List of Input ClusterIDs to be used for matching + * @param outClusterList List of Output ClusterIDs to be used for matching + */ + public static buildMatchDescriptorRequest(target: NodeId, profileId: ProfileId, inClusterList: ClusterId[], outClusterList: ClusterId[],) + : Buffer { + const buffalo = new BuffaloZdo(Buffer.alloc(MAX_BUFFER_SIZE), ZDO_MESSAGE_OVERHEAD); + + buffalo.writeUInt16(target); + buffalo.writeUInt16(profileId); + buffalo.writeUInt8(inClusterList.length); + buffalo.writeListUInt16(inClusterList); + buffalo.writeUInt8(outClusterList.length); + buffalo.writeListUInt16(outClusterList); + + return buffalo.getWritten(); + } + + /** + * @see ClusterId.SYSTEM_SERVER_DISCOVERY_REQUEST + * @param serverMask See Table 2-34 for bit assignments. + */ + public static buildSystemServiceDiscoveryRequest(serverMask: ServerMask): Buffer { + const buffalo = new BuffaloZdo(Buffer.alloc(MAX_BUFFER_SIZE), ZDO_MESSAGE_OVERHEAD); + + buffalo.writeUInt16(Utils.createServerMask(serverMask)); + + return buffalo.getWritten(); + } + + /** + * @see ClusterId.PARENT_ANNOUNCE + * @param children The IEEE addresses of the children bound to the parent. + */ + public static buildParentAnnounce(children: EUI64[]): Buffer { + const buffalo = new BuffaloZdo(Buffer.alloc(MAX_BUFFER_SIZE), ZDO_MESSAGE_OVERHEAD); + + buffalo.writeUInt8(children.length); + + for (const child of children) { + buffalo.writeIeeeAddr(child); + } + + return buffalo.getWritten(); + } + + /** + * @see ClusterId.BIND_REQUEST + * + * @param source The IEEE address for the source. + * @param sourceEndpoint The source endpoint for the binding entry. + * @param clusterId The identifier of the cluster on the source device that is bound to the destination. + * @param type The addressing mode for the destination address used in this command, either ::UNICAST_BINDING, ::MULTICAST_BINDING. + * @param destination The destination address for the binding entry. IEEE for ::UNICAST_BINDING. + * @param groupAddress The destination address for the binding entry. Group ID for ::MULTICAST_BINDING. + * @param destinationEndpoint The destination endpoint for the binding entry. Only if ::UNICAST_BINDING. + */ + public static buildBindRequest(source: EUI64, sourceEndpoint: number, clusterId: ClusterId, type: number, destination: EUI64, + groupAddress: number, destinationEndpoint: number): Buffer { + const buffalo = new BuffaloZdo(Buffer.alloc(MAX_BUFFER_SIZE), ZDO_MESSAGE_OVERHEAD); + + buffalo.writeIeeeAddr(source); + buffalo.writeUInt8(sourceEndpoint); + buffalo.writeUInt16(clusterId); + buffalo.writeUInt8(type); + + switch (type) { + case UNICAST_BINDING: { + buffalo.writeIeeeAddr(destination); + buffalo.writeUInt8(destinationEndpoint); + break; + } + case MULTICAST_BINDING: { + buffalo.writeUInt16(groupAddress); + break; + } + default: + throw new ZdoStatusError(Status.NOT_SUPPORTED); + } + + return buffalo.getWritten(); + } + + /** + * @see ClusterId.UNBIND_REQUEST + * + * @param source The IEEE address for the source. + * @param sourceEndpoint The source endpoint for the binding entry. + * @param clusterId The identifier of the cluster on the source device that is bound to the destination. + * @param type The addressing mode for the destination address used in this command, either ::UNICAST_BINDING, ::MULTICAST_BINDING. + * @param destination The destination address for the binding entry. IEEE for ::UNICAST_BINDING. + * @param groupAddress The destination address for the binding entry. Group ID for ::MULTICAST_BINDING. + * @param destinationEndpoint The destination endpoint for the binding entry. Only if ::UNICAST_BINDING. + */ + public static buildUnbindRequest(source: EUI64, sourceEndpoint: number, clusterId: ClusterId, type: ClusterId, destination: EUI64, + groupAddress: number, destinationEndpoint: number): Buffer { + const buffalo = new BuffaloZdo(Buffer.alloc(MAX_BUFFER_SIZE), ZDO_MESSAGE_OVERHEAD); + + buffalo.writeIeeeAddr(source); + buffalo.writeUInt8(sourceEndpoint); + buffalo.writeUInt16(clusterId); + buffalo.writeUInt8(type); + + switch (type) { + case UNICAST_BINDING: { + buffalo.writeIeeeAddr(destination); + buffalo.writeUInt8(destinationEndpoint); + break; + } + case MULTICAST_BINDING: { + buffalo.writeUInt16(groupAddress); + break; + } + default: + throw new ZdoStatusError(Status.NOT_SUPPORTED); + } + + return buffalo.getWritten(); + } + + /** + * @see ClusterId.CLEAR_ALL_BINDINGS_REQUEST + */ + public static buildClearAllBindingsRequest(tlv: ClearAllBindingsReqEUI64TLV): Buffer { + const buffalo = new BuffaloZdo(Buffer.alloc(MAX_BUFFER_SIZE), ZDO_MESSAGE_OVERHEAD); + + // ClearAllBindingsReqEUI64TLV: Local: ID: 0x00 + buffalo.writeUInt8(0x00); + buffalo.writeUInt8(tlv.eui64List.length * EUI64_SIZE + 1 - 1); + buffalo.writeUInt8(tlv.eui64List.length); + + for (const entry of tlv.eui64List) { + buffalo.writeIeeeAddr(entry); + } + + return buffalo.getWritten(); + } + + /** + * @see ClusterId.LQI_TABLE_REQUEST + * @param startIndex Starting Index for the requested elements of the Neighbor Table. + */ + public static buildLqiTableRequest(startIndex: number): Buffer { + const buffalo = new BuffaloZdo(Buffer.alloc(MAX_BUFFER_SIZE), ZDO_MESSAGE_OVERHEAD); + + buffalo.writeUInt8(startIndex); + + return buffalo.getWritten(); + } + + /** + * @see ClusterId.ROUTING_TABLE_REQUEST + * @param startIndex Starting Index for the requested elements of the Neighbor Table. + */ + public static buildRoutingTableRequest(startIndex: number): Buffer { + const buffalo = new BuffaloZdo(Buffer.alloc(MAX_BUFFER_SIZE), ZDO_MESSAGE_OVERHEAD); + + buffalo.writeUInt8(startIndex); + + return buffalo.getWritten(); + } + + /** + * @see ClusterId.BINDING_TABLE_REQUEST + * @param startIndex Starting Index for the requested elements of the Neighbor Table. + */ + public static buildBindingTableRequest(startIndex: number): Buffer { + const buffalo = new BuffaloZdo(Buffer.alloc(MAX_BUFFER_SIZE), ZDO_MESSAGE_OVERHEAD); + + buffalo.writeUInt8(startIndex); + + return buffalo.getWritten(); + } + + /** + * @see ClusterId.LEAVE_REQUEST + * @param deviceAddress All zeros if the target is to remove itself from the network or + * the EUI64 of a child of the target device to remove that child. + * @param leaveRequestFlags A bitmask of leave options. Include ::AND_REJOIN if the target is to rejoin the network immediately after leaving. + */ + public static buildLeaveRequest(deviceAddress: EUI64, leaveRequestFlags: LeaveRequestFlags): Buffer { + const buffalo = new BuffaloZdo(Buffer.alloc(MAX_BUFFER_SIZE), ZDO_MESSAGE_OVERHEAD); + + buffalo.writeIeeeAddr(deviceAddress); + buffalo.writeUInt8(leaveRequestFlags); + + return buffalo.getWritten(); + } + + /** + * @see ClusterId.PERMIT_JOINING_REQUEST + * @param duration A value of 0x00 disables joining. A value of 0xFF enables joining. Any other value enables joining for that number of seconds. + * @param authentication Controls Trust Center authentication behavior. + * This field SHALL always have a value of 1, indicating a request to change the Trust Center policy. + * If a frame is received with a value of 0, it shall be treated as having a value of 1. + */ + public static buildPermitJoining(duration: number, authentication: number, tlvs: TLV[]): Buffer { + const buffalo = new BuffaloZdo(Buffer.alloc(MAX_BUFFER_SIZE), ZDO_MESSAGE_OVERHEAD); + + buffalo.writeUInt8(duration); + buffalo.writeUInt8(authentication); + // BeaconAppendixEncapsulationGlobalTLV + // - SupportedKeyNegotiationMethodsGlobalTLV + // - FragmentationParametersGlobalTLV + buffalo.writeGlobalTLVs(tlvs); + + return buffalo.getWritten(); + } + + /** + * @see ClusterId.NWK_UPDATE_REQUEST + * @param channels See Table 3-7 for details on the 32-bit field structure.. + * @param duration A value used to calculate the length of time to spend scanning each channel. + * The time spent scanning each channel is (aBaseSuperframeDuration * (2n + 1)) symbols, where n is the value of the duration parameter. + * If has a value of 0xfe this is a request for channel change. + * If has a value of 0xff this is a request to change the apsChannelMaskList and nwkManagerAddr attributes. + * @param count This field represents the number of energy scans to be conducted and reported. + * This field SHALL be present only if the duration is within the range of 0x00 to 0x05. + * @param nwkUpdateId The value of the nwkUpdateId contained in this request. + * This value is set by the Network Channel Manager prior to sending the message. + * This field SHALL only be present if the duration is 0xfe or 0xff. + * If the ScanDuration is 0xff, then the value in the nwkUpdateID SHALL be ignored. + * @param nwkManagerAddr This field SHALL be present only if the duration is set to 0xff, and, where present, + * indicates the NWK address for the device with the Network Manager bit set in its Node Descriptor. + */ + private static buildNwkUpdateRequest(channels: number[], duration: number, count: number | null, nwkUpdateId: number | null, + nwkManagerAddr: number | null): Buffer { + const buffalo = new BuffaloZdo(Buffer.alloc(MAX_BUFFER_SIZE), ZDO_MESSAGE_OVERHEAD); + + buffalo.writeUInt32(ZSpecUtils.channelsToUInt32Mask(channels)); + buffalo.writeUInt8(duration); + + if (count != null && (duration >= 0x00 && duration <= 0x05)) { + buffalo.writeUInt8(count); + } + + // TODO: What does "This value is set by the Network Channel Manager prior to sending the message." mean exactly?? + // (isn't used/mentioned in EmberZNet, confirmed working if not set at all for channel change) + // for now, allow to bypass with null, otherwise should throw if null and duration passes below conditions (see NwkEnhancedUpdateRequest) + if (nwkUpdateId != null && (duration === 0xFE || duration === 0xFF)) { + buffalo.writeUInt8(nwkUpdateId); + } + + if (nwkManagerAddr != null && duration === 0xFF) { + buffalo.writeUInt16(nwkManagerAddr); + } + + return buffalo.getWritten(); + } + + /** + * Shortcut for @see BuffaloZdo.buildNwkUpdateRequest + */ + public static buildScanChannelsRequest(scanChannels: number[], duration: number, count: number): Buffer { + return BuffaloZdo.buildNwkUpdateRequest(scanChannels, duration, count, null, null); + } + + /** + * Shortcut for @see BuffaloZdo.buildNwkUpdateRequest + */ + public static buildChannelChangeRequest(channel: number, nwkUpdateId: number | null): Buffer { + return BuffaloZdo.buildNwkUpdateRequest([channel], 0xFE, null, nwkUpdateId, null); + } + + /** + * Shortcut for @see BuffaloZdo.buildNwkUpdateRequest + */ + public static buildSetActiveChannelsAndNwkManagerIdRequest(channels: number[], nwkUpdateId: number | null, nwkManagerAddr: NodeId): Buffer { + return BuffaloZdo.buildNwkUpdateRequest(channels, 0xFF, null, nwkUpdateId, nwkManagerAddr); + } + + /** + * @see ClusterId.NWK_ENHANCED_UPDATE_REQUEST + * @param channelPages The set of channels (32-bit bitmap) for each channel page. + * The five most significant bits (b27,..., b31) represent the binary encoded Channel Page. + * The 27 least significant bits (b0, b1,... b26) indicate which channels are to be scanned + * (1 = scan, 0 = do not scan) for each of the 27 valid channels + * If duration is in the range 0x00 to 0x05, SHALL be restricted to a single page. + * @param duration A value used to calculate the length of time to spend scanning each channel. + * The time spent scanning each channel is (aBaseSuperframeDuration * (2n + 1)) symbols, where n is the value of the duration parameter. + * If has a value of 0xfe this is a request for channel change. + * If has a value of 0xff this is a request to change the apsChannelMaskList and nwkManagerAddr attributes. + * @param count This field represents the number of energy scans to be conducted and reported. + * This field SHALL be present only if the duration is within the range of 0x00 to 0x05. + * @param nwkUpdateId The value of the nwkUpdateId contained in this request. + * This value is set by the Network Channel Manager prior to sending the message. + * This field SHALL only be present if the duration is 0xfe or 0xff. + * If the ScanDuration is 0xff, then the value in the nwkUpdateID SHALL be ignored. + * @param nwkManagerAddr This field SHALL be present only if the duration is set to 0xff, and, where present, + * indicates the NWK address for the device with the Network Manager bit set in its Node Descriptor. + * @param configurationBitmask Defined in defined in section 2.4.3.3.12. + * The configurationBitmask must be added to the end of the list of parameters. + * This octet may or may not be present. + * If not present then assumption should be that it is enhanced active scan. + * Bit 0: This bit determines whether to do an Active Scan or Enhanced Active Scan. + * When the bit is set to 1 it indicates an Enhanced Active Scan. + * And in case of Enhanced Active scan EBR shall be sent with EPID filter instead of PJOIN filter. + * Bit 1-7: Reserved + */ + private static buildNwkEnhancedUpdateRequest(channelPages: number[], duration: number, count: number | null, + nwkUpdateId: number | null, nwkManagerAddr: NodeId | null, configurationBitmask: number | null): Buffer { + const buffalo = new BuffaloZdo(Buffer.alloc(MAX_BUFFER_SIZE), ZDO_MESSAGE_OVERHEAD); + + buffalo.writeUInt8(channelPages.length); + + for (const channelPage of channelPages) { + buffalo.writeUInt32(channelPage); + } + + buffalo.writeUInt8(duration); + + if (count != null && (duration >= 0x00 && duration <= 0x05)) { + buffalo.writeUInt8(count); + } + + if (nwkUpdateId != null && (duration === 0xFE || duration === 0xFF)) { + buffalo.writeUInt8(nwkUpdateId); + } + + if (nwkManagerAddr != null && duration === 0xFF) { + buffalo.writeUInt16(nwkManagerAddr); + } + + /* istanbul ignore else */ + if (configurationBitmask != null) { + buffalo.writeUInt8(configurationBitmask); + } + + return buffalo.getWritten(); + } + + /** + * Shortcut for @see BuffaloZdo.buildNwkEnhancedUpdateRequest + */ + public static buildEnhancedScanChannelsRequest(channelPages: number[], duration: number, count: number, + configurationBitmask: number | null): Buffer { + return BuffaloZdo.buildNwkEnhancedUpdateRequest(channelPages, duration, count, null, null, configurationBitmask); + } + + /** + * Shortcut for @see BuffaloZdo.buildNwkEnhancedUpdateRequest + */ + public static buildEnhancedChannelChangeRequest(channelPage: number, nwkUpdateId: number | null, configurationBitmask: number | null): Buffer { + return BuffaloZdo.buildNwkEnhancedUpdateRequest([channelPage], 0xFE, null, nwkUpdateId, null, configurationBitmask); + } + + /** + * Shortcut for @see BuffaloZdo.buildNwkEnhancedUpdateRequest + */ + public static buildEnhancedSetActiveChannelsAndNwkManagerIdRequest(channelPages: number[], nwkUpdateId: number | null, + nwkManagerAddr: NodeId, configurationBitmask: number | null): Buffer { + return BuffaloZdo.buildNwkEnhancedUpdateRequest(channelPages, 0xFF, null, nwkUpdateId, nwkManagerAddr, configurationBitmask); + } + + /** + * @see ClusterId.NWK_IEEE_JOINING_LIST_REQUEST + * @param startIndex The starting index into the receiving device’s nwkIeeeJoiningList that SHALL be sent back. + */ + public static buildNwkIEEEJoiningListRequest(startIndex: number): Buffer { + const buffalo = new BuffaloZdo(Buffer.alloc(MAX_BUFFER_SIZE), ZDO_MESSAGE_OVERHEAD); + + buffalo.writeUInt8(startIndex); + + return buffalo.getWritten(); + } + + /** + * @see ClusterId.NWK_BEACON_SURVEY_REQUEST + */ + public static buildNwkBeaconSurveyRequest(tlv: BeaconSurveyConfigurationTLV): Buffer { + const buffalo = new BuffaloZdo(Buffer.alloc(MAX_BUFFER_SIZE), ZDO_MESSAGE_OVERHEAD); + + // BeaconSurveyConfigurationTLV: Local: ID: 0x00 + buffalo.writeUInt8(0x00); + buffalo.writeUInt8(2 + (tlv.scanChannelList.length * 4) - 1); + buffalo.writeUInt8(tlv.scanChannelList.length); + buffalo.writeListUInt32(tlv.scanChannelList); + buffalo.writeUInt8(tlv.configurationBitmask); + + return buffalo.getWritten(); + } + + /** + * @see ClusterId.START_KEY_NEGOTIATION_REQUEST + */ + public static buildStartKeyNegotiationRequest(tlv: Curve25519PublicPointTLV): Buffer { + const buffalo = new BuffaloZdo(Buffer.alloc(MAX_BUFFER_SIZE), ZDO_MESSAGE_OVERHEAD); + + // Curve25519PublicPointTLV: Local: ID: 0x00 + buffalo.writeUInt8(0x00); + buffalo.writeUInt8(EUI64_SIZE + CURVE_PUBLIC_POINT_SIZE - 1); + buffalo.writeIeeeAddr(tlv.eui64); + buffalo.writeBuffer(tlv.publicPoint, CURVE_PUBLIC_POINT_SIZE); + + return buffalo.getWritten(); + } + + /** + * @see ClusterId.RETRIEVE_AUTHENTICATION_TOKEN_REQUEST + */ + public static buildRetrieveAuthenticationTokenRequest(tlv: AuthenticationTokenIdTLV): Buffer { + const buffalo = new BuffaloZdo(Buffer.alloc(MAX_BUFFER_SIZE), ZDO_MESSAGE_OVERHEAD); + + // AuthenticationTokenIdTLV: Local: ID: 0x00 + buffalo.writeUInt8(0x00); + buffalo.writeUInt8(1 - 1); + buffalo.writeUInt8(tlv.tlvTypeTagId); + + return buffalo.getWritten(); + } + + /** + * @see ClusterId.GET_AUTHENTICATION_LEVEL_REQUEST + */ + public static buildGetAuthenticationLevelRequest(tlv: TargetIEEEAddressTLV): Buffer { + const buffalo = new BuffaloZdo(Buffer.alloc(MAX_BUFFER_SIZE), ZDO_MESSAGE_OVERHEAD); + + // TargetIEEEAddressTLV: Local: ID: 0x00 + buffalo.writeUInt8(0x00); + buffalo.writeUInt8(EUI64_SIZE - 1); + buffalo.writeIeeeAddr(tlv.ieee); + + return buffalo.getWritten(); + } + + /** + * @see ClusterId.SET_CONFIGURATION_REQUEST + */ + public static buildSetConfigurationRequest(nextPanIdChange: NextPanIdChangeGlobalTLV, nextChannelChange: NextChannelChangeGlobalTLV, + configurationParameters: ConfigurationParametersGlobalTLV): Buffer { + const buffalo = new BuffaloZdo(Buffer.alloc(MAX_BUFFER_SIZE), ZDO_MESSAGE_OVERHEAD); + + buffalo.writeGlobalTLV({tagId: GlobalTLV.NEXT_PAN_ID_CHANGE, length: PAN_ID_SIZE, tlv: nextPanIdChange}); + buffalo.writeGlobalTLV({tagId: GlobalTLV.NEXT_CHANNEL_CHANGE, length: 4, tlv: nextChannelChange}); + buffalo.writeGlobalTLV({tagId: GlobalTLV.CONFIGURATION_PARAMETERS, length: 2, tlv: configurationParameters}); + + return buffalo.getWritten(); + } + + /** + * @see ClusterId.GET_CONFIGURATION_REQUEST + * @param tlvIds The IDs of each TLV that are being requested. + * Maximum number dependent on the underlying maximum size of the message as allowed by fragmentation. + */ + public static buildGetConfigurationRequest(tlvIds: number[]): Buffer { + const buffalo = new BuffaloZdo(Buffer.alloc(MAX_BUFFER_SIZE), ZDO_MESSAGE_OVERHEAD); + + buffalo.writeUInt8(tlvIds.length); + + for (const tlvId of tlvIds) { + buffalo.writeUInt8(tlvId); + } + + return buffalo.getWritten(); + } + + /** + * @see ClusterId.START_KEY_UPDATE_REQUEST + */ + public static buildStartKeyUpdateRequest(selectedKeyNegotiationMethod: SelectedKeyNegotiationMethodTLV, + fragmentationParameters: FragmentationParametersGlobalTLV): Buffer { + const buffalo = new BuffaloZdo(Buffer.alloc(MAX_BUFFER_SIZE), ZDO_MESSAGE_OVERHEAD); + + // SelectedKeyNegotiationMethodTLV: Local: ID: 0x00 + buffalo.writeUInt8(0x00); + buffalo.writeUInt8(EUI64_SIZE + 2 - 1); + buffalo.writeUInt8(selectedKeyNegotiationMethod.protocol); + buffalo.writeUInt8(selectedKeyNegotiationMethod.presharedSecret); + buffalo.writeIeeeAddr(selectedKeyNegotiationMethod.sendingDeviceEui64); + + { + let length = 2; + + /* istanbul ignore else */ + if (fragmentationParameters.fragmentationOptions) { + length += 1; + } + + /* istanbul ignore else */ + if (fragmentationParameters.maxIncomingTransferUnit) { + length += 2; + } + + buffalo.writeGlobalTLV({tagId: GlobalTLV.FRAGMENTATION_PARAMETERS, length, tlv: fragmentationParameters}); + } + + return buffalo.getWritten(); + } + + /** + * @see ClusterId.DECOMMISSION_REQUEST + */ + public static buildDecommissionRequest(tlv: DeviceEUI64ListTLV): Buffer { + const buffalo = new BuffaloZdo(Buffer.alloc(MAX_BUFFER_SIZE), ZDO_MESSAGE_OVERHEAD); + + // DeviceEUI64ListTLV: Local: ID: 0x00 + buffalo.writeUInt8(0x00); + buffalo.writeUInt8(tlv.eui64List.length * EUI64_SIZE + 1 - 1); + buffalo.writeUInt8(tlv.eui64List.length); + + for (const eui64 of tlv.eui64List) { + buffalo.writeIeeeAddr(eui64); + } + + return buffalo.getWritten(); + } + + /** + * @see ClusterId.CHALLENGE_REQUEST + */ + public static buildChallengeRequest(tlv: APSFrameCounterChallengeTLV): Buffer { + const buffalo = new BuffaloZdo(Buffer.alloc(MAX_BUFFER_SIZE), ZDO_MESSAGE_OVERHEAD); + + // APSFrameCounterChallengeTLV: Local: ID: 0x00 + buffalo.writeUInt8(0x00); + buffalo.writeUInt8(EUI64_SIZE + CHALLENGE_VALUE_SIZE - 1); + buffalo.writeIeeeAddr(tlv.senderEui64); + buffalo.writeBuffer(tlv.challengeValue, CHALLENGE_VALUE_SIZE); + + return buffalo.getWritten(); + } + + //-- RESPONSES + + public static readResponse(clusterId: ZdoClusterId, buffer: Buffer): unknown { + const buffalo = new BuffaloZdo(buffer, ZDO_MESSAGE_OVERHEAD);// set pos to skip `transaction sequence number` + + switch(clusterId) { + case ZdoClusterId.NETWORK_ADDRESS_RESPONSE: { + return buffalo.readNetworkAddressResponse(); + } + case ZdoClusterId.IEEE_ADDRESS_RESPONSE: { + return buffalo.readIEEEAddressResponse(); + } + case ZdoClusterId.NODE_DESCRIPTOR_RESPONSE: { + return buffalo.readNodeDescriptorResponse(); + } + case ZdoClusterId.POWER_DESCRIPTOR_RESPONSE: { + return buffalo.readPowerDescriptorResponse(); + } + case ZdoClusterId.SIMPLE_DESCRIPTOR_RESPONSE: { + return buffalo.readSimpleDescriptorResponse(); + } + case ZdoClusterId.ACTIVE_ENDPOINTS_RESPONSE: { + return buffalo.readActiveEndpointsResponse(); + } + case ZdoClusterId.MATCH_DESCRIPTORS_RESPONSE: { + return buffalo.readMatchDescriptorsResponse(); + } + case ZdoClusterId.END_DEVICE_ANNOUNCE: { + return buffalo.readEndDeviceAnnounce(); + } + case ZdoClusterId.SYSTEM_SERVER_DISCOVERY_RESPONSE: { + return buffalo.readSystemServerDiscoveryResponse(); + } + case ZdoClusterId.PARENT_ANNOUNCE_RESPONSE: { + return buffalo.readParentAnnounceResponse(); + } + case ZdoClusterId.BIND_RESPONSE: { + return buffalo.readBindResponse(); + } + case ZdoClusterId.UNBIND_RESPONSE: { + return buffalo.readUnbindResponse(); + } + case ZdoClusterId.CLEAR_ALL_BINDINGS_RESPONSE: { + return buffalo.readClearAllBindingsResponse(); + } + case ZdoClusterId.LQI_TABLE_RESPONSE: { + return buffalo.readLQITableResponse(); + } + case ZdoClusterId.ROUTING_TABLE_RESPONSE: { + return buffalo.readRoutingTableResponse(); + } + case ZdoClusterId.BINDING_TABLE_RESPONSE: { + return buffalo.readBindingTableResponse(); + } + case ZdoClusterId.LEAVE_RESPONSE: { + return buffalo.readLeaveResponse(); + } + case ZdoClusterId.PERMIT_JOINING_RESPONSE: { + return buffalo.readPermitJoiningResponse(); + } + case ZdoClusterId.NWK_UPDATE_RESPONSE: { + return buffalo.readNwkUpdateResponse(); + } + case ZdoClusterId.NWK_ENHANCED_UPDATE_RESPONSE: { + return buffalo.readNwkEnhancedUpdateResponse(); + } + case ZdoClusterId.NWK_IEEE_JOINING_LIST_REPONSE: { + return buffalo.readNwkIEEEJoiningListResponse(); + } + case ZdoClusterId.NWK_UNSOLICITED_ENHANCED_UPDATE_RESPONSE: { + return buffalo.readNwkUnsolicitedEnhancedUpdateResponse(); + } + case ZdoClusterId.NWK_BEACON_SURVEY_RESPONSE: { + return buffalo.readNwkBeaconSurveyResponse(); + } + case ZdoClusterId.START_KEY_NEGOTIATION_RESPONSE: { + return buffalo.readStartKeyNegotiationResponse(); + } + case ZdoClusterId.RETRIEVE_AUTHENTICATION_TOKEN_RESPONSE: { + return buffalo.readRetrieveAuthenticationTokenResponse(); + } + case ZdoClusterId.GET_AUTHENTICATION_LEVEL_RESPONSE: { + return buffalo.readGetAuthenticationLevelResponse(); + } + case ZdoClusterId.SET_CONFIGURATION_RESPONSE: { + return buffalo.readSetConfigurationResponse(); + } + case ZdoClusterId.GET_CONFIGURATION_RESPONSE: { + return buffalo.readGetConfigurationResponse(); + } + case ZdoClusterId.START_KEY_UPDATE_RESPONSE: { + return buffalo.readStartKeyUpdateResponse(); + } + case ZdoClusterId.DECOMMISSION_RESPONSE: { + return buffalo.readDecommissionResponse(); + } + case ZdoClusterId.CHALLENGE_RESPONSE: { + return buffalo.readChallengeResponse(); + } + default: { + throw new Error(`Unsupported response reading for cluster ID '${clusterId}'.`); + } + } + } + + /** + * @see ClusterId.NETWORK_ADDRESS_RESPONSE + */ + public readNetworkAddressResponse(): NetworkAddressResponse { + const status: Status = this.readUInt8(); + + if (status !== Status.SUCCESS) { + // INV_REQUESTTYPE or DEVICE_NOT_FOUND + throw new ZdoStatusError(status); + } else { + const eui64 = this.readIeeeAddr(); + const nwkAddress = this.readUInt16(); + let assocDevCount: number = 0; + let startIndex: number = 0; + let assocDevList: number[] = []; + + if (this.isMore()) { + assocDevCount = this.readUInt8(); + startIndex = this.readUInt8(); + + assocDevList = this.readListUInt16(assocDevCount); + } + + return { + eui64, + nwkAddress, + startIndex, + assocDevList, + }; + } + } + + /** + * @see ClusterId.IEEE_ADDRESS_RESPONSE + */ + public readIEEEAddressResponse(): IEEEAddressResponse { + const status: Status = this.readUInt8(); + + if (status !== Status.SUCCESS) { + // INV_REQUESTTYPE or DEVICE_NOT_FOUND + throw new ZdoStatusError(status); + } else { + const eui64 = this.readIeeeAddr(); + const nwkAddress = this.readUInt16(); + let assocDevCount: number = 0; + let startIndex: number = 0; + let assocDevList: number[] = []; + + if (this.isMore()) { + assocDevCount = this.readUInt8(); + startIndex = this.readUInt8(); + assocDevList = this.readListUInt16(assocDevCount); + } + + return { + eui64, + nwkAddress, + startIndex, + assocDevList, + }; + } + } + + /** + * @see ClusterId.NODE_DESCRIPTOR_RESPONSE + */ + public readNodeDescriptorResponse(): NodeDescriptorResponse { + const status: Status = this.readUInt8(); + + if (status !== Status.SUCCESS) { + // DEVICE_NOT_FOUND, INV_REQUESTTYPE, or NO_DESCRIPTOR + throw new ZdoStatusError(status); + } else { + const nwkAddress = this.readUInt16(); + // in bits: [logical type: 3] [deprecated: 1] [deprecated: 1] [fragmentation supported (R23): 1] [reserved/unused: 2] + const nodeDescByte1 = this.readUInt8(); + // in bits: [aps flags: 3] [frequency band: 5] + const nodeDescByte2 = this.readUInt8(); + const macCapFlags = Utils.getMacCapFlags(this.readUInt8()); + const manufacturerCode = this.readUInt16(); + const maxBufSize = this.readUInt8(); + const maxIncTxSize = this.readUInt16(); + const serverMask = Utils.getServerMask(this.readUInt16()); + const maxOutTxSize = this.readUInt16(); + const deprecated1 = this.readUInt8(); + // Global: FragmentationParametersGlobalTLV + const tlvs: TLV[] = this.readTLVs(); + + return { + nwkAddress, + logicalType: (nodeDescByte1 & 0x07), + fragmentationSupported: (serverMask.stackComplianceResivion >= 23) ? ((nodeDescByte1 & 0x20) >> 5) === 1 : null, + apsFlags: (nodeDescByte2 & 0x07), + frequencyBand: (nodeDescByte2 & 0xF8) >> 3, + capabilities: macCapFlags, + manufacturerCode, + maxBufSize, + maxIncTxSize, + serverMask, + maxOutTxSize, + deprecated1, + tlvs, + }; + } + } + + /** + * @see ClusterId.POWER_DESCRIPTOR_RESPONSE + */ + public readPowerDescriptorResponse(): PowerDescriptorResponse { + const status: Status = this.readUInt8(); + + if (status !== Status.SUCCESS) { + // DEVICE_NOT_FOUND, INV_REQUESTTYPE, or NO_DESCRIPTOR + throw new ZdoStatusError(status); + } else { + const nwkAddress = this.readUInt16(); + const byte1 = this.readUInt8(); + const byte2 = this.readUInt8(); + + return { + nwkAddress, + currentPowerMode: (byte1 & 0xF), + availPowerSources: ((byte1 >> 4) & 0xF), + currentPowerSource: (byte2 & 0xF), + currentPowerSourceLevel: ((byte2 >> 4) & 0xF), + }; + } + } + + /** + * @see ClusterId.SIMPLE_DESCRIPTOR_RESPONSE + */ + public readSimpleDescriptorResponse(): SimpleDescriptorResponse { + const status: Status = this.readUInt8(); + + if (status !== Status.SUCCESS) { + // INVALID_EP, NOT_ACTIVE, DEVICE_NOT_FOUND, INV_REQUESTTYPE or NO_DESCRIPTOR + throw new ZdoStatusError(status); + } else { + const nwkAddress = this.readUInt16(); + // Length in bytes of the Simple Descriptor to follow. [0x00-0xff] + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const length = this.readUInt8(); + const endpoint = this.readUInt8(); + const profileId = this.readUInt16(); + const deviceId = this.readUInt16(); + const deviceVersion = this.readUInt8(); + const inClusterCount = this.readUInt8(); + const inClusterList = this.readListUInt16(inClusterCount);// empty if inClusterCount==0 + const outClusterCount = this.readUInt8(); + const outClusterList = this.readListUInt16(outClusterCount);// empty if outClusterCount==0 + + return { + nwkAddress, + endpoint, + profileId, + deviceId, + deviceVersion, + inClusterList, + outClusterList, + }; + } + } + + /** + * @see ClusterId.ACTIVE_ENDPOINTS_RESPONSE + */ + public readActiveEndpointsResponse(): ActiveEndpointsResponse { + const status: Status = this.readUInt8(); + + if (status !== Status.SUCCESS) { + // DEVICE_NOT_FOUND, INV_REQUESTTYPE, or NO_DESCRIPTOR + throw new ZdoStatusError(status); + } else { + const nwkAddress = this.readUInt16(); + const endpointCount = this.readUInt8(); + const endpointList = this.readListUInt8(endpointCount); + + return { + nwkAddress, + endpointList, + }; + } + } + + /** + * @see ClusterId.MATCH_DESCRIPTORS_RESPONSE + */ + public readMatchDescriptorsResponse(): MatchDescriptorsResponse { + const status: Status = this.readUInt8(); + + if (status !== Status.SUCCESS) { + // DEVICE_NOT_FOUND, INV_REQUESTTYPE, or NO_DESCRIPTOR + throw new ZdoStatusError(status); + } else { + const nwkAddress = this.readUInt16(); + const endpointCount = this.readUInt8(); + const endpointList = this.readListUInt8(endpointCount); + + return { + nwkAddress, + endpointList, + }; + } + } + + /** + * @see ClusterId.END_DEVICE_ANNOUNCE + */ + public readEndDeviceAnnounce(): EndDeviceAnnounce { + const nwkAddress = this.readUInt16(); + const eui64 = this.readIeeeAddr(); + /** @see MACCapabilityFlags */ + const capabilities = this.readUInt8(); + + return {nwkAddress, eui64, capabilities: Utils.getMacCapFlags(capabilities)}; + } + + /** + * @see ClusterId.SYSTEM_SERVER_DISCOVERY_RESPONSE + */ + public readSystemServerDiscoveryResponse(): SystemServerDiscoveryResponse { + const status: Status = this.readUInt8(); + + if (status !== Status.SUCCESS) { + // Shouldn't happen + throw new ZdoStatusError(status); + } else { + const serverMask = Utils.getServerMask(this.readUInt16()); + + return { + serverMask, + }; + } + } + + /** + * @see ClusterId.PARENT_ANNOUNCE_RESPONSE + */ + public readParentAnnounceResponse(): ParentAnnounceResponse { + const status: Status = this.readUInt8(); + + if (status !== Status.SUCCESS) { + // NOT_SUPPORTED + throw new ZdoStatusError(status); + } else { + const numberOfChildren = this.readUInt8(); + const children: EUI64[] = []; + + for (let i = 0; i < numberOfChildren; i++) { + const childEui64 = this.readIeeeAddr(); + + children.push(childEui64); + } + + return {children}; + } + } + + /** + * @see ClusterId.BIND_RESPONSE + * @returns No response payload, throws if not success + */ + public readBindResponse(): void { + const status: Status = this.readUInt8(); + + /* istanbul ignore else */ + if (status !== Status.SUCCESS) { + // NOT_SUPPORTED, INVALID_EP, TABLE_FULL, or NOT_AUTHORIZED + throw new ZdoStatusError(status); + } + } + + /** + * @see ClusterId.UNBIND_RESPONSE + * @returns No response payload, throws if not success + */ + public readUnbindResponse(): void { + const status: Status = this.readUInt8(); + + /* istanbul ignore else */ + if (status !== Status.SUCCESS) { + // NOT_SUPPORTED, INVALID_EP, NO_ENTRY or NOT_AUTHORIZED + throw new ZdoStatusError(status); + } + } + + /** + * @see ClusterId.CLEAR_ALL_BINDINGS_RESPONSE + * @returns No response payload, throws if not success + */ + public readClearAllBindingsResponse(): void { + const status: Status = this.readUInt8(); + + /* istanbul ignore else */ + if (status !== Status.SUCCESS) { + // NOT_SUPPORTED, NOT_AUTHORIZED, INV_REQUESTTYPE, or NO_MATCH. + throw new ZdoStatusError(status); + } + } + + /** + * @see ClusterId.LQI_TABLE_RESPONSE + */ + public readLQITableResponse(): LQITableResponse { + const status: Status = this.readUInt8(); + + if (status !== Status.SUCCESS) { + // NOT_SUPPORTED or any status code returned from the NLME-GET.confirm primitive. + throw new ZdoStatusError(status); + } else { + const neighborTableEntries = this.readUInt8(); + const startIndex = this.readUInt8(); + // [0x00-0x02] + const entryCount = this.readUInt8(); + const entryList: LQITableEntry[] = []; + + for (let i = 0; i < entryCount; i++) { + const extendedPanId = this.readListUInt8(EXTENDED_PAN_ID_SIZE); + const eui64 = this.readIeeeAddr(); + const nwkAddress = this.readUInt16(); + const deviceTypeByte = this.readUInt8(); + const permitJoiningByte = this.readUInt8(); + const depth = this.readUInt8(); + const lqi = this.readUInt8(); + + entryList.push({ + extendedPanId, + eui64, + nwkAddress, + deviceType: deviceTypeByte & 0x03, + rxOnWhenIdle: (deviceTypeByte & 0x0C) >> 2, + relationship: (deviceTypeByte & 0x70) >> 4, + reserved1: (deviceTypeByte & 0x10) >> 7, + permitJoining: permitJoiningByte & 0x03, + reserved2: (permitJoiningByte & 0xFC) >> 2, + depth, + lqi, + }); + } + + return { + neighborTableEntries, + startIndex, + entryList, + }; + } + } + + /** + * @see ClusterId.ROUTING_TABLE_RESPONSE + */ + public readRoutingTableResponse(): RoutingTableResponse { + const status: Status = this.readUInt8(); + + if (status !== Status.SUCCESS) { + // NOT_SUPPORTED or any status code returned from the NLMEGET.confirm primitive. + throw new ZdoStatusError(status); + } else { + const routingTableEntries = this.readUInt8(); + const startIndex = this.readUInt8(); + // [0x00-0xFF] + const entryCount = this.readUInt8(); + const entryList: RoutingTableEntry[] = []; + + for (let i = 0; i < entryCount; i++) { + const destinationAddress = this.readUInt16(); + const statusByte = this.readUInt8(); + const nextHopAddress = this.readUInt16(); + + entryList.push({ + destinationAddress, + status: statusByte & 0x07, + memoryConstrained: (statusByte & 0x08) >> 3, + manyToOne: (statusByte & 0x10) >> 4, + routeRecordRequired: (statusByte & 0x20) >> 5, + reserved1: (statusByte & 0xC0) >> 6, + nextHopAddress, + }); + } + + return { + routingTableEntries, + startIndex, + entryList, + }; + } + } + + /** + * @see ClusterId.BINDING_TABLE_RESPONSE + */ + public readBindingTableResponse(): BindingTableResponse { + const status: Status = this.readUInt8(); + + if (status !== Status.SUCCESS) { + // NOT_SUPPORTED or any status code returned from the APSMEGET.confirm primitive. + throw new ZdoStatusError(status); + } else { + const bindingTableEntries = this.readUInt8(); + const startIndex = this.readUInt8(); + // [0x00-0xFF] + const entryCount = this.readUInt8(); + const entryList: BindingTableEntry[] = []; + + for (let i = 0; i < entryCount; i++) { + const sourceEui64 = this.readIeeeAddr(); + const sourceEndpoint = this.readUInt8(); + const clusterId = this.readUInt16(); + const destAddrMode = this.readUInt8(); + const dest = (destAddrMode === 0x01) ? this.readUInt16() : ((destAddrMode === 0x03) ? this.readIeeeAddr() : null); + const destEndpoint = (destAddrMode === 0x03) ? this.readUInt8() : null; + + entryList.push({ + sourceEui64, + sourceEndpoint, + clusterId, + destAddrMode, + dest, + destEndpoint, + }); + } + + return { + bindingTableEntries, + startIndex, + entryList, + }; + } + } + + /** + * @see ClusterId.LEAVE_RESPONSE + * @returns No response payload, throws if not success + */ + public readLeaveResponse(): void { + const status: Status = this.readUInt8(); + + /* istanbul ignore else */ + if (status !== Status.SUCCESS) { + // NOT_SUPPORTED, NOT_AUTHORIZED or any status code returned from the NLMELEAVE.confirm primitive. + throw new ZdoStatusError(status); + } + } + + /** + * @see ClusterId.PERMIT_JOINING_RESPONSE + * @returns No response payload, throws if not success + */ + public readPermitJoiningResponse(): void { + const status: Status = this.readUInt8(); + + /* istanbul ignore else */ + if (status !== Status.SUCCESS) { + // INV_REQUESTTYPE, NOT_AUTHORIZED, or any status code returned from the NLME-PERMIT-JOINING.confirm primitive. + throw new ZdoStatusError(status); + } + } + + /** + * @see ClusterId.NWK_UPDATE_RESPONSE + */ + public readNwkUpdateResponse(): NwkUpdateResponse { + const status: Status = this.readUInt8(); + + if (status !== Status.SUCCESS) { + // INV_REQUESTTYPE, NOT_SUPPORTED, or any status values returned from the MLME-SCAN.confirm primitive + throw new ZdoStatusError(status); + } else { + const scannedChannels = this.readUInt32(); + const totalTransmissions = this.readUInt16(); + const totalFailures = this.readUInt16(); + // [0x00-0xFF] + const entryCount = this.readUInt8(); + const entryList = this.readListUInt8(entryCount); + + return { + scannedChannels, + totalTransmissions, + totalFailures, + entryList, + }; + } + } + + /** + * @see ClusterId.NWK_ENHANCED_UPDATE_RESPONSE + */ + public readNwkEnhancedUpdateResponse(): NwkEnhancedUpdateResponse { + const status: Status = this.readUInt8(); + + if (status !== Status.SUCCESS) { + // INV_REQUESTTYPE, NOT_SUPPORTED, or any status values returned from the MLME-SCAN.confirm primitive. + throw new ZdoStatusError(status); + } else { + const scannedChannels = this.readUInt32(); + const totalTransmissions = this.readUInt16(); + const totalFailures = this.readUInt16(); + // [0x00-0xFF] + const entryCount = this.readUInt8(); + const entryList = this.readListUInt8(entryCount); + + return { + scannedChannels, + totalTransmissions, + totalFailures, + entryList, + }; + } + } + + /** + * @see ClusterId.NWK_IEEE_JOINING_LIST_REPONSE + */ + public readNwkIEEEJoiningListResponse(): NwkIEEEJoiningListResponse { + const status: Status = this.readUInt8(); + + if (status !== Status.SUCCESS) { + // INV_REQUESTTYPE, or NOT_SUPPORTED + throw new ZdoStatusError(status); + } else { + const updateId = this.readUInt8(); + const joiningPolicy = this.readUInt8(); + // [0x00-0xFF] + const entryListTotal = this.readUInt8(); + let startIndex: number; + let entryList: EUI64[]; + + if (entryListTotal > 0) { + startIndex = this.readUInt8(); + const entryCount = this.readUInt8(); + entryList = []; + + for (let i = 0; i < entryCount; i++) { + const ieee = this.readIeeeAddr(); + + entryList.push(ieee); + } + } + + return { + updateId, + joiningPolicy, + entryListTotal, + startIndex, + entryList, + }; + } + } + + /** + * @see ClusterId.NWK_UNSOLICITED_ENHANCED_UPDATE_RESPONSE + */ + public readNwkUnsolicitedEnhancedUpdateResponse(): NwkUnsolicitedEnhancedUpdateResponse { + const status: Status = this.readUInt8(); + + if (status !== Status.SUCCESS) { + // ?? + throw new ZdoStatusError(status); + } else { + const channelInUse = this.readUInt32(); + const macTxUCastTotal = this.readUInt16(); + const macTxUCastFailures = this.readUInt16(); + const macTxUCastRetries = this.readUInt16(); + const timePeriod = this.readUInt8(); + + return { + channelInUse, + macTxUCastTotal, + macTxUCastFailures, + macTxUCastRetries, + timePeriod, + }; + } + } + + /** + * @see ClusterId.NWK_BEACON_SURVEY_RESPONSE + */ + public readNwkBeaconSurveyResponse(): NwkBeaconSurveyResponse { + const status: Status = this.readUInt8(); + + if (status !== Status.SUCCESS) { + // INV_REQUESTTYPE, or NOT_SUPPORTED + throw new ZdoStatusError(status); + } else { + const localTLVs = new Map([ + // Local: ID: 0x01: BeaconSurveyResultsTLV + [0x01, this.readBeaconSurveyResultsTLV], + // Local: ID: 0x02: PotentialParentsTLV + [0x02, this.readPotentialParentsTLV], + ]); + const tlvs: TLV[] = this.readTLVs(localTLVs); + + return { + tlvs, + }; + } + } + + /** + * @see ClusterId.START_KEY_NEGOTIATION_RESPONSE + */ + public readStartKeyNegotiationResponse(): StartKeyNegotiationResponse { + const status: Status = this.readUInt8(); + + if (status !== Status.SUCCESS) { + // INVALID_TLV, MISSING_TLV, TEMPORARY_FAILURE, NOT_AUTHORIZED + throw new ZdoStatusError(status); + } else { + const localTLVs = new Map([ + // Local: ID: 0x00: Curve25519PublicPointTLV + [0x00, this.readCurve25519PublicPointTLV], + ]); + const tlvs: TLV[] = this.readTLVs(localTLVs); + + return { + tlvs, + }; + } + } + + /** + * @see ClusterId.RETRIEVE_AUTHENTICATION_TOKEN_RESPONSE + */ + public readRetrieveAuthenticationTokenResponse (): RetrieveAuthenticationTokenResponse { + const status: Status = this.readUInt8(); + + if (status !== Status.SUCCESS) { + throw new ZdoStatusError(status); + } else { + // no local TLV + const tlvs: TLV[] = this.readTLVs(); + + return { + tlvs, + }; + } + } + + /** + * @see ClusterId.GET_AUTHENTICATION_LEVEL_RESPONSE + */ + public readGetAuthenticationLevelResponse (): GetAuthenticationLevelResponse { + const status: Status = this.readUInt8(); + + if (status !== Status.SUCCESS) { + // NOT_SUPPORTED, INV_REQUESTTYPE, MISSING_TLV, and NOT_AUTHORIZED + throw new ZdoStatusError(status); + } else { + const localTLVs = new Map([ + // Local: ID: 0x00: DeviceAuthenticationLevelTLV + [0x00, this.readDeviceAuthenticationLevelTLV], + ]); + const tlvs: TLV[] = this.readTLVs(localTLVs); + + return { + tlvs, + }; + } + } + + /** + * @see ClusterId.SET_CONFIGURATION_RESPONSE + */ + public readSetConfigurationResponse(): SetConfigurationResponse { + const status: Status = this.readUInt8(); + + if (status !== Status.SUCCESS) { + // INV_REQUESTTYPE, or NOT_SUPPORTED + throw new ZdoStatusError(status); + } else { + const localTLVs = new Map([ + // Local: ID: 0x00: ProcessingStatusTLV + [0x00, this.readProcessingStatusTLV], + ]); + const tlvs: TLV[] = this.readTLVs(localTLVs); + + return { + tlvs, + }; + } + } + + /** + * @see ClusterId.GET_CONFIGURATION_RESPONSE + */ + public readGetConfigurationResponse(): GetConfigurationResponse { + const status: Status = this.readUInt8(); + + if (status !== Status.SUCCESS) { + // INV_REQUESTTYPE, or NOT_SUPPORTED + throw new ZdoStatusError(status); + } else { + // Global: IDs: x, y, z + const tlvs: TLV[] = this.readTLVs(); + + return { + tlvs, + }; + } + } + + /** + * @see ClusterId.START_KEY_UPDATE_RESPONSE + * @returns No response payload, throws if not success + */ + public readStartKeyUpdateResponse(): void { + const status: Status = this.readUInt8(); + + /* istanbul ignore else */ + if (status !== Status.SUCCESS) { + // INV_REQUESTTYPE, NOT_AUTHORIZED or NOT_SUPPORTED + throw new ZdoStatusError(status); + } + } + + /** + * @see ClusterId.DECOMMISSION_RESPONSE + * @returns No response payload, throws if not success + */ + public readDecommissionResponse(): void { + const status: Status = this.readUInt8(); + + /* istanbul ignore else */ + if (status !== Status.SUCCESS) { + // INV_REQUESTTYPE, NOT_AUTHORIZED or NOT_SUPPORTED + throw new ZdoStatusError(status); + } + } + + /** + * @see ClusterId.CHALLENGE_RESPONSE + */ + public readChallengeResponse(): ChallengeResponse { + const status: Status = this.readUInt8(); + + if (status !== Status.SUCCESS) { + throw new ZdoStatusError(status); + } else { + const localTLVs = new Map([ + // Local: ID: 0x00: APSFrameCounterResponseTLV + [0x00, this.readAPSFrameCounterResponseTLV], + ]); + const tlvs: TLV[] = this.readTLVs(localTLVs); + + return { + tlvs, + }; + } + } +} diff --git a/src/zspec/zdo/definition/clusters.ts b/src/zspec/zdo/definition/clusters.ts new file mode 100644 index 0000000000..dc48f76e8c --- /dev/null +++ b/src/zspec/zdo/definition/clusters.ts @@ -0,0 +1,728 @@ +/** + * Defines for ZigBee device profile cluster IDs follow. + * These include descriptions of the formats of the messages. + * + * Note that each message starts with a 1-byte transaction sequence number. + * This sequence number is used to match a response command frame to the request frame that it is replying to. + * The application shall maintain a 1-byte counter that is copied into this field and incremented by one for each command sent. + * When a value of 0xff is reached, the next command shall re-start the counter with a value of 0x00. + * + * The Device Profile describes devices in one of two configurations: + * - Client: A client issues requests to the server via Device Profile messages. + * - Server: A server issues responses to the client that initiated the Device Profile message. + * + * Restricted Mode (`apsZdoRestrictedMode`) is a mode where a device will conditionally accept specific ZDO commands, + * depending on the restricted criteria, source address, and encryption policy of the incoming command. + * If a command is accepted, it is subject to normal command processing. + * The acceptance criteria is explain further below: + * 1. If the command is marked as “Yes” in the Restricted Command column, do the following: + * - a. If `apsZdoRestrictedMode` in the AIB is set to FALSE, the command is not restricted. + * - i. Go to Step 2. + * - b. If the sender is the Trust Center AND has APS encryption, the command is not restricted. + * - i. Go to Step 2. + * - c. Otherwise, the command SHALL NOT be processed. The receiver SHALL do the following: + * - i. If the command was broadcast, no error is generated. + * - No more processing is done. + * - ii. If the command was unicast, generate an error message. Create the corresponding ZDO Response frame with a status of NOT_AUTHORIZED. + * - No more processing is done. + * 2. Continue processing the command normally. + */ +export enum ClusterId { + //------------------------------------------------------------------------------------------------- + //-- Device and Service Discovery Client Services + + /** + * Request: [transaction sequence number: 1] + * [EUI64:8] [request type:1] [start index:1] + * [request type] = 0x00 single address response, ignore the start index + * = 0x01 extended response, sends kid's IDs as well + */ + NETWORK_ADDRESS_REQUEST = 0x0000, + /** + * Response: [transaction sequence number: 1] + * [status:1] [EUI64:8] [node ID:2] + * [assoc dev count:1] [start index:1] [assoc dev list:2]* + */ + NETWORK_ADDRESS_RESPONSE = 0x8000, + + /** + * Request: [transaction sequence number: 1] + * [node ID:2] [request type:1] [start index:1] + * [request type] = 0x00 single address response, ignore the start index + * = 0x01 extended response, sends kid's IDs as well + */ + IEEE_ADDRESS_REQUEST = 0x0001, + /** + * Response: [transaction sequence number: 1] + * [status:1] [EUI64:8] [node ID:2] + * [assoc dev count:1] [start index:1] [assoc dev list:2]* + */ + IEEE_ADDRESS_RESPONSE = 0x8001, + + /** + * Request: [transaction sequence number: 1] [node ID:2] [tlvs: varies] + */ + NODE_DESCRIPTOR_REQUEST = 0x0002, + /** + * Response: [transaction sequence number: 1] [status:1] [node ID:2] + * [node descriptor: 13] [tlvs: varies] + * + * Node Descriptor field is divided into subfields of bitmasks as follows: + * (Note: All lengths below are given in bits rather than bytes.) + * Logical Type: 3 + * Complex Descriptor Available: 1 + * User Descriptor Available: 1 + * (reserved/unused): 3 + * APS Flags: 3 + * Frequency Band: 5 + * MAC capability flags: 8 + * Manufacturer Code: 16 + * Maximum buffer size: 8 + * Maximum incoming transfer size: 16 + * Server mask: 16 + * Maximum outgoing transfer size: 16 + * Descriptor Capability Flags: 8 + * See ZigBee document 053474, Section 2.3.2.3 for more details. + */ + NODE_DESCRIPTOR_RESPONSE = 0x8002, + + /** + * Request: [transaction sequence number: 1] [node ID:2] + */ + POWER_DESCRIPTOR_REQUEST = 0x0003, + /** + * Response: [transaction sequence number: 1] [status:1] [node ID:2] + * [current power mode, available power sources:1] + * [current power source, current power source level:1] + * See ZigBee document 053474, Section 2.3.2.4 for more details. + */ + POWER_DESCRIPTOR_RESPONSE = 0x8003, + + /** + * Request: [transaction sequence number: 1] + * [node ID:2] [endpoint:1] + */ + SIMPLE_DESCRIPTOR_REQUEST = 0x0004, + /** + * Response: [transaction sequence number: 1] + * [status:1] [node ID:2] [length:1] [endpoint:1] + * [app profile ID:2] [app device ID:2] + * [app device version, app flags:1] + * [input cluster count:1] [input cluster:2]* + * [output cluster count:1] [output cluster:2]* + */ + SIMPLE_DESCRIPTOR_RESPONSE = 0x8004, + + /** + * Request: [transaction sequence number: 1] [node ID:2] + */ + ACTIVE_ENDPOINTS_REQUEST = 0x0005, + /** + * Response: [transaction sequence number: 1] + * [status:1] [node ID:2] [endpoint count:1] [endpoint:1]* + */ + ACTIVE_ENDPOINTS_RESPONSE = 0x8005, + + /** + * Request: [transaction sequence number: 1] + * [node ID:2] [app profile ID:2] + * [input cluster count:1] [input cluster:2]* + * [output cluster count:1] [output cluster:2]* + */ + MATCH_DESCRIPTORS_REQUEST = 0x0006, + /** + * Response: [transaction sequence number: 1] + * [status:1] [node ID:2] [endpoint count:1] [endpoint:1]* + */ + MATCH_DESCRIPTORS_RESPONSE = 0x8006, + + /** DEPRECATED */ + // COMPLEX_DESCRIPTOR_REQUEST = 0x0010, + /** DEPRECATED */ + // COMPLEX_DESCRIPTOR_RESPONSE = 0x8010, + /** DEPRECATED */ + // USER_DESCRIPTOR_REQUEST = 0x0011, + /** DEPRECATED */ + // USER_DESCRIPTOR_RESPONSE = 0x8011, + /** DEPRECATED */ + // DISCOVERY_REGISTER_REQUEST = 0x0012, + /** DEPRECATED */ + // DISCOVERY_REGISTER_RESPONSE = 0x8012, + + /** + * Request: [transaction sequence number: 1] + * [node ID:2] [EUI64:8] [capabilities:1] + */ + END_DEVICE_ANNOUNCE = 0x0013, + + /** DEPRECATED */ + // USER_DESCRIPTOR_SET = 0x0014, + /** DEPRECATED */ + // USER_DESCRIPTOR_CONFIRM = 0x8014, + + /** + * This is broadcast and only servers which have matching services respond. + * + * Request: [transaction sequence number: 1] [server mask:2] + */ + SYSTEM_SERVER_DISCOVERY_REQUEST = 0x0015, + /** + * The response contains the request services that the recipient provides. + * + * Response: [transaction sequence number: 1] + * [status (== EMBER_ZDP_SUCCESS):1] [server mask:2] + */ + SYSTEM_SERVER_DISCOVERY_RESPONSE = 0x8015, + + /** DEPRECATED */ + // DISCOVERY_STORE_REQUEST = 0x0016, + /** DEPRECATED */ + // DISCOVERY_STORE_RESPONSE = 0x8016, + /** DEPRECATED */ + // NODE_DESCRIPTOR_STORE_REQUEST = 0x0017, + /** DEPRECATED */ + // NODE_DESCRIPTOR_STORE_RESPONSE = 0x8017, + /** DEPRECATED */ + // POWER_DESCRIPTOR_STORE_REQUEST = 0x0018, + /** DEPRECATED */ + // POWER_DESCRIPTOR_STORE_RESPONSE = 0x8018, + /** DEPRECATED */ + // ACTIVE_ENDPOINTS_STORE_REQUEST = 0x0019, + /** DEPRECATED */ + // ACTIVE_ENDPOINTS_STORE_RESPONSE = 0x8019, + /** DEPRECATED */ + // SIMPLE_DESCRIPTOR_STORE_REQUEST = 0x001A, + /** DEPRECATED */ + // SIMPLE_DESCRIPTOR_STORE_RESPONSE = 0x801A, + /** DEPRECATED */ + // REMOVE_NODE_CACHE_REQUEST = 0x001B, + /** DEPRECATED */ + // REMOVE_NODE_CACHE_RESPONSE = 0x801B, + /** DEPRECATED */ + // FIND_NODE_CACHE_REQUEST = 0x001C, + /** DEPRECATED */ + // FIND_NODE_CACHE_RESPONSE = 0x801C, + /** DEPRECATED */ + // EXTENDED_SIMPLE_DESCRIPTOR_REQUEST = 0x001D, + /** DEPRECATED */ + // EXTENDED_SIMPLE_DESCRIPTOR_RESPONSE = 0x801D, + /** DEPRECATED */ + // EXTENDED_ACTIVE_ENDPOINTS_REQUEST = 0x001E, + /** DEPRECATED */ + // EXTENDED_ACTIVE_ENDPOINTS_RESPONSE = 0x801E, + + /** + * This is broadcast and only servers which have matching children respond. + * + * Request: [transaction sequence number: 1] + * [number of children:1] [child EUI64:8]* + */ + PARENT_ANNOUNCE = 0x001F, + /** + * The response contains the list of children that the recipient now holds. + * + * Response: [transaction sequence number: 1] + * [status: 1] [number of children:1] [child EUI64:8]* + */ + PARENT_ANNOUNCE_RESPONSE = 0x801F, + + + //------------------------------------------------------------------------------------------------- + //-- Bind, Unbind, and Bind Management Client Services Primitives + + + /** DEPRECATED */ + // END_DEVICE_BIND_REQUEST = 0x0020, + /** DEPRECATED */ + // END_DEVICE_BIND_RESPONSE = 0x8020, + + /** + * There are two possible formats, depending on whether the destination is a group address or a device address. + * Device addresses include an endpoint, groups don't. + * + * Request: [transaction sequence number: 1] + * [source EUI64:8] [source endpoint:1] + * [cluster ID:2] [destination address:3 or 10] + * Destination address: + * [0x01:1] [destination group:2] + * Or: + * [0x03:1] [destination EUI64:8] [destination endpoint:1] + * + */ + BIND_REQUEST = 0x0021, + /** + * Response: [transaction sequence number: 1] [status:1] + */ + BIND_RESPONSE = 0x8021, + /** + * There are two possible formats, depending on whether the destination is a group address or a device address. + * Device addresses include an endpoint, groups don't. + * + * Request: [transaction sequence number: 1] + * [source EUI64:8] [source endpoint:1] + * [cluster ID:2] [destination address:3 or 10] + * Destination address: + * [0x01:1] [destination group:2] + * Or: + * [0x03:1] [destination EUI64:8] [destination endpoint:1] + * + */ + UNBIND_REQUEST = 0x0022, + /** + * Response: [transaction sequence number: 1] [status:1] + */ + UNBIND_RESPONSE = 0x8022, + + /** DEPRECATED */ + // BIND_REGISTER_REQUEST = 0x0023, + /** DEPRECATED */ + // BIND_REGISTER_RESPONSE = 0x8023, + /** DEPRECATED */ + // REPLACE_DEVICE_REQUEST = 0x0024, + /** DEPRECATED */ + // REPLACE_DEVICE_RESPONSE = 0x8024, + /** DEPRECATED */ + // STORE_BACKUP_BIND_ENTRY_REQUEST = 0x0025, + /** DEPRECATED */ + // STORE_BACKUP_BIND_ENTRY_RESPONSE = 0x8025, + /** DEPRECATED */ + // REMOVE_BACKUP_BIND_ENTRY_REQUEST = 0x0026, + /** DEPRECATED */ + // REMOVE_BACKUP_BIND_ENTRY_RESPONSE = 0x8026, + /** DEPRECATED */ + // BACKUP_BIND_TABLE_REQUEST = 0x0027, + /** DEPRECATED */ + // BACKUP_BIND_TABLE_RESPONSE = 0x8027, + /** DEPRECATED */ + // RECOVER_BIND_TABLE_REQUEST = 0x0028, + /** DEPRECATED */ + // RECOVER_BIND_TABLE_RESPONSE = 0x8028, + /** DEPRECATED */ + // BACKUP_SOURCE_BIND_REQUEST = 0x0029, + /** DEPRECATED */ + // BACKUP_SOURCE_BIND_RESPONSE = 0x8029, + /** DEPRECATED */ + // RECOVER_SOURCE_BIND_REQUEST = 0x002A, + /** DEPRECATED */ + // RECOVER_SOURCE_BIND_RESPONSE = 0x802A, + + /** + * Request: [transaction sequence number: 1] + * [tlvs: Variable] + * tlvs: [Count N:1][EUI64 1:8]...[EUI64 N:8] + */ + CLEAR_ALL_BINDINGS_REQUEST = 0x002B, + /** + * Response: [transaction sequence number: 1] [status:1] + */ + CLEAR_ALL_BINDINGS_RESPONSE = 0x802B, + + + + //------------------------------------------------------------------------------------------------- + //-- Network Management Client Services + + /** DEPRECATED */ + // NETWORK_DISCOVERY_REQUEST = 0x0030, + /** DEPRECATED */ + // NETWORK_DISCOVERY_RESPONSE = 0x8030, + + /** + * Request: [transaction sequence number: 1] [start index:1] + */ + LQI_TABLE_REQUEST = 0x0031, + /** + * Response: [transaction sequence number: 1] [status:1] + * [neighbor table entries:1] [start index:1] + * [entry count:1] [entry:22]* + * [entry] = [extended PAN ID:8] [EUI64:8] [node ID:2] + * [device type, RX on when idle, relationship:1] + * [permit joining:1] [depth:1] [LQI:1] + * + * The device-type byte has the following fields: + * + * Name Mask Values + * + * device type 0x03 0x00 coordinator + * 0x01 router + * 0x02 end device + * 0x03 unknown + * + * rx mode 0x0C 0x00 off when idle + * 0x04 on when idle + * 0x08 unknown + * + * relationship 0x70 0x00 parent + * 0x10 child + * 0x20 sibling + * 0x30 other + * 0x40 previous child + * reserved 0x10 + * + * The permit-joining byte has the following fields + * + * Name Mask Values + * + * permit joining 0x03 0x00 not accepting join requests + * 0x01 accepting join requests + * 0x02 unknown + * reserved 0xFC + * + */ + LQI_TABLE_RESPONSE = 0x8031, + + /** + * Request: [transaction sequence number: 1] [start index:1] + */ + ROUTING_TABLE_REQUEST = 0x0032, + /** + * Response: [transaction sequence number: 1] [status:1] + * [routing table entries:1] [start index:1] + * [entry count:1] [entry:5]* + * [entry] = [destination address:2] + * [status:1] + * [next hop:2] + * + * + * The status byte has the following fields: + * Name Mask Values + * + * status 0x07 0x00 active + * 0x01 discovery underway + * 0x02 discovery failed + * 0x03 inactive + * 0x04 validation underway + * + * flags 0x38 + * 0x08 memory constrained + * 0x10 many-to-one + * 0x20 route record required + * + * reserved 0xC0 + */ + ROUTING_TABLE_RESPONSE = 0x8032, + + /** + * Request: [transaction sequence number: 1] [start index:1] + */ + BINDING_TABLE_REQUEST = 0x0033, + /** + * Response: [transaction sequence number: 1] + * [status:1] [binding table entries:1] [start index:1] + * [entry count:1] [entry:14/21]* + * [entry] = [source EUI64:8] [source endpoint:1] [cluster ID:2] + * [dest addr mode:1] [dest:2/8] [dest endpoint:0/1] + * + * @note If Dest. Address Mode = 0x03, then the Long Dest. Address will be + * used and Dest. endpoint will be included. If Dest. Address Mode = 0x01, + * then the Short Dest. Address will be used and there will be no Dest. + * endpoint. + */ + BINDING_TABLE_RESPONSE = 0x8033, + + /** + * Stacks certified prior to Revision 21 MAY or MAY NOT support this command. + * If this management command is not supported, a status of NOT_SUPPORTED SHALL be returned. + * All stacks certified to Revision 21 and later SHALL support this command. + * + * Request: [transaction sequence number: 1] [EUI64:8] [flags:1] + * The flag bits are: + * 0x40 remove children + * 0x80 rejoin + */ + LEAVE_REQUEST = 0x0034, + /** + * Response: [transaction sequence number: 1] [status:1] + */ + LEAVE_RESPONSE = 0x8034, + + /** DEPRECATED */ + // DIRECT_JOIN_REQUEST = 0x0035, + /** DEPRECATED */ + // DIRECT_JOIN_RESPONSE = 0x8035, + + /** + * Request: [transaction sequence number: 1] + * [duration:1] [permit authentication:1] + */ + PERMIT_JOINING_REQUEST = 0x0036, + /** + * No response if broadcasted to all routers. + * + * Response: [transaction sequence number: 1] [status:1] + */ + PERMIT_JOINING_RESPONSE = 0x8036, + + /** DEPRECATED */ + // CACHE_REQUEST = 0x0037, + /** DEPRECATED */ + // CACHE_RESPONSE = 0x8037, + + /** + * Request: [transaction sequence number: 1] + * [scan channels:4] [duration:1] [count:0/1] [nwkUpdateId:0/1] [manager:0/2] + * + * If the duration is in 0x00 ... 0x05, 'count' is present but + * not 'manager'. Perform 'count' scans of the given duration on the + * given channels. + * + * If duration is 0xFE, 'channels' should have a single channel + * and 'count' and 'manager' are not present. Switch to the indicated + * channel. + * + * If duration is 0xFF, 'count' is not present. Set the active + * channels and the network manager ID to the values given. + * + * Unicast requests always get a response, which is INVALID_REQUEST if the + * duration is not a legal value. + */ + NWK_UPDATE_REQUEST = 0x0038, + /** + * Response: [transaction sequence number: 1] [status:1] + * [scanned channels:4] [transmissions:2] [failures:2] + * [energy count:1] [energy:1]* + */ + NWK_UPDATE_RESPONSE = 0x8038, + + /** + * Request: [transaction sequence number: 1] + * [scan channels list structure: Variable] [duration:1] [count:0/1] [nwkUpdateId:0/1] [manager:0/2] [configuration bitmask:0/1] + */ + NWK_ENHANCED_UPDATE_REQUEST = 0x0039, + /** + * Response: [transaction sequence number: 1] [status:1] + * [scanned channels:4] [transmissions:2] [failures:2] + * [energy count:1] [energy:1]* + */ + NWK_ENHANCED_UPDATE_RESPONSE = 0x8039, + + /** + * Request: [transaction sequence number: 1] + * [start index: 1] + */ + NWK_IEEE_JOINING_LIST_REQUEST = 0x003A, + /** + * Response: [transaction sequence number: 1] [status: 1] [ieee joining list update id: 1] [joining policy: 1] + * [ieee joining list total: 1] [start index: 1] [ieee joining count: 1] [ieee:8]* + */ + NWK_IEEE_JOINING_LIST_REPONSE = 0x803A, + + /** + * Response: [transaction sequence number: 1] [status: 1] [channel in use: 4] [mac tx ucast total: 2] [mac tx ucast failures: 2] + * [mac tx ucast retries: 2] [period of time for results: 1] + */ + NWK_UNSOLICITED_ENHANCED_UPDATE_RESPONSE = 0x803B, + + /** + * This command can be used by a remote device to survey the end devices to determine how many potential parents they have access to. + * + * Request: [transaction sequence number: 1] + * [TLVs: varies] + * + * Contains one Beacon Survey Configuration TLV (variable octets), + * which contain the ScanChannelListStructure (variable length) + * and the ConfigurationBitmask (1 octet). This information provides + * the configuration for the end device's beacon survey. + * See R23 spec section 2.4.3.3.12 for the request and 3.2.2.2.1 + * for the ChannelListStructure. + */ + NWK_BEACON_SURVEY_REQUEST = 0x003C, + /** + * + * Response: [transaction sequence number: 1] + * [status: 1] + * [TLVs: varies] + * + * Contains one Beacon Survey Results TLV (4 octets), which contain + * the number of on-network, off-network, potential parent and total + * beacons recorded. If the device that received the request is not a + * router, a Potential Parent TLV (variable octects) will be found. This + * will contain information on the device's current parent, as well as + * any potential parents found via beacons (up to a maximum of 5). A + * Pan ID Conflict TLV can also found in the response. + * See R23 spec section 2.4.4.3.13 for the response. + */ + NWK_BEACON_SURVEY_RESPONSE = 0x803C, + + + + //------------------------------------------------------------------------------------------------- + //-- Security Client Services + + /** + * + * Request: [transaction sequence number: 1] + * [TLVs: varies] + * + * Contains one or more Curve25519 Public Point TLVs (40 octets), + * which contain an EUI64 and the 32-byte Curve public point. + * See R23 spec section 2.4.3.4.1 + * + * @note This command SHALL NOT be APS encrypted regardless of + * whether sent before or after the device joins the network. + * This command SHALL be network encrypted if the device has a + * network key, i.e. it has joined the network earlier and wants + * to negotiate or renegotiate a new link key; otherwise, if it + * is used prior to joining the network, it SHALL NOT be network + * encrypted. + */ + START_KEY_NEGOTIATION_REQUEST = 0x0040, + /** + * + * Response: [transaction sequence number: 1] [status:1] + * [TLVs: varies] + * + * Contains one or more Curve25519 Public Point TLVs (40 octets), + * which contain an EUI64 and the 32-byte Curve public point, or + * Local TLVs. + * See R23 spec section 2.4.4.4.1 + * + * @note This command SHALL NOT be APS encrypted. When performing + * Key Negotiation with an unauthenticated neighbor that is not + * yet on the network, network layer encryption SHALL NOT be used + * on the message. If the message is being sent to unauthenticated + * device that is not on the network and is not a neighbor, it + * SHALL be relayed as described in section 4.6.3.7.7. Otherwise + * the message SHALL have network layer encryption. + */ + START_KEY_NEGOTIATION_RESPONSE = 0x8040, + + /** + * + * Request: [transaction sequence number: 1] + * [TLVs: varies] + * + * Contains one or more Authentication Token ID TLVs (1 octet), + * which contain the TLV Type Tag ID of the source of the + * authentication token. See R23 spec section 2.4.3.4.2 + */ + RETRIEVE_AUTHENTICATION_TOKEN_REQUEST = 0x0041, + /** + * + * Response: [transaction sequence number: 1] [status:1] + * [TLVs: varies] + * + * Contains one or more 128-bit Symmetric Passphrase Global TLVs + * (16 octets), which contain the symmetric passphrase authentication + * token. See R23 spec section 2.4.4.4.2 + */ + RETRIEVE_AUTHENTICATION_TOKEN_RESPONSE = 0x8041, + + /** + * + * Request: [transaction sequence number: 1] + * [TLVs: varies] + * + * Contains one or more Target IEEE Address TLVs (8 octets), + * which contain the EUI64 of the device of interest. + * See R23 spec section 2.4.3.4.3 + */ + GET_AUTHENTICATION_LEVEL_REQUEST = 0x0042, + /** + * + * Response: [transaction sequence number: 1] [status:1] + * [TLVs: varies] + * + * Contains one or more Device Authentication Level TLVs + * (10 octets), which contain the EUI64 of the inquired device, + * along with the its initial join method and its active link + * key update method. + * See R23 spec section 2.4.4.4.3 + */ + GET_AUTHENTICATION_LEVEL_RESPONSE = 0x8042, + + /** + * + * Request: [transaction sequence number: 1] + * [TLVs: varies] + * + * Contains one or more Global TLVs (1 octet), + * which contain the TLV Type Tag ID, and their + * value. + */ + SET_CONFIGURATION_REQUEST = 0x0043, + /** + * + * Response: [transaction sequence number: 1] [status:1] + */ + SET_CONFIGURATION_RESPONSE = 0x8043, + + /** + * + * Request: [transaction sequence number: 1] + * [TLVs: varies] + * + * Contains one or more TLVs (1 octet), + * which the sender wants to get information + */ + GET_CONFIGURATION_REQUEST = 0x0044, + /** + * + * Response: [transaction sequence number: 1] [status:1] + * [TLVs: varies] + * + * Contains one or more TLV tag Ids and their values + * in response to the request + */ + GET_CONFIGURATION_RESPONSE = 0x8044, + + /** + * + * Request: [transaction sequence number: 1] + * [TLVs: varies] + * + * Contains one or more TLVs. These TLVs can be Selected Key + * Negotiation Method TLVs (10 octets), Fragmentation Parameters + * Global TLVs (5 octets), or other TLVs. + * See R23 spec section 2.4.3.4.6 + * + * @note This SHALL NOT be APS encrypted or NWK encrypted if the + * link key update mechanism is done as part of the initial join + * and before the receiving device has been issued a network + * key. This SHALL be both APS encrypted and NWK encrypted if + * the link key update mechanism is performed to refresh the + * link key when the receiving device has the network key and + * has previously successfully joined the network. + */ + START_KEY_UPDATE_REQUEST = 0x0045, + /** + * + * Response: [transaction sequence number: 1] [status:1] + * + * See R23 spec section 2.4.4.4.6 + * + * @note This command SHALL be APS encrypted. + */ + START_KEY_UPDATE_RESPONSE = 0x8045, + + /** + * Request: [transaction sequence number: 1] + * [security decommission request EUI64 TLV:Variable] + * Security Decommission request EUI64 TLV: + * [Count N:1][EUI64 1:8]...[EUI64 N:8] + */ + DECOMMISSION_REQUEST = 0x0046, + /** + * + * Response: [transaction sequence number: 1] [status:1] + */ + DECOMMISSION_RESPONSE = 0x8046, + + /** + * Request: [transaction sequence number: 1] + * [TLVs: varies] + * + * Contains at least the APS Frame Counter Challenge TLV, which holds the + * sender EUI and the 64 bit challenge value. + */ + CHALLENGE_REQUEST = 0x0047, + /** + * Response: [transaction sequence number: 1] + * [TLVs: varies] + * + * Contains at least the APS Frame Counter Response TLV, which holds the + * sender EUI, received challenge value, APS frame counter, challenge + * security frame counter, and 8-byte MIC. + */ + CHALLENGE_RESPONSE = 0x8047, +}; diff --git a/src/zspec/zdo/definition/consts.ts b/src/zspec/zdo/definition/consts.ts new file mode 100644 index 0000000000..02f8808e3b --- /dev/null +++ b/src/zspec/zdo/definition/consts.ts @@ -0,0 +1,16 @@ +/** The endpoint where the ZigBee Device Object (ZDO) resides. */ +export const ZDO_ENDPOINT = 0; + +/** The profile ID used by the ZigBee Device Object (ZDO). */ +export const ZDO_PROFILE_ID = 0x0000; + +/** ZDO messages start with a sequence number. */ +export const ZDO_MESSAGE_OVERHEAD = 1; + +export const MULTICAST_BINDING = 0x01; +export const UNICAST_BINDING = 0x03; + +/** 64-bit challenge value used by CHALLENGE_REQUEST/CHALLENGE_RESPONSE clusters */ +export const CHALLENGE_VALUE_SIZE = 8; +/** The 256-bit Curve 25519 public point. */ +export const CURVE_PUBLIC_POINT_SIZE = 32; diff --git a/src/zspec/zdo/definition/enums.ts b/src/zspec/zdo/definition/enums.ts new file mode 100644 index 0000000000..e2366e0ae4 --- /dev/null +++ b/src/zspec/zdo/definition/enums.ts @@ -0,0 +1,88 @@ +export enum LeaveRequestFlags { + /** Leave and rejoin. */ + AND_REJOIN = 0x80, + /** DEPRECATED */ + // AND_REMOVE_CHILDREN = 0x40, + /** Leave. */ + WITHOUT_REJOIN = 0x00, +}; + +export enum JoiningPolicy { + /** Any device is allowed to join. */ + ALL_JOIN = 0x00, + /** Only devices on the mibJoiningIeeeList are allowed to join. */ + IEEELIST_JOIN = 0x01, + /** No device is allowed to join. */ + NO_JOIN = 0x02, +}; + +//------------------------------------------------------------------------------------------------- +//-- TLVs + +export enum SelectedKeyNegotiationProtocol { + /** (Zigbee 3.0 Mechanism) */ + RESERVED = 0, + /** SPEKE using Curve25519 with Hash AES-MMO-128 */ + SPEKE_CURVE25519_AESMMO128 = 1, + /** SPEKE using Curve25519 with Hash SHA-256 */ + SPEKE_CURVE25519_SHA256 = 2, + // 3 – 255 Reserved +}; + +export enum SelectedPreSharedSecret { + /** Symmetric Authentication Token */ + SYMMETRIC_AUTHENTICATION_TOKEN = 0, + /** Pre-configured link-ley derived from installation code */ + PRECONFIGURED_LINKKEY_DERIVED_FROM_INSTALL_CODE = 1, + /** Variable-length pass code (for PAKE protocols) */ + PAKE_VARIABLE_LENGTH_PASS_CODE = 2, + /** Basic Authorization Key */ + BASIC_AUTHORIZATION_KEY = 3, + /** Administrative Authorization Key */ + ADMIN_AUTHORIZATION_KEY = 4, + // 5 – 254 Reserved, + /** Anonymous Well-Known Secret */ + ANONYMOUS_WELLKNOWN_SECRET = 255, +}; + +export enum InitialJoinMethod { + ANONYMOUS = 0x00, + INSTALL_CODE_KEY = 0x01, + WELLKNOWN_PASSPHRASE = 0x02, + INSTALL_CODE_PASSPHRASE = 0x03, +} + +export enum ActiveLinkKeyType { + NOT_UPDATED = 0x00, + KEY_REQUEST_METHOD = 0x01, + UNAUTHENTICATED_KEY_NEGOTIATION = 0x02, + AUTHENTICATED_KEY_NEGOTIATION = 0x03, + APPLICATION_DEFINED_CERTIFICATE_BASED_MUTUAL_AUTHENTICATION = 0x04, +} + +export enum GlobalTLV { + /** Minimum Length 2-byte */ + MANUFACTURER_SPECIFIC = 64, + /** Minimum Length 2-byte */ + SUPPORTED_KEY_NEGOTIATION_METHODS = 65, + /** Minimum Length 4-byte XXX: spec min doesn't make sense, this is one pan id => 2-byte??? */ + PAN_ID_CONFLICT_REPORT = 66, + /** Minimum Length 2-byte */ + NEXT_PAN_ID_CHANGE = 67, + /** Minimum Length 4-byte */ + NEXT_CHANNEL_CHANGE = 68, + /** Minimum Length 16-byte */ + SYMMETRIC_PASSPHRASE = 69, + /** Minimum Length 2-byte */ + ROUTER_INFORMATION = 70, + /** Minimum Length 2-byte */ + FRAGMENTATION_PARAMETERS = 71, + JOINER_ENCAPSULATION = 72 , + BEACON_APPENDIX_ENCAPSULATION = 73 , + // Reserved = 74, + /** Minimum Length 2-byte XXX: min not in spec??? */ + CONFIGURATION_PARAMETERS = 75 , + /** Refer to the Zigbee Direct specification for more details. */ + DEVICE_CAPABILITY_EXTENSION = 76, + // Reserved = 77-255 +}; diff --git a/src/zspec/zdo/definition/status.ts b/src/zspec/zdo/definition/status.ts new file mode 100644 index 0000000000..322ed09aa0 --- /dev/null +++ b/src/zspec/zdo/definition/status.ts @@ -0,0 +1,105 @@ +/** + * ZDO response status. + * + * Most responses to ZDO commands contain a status byte. + * The meaning of this byte is defined by the ZigBee Device Profile. + * + * Zigbee Document – 05-3474-23 - Table 2-129. ZDP Enumerations Description + * + * uint8_t + */ +export enum Status { + /** The requested operation or transmission was completed successfully. */ + SUCCESS = 0x00, + // 0x01 – 0x7F are reserved + /** The supplied request type was invalid. */ + INV_REQUESTTYPE = 0x80, + /** The requested device did not exist on a device following a child descriptor request to a parent. */ + DEVICE_NOT_FOUND = 0x81, + /** The supplied endpoint was equal to 0x00 or 0xff. */ + INVALID_EP = 0x82, + /** The requested endpoint is not described by a simple descriptor. */ + NOT_ACTIVE = 0x83, + /** The requested optional feature is not supported on the target device. */ + NOT_SUPPORTED = 0x84, + /** A timeout has occurred with the requested operation. */ + TIMEOUT = 0x85, + /** failure to match any suitable clusters. */ + NO_MATCH = 0x86, + // 0x87 is reserved = 0x87, + /** The unbind request was unsuccessful due to the coordinator or source device not having an entry in its binding table to unbind. */ + NO_ENTRY = 0x88, + /** A child descriptor was not available following a discovery request to a parent. */ + NO_DESCRIPTOR = 0x89, + /** The device does not have storage space to support the requested operation. */ + INSUFFICIENT_SPACE = 0x8a, + /** The device is not in the proper state to support the requested operation. */ + NOT_PERMITTED = 0x8b, + /** The device does not have table space to support the operation. */ + TABLE_FULL = 0x8c, + /** The device has rejected the command due to security restrictions. */ + NOT_AUTHORIZED = 0x8d, + /** The device does not have binding table space to support the operation. */ + DEVICE_BINDING_TABLE_FULL = 0x8e, + /** The index in the received command is out of bounds. */ + INVALID_INDEX = 0x8f, + /** The response was too large to fit in a single unfragmented message. */ + FRAME_TOO_LARGE = 0x90, + /** The requested Key Negotiation Method was not accepted. */ + BAD_KEY_NEGOTIATION_METHOD = 0x91, + /** The request encountered a temporary failure but a retry at a later time should be attempted and may succeed. */ + TEMPORARY_FAILURE = 0x92, + // 0x92 – 0xff are reserved + + //-- NWK layer statuses included because some like TLV-related are used as ZDO status + + /** An invalid or out-of-range parameter has been passed to a primitive from the next higher layer. */ + NWK_LAYER_INVALID_PARAMETER = 0xc1, + /** The next higher layer has issued a request that is invalid or cannot be executed given the current state of the NWK layer. */ + NWK_LAYER_INV_REQUESTTYPE = 0xc2, + /** An NLME-JOIN.request has been disallowed. */ + NWK_LAYER_NOT_PERMITTED = 0xc3, + /** An NLME-NETWORK-FORMATION.request has failed to start a network. */ + NWK_LAYER_STARTUP_FAILURE = 0xc4, + /** + * A device with the address supplied to the NLME-ADDNEIGHBOR. request is already present in the neighbor table of the device + * on which the NLME-ADD-NEIGHBOR.request was issued. + */ + NWK_LAYER_ALREADY_PRESENT = 0xc5, + /** Used to indicate that an NLME-SYNC.request has failed at the MAC layer. */ + NWK_LAYER_SYNC_FAILURE = 0xc6, + /** An NLME-JOIN-DIRECTLY.request has failed because there is no more room in the neighbor table. */ + NWK_LAYER_NEIGHBOR_TABLE_FULL = 0xc7, + /** An NLME-LEAVE.request has failed because the device addressed in the parameter list is not in the neighbor table of the issuing device. */ + NWK_LAYER_UNKNOWN_DEVICE = 0xc8, + /** An NLME-GET.request or NLME-SET.request has been issued with an unknown attribute identifier. */ + NWK_LAYER_UNSUPPORTED_ATTRIBUTE = 0xc9, + /** An NLME-JOIN.request has been issued in an environment where no networks are detectable. */ + NWK_LAYER_NO_NETWORKS = 0xca, + // Reserved 0xcb Reserved for future use. + /** Security processing has been attempted on an outgoing frame, and has failed because the frame counter has reached its maximum value. */ + NWK_LAYER_MAX_FRM_COUNTER = 0xcc, + /** Security processing has been attempted on an outgoing frame, and has failed because no key was available with which to process it. */ + NWK_LAYER_NO_KEY = 0xcd, + /** Security processing has been attempted on an outgoing frame, and has failed because the security engine produced erroneous output. */ + NWK_LAYER_BAD_CCM_OUTPUT = 0xce, + /** Reserved for future use. */ + NWK_LAYER_Reserved = 0xcf, + /** An attempt to discover a route has failed due to a reason other than a lack of routing capacity. */ + NWK_LAYER_ROUTE_DISCOVERY_FAILED = 0xd0, + /** + * An NLDE-DATA.request has failed due to a routing failure on the sending device or an NLME-ROUTE-DISCOVERY.request has failed due to + * the cause cited in the accompanying NetworkStatusCode. + */ + NWK_LAYER_ROUTE_ERROR = 0xd1, + /** An attempt to send a broadcast frame has failed because there is no room in the BTT. */ + NWK_LAYER_BT_TABLE_FULL = 0xd2, + /** An NLDE-DATA.request has failed due to insufficient buffering available. */ + NWK_LAYER_FRAME_NOT_BUFFERED = 0xd3, + /** An attempt was made to use a MAC Interface with a state that is currently set to FALSE (disabled) or that is unknown to the stack.. */ + NWK_LAYER_INVALID_INTERFACE = 0xd5, + /** A required TLV for processing the request was not present. */ + NWK_LAYER_MISSING_TLV = 0xD6, + /** A TLV was malformed or missing relevant information. */ + NWK_LAYER_INVALID_TLV = 0xD7, +}; diff --git a/src/zspec/zdo/definition/tstypes.ts b/src/zspec/zdo/definition/tstypes.ts new file mode 100644 index 0000000000..d0fe74e586 --- /dev/null +++ b/src/zspec/zdo/definition/tstypes.ts @@ -0,0 +1,866 @@ +import {ClusterId, EUI64, ExtendedPanId, NodeId, PanId, ProfileId} from '../../tstypes'; +import { + JoiningPolicy, + ActiveLinkKeyType, + InitialJoinMethod, + SelectedKeyNegotiationProtocol, + SelectedPreSharedSecret +} from './enums'; +import {Status} from './status'; + +/** + * Bits: + * - [alternatePANCoordinator: 1] + * - [deviceType: 1] + * - [powerSource: 1] + * - [rxOnWhenIdle: 1] + * - [reserved1: 1] + * - [reserved2: 1] + * - [securityCapability: 1] + * - [securityCapability: 1] + */ +export type MACCapabilityFlags = { + /** + * The alternate PAN coordinator sub-field is one bit in length and shall be set to 1 if this node is capable of becoming a PAN coordinator. + * Otherwise, the alternative PAN coordinator sub-field shall be set to 0. + */ + alternatePANCoordinator: number; + /** + * The device type sub-field is one bit in length and shall be set to 1 if this node is a full function device (FFD). + * Otherwise, the device type sub-field shall be set to 0, indicating a reduced function device (RFD). + */ + deviceType: number; + /** + * The power source sub-field is one bit in length and shall be set to 1 if the current power source is mains power. + * Otherwise, the power source sub-field shall be set to 0. + * This information is derived from the node current power source field of the node power descriptor. + */ + powerSource: number; + /** + * The receiver on when idle sub-field is one bit in length and shall be set to 1 if the device does not disable its receiver to + * conserve power during idle periods. + * Otherwise, the receiver on when idle sub-field shall be set to 0 (see also section 2.3.2.4.) + */ + rxOnWhenIdle: number; + reserved1: number; + reserved2: number; + /** + * The security capability sub-field is one bit in length and shall be set to 1 if the device is capable of sending and receiving + * frames secured using the security suite specified in [B1]. + * Otherwise, the security capability sub-field shall be set to 0. + */ + securityCapability: number; + /** The allocate address sub-field is one bit in length and shall be set to 0 or 1. */ + allocateAddress: number; +}; + +/** + * Bits: + * - [primaryTrustCenter: 1] + * - [backupTrustCenter: 1] + * - [deprecated1: 1] + * - [deprecated2: 1] + * - [deprecated3: 1] + * - [deprecated4: 1] + * - [networkManager: 1] + * - [reserved1: 1] + * - [reserved2: 1] + * - [stackComplianceResivion: 7] + */ +export type ServerMask = { + primaryTrustCenter: number, + backupTrustCenter: number, + deprecated1: number, + deprecated2: number, + deprecated3: number, + deprecated4: number, + networkManager: number, + reserved1: number, + reserved2: number, + /** + * Indicate the Revision of the Zigbee Pro Core specification that the running stack is implemented to. + * Prior to Revision 21 of the specification these bits were reserved and thus set to 0. + * A stack that is compliant to Revision 23 would set these bits to 23 (0010111b). + * A stack SHALL indicate the Revision of the specification it is compliant to by setting these bits. + */ + stackComplianceResivion: number, +}; + + +//------------------------------------------------------------------------------------------------- +//-- Responses + +export type LQITableEntry = { + /** + * The 64-bit extended PAN identifier of the neighboring device. + * + * 64-bit + */ + extendedPanId: ExtendedPanId; + /** + * 64-bit IEEE address that is unique to every device. + * If this value is unknown at the time of the request, this field shall be set to 0xffffffffffffffff. + * + * 64-bit + */ + eui64: EUI64; + /** The 16-bit network address of the neighboring device. 16-bit */ + nwkAddress: NodeId; + /** + * The type of the neighbor device: + * 0x00 = ZigBee coordinator + * 0x01 = ZigBee router + * 0x02 = ZigBee end device + * 0x03 = Unknown + * + * 2-bit + */ + deviceType: number; + /** + * Indicates if neighbor's receiver is enabled during idle portions of the CAP: + * 0x00 = Receiver is off + * 0x01 = Receiver is on + * 0x02 = unknown + * + * 2-bit + */ + rxOnWhenIdle: number; + /** + * The relationship between the neighbor and the current device: + * 0x00 = neighbor is the parent + * 0x01 = neighbor is a child + * 0x02 = neighbor is a sibling + * 0x03 = None of the above + * 0x04 = previous child + * + * 3-bit + */ + relationship: number; + /** This reserved bit shall be set to 0. 1-bit */ + reserved1: number; + /** + * An indication of whether the neighbor device is accepting join requests: + * 0x00 = neighbor is not accepting join requests + * 0x01 = neighbor is accepting join requests + * 0x02 = unknown + * + * 2-bit + */ + permitJoining: number; + /** Each of these reserved bits shall be set to 0. 6-bit */ + reserved2: number; + /** + * The tree depth of the neighbor device. + * A value of 0x00 indicates that the device is the ZigBee coordinator for the network + * + * 8-bit + */ + depth: number; + /** + * The estimated link quality for RF transmissions from this device. + * See [B1] for discussion of how this is calculated. + * + * 8-bit + */ + lqi: number; +}; + +export type RoutingTableEntry = { + /** 16-bit network address of this route */ + 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: number; + /** + * A flag indicating whether the device is a memory constrained concentrator + * + * 1-bit + */ + memoryConstrained: number; + /** + * A flag indicating that the destination is a concentrator that issued a many-to-one request + * + * 1-bit + */ + manyToOne: number; + /** + * A flag indicating that a route record command frame should be sent to the destination prior to the next data packet. + * + * 1-bit + */ + routeRecordRequired: number; + /** 2-bit */ + reserved1: number; + /** 16-bit network address of the next hop on the way to the destination. */ + nextHopAddress: number; +}; + +export type BindingTableEntry = { + /** The source IEEE address for the binding entry. */ + sourceEui64: EUI64; + /** The source endpoint for the binding entry. */ + sourceEndpoint: number; + /** The identifier of the cluster on the source device that is bound to the destination device. */ + clusterId: ClusterId; + /** + * The addressing mode for the destination address. This field can take one of the non-reserved values from the following list: + * - 0x00 = reserved + * - 0x01 = 16-bit group address for DstAddr and DstEndpoint not present + * - 0x02 = reserved + * - 0x03 = 64-bit extended address for DstAddr and DstEndp present + * - 0x04 – 0xff = reserved + */ + destAddrMode: number; + /** The destination address for the binding entry. 2-byte or 8-byte */ + dest: NodeId | EUI64; + /** + * This field shall be present only if the DstAddrMode field has a value of 0x03 and, if present, + * shall be the destination endpoint for the binding entry. + */ + destEndpoint?: number; +}; + +export type NetworkAddressResponse = { + /** 64-bit address for the Remote Device. */ + eui64: EUI64; + /** 16-bit address for the Remote Device. */ + nwkAddress: NodeId; + /** + * Starting index into the list of associated devices for this report. + * If the RequestType in the request is Extended Response and there are no associated devices on the Remote Device, + * this field SHALL NOT be included in the frame. + * If an error occurs or the Request Type in the request is for a Single Device Response, this field SHALL NOT be included in the frame. + */ + startIndex: number; + /** + * A list of 16-bit addresses, one corresponding to each associated device to Remote Device; + * The number of 16-bit network addresses contained in this field is specified in the NumAssocDev field. + * If the RequestType in the request is Extended Response and there are no associated devices on the Remote Device, + * this field SHALL NOT be included in the frame. + * If an error occurs or the Request Type in the request is for a Single Device Response, this field SHALL NOT be included in the frame. + */ + assocDevList: NodeId[]; +}; + +export type IEEEAddressResponse = { + /** @see NetworkAddressResponse.eui64 */ + eui64: EUI64; + /** @see NetworkAddressResponse.nwkAddress */ + nwkAddress: NodeId; + /** @see NetworkAddressResponse.startIndex */ + startIndex: number; + /** @see NetworkAddressResponse.assocDevList */ + assocDevList: NodeId[]; +}; + +export type NodeDescriptorResponse = { + /** NWK address for the request. */ + nwkAddress: NodeId; + /** 000 == Zigbee Coordinator, 001 == Zigbee Router, 010 === Zigbee End Device, 011-111 === Reserved */ + logicalType: number; + /** R23 and above (determined via other means if not). Indicates whether the device supports fragmentation at the APS layer. */ + fragmentationSupported: boolean | null, + /** Specifies the application support sub-layer capabilities of the node. Currently not supported, should be zero */ + apsFlags: number; + /** + * Specifies the frequency bands that are supported by the underlying IEEE Std 802.15.4 radio(s) utilized by the node. + * Bits: + * - 0 = 868 – 868.6 MHz + * - 1 = Reserved + * - 2 = 902 – 928 MHz + * - 3 = 2400 – 2483.5 MHz + * - 4 = GB Smart Energy sub-GHz bands: (863-876MHz and 915-921MHz) + */ + frequencyBand: number; + /** Specifies the node capabilities. */ + capabilities: MACCapabilityFlags; + /** Specifies a manufacturer code that is allocated by the Connectivity Standards Alliance, relating the manufacturer to the device. */ + manufacturerCode: number; + /** + * Specifies the maximum size, in octets, of the network sub-layer data unit (NSDU) for this node. + * This is the maximum size of data or commands passed to or from the application by the application support sub-layer, + * before any fragmentation or re-assembly. + * This field can be used as a high-level indication for network management. + * Valid range of 0x00-0x7f. + */ + maxBufSize: number; + /** + * Indicates the device's apsMaxSizeASDU AIB value. + * Specifies the maximum size, in octets, of the application sub-layer data unit (ASDU) that can be transferred to this node + * in one single message transfer. + * This value can exceed the value of the node maximum buffer size field (see section 2.3.2.3.9) through the use of fragmentation. + * Valid range of 0x0000-0x7fff. + */ + maxIncTxSize: number; + /** The system server capabilities of this node */ + serverMask: ServerMask; + /** + * Specifies the maximum size, in octets, of the application sub-layer data unit (ASDU) that can be transferred + * from this node in one single message transfer. + * This value can exceed the value of the node 2777 maximum buffer size field (see section 2.3.2.3.9) through the use of fragmentation. + * Valid range of 0x0000-0x7fff. + */ + maxOutTxSize: number; + deprecated1: number; +} & TLVs; + +export type PowerDescriptorResponse = { + /** NWK address for the request. */ + nwkAddress: NodeId; + /** + * - 0000 == receiver sync'ed with receiver on when idle subfield of the node descriptor + * - 0001 == receiver comes on periodically as defined by the node power descriptor + * - 0010 == receiver comes on when stimulated, for example, by a user pressing a button + * - 0011-1111 reserved + */ + currentPowerMode: number; + /** + * Bits: + * - 0 == constants (mains) power + * - 1 == rechargeable battery + * - 2 == disposable battery + * - 3 == reserved + */ + availPowerSources: number; + /** + * Bits: + * - 0 == constants (mains) power + * - 1 == rechargeable battery + * - 2 == disposable battery + * - 3 == reserved + */ + currentPowerSource: number; + /** + * - 0000 == critical + * - 0100 == 33% + * - 1000 == 66% + * - 1100 == 100% + */ + currentPowerSourceLevel: number; +}; + +export type SimpleDescriptorResponse = { + /** NWK address for the request. */ + nwkAddress: NodeId; + /** + * Specifies the endpoint within the node to which this description refers. + * Applications SHALL only use endpoints 1-254. + * Endpoints 241-254 SHALL be used only with the approval of the Connectivity Standards Alliance. + * The Green Power cluster, if implemented, SHALL use endpoint 242. + */ + endpoint: number; + /** + * Specifies the profile that is supported on this endpoint. + * Profile identifiers SHALL be obtained from the Connectivity Standards Alliance. + */ + profileId: ProfileId; + /** + * Specifies the device description supported on this endpoint. + * Device description identifiers SHALL be obtained from the Connectivity Standards Alliance. + */ + deviceId: number; + /** + * Specifies the version of the device description supported on this endpoint. + * The application device version field SHALL be set to one of the non-reserved values listed in Table 2-41. + * Default SHALL be 0000 unless otherwise defined by the application profile. + * Valid range 0000-1111, others reserved + */ + deviceVersion: number; + /** + * Specifies the list of input clusters supported on this endpoint, for use during the service discovery and binding procedures. + * The application input cluster list field SHALL be included only if the value of the application input cluster count field is greater than zero. + */ + inClusterList: ClusterId[]; + /** + * Specifies the list of output clusters supported on this endpoint, for use during the service discovery and binding procedures. + * The application output cluster list field SHALL be included only if the value of the application output cluster count field + * is greater than zero. + */ + outClusterList: ClusterId[]; +}; + +export type ActiveEndpointsResponse = { + /** NWK address for the request. */ + nwkAddress: NodeId; + /** List of bytes each of which represents an 8-bit endpoint. */ + endpointList: number[]; +}; + +export type MatchDescriptorsResponse = { + /** NWK address for the request. */ + nwkAddress: NodeId; + /** List of bytes each of which represents an 8-bit endpoint. */ + endpointList: number[]; +}; + +export type EndDeviceAnnounce = { + /** NWK address for the request. */ + nwkAddress: NodeId; + eui64: EUI64; + capabilities: MACCapabilityFlags; +}; + +export type SystemServerDiscoveryResponse = { + /** The system server capabilities of this node */ + serverMask: ServerMask; +}; + +export type ParentAnnounceResponse = { + children: EUI64[]; +}; + +export type LQITableResponse = { + /** [0x00-0xFF] Total number of neighbor table entries within the remote device */ + neighborTableEntries: number; + /** [0x00-0xFF] Starting index within the neighbor table to begin reporting for the NeighborTableList */ + startIndex: number; + /** + * A list of descriptors, beginning with the StartIndex element and continuing for NeighborTableListCount, + * of the elements in the Remote Device's Neighbor Table including the device address and associated LQI ( @see LQITableEntry ). + */ + entryList: LQITableEntry[]; +}; + +export type RoutingTableResponse = { + /** [0x00-0xFF] Total number of Routing Table entries within the Remote Device. */ + routingTableEntries: number; + /** [0x00-0xFF] Starting index within the Routing Table to begin reporting for the RoutingTableList. */ + startIndex: number; + /** + * A list of descriptors, beginning with the StartIndex element and continuing for RoutingTableListCount, + * of the elements in the Remote Device's Routing Table ( @see RoutingTableEntry ). + */ + entryList: RoutingTableEntry[]; +}; + +export type BindingTableResponse = { + /** [0x00-0xFF] Total number of Binding Table entries within the Remote Device. */ + bindingTableEntries: number; + /** [0x00-0xFF] Starting index within the Binding Table to begin reporting for the BindingTableList. */ + startIndex: number; + /** + * A list of descriptors, beginning with the StartIndex element and continuing for BindingTableList- Count, + * of the elements in the Remote Device's Binding Table ( @see BindingTableEntry ). + */ + entryList: BindingTableEntry[]; +}; + +export type NwkUpdateResponse = { + /** + * The five most significant bits (b27,..., b31) represent the binary encoded Channel Page. + * The 27 least significant bits (b0, b1,... b26) indicate which channels were scanned (1 = scan, 0 = do not scan) + * for each of the 27 valid channels. + */ + scannedChannels: number; + /** Count of the total transmissions reported by the device. */ + totalTransmissions: number; + /** Sum of the total transmission failures reported by the device. */ + totalFailures: number; + /** + * The result of an energy measurement made on this channel in accordance with [B1]. + * 0xff if there is too much interference on this channel. + */ + entryList: number[]; +}; + +export type NwkEnhancedUpdateResponse = { + /** + * The five most significant bits (b27,..., b31) represent the binary encoded Channel Page. + * The 27 least significant bits (b0, b1,... b26) indicate which channels were scanned (1 = scan, 0 = do not scan) + * for each of the 27 valid channels. + */ + scannedChannels: number; + /** Count of the total transmissions reported by the device. */ + totalTransmissions: number; + /** Sum of the total transmission failures reported by the device. */ + totalFailures: number; + /** + * The result of an energy measurement made on this channel in accordance with [B1]. + * 0xff if there is too much interference on this channel. + */ + entryList: number[]; +}; + +export type NwkIEEEJoiningListResponse = { + /** + * The issue ID of the IeeeJoiningList. + * This field SHALL start at 0 and increment for each change to the IeeeJoiningList, + * or each change to the Joining Policy wrapping to 0 after 0xFF. + */ + updateId: number; + /** This is an enumeration. */ + joiningPolicy: JoiningPolicy; + /** The total number of IEEE Joining Addresses contained in the response. */ + entryListTotal: number; + /** The starting index in the mibIeeeJoiningList. This field SHALL be omitted if the entryListTotal is 0. */ + startIndex?: number; + /** A list of IEEE addresses from the mibIeeeJoiningList. This field SHALL be omitted if the entryListTotal is 0. */ + entryList?: EUI64[]; +}; + +export type NwkUnsolicitedEnhancedUpdateResponse = { + /** + * The five most significant bits (b27,..., b31) represent the binary encoded Channel Page. + * The 27 least significant bits (b0, b1,... b26) indicate which channels is in use (1 = in use, 0 = not in use) + * for each of the 27 valid channels. + */ + channelInUse: number; + /** + * Total number of Mac Tx Transactions to attempt to send a message (but not counting retries) + */ + macTxUCastTotal: number; + /** + * Total number of failed Tx Transactions. So if the Mac sent a single packet, it will be retried 4 times without ACK, that counts as 1 failure. + */ + macTxUCastFailures: number; + /** + * Total number of Mac Retries regardless of whether the transaction resulted in success or failure. + */ + macTxUCastRetries: number; + /** Time period over which MACTxyyy results are measured (in minutes) */ + timePeriod: number; +}; + +export type NwkBeaconSurveyResponse = TLVs; + +export type StartKeyNegotiationResponse = TLVs; + +export type RetrieveAuthenticationTokenResponse = TLVs; + +export type GetAuthenticationLevelResponse = TLVs; + +export type SetConfigurationResponse = TLVs; + +export type GetConfigurationResponse = TLVs; + +export type ChallengeResponse = TLVs; + + +//------------------------------------------------------------------------------------------------- +//-- TLVs + +//-- Global TLVs + +/** Defined outside the Zigbee specification. Only TLV that can be added more than once to same frame. */ +export type ManufacturerSpecificGlobalTLV = { + /** 2-byte */ + zigbeeManufacturerId: number; + additionalData: Buffer; +}; + +export type SupportedKeyNegotiationMethodsGlobalTLV = { + /** + * Bits: + * - 0 Static Key Request (Zigbee 3.0 Mechanism) + * - 1 SPEKE using Curve25519 with Hash AES-MMO-128 + * - 2 SPEKE using Curve25519 with Hash SHA-256 + * - 3 – 7 Reserved + */ + keyNegotiationProtocolsBitmask: number; + /** + * Bits: + * - 0 Symmetric Authentication Token + * - This is a token unique to the Trust Center and network that the device is running on, and is assigned by the Trust center after joining. + * The token is used to renegotiate a link key using the Key Negotiation protocol and is good for the life of the device on the network. + * - 1 Install Code Key + * - 128-bit pre-configured link-key derived from install code + * - 2 Passcode Key + * - A variable length passcode for PAKE protocols. This passcode can be shorter for easy entry by a user. + * - 3 Basic Access Key + * - This key is used by other Zigbee specifications for joining with an alternate pre-shared secret. + * The definition and usage is defined by those specifications. The usage is optional by the core Zigbee specification. + * - 4 Administrative Access Key + * - This key is used by other Zigbee specifications for joining with an alternate pre-shared secret. + * The definition and usage is defined by those specifications. The usage is optional by the core Zigbee specification. + * - 5-7 Reserved - + */ + preSharedSecretsBitmask: number; + /** XXX: Assumed optional from minimum length of TLV in spec */ + sourceDeviceEui64?: EUI64; +}; + +export type PanIdConflictReportGlobalTLV = { + /** 2-byte */ + nwkPanIdConflictCount: number; +}; + +export type NextPanIdChangeGlobalTLV = { + /** 2-bytes in length and indicates the next channel that will be used once a Network Update command is received to change PAN IDs. */ + panId: PanId; +}; + +export type NextChannelChangeGlobalTLV = { + /** 4-bytes in length and indicates the next channel that will be used once a start channel change command is received. */ + channel: number; +}; + +export type SymmetricPassphraseGlobalTLV = { + /** 16-byte 128-bit */ + passphrase: Buffer; +}; + +export type RouterInformationGlobalTLV = { + /** + * Bits: + * - 0 Hub Connectivity + * - This bit indicates the state of nwkHubConnectivity from the NIB of the local device. + * It advertises whether the router has connectivity to a Hub device as defined by the higher-level application layer. + * A value of 1 means there is connectivity, and a value of 0 means there is no current Hub connectivity. + * 1 Uptime + * - This 1-bit value indicates the uptime of the router. A value of 1 indicates the router has been up for more than 24 hours. + * A value of 0 indicates the router has been up for less than 24 hours. + * 2 Preferred Parent + * - This bit indicates the state of nwkPreferredParent from the NIB of the local device. + * When supported, it extends Hub Connecivity, advertising the devices capacity to be the parent for an additional device. + * A value of 1 means that this device should be preferred. A value of 0 indicates that it should not be preferred. + * Devices that do not make this determination SHALL always report a value of 0. + * 3 Battery Backup + * - This bit indicates that the router has battery backup and thus will not be affected by temporary losses in power. + * 4 Enhanced Beacon Request Support + * - When this bit is set to 1, it indicates that the router supports responding to Enhanced beacon requests as defined by IEEE Std 802.15.4. + * A zero for this bit indicates the device has no support for responding to enhanced beacon requests. + * 5 MAC Data Poll Keepalive Support + * - This indicates that the device has support for the MAC Data Poll Keepalive method for End Device timeouts. + * 6 End Device Keepalive Support + * - This indicates that the device has support for the End Device Keepalive method for End Device timeouts. + * 7 Power Negotiation Support + * - This indicates the device has support for Power Negotiation with end devices. + * 8-15 Reserved These bits SHALL be set to 0. + */ + bitmask: number; +}; + +export type FragmentationParametersGlobalTLV = { + /** This indicates the node ID of the device that the subsequent fragmentation parameters apply to. */ + nwkAddress: NodeId; + /** + * This bitfield indicates what fragmentation options are supported by the device. + * It has the following enumerated bits: + * - Bit 0 = APS Fragmentation Supported. Set to 1 to indicate support; 0 to indicate no support + * If set to 1, the maximum reassembled message size is indicated by the Maximum Incoming Transfer Unit. + * - Bit 1-7 = Reserved for future use + * + * XXX: Assumed optional from minimum length of TLV in spec + */ + fragmentationOptions?: number; + /** + * This is a copy of the local device’s apsMaxSizeASDU AIB value. + * This indicates the maximum reassembled message size at the application layer after fragmentation has been applied + * on the message at the lower layers. + * A device supporting fragmentation would set this field to be larger than the normal payload size of the underlying NWK and MAC layer. + * + * XXX: Assumed optional from minimum length of TLV in spec + */ + maxIncomingTransferUnit?: number; +}; + +export type JoinerEncapsulationGlobalTLV = { + additionalTLVs: TLV[]; +}; + +export type BeaconAppendixEncapsulationGlobalTLV = { + /** At least `SupportedKeyNegotiationMethodsGlobalTLV`, `FragmentationParametersGlobalTLV` */ + additionalTLVs: TLV[]; +}; + +export type ConfigurationParametersGlobalTLV = { + /** + * 2-bytes in length and indicates various parameters about how the stack SHALL behave + * Bit: + * - 0 AIB apsZdoRestrictedMode + * - 1 Device Security Policy requireLinkKeyEncryptionForApsTransportKey + * - 2 NIB nwkLeaveRequestAllowed + * - 3–15 Reserved Reserved + */ + configurationParameters: number; +}; + +export type DeviceCapabilityExtensionGlobalTLV = { + data: Buffer; +}; + +//-- Local TLVs + +/** + * NOTE: The Maximum Transmission Unit (MTU) of the underlying message will limit the maximum range of this field. + */ +export type ClearAllBindingsReqEUI64TLV = { + /** A list of EUI64 that SHALL trigger corresponding bindings to be deleted. */ + eui64List: EUI64[]; +}; + +export type BeaconSurveyConfigurationTLV = { + /** + * The list of channels and pages over which the scan is to be done. + * For more information on the Channel List structure see section 3.2.2.2.1. + */ + scanChannelList: number[]; + /** + * - 0 Active or Enhanced Scan This bit determines whether to do an Active Scan or Enhanced Active Scan. + * When the bit is set to 1 it indicates an Enhanced Active Scan. + * And in case of Enhanced Active scan EBR shall be sent with EPID filter instead of PJOIN filter. + * - 1 – 7 Reserved - + */ + configurationBitmask: number; +}; + +export type Curve25519PublicPointTLV = { + /** This indicates the EUI64 of the device that generated the public point. */ + eui64: EUI64; + /** The 32-byte Curve public point. */ + publicPoint: Buffer; +}; + +export type AuthenticationTokenIdTLV = { + /** The Global TLV Type Tag ID being requested for an authentication token. */ + tlvTypeTagId: number; +}; + +export type TargetIEEEAddressTLV = { + /** Extended address of the device whose security level is requested. */ + ieee: EUI64; +}; + +export type SelectedKeyNegotiationMethodTLV = { + /** + * The enumeration of the key negotiation method the sender is requesting to use in key negotiation. + */ + protocol: SelectedKeyNegotiationProtocol; + /** + * The enumeration indicating the pre-shared secret that the sending device is requesting to be used in the key negotiation. + */ + presharedSecret: SelectedPreSharedSecret; + /** The value of the EUI64 of the device sending the message. This field SHALL always be present. */ + sendingDeviceEui64: EUI64; +}; + +export type DeviceEUI64ListTLV = { + /** A list of EUI64 that shall trigger decommissioning operations. Count: [0x00-0xFF] */ + eui64List: EUI64[]; +}; + +export type APSFrameCounterChallengeTLV = { + /** The EUI64 of the device that generated the frame. */ + senderEui64: EUI64; + /** + * A randomly generated 64-bit value sent to a device to prove they have the link key. + * This allows the initiator to detect replayed challenge response frames. + */ + challengeValue: Buffer; +}; + +export type APSFrameCounterResponseTLV = { + /** The EUI64 of the device that is responding to the Security_Challenge_req with its own challenge. */ + responderEui64: EUI64; + /** A randomly generated 64-bit value previously received in the APSFrameCounterChallengeTLV. */ + receivedChallengeValue: Buffer; + /** The current outgoing APS security frame counter held by the Responder EUI64 device. */ + apsFrameCounter: number; + /** + * The AES-CCM-128 outgoing frame counter used to generate the MIC over the octet sequence + * { tag || length || responder EUI-64 || received challenge value || APS frame counter } + * using the special nonce and AES-128 key for frame counter synchronization. + */ + challengeSecurityFrameCounter: number; + /** + * The AES-128-CCM 64-bit MIC (security level 2) on all previous fields of this TLV, + * excluding the challenge security frame counter, including Tag ID and length fields. + */ + mic: Buffer; +}; + +export type BeaconSurveyResultsTLV = { + /** The total number of IEEE Std 802.15.4 beacons received during the scan. */ + totalBeaconsReceived: number; + /** The total number of Zigbee Network beacons where the Extended PAN ID matches the local device’s nwkExtendedPanId. */ + onNetworkBeacons: number; + /** + * The total number of Zigbee Network beacons where the Extended PAN ID matches and the Zigbee Beacon payload indicates + * End Device Capacity = TRUE. + */ + potentialParentBeacons: number; + /** + * The total number of IEEE Std 802.15.4 beacons from other Zigbee networks or other IEEE Std 802.15.4 networks. + * Other Zigbee network beacons are defined as when the Extended PAN ID does not match the local Extended PAN ID. + */ + otherNetworkBeacons: number; +}; + +export type PotentialParentsTLV = { + /** The short address that is the current parent for the device. For a router or coordinator this value SHALL be set to 0xFFFF. */ + currentParentNwkAddress: number; + /** The value of the LQA of the current parent. */ + currentParentLQA: number; + /** + * This is the count of additional potential parent short addresses and their associated LQA. + * If there are no other potential parents this SHALL indicate 0. This value SHALL not be greater than 5. + */ + entryCount: number; + potentialParents: { + /** The short address for a potential parent that the device can hear a beacon for. */ + nwkAddress: number; + /** The LQA value of the associated potential parent. */ + lqa: number; + }[]; +}; + +export type DeviceAuthenticationLevelTLV = { + /** 64-bit address for the node that is being inquired about. */ + remoteNodeIeee: EUI64; + /** This indicates the joining method that was used when the device joined the network. */ + initialJoinMethod: InitialJoinMethod; + /** This indicates what Link Key update method was used to create the current active Link Key. */ + activeLinkKeyType: ActiveLinkKeyType; +}; + +export type ProcessingStatusTLV = { + /** + * indicate the number of Tag ID and Processing Status pairs are present in the full TLV. + * The count may be zero, indicating that there were no known TLVs in the previous message that could be processed. + */ + count: number; + tlvs: { + /** Indicate a previously received TLV tag ID */ + tagId: number; + /** The associated status of whether it is processed */ + processingStatus: Status.SUCCESS | Status.INV_REQUESTTYPE | Status.NOT_SUPPORTED; + }[]; +}; + +export type LocalTLVType = (ClearAllBindingsReqEUI64TLV | BeaconSurveyConfigurationTLV | Curve25519PublicPointTLV | AuthenticationTokenIdTLV + | TargetIEEEAddressTLV | SelectedKeyNegotiationMethodTLV | DeviceEUI64ListTLV | APSFrameCounterChallengeTLV | APSFrameCounterResponseTLV + | BeaconSurveyResultsTLV | PotentialParentsTLV | DeviceAuthenticationLevelTLV | ProcessingStatusTLV); + +export type LocalTLVReader = (length: number) => LocalTLVType; + +export type TLV = { + /** 1-byte - 0-63: Local, 64-255: Global */ + tagId: number; + /** + * The Length byte encodes the number of bytes in the value field -1. + * This means that a TLV with length field of 3 is expected to contain 4 bytes of data in the value field. + * + * WARNING: This field is assumed to always include the +1 offset, and logic should do the appropriate reversal when writing the actual buffer. + * + * 1-byte + */ + length: number; + /** Size = ${length + 1} */ + tlv: (ManufacturerSpecificGlobalTLV | SupportedKeyNegotiationMethodsGlobalTLV | PanIdConflictReportGlobalTLV | NextPanIdChangeGlobalTLV + | NextChannelChangeGlobalTLV | SymmetricPassphraseGlobalTLV | RouterInformationGlobalTLV | FragmentationParametersGlobalTLV + | JoinerEncapsulationGlobalTLV | BeaconAppendixEncapsulationGlobalTLV | ConfigurationParametersGlobalTLV | DeviceCapabilityExtensionGlobalTLV + | LocalTLVType); +}; + +export type TLVs = { + tlvs: TLV[]; +}; diff --git a/src/zspec/zdo/index.ts b/src/zspec/zdo/index.ts new file mode 100644 index 0000000000..963beb9669 --- /dev/null +++ b/src/zspec/zdo/index.ts @@ -0,0 +1,6 @@ +export * from './definition/consts'; +export * from './definition/enums'; +export {ClusterId} from './definition/clusters'; +export {Status} from './definition/status'; +export {ZdoStatusError as StatusError} from './zdoStatusError'; +export * as Utils from './utils'; diff --git a/src/zspec/zdo/utils.ts b/src/zspec/zdo/utils.ts new file mode 100644 index 0000000000..e72adfb196 --- /dev/null +++ b/src/zspec/zdo/utils.ts @@ -0,0 +1,78 @@ +import {ClusterId} from './definition/clusters'; +import {MACCapabilityFlags, ServerMask} from './definition/tstypes'; + +/** + * Get a the response cluster ID corresponding to a request. + * May be null if cluster does not have a response. + * @param requestClusterId + * @returns + */ +export const getResponseClusterId = (requestClusterId: ClusterId): ClusterId | null => { + if ((0x8000 < requestClusterId) || requestClusterId === ClusterId.END_DEVICE_ANNOUNCE) { + return null; + } + + const responseClusterId = requestClusterId + 0x8000; + + if (ClusterId[responseClusterId] == undefined) { + return null; + } + + return responseClusterId; +}; + +/** + * Get the values for the bitmap `Mac Capability Flags Field` as per spec. + * Given value is assumed to be a proper 1-byte length. + * @param capabilities + * @returns + */ +export const getMacCapFlags = (capabilities: number): MACCapabilityFlags => { + return { + alternatePANCoordinator: (capabilities & 0x01), + deviceType: (capabilities & 0x02) >> 1, + powerSource: (capabilities & 0x04) >> 2, + rxOnWhenIdle: (capabilities & 0x08) >> 3, + reserved1: (capabilities & 0x10) >> 4, + reserved2: (capabilities & 0x20) >> 5, + securityCapability: (capabilities & 0x40) >> 6, + allocateAddress: (capabilities & 0x80) >> 7, + }; +}; + + +/** + * Get the values for the bitmap `Server Mask Field` as per spec. + * Given value is assumed to be a proper 2-byte length. + * @param serverMask + * @returns + */ +export const getServerMask = (serverMask: number): ServerMask => { + return { + primaryTrustCenter: (serverMask & 0x01), + backupTrustCenter: (serverMask & 0x02) >> 1, + deprecated1: (serverMask & 0x04) >> 2, + deprecated2: (serverMask & 0x08) >> 3, + deprecated3: (serverMask & 0x10) >> 4, + deprecated4: (serverMask & 0x20) >> 5, + networkManager: (serverMask & 0x40) >> 6, + reserved1: (serverMask & 0x80) >> 7, + reserved2: (serverMask & 0x100) >> 8, + stackComplianceResivion: (serverMask & 0xFE00) >> 9, + }; +}; + +export const createServerMask = (serverMask: ServerMask): number => { + return ( + (serverMask.primaryTrustCenter) & 0x01 | + (serverMask.backupTrustCenter << 1) & 0x02 | + (serverMask.deprecated1 << 2) & 0x04 | + (serverMask.deprecated2 << 3) & 0x08 | + (serverMask.deprecated3 << 4) & 0x10 | + (serverMask.deprecated4 << 5) & 0x20 | + (serverMask.networkManager << 6) & 0x40 | + (serverMask.reserved1 << 7) & 0x80 | + (serverMask.reserved2 << 8) & 0x100 | + (serverMask.stackComplianceResivion << 9) & 0xFE00 + ); +}; diff --git a/src/zspec/zdo/zdoStatusError.ts b/src/zspec/zdo/zdoStatusError.ts new file mode 100644 index 0000000000..e011423807 --- /dev/null +++ b/src/zspec/zdo/zdoStatusError.ts @@ -0,0 +1,10 @@ +import {Status} from './definition/status'; + +export class ZdoStatusError extends Error { + public code: Status; + + constructor (code: Status) { + super(`Status '${Status[code]}'`); + this.code = code; + } +} diff --git a/test/adapter/z-stack/adapter.test.ts b/test/adapter/z-stack/adapter.test.ts index 2853e10f0b..7c591348bd 100644 --- a/test/adapter/z-stack/adapter.test.ts +++ b/test/adapter/z-stack/adapter.test.ts @@ -8,7 +8,7 @@ import {ZnpVersion} from "../../../src/adapter/z-stack/adapter/tstype"; import * as Structs from "../../../src/adapter/z-stack/structs" import * as fs from "fs"; import * as path from "path"; -import * as Zcl from '../../../src/zcl'; +import * as Zcl from '../../../src/zspec/zcl'; import * as Constants from '../../../src/adapter/z-stack/constants'; import {ZclPayload} from "../../../src/adapter/events"; import {UnifiedBackupStorage} from "../../../src/models"; @@ -1097,14 +1097,14 @@ const basicMocks = () => { }); }; -const touchlinkScanRequest = Zcl.ZclFrame.create( +const touchlinkScanRequest = Zcl.Frame.create( Zcl.FrameType.SPECIFIC, Zcl.Direction.CLIENT_TO_SERVER, false, null, 12, 'scanRequest', Zcl.Utils.getCluster('touchlink', null, {}).ID, {transactionID: 1, zigbeeInformation: 4, touchlinkInformation: 18}, {}, ); -const touchlinkScanResponse = Zcl.ZclFrame.create( +const touchlinkScanResponse = Zcl.Frame.create( Zcl.FrameType.SPECIFIC, Zcl.Direction.SERVER_TO_CLIENT, false, null, 12, 'scanResponse', Zcl.Utils.getCluster('touchlink', null, {}).ID, {transactionID: 1, rssiCorrection: 10, zigbeeInformation: 5, touchlinkInformation: 6, keyBitmask: 12, responseID: 11, @@ -1113,7 +1113,7 @@ const touchlinkScanResponse = Zcl.ZclFrame.create( {}, ); -const touchlinkIdentifyRequest = Zcl.ZclFrame.create( +const touchlinkIdentifyRequest = Zcl.Frame.create( Zcl.FrameType.SPECIFIC, Zcl.Direction.CLIENT_TO_SERVER, false, null, 12, 'identifyRequest', Zcl.Utils.getCluster('touchlink', null, {}).ID, {transactionID: 1, duration: 65535}, @@ -2159,7 +2159,7 @@ describe("zstack-adapter", () => { await adapter.start(); mockZnpRequest.mockClear(); - const frame = Zcl.ZclFrame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.CLIENT_TO_SERVER, true, null, 100, 'writeNoRsp', 0, [{attrId: 0, dataType:0, attrData: null}], {}); + const frame = Zcl.Frame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.CLIENT_TO_SERVER, true, null, 100, 'writeNoRsp', 0, [{attrId: 0, dataType:0, attrData: null}], {}); await adapter.sendZclFrameToEndpoint('0x02', 2, 20, frame, 10000, false, false); expect(mockQueueExecute.mock.calls[0][1]).toBe(2); expect(mockZnpRequest).toBeCalledTimes(1); @@ -2172,7 +2172,7 @@ describe("zstack-adapter", () => { dataConfirmCodeReset = true; await adapter.start(); mockZnpRequest.mockClear(); - const frame = Zcl.ZclFrame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.CLIENT_TO_SERVER, true, null, 100, 'writeNoRsp', 0, [{attrId: 0, dataType:0, attrData: null}], {}); + const frame = Zcl.Frame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.CLIENT_TO_SERVER, true, null, 100, 'writeNoRsp', 0, [{attrId: 0, dataType:0, attrData: null}], {}); await adapter.sendZclFrameToEndpoint('0x02', 2, 20, frame, 10000, false, false); expect(mockQueueExecute.mock.calls[0][1]).toBe(2); expect(mockZnpRequest).toBeCalledTimes(2); @@ -2184,7 +2184,7 @@ describe("zstack-adapter", () => { await adapter.start(); dataConfirmCode = 201; mockZnpRequest.mockClear(); - const frame = Zcl.ZclFrame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.CLIENT_TO_SERVER, true, null, 100, 'writeNoRsp', 0, [{attrId: 0, dataType:0, attrData: null}], {}); + const frame = Zcl.Frame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.CLIENT_TO_SERVER, true, null, 100, 'writeNoRsp', 0, [{attrId: 0, dataType:0, attrData: null}], {}); let error; try {await adapter.sendZclFrameToEndpoint('0x02', 2, 20, frame, 10000, false, false)} catch (e) {error = e;} expect(error.message).toStrictEqual("Data request failed with error: 'undefined' (201)"); @@ -2193,10 +2193,10 @@ describe("zstack-adapter", () => { it('Send zcl frame network address with default response', async () => { basicMocks(); await adapter.start(); - const defaultReponse = Zcl.ZclFrame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.SERVER_TO_CLIENT, true, null, 100, 'defaultRsp', 0, {cmdId: 0, status: 0}, {}); + const defaultReponse = Zcl.Frame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.SERVER_TO_CLIENT, true, null, 100, 'defaultRsp', 0, {cmdId: 0, status: 0}, {}); const object = {type: Type.AREQ, subsystem: Subsystem.AF, command: 'incomingMsg', payload: {clusterid: 0, srcendpoint: 20, srcaddr: 2, linkquality: 101, groupid: 12, data: defaultReponse.toBuffer()}}; mockZnpRequest.mockClear(); - const frame = Zcl.ZclFrame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.CLIENT_TO_SERVER, false, null, 100, 'writeNoRsp', 0, [{attrId: 0, dataType:0, attrData: null}], {}); + const frame = Zcl.Frame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.CLIENT_TO_SERVER, false, null, 100, 'writeNoRsp', 0, [{attrId: 0, dataType:0, attrData: null}], {}); const request = adapter.sendZclFrameToEndpoint('0x02', 2, 20, frame, 10000, false, false); znpReceived(object); await request; @@ -2210,7 +2210,7 @@ describe("zstack-adapter", () => { await adapter.start(); dataConfirmCode = 240; mockZnpRequest.mockClear(); - const frame = Zcl.ZclFrame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.CLIENT_TO_SERVER, true, null, 100, 'writeNoRsp', 0, [{attrId: 0, dataType:0, attrData: null}], {}); + const frame = Zcl.Frame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.CLIENT_TO_SERVER, true, null, 100, 'writeNoRsp', 0, [{attrId: 0, dataType:0, attrData: null}], {}); const response = adapter.sendZclFrameToEndpoint('0x02', 2, 20, frame, 10000, false, false); let error; try {await response} catch(e) {error = e;} @@ -2235,7 +2235,7 @@ describe("zstack-adapter", () => { dataConfirmCode = 240; assocGetWithAddressNodeRelation = 255; mockZnpRequest.mockClear(); - const frame = Zcl.ZclFrame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.CLIENT_TO_SERVER, true, null, 100, 'writeNoRsp', 0, [{attrId: 0, dataType:0, attrData: null}], {}); + const frame = Zcl.Frame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.CLIENT_TO_SERVER, true, null, 100, 'writeNoRsp', 0, [{attrId: 0, dataType:0, attrData: null}], {}); const response = adapter.sendZclFrameToEndpoint('0x02', 2, 20, frame, 10000, false, false); let error; try {await response} catch(e) {error = e;} @@ -2257,7 +2257,7 @@ describe("zstack-adapter", () => { await adapter.start(); dataConfirmCode = 233; mockZnpRequest.mockClear(); - const frame = Zcl.ZclFrame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.CLIENT_TO_SERVER, true, null, 100, 'writeNoRsp', 0, [{attrId: 0, dataType:0, attrData: null}], {}); + const frame = Zcl.Frame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.CLIENT_TO_SERVER, true, null, 100, 'writeNoRsp', 0, [{attrId: 0, dataType:0, attrData: null}], {}); const response = adapter.sendZclFrameToEndpoint('0x02', 2, 20, frame, 10000, false, false); let error; try {await response} catch(e) {error = e;} @@ -2278,7 +2278,7 @@ describe("zstack-adapter", () => { await adapter.start(); dataConfirmCode = 233; mockZnpRequest.mockClear(); - const frame = Zcl.ZclFrame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.CLIENT_TO_SERVER, true, null, 100, 'writeNoRsp', 0, [{attrId: 0, dataType:0, attrData: null}], {}); + const frame = Zcl.Frame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.CLIENT_TO_SERVER, true, null, 100, 'writeNoRsp', 0, [{attrId: 0, dataType:0, attrData: null}], {}); const response = adapter.sendZclFrameToEndpoint('0x03', 2, 20, frame, 10000, false, false); let error; try {await response} catch(e) {error = e;} @@ -2300,7 +2300,7 @@ describe("zstack-adapter", () => { await adapter.start(); dataConfirmCode = 233; mockZnpRequest.mockClear(); - const frame = Zcl.ZclFrame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.CLIENT_TO_SERVER, true, null, 100, 'writeNoRsp', 0, [{attrId: 0, dataType:0, attrData: null}], {}); + const frame = Zcl.Frame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.CLIENT_TO_SERVER, true, null, 100, 'writeNoRsp', 0, [{attrId: 0, dataType:0, attrData: null}], {}); const response = adapter.sendZclFrameToEndpoint('0x03', 2, 20, frame, 10000, false, true, null); let error; try {await response} catch(e) {error = e;} @@ -2315,7 +2315,7 @@ describe("zstack-adapter", () => { dataConfirmCode = 9999; dataConfirmCodeReset = true; mockZnpRequest.mockClear(); - const frame = Zcl.ZclFrame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.CLIENT_TO_SERVER, true, null, 100, 'writeNoRsp', 0, [{attrId: 0, dataType:0, attrData: null}], {}); + const frame = Zcl.Frame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.CLIENT_TO_SERVER, true, null, 100, 'writeNoRsp', 0, [{attrId: 0, dataType:0, attrData: null}], {}); const response = adapter.sendZclFrameToEndpoint('0x02', 2, 20, frame, 10000, false, false); let error; try {await response} catch(e) {error = e;} @@ -2329,7 +2329,7 @@ describe("zstack-adapter", () => { await adapter.start(); mockZnpRequest.mockClear(); - const frame = Zcl.ZclFrame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.CLIENT_TO_SERVER, true, null, 100, 'read', 0, [{attrId: 0}], {}); + const frame = Zcl.Frame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.CLIENT_TO_SERVER, true, null, 100, 'read', 0, [{attrId: 0}], {}); await adapter.sendZclFrameToGroup(25, frame, 1); expect(mockQueueExecute.mock.calls[0][1]).toBe(undefined); expect(mockZnpRequest).toBeCalledTimes(1); @@ -2342,7 +2342,7 @@ describe("zstack-adapter", () => { dataConfirmCodeReset = true; await adapter.start(); mockZnpRequest.mockClear(); - const frame = Zcl.ZclFrame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.CLIENT_TO_SERVER, false, null, 100, 'writeNoRsp', 0, [{attrId: 0, dataType:0, attrData: null}], {}); + const frame = Zcl.Frame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.CLIENT_TO_SERVER, false, null, 100, 'writeNoRsp', 0, [{attrId: 0, dataType:0, attrData: null}], {}); await adapter.sendZclFrameToGroup(25, frame, 1); expect(mockQueueExecute.mock.calls[0][1]).toBe(undefined); expect(mockZnpRequest).toBeCalledTimes(2); @@ -2354,7 +2354,7 @@ describe("zstack-adapter", () => { await adapter.start(); mockZnpRequest.mockClear(); - const frame = Zcl.ZclFrame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.CLIENT_TO_SERVER, true, null, 100, 'read', 0, [{attrId: 0}], {}); + const frame = Zcl.Frame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.CLIENT_TO_SERVER, true, null, 100, 'read', 0, [{attrId: 0}], {}); await adapter.sendZclFrameToAll(242, frame, 250, BroadcastAddress.DEFAULT); expect(mockQueueExecute.mock.calls[0][1]).toBe(undefined); expect(mockZnpRequest).toBeCalledTimes(1); @@ -2366,7 +2366,7 @@ describe("zstack-adapter", () => { await adapter.start(); mockZnpRequest.mockClear(); - const frame = Zcl.ZclFrame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.CLIENT_TO_SERVER, true, null, 100, 'read', 0, [{attrId: 0}], {}); + const frame = Zcl.Frame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.CLIENT_TO_SERVER, true, null, 100, 'read', 0, [{attrId: 0}], {}); await adapter.sendZclFrameToAll(255, frame, 1, BroadcastAddress.RX_ON_WHEN_IDLE); expect(mockQueueExecute.mock.calls[0][1]).toBe(undefined); expect(mockZnpRequest).toBeCalledTimes(1); @@ -2378,7 +2378,7 @@ describe("zstack-adapter", () => { await adapter.start(); mockZnpRequest.mockClear(); - const frame = Zcl.ZclFrame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.CLIENT_TO_SERVER, true, null, 100, 'read', 0, [{attrId: 0}], {}); + const frame = Zcl.Frame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.CLIENT_TO_SERVER, true, null, 100, 'read', 0, [{attrId: 0}], {}); await adapter.sendZclFrameToAll(255, frame, 1, BroadcastAddress.SLEEPY); expect(mockQueueExecute.mock.calls[0][1]).toBe(undefined); expect(mockZnpRequest).toBeCalledTimes(1); @@ -2397,7 +2397,7 @@ describe("zstack-adapter", () => { transactionID = 0; } - const frame = Zcl.ZclFrame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.CLIENT_TO_SERVER, true, null, 100, 'writeNoRsp', 0, [{attrId: 0, dataType:0, attrData: null}], {}); + const frame = Zcl.Frame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.CLIENT_TO_SERVER, true, null, 100, 'writeNoRsp', 0, [{attrId: 0, dataType:0, attrData: null}], {}); await adapter.sendZclFrameToEndpoint('0x02', 2, 20, frame, 10000, false, false); } @@ -2420,7 +2420,7 @@ describe("zstack-adapter", () => { dataConfirmCode = 184; let error; mockZnpRequest.mockClear(); - const frame = Zcl.ZclFrame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.CLIENT_TO_SERVER, true, null, 100, 'read', 0, [{attrId: 0}], {}); + const frame = Zcl.Frame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.CLIENT_TO_SERVER, true, null, 100, 'read', 0, [{attrId: 0}], {}); try {await adapter.sendZclFrameToGroup(25, frame);} catch (e) { error = e}; expect(mockQueueExecute.mock.calls[0][1]).toBe(undefined); expect(mockZnpRequest).toBeCalledTimes(1); @@ -2434,9 +2434,9 @@ describe("zstack-adapter", () => { mockZnpRequest.mockClear(); - const responseMismatchFrame = Zcl.ZclFrame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.SERVER_TO_CLIENT, true, null, 102, 'readRsp', 0, [{attrId: 0, attrData: 5, dataType: 32, status: 0}], {}); - const responseFrame = Zcl.ZclFrame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.SERVER_TO_CLIENT, true, null, 100, 'readRsp', 0, [{attrId: 0, attrData: 2, dataType: 32, status: 0}], {}); - const frame = Zcl.ZclFrame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.CLIENT_TO_SERVER, true, null, 100, 'read', 0, [{attrId: 0}], {}); + const responseMismatchFrame = Zcl.Frame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.SERVER_TO_CLIENT, true, null, 102, 'readRsp', 0, [{attrId: 0, attrData: 5, dataType: 32, status: 0}], {}); + const responseFrame = Zcl.Frame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.SERVER_TO_CLIENT, true, null, 100, 'readRsp', 0, [{attrId: 0, attrData: 2, dataType: 32, status: 0}], {}); + const frame = Zcl.Frame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.CLIENT_TO_SERVER, true, null, 100, 'read', 0, [{attrId: 0}], {}); const object = {type: Type.AREQ, subsystem: Subsystem.AF, command: 'incomingMsg', payload: {clusterid: 0, srcendpoint: 20, srcaddr: 2, linkquality: 101, groupid: 12, data: responseFrame.toBuffer()}}; const objectMismatch = {type: Type.AREQ, subsystem: Subsystem.AF, command: 'incomingMsg', payload: {clusterid: 0, srcendpoint: 20, srcaddr: 2, linkquality: 101, groupid: 12, data: responseMismatchFrame.toBuffer()}}; const response = adapter.sendZclFrameToEndpoint('0x02', 2, 20, frame, 10000, false, false); @@ -2461,12 +2461,12 @@ describe("zstack-adapter", () => { mockZnpRequest.mockClear(); - const responseMismatchFrame = Zcl.ZclFrame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.SERVER_TO_CLIENT, true, null, 102, 'readRsp', 0, [{attrId: 0, attrData: 5, dataType: 32, status: 0}], {}); - const responseFrame = Zcl.ZclFrame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.SERVER_TO_CLIENT, true, null, 100, 'readRsp', 0, [{attrId: 0, attrData: 2, dataType: 32, status: 0}], {}); - const frame = Zcl.ZclFrame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.CLIENT_TO_SERVER, false, null, 100, 'read', 0, [{attrId: 0}], {}); + const responseMismatchFrame = Zcl.Frame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.SERVER_TO_CLIENT, true, null, 102, 'readRsp', 0, [{attrId: 0, attrData: 5, dataType: 32, status: 0}], {}); + const responseFrame = Zcl.Frame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.SERVER_TO_CLIENT, true, null, 100, 'readRsp', 0, [{attrId: 0, attrData: 2, dataType: 32, status: 0}], {}); + const frame = Zcl.Frame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.CLIENT_TO_SERVER, false, null, 100, 'read', 0, [{attrId: 0}], {}); const object = {type: Type.AREQ, subsystem: Subsystem.AF, command: 'incomingMsg', payload: {clusterid: 0, srcendpoint: 20, srcaddr: 2, linkquality: 101, groupid: 12, data: responseFrame.toBuffer()}}; const objectMismatch = {type: Type.AREQ, subsystem: Subsystem.AF, command: 'incomingMsg', payload: {clusterid: 0, srcendpoint: 20, srcaddr: 2, linkquality: 101, groupid: 12, data: responseMismatchFrame.toBuffer()}}; - const defaultReponse = Zcl.ZclFrame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.SERVER_TO_CLIENT, true, null, 100, 'defaultRsp', 0, {cmdId: 0, status: 0}, {}); + const defaultReponse = Zcl.Frame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.SERVER_TO_CLIENT, true, null, 100, 'defaultRsp', 0, {cmdId: 0, status: 0}, {}); const defaultObject = {type: Type.AREQ, subsystem: Subsystem.AF, command: 'incomingMsg', payload: {clusterid: 0, srcendpoint: 20, srcaddr: 2, linkquality: 101, groupid: 12, data: defaultReponse.toBuffer()}}; const response = adapter.sendZclFrameToEndpoint('0x02', 2, 20, frame, 10000, false, false); znpReceived(objectMismatch); @@ -2489,7 +2489,7 @@ describe("zstack-adapter", () => { basicMocks(); await adapter.start(); dataConfirmCode = 201; - const frame = Zcl.ZclFrame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.CLIENT_TO_SERVER, false, null, 100, 'read', 0, [{attrId: 0}], {}); + const frame = Zcl.Frame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.CLIENT_TO_SERVER, false, null, 100, 'read', 0, [{attrId: 0}], {}); let error; try {await adapter.sendZclFrameToEndpoint('0x02', 2, 20, frame, 10000, false, false)} catch (e) {error = e;} expect(error.message).toStrictEqual("Data request failed with error: 'undefined' (201)"); @@ -2499,7 +2499,7 @@ describe("zstack-adapter", () => { basicMocks(); await adapter.start(); dataConfirmCode = 201; - const frame = Zcl.ZclFrame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.CLIENT_TO_SERVER, true, null, 100, 'read', 0, [{attrId: 0}], {}); + const frame = Zcl.Frame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.CLIENT_TO_SERVER, true, null, 100, 'read', 0, [{attrId: 0}], {}); let error; try {await adapter.sendZclFrameToEndpoint('0x02', 2, 20, frame, 10000, false, false)} catch (e) {error = e;} expect(error.message).toStrictEqual("Data request failed with error: 'undefined' (201)"); @@ -2511,8 +2511,8 @@ describe("zstack-adapter", () => { mockZnpRequest.mockClear(); assocGetWithAddressNodeRelation = 2; - const responseMismatchFrame = Zcl.ZclFrame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.SERVER_TO_CLIENT, true, null, 102, 'readRsp', 0, [{attrId: 0, attrData: 5, dataType: 32, status: 0}], {}); - const frame = Zcl.ZclFrame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.CLIENT_TO_SERVER, false, null, 100, 'read', 0, [{attrId: 0}], {}); + const responseMismatchFrame = Zcl.Frame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.SERVER_TO_CLIENT, true, null, 102, 'readRsp', 0, [{attrId: 0, attrData: 5, dataType: 32, status: 0}], {}); + const frame = Zcl.Frame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.CLIENT_TO_SERVER, false, null, 100, 'read', 0, [{attrId: 0}], {}); const objectMismatch = {type: Type.AREQ, subsystem: Subsystem.AF, command: 'incomingMsg', payload: {clusterid: 0, srcendpoint: 20, srcaddr: 2, linkquality: 101, groupid: 12, data: responseMismatchFrame.toBuffer()}}; let error; try { @@ -2539,8 +2539,8 @@ describe("zstack-adapter", () => { mockZnpRequest.mockClear(); - const responseMismatchFrame = Zcl.ZclFrame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.SERVER_TO_CLIENT, true, null, 102, 'readRsp', 0, [{attrId: 0, attrData: 5, dataType: 32, status: 0}], {}); - const frame = Zcl.ZclFrame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.CLIENT_TO_SERVER, false, null, 100, 'read', 0, [{attrId: 0}], {}); + const responseMismatchFrame = Zcl.Frame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.SERVER_TO_CLIENT, true, null, 102, 'readRsp', 0, [{attrId: 0, attrData: 5, dataType: 32, status: 0}], {}); + const frame = Zcl.Frame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.CLIENT_TO_SERVER, false, null, 100, 'read', 0, [{attrId: 0}], {}); const objectMismatch = {type: Type.AREQ, subsystem: Subsystem.AF, command: 'incomingMsg', payload: {clusterid: 0, srcendpoint: 20, srcaddr: 2, linkquality: 101, groupid: 12, data: responseMismatchFrame.toBuffer()}}; let error; try { @@ -2568,8 +2568,8 @@ describe("zstack-adapter", () => { await adapter.start(); mockZnpRequest.mockClear(); - const responseFrame = Zcl.ZclFrame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.SERVER_TO_CLIENT, true, null, 100, 'readRsp', 0, [{attrId: 0, attrData: 2, dataType: 32, status: 0}], {}); - const frame = Zcl.ZclFrame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.CLIENT_TO_SERVER, false, null, 100, 'read', 0, [{attrId: 0}], {}); + const responseFrame = Zcl.Frame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.SERVER_TO_CLIENT, true, null, 100, 'readRsp', 0, [{attrId: 0, attrData: 2, dataType: 32, status: 0}], {}); + const frame = Zcl.Frame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.CLIENT_TO_SERVER, false, null, 100, 'read', 0, [{attrId: 0}], {}); const object = {type: Type.AREQ, subsystem: Subsystem.AF, command: 'incomingMsg', payload: {clusterid: 0, srcendpoint: 20, srcaddr: 2, linkquality: 101, groupid: 12, data: responseFrame.toBuffer()}}; const response = adapter.sendZclFrameToEndpoint('0x02', 2, 20, frame, 10000, false, false); znpReceived(object); @@ -2701,7 +2701,7 @@ describe("zstack-adapter", () => { basicMocks(); await adapter.start(); let zclData; - const responseFrame = Zcl.ZclFrame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.SERVER_TO_CLIENT, true, null, 100, 'readRsp', 0, [{attrId: 0, attrData: 2, dataType: 32, status: 0}], {}); + const responseFrame = Zcl.Frame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.SERVER_TO_CLIENT, true, null, 100, 'readRsp', 0, [{attrId: 0, attrData: 2, dataType: 32, status: 0}], {}); const object = {type: Type.AREQ, subsystem: Subsystem.AF, command: 'incomingMsgExt', payload: {clusterid: 0, srcendpoint: 20, srcaddr: 2, linkquality: 101, groupid: 12, data: responseFrame.toBuffer()}}; adapter.on("zclPayload", (p) => {zclData = p;}) znpReceived(object); @@ -2940,7 +2940,7 @@ describe("zstack-adapter", () => { basicMocks(); await adapter.start(); - const responseFrame = Zcl.ZclFrame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.SERVER_TO_CLIENT, true, null, 100, 'readRsp', 0, [{attrId: 0, attrData: 2, dataType: 32, status: 0}], {}); + const responseFrame = Zcl.Frame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.SERVER_TO_CLIENT, true, null, 100, 'readRsp', 0, [{attrId: 0, attrData: 2, dataType: 32, status: 0}], {}); const object = {type: Type.AREQ, subsystem: Subsystem.AF, command: 'incomingMsg', payload: {clusterid: 0, srcendpoint: 20, srcaddr: 2, linkquality: 101, groupid: 12, data: responseFrame.toBuffer()}}; const wait = adapter.waitFor(2, 20, 0, 1, 100, 0, 1, 10); znpReceived(object); @@ -2954,7 +2954,7 @@ describe("zstack-adapter", () => { }); it('Command should fail when in interpan', async () => { - const frame = Zcl.ZclFrame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.CLIENT_TO_SERVER, true, null, 100, 'writeNoRsp', 0, [{attrId: 0, dataType:0, attrData: null}], {}); + const frame = Zcl.Frame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.CLIENT_TO_SERVER, true, null, 100, 'writeNoRsp', 0, [{attrId: 0, dataType:0, attrData: null}], {}); basicMocks(); await adapter.start(); diff --git a/test/adapter/z-stack/znp.test.ts b/test/adapter/z-stack/znp.test.ts index 0d7149f303..6412f5bed1 100644 --- a/test/adapter/z-stack/znp.test.ts +++ b/test/adapter/z-stack/znp.test.ts @@ -1,7 +1,6 @@ import "regenerator-runtime/runtime"; import {Znp, ZpiObject} from '../../../src/adapter/z-stack/znp'; import {SerialPort} from '../../../src/adapter/serialPort'; -import net from 'net'; import {Frame as UnpiFrame, Constants as UnpiConstants} from '../../../src/adapter/z-stack/unpi'; import {duplicateArray, ieeeaAddr1, ieeeaAddr2} from '../../testUtils'; import BuffaloZnp from '../../../src/adapter/z-stack/znp/buffaloZnp'; diff --git a/test/buffalo.test.ts b/test/buffalo.test.ts index bad1a201f9..522e8cac2c 100644 --- a/test/buffalo.test.ts +++ b/test/buffalo.test.ts @@ -1,7 +1,6 @@ import "regenerator-runtime/runtime"; import {Buffalo} from '../src/buffalo'; -import {duplicateArray, ieeeaAddr1, ieeeaAddr2} from './testUtils'; -import DataType from "../src/zcl/definition/dataType"; +import {ieeeaAddr1, ieeeaAddr2} from './testUtils'; describe('Buffalo', () => { it('is more', () => { diff --git a/test/controller.test.ts b/test/controller.test.ts index ce5f563add..2dfd8be0ec 100755 --- a/test/controller.test.ts +++ b/test/controller.test.ts @@ -5,9 +5,8 @@ import {DeconzAdapter} from '../src/adapter/deconz/adapter'; import {ZiGateAdapter} from "../src/adapter/zigate/adapter"; import equals from 'fast-deep-equal/es6'; import fs from 'fs'; -import { ZclFrame, ZclHeader } from "../src/zcl"; import { Device, Group} from "../src/controller/model"; -import * as Zcl from '../src/zcl'; +import * as Zcl from '../src/zspec/zcl'; import zclTransactionSequenceNumber from '../src/controller/helpers/zclTransactionSequenceNumber'; import Request from '../src/controller/helpers/request'; import {Adapter} from '../src/adapter'; @@ -17,7 +16,6 @@ import * as Models from "../src/models"; import * as Utils from "../src/utils"; import Bonjour, {BrowserConfig, Service} from 'bonjour-service'; import {setLogger} from "../src/utils/logger"; -import {createCustomCluster} from "../src/zcl/utils"; import {BroadcastAddress} from "../src/zspec/enums"; import ZclTransactionSequenceNumber from "../src/controller/helpers/zclTransactionSequenceNumber"; const globalSetImmediate = setImmediate; @@ -83,7 +81,7 @@ let configureReportStatus = 0; let configureReportDefaultRsp = false; const restoreMocksendZclFrameToEndpoint = () => { - mocksendZclFrameToEndpoint.mockImplementation((ieeeAddr, networkAddress, endpoint, frame: ZclFrame) => { + mocksendZclFrameToEndpoint.mockImplementation((ieeeAddr, networkAddress, endpoint, frame: Zcl.Frame) => { if (frame.header.isGlobal && frame.isCommand('read') && (frame.isCluster('genBasic') || frame.isCluster('ssIasZone') || frame.isCluster('genPollCtrl') || frame.isCluster('hvacThermostat'))) { const payload = []; const cluster = frame.cluster; @@ -110,7 +108,7 @@ const restoreMocksendZclFrameToEndpoint = () => { if (networkAddress === 170 && frame.header.isGlobal && frame.isCluster('ssIasZone') && frame.isCommand('write') && frame.payload[0].attrId === 16) { // Write of ias cie address - const response = Zcl.ZclFrame.create( + const response = Zcl.Frame.create( Zcl.FrameType.SPECIFIC, Zcl.Direction.SERVER_TO_CLIENT, false, null, 1, 'enrollReq', Zcl.Utils.getCluster('ssIasZone').ID, {zonetype: 0, manucode: 1} @@ -302,7 +300,7 @@ const mockDevices = { }, } -const mockZclFrame = ZclFrame; +const mockZclFrame = Zcl.Frame; // Mock realPathSync jest.mock('../src/utils/realpathSync', () => { @@ -806,7 +804,7 @@ describe('Controller', () => { }); it('Controller should ignore touchlink messages', async () => { - const frame = Zcl.ZclFrame.create( + const frame = Zcl.Frame.create( Zcl.FrameType.SPECIFIC, Zcl.Direction.SERVER_TO_CLIENT, false, null, 1, 'scanResponse', Zcl.Utils.getCluster('touchlink').ID, {transactionID: 1, rssiCorrection: 1, zigbeeInformation: 1, touchlinkInformation: 1, keyBitmask: 1, responseID: 1, extendedPanID: '0x001788010de23e6e', networkUpdateID: 1, logicalChannel: 1, panID: 1, networkAddress: 1, numberOfSubDevices: 1, totalGroupIdentifiers: 1} @@ -1334,7 +1332,7 @@ describe('Controller', () => { it('Receive zclData occupancy report', async () => { const buffer = Buffer.from([24,169,10,0,0,24,1]); - const frame = ZclFrame.fromBuffer(Zcl.Utils.getCluster("msOccupancySensing").ID, ZclHeader.fromBuffer(buffer), buffer); + const frame = Zcl.Frame.fromBuffer(Zcl.Utils.getCluster("msOccupancySensing").ID, Zcl.Header.fromBuffer(buffer), buffer); await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); await mockAdapterEvents['zclPayload']({ @@ -1455,7 +1453,7 @@ describe('Controller', () => { clusterID: 9, address: 129, data: Buffer.from([0, 1]), - header: new Zcl.ZclHeader({direction: 0, disableDefaultResponse: false, frameType: 1, manufacturerSpecific: false, reservedBits: 0}, 0, 1, 0), + header: new Zcl.Header({direction: 0, disableDefaultResponse: false, frameType: 1, manufacturerSpecific: false, reservedBits: 0}, 0, 1, 0), endpoint: 1, linkquality: 50, groupID: 1, @@ -1632,7 +1630,7 @@ describe('Controller', () => { it('Receive zclData from unkonwn device shouldnt emit anything', async () => { const buffer = Buffer.from([24,169,10,0,0,24,1]); - const frame = ZclFrame.fromBuffer(Zcl.Utils.getCluster("msOccupancySensing").ID, ZclHeader.fromBuffer(buffer), buffer); + const frame = Zcl.Frame.fromBuffer(Zcl.Utils.getCluster("msOccupancySensing").ID, Zcl.Header.fromBuffer(buffer), buffer); await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); await mockAdapterEvents['zclPayload']({ @@ -1651,7 +1649,7 @@ describe('Controller', () => { it('Receive readResponse from unknown endpoint', async () => { const buffer = Buffer.from([8, 1, 1, 1, 0, 0, 32, 3]); - const frame = ZclFrame.fromBuffer(Zcl.Utils.getCluster("genBasic").ID, ZclHeader.fromBuffer(buffer), buffer); + const frame = Zcl.Frame.fromBuffer(Zcl.Utils.getCluster("genBasic").ID, Zcl.Header.fromBuffer(buffer), buffer); await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); await mockAdapterEvents['zclPayload']({ @@ -1785,7 +1783,7 @@ describe('Controller', () => { it('Receive cluster command', async () => { const buffer = Buffer.from([0x05, 0x7c, 0x11, 0x1d, 0x07, 0x00, 0x01, 0x0d, 0x00]); - const frame = ZclFrame.fromBuffer(5, ZclHeader.fromBuffer(buffer), buffer); + const frame = Zcl.Frame.fromBuffer(5, Zcl.Header.fromBuffer(buffer), buffer); await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); await mockAdapterEvents['zclPayload']({ @@ -1893,7 +1891,7 @@ describe('Controller', () => { }); it('Receive cluster command from unknown cluster', async () => { - const frame = ZclFrame.create(1, 1, false, 4476, 29, 1, 5, {groupid: 1, sceneid: 1, status: 0, transtime: 0, scenename: '', extensionfieldsets: []}); + const frame = Zcl.Frame.create(1, 1, false, 4476, 29, 1, 5, {groupid: 1, sceneid: 1, status: 0, transtime: 0, scenename: '', extensionfieldsets: []}); await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); await mockAdapterEvents['zclPayload']({ @@ -1911,7 +1909,7 @@ describe('Controller', () => { }); it('Receive zclData send default response', async () => { - const frame = ZclFrame.create(1, 1, false, 4476, 29, 1, 5, {groupid: 1, sceneid: 1, status: 0, transtime: 0, scenename: '', extensionfieldsets: []}); + const frame = Zcl.Frame.create(1, 1, false, 4476, 29, 1, 5, {groupid: 1, sceneid: 1, status: 0, transtime: 0, scenename: '', extensionfieldsets: []}); await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); mocksendZclFrameToEndpoint.mockClear(); @@ -1935,7 +1933,7 @@ describe('Controller', () => { }); it('Receive zclData dont send default resopnse with skipDefaultResponse', async () => { - const frame = ZclFrame.create(1, 1, false, 4476, 29, 1, 5, {groupid: 1, sceneid: 1, status: 0, transtime: 0, scenename: '', extensionfieldsets: []}); + const frame = Zcl.Frame.create(1, 1, false, 4476, 29, 1, 5, {groupid: 1, sceneid: 1, status: 0, transtime: 0, scenename: '', extensionfieldsets: []}); await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); mocksendZclFrameToEndpoint.mockClear(); @@ -1957,7 +1955,7 @@ describe('Controller', () => { }); it('Receive zclData dont send default resopnse when broadcast', async () => { - const frame = ZclFrame.create(1, 1, false, 4476, 29, 1, 5, {groupid: 1, sceneid: 1, status: 0, transtime: 0, scenename: '', extensionfieldsets: []}); + const frame = Zcl.Frame.create(1, 1, false, 4476, 29, 1, 5, {groupid: 1, sceneid: 1, status: 0, transtime: 0, scenename: '', extensionfieldsets: []}); await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); mocksendZclFrameToEndpoint.mockClear(); @@ -1977,7 +1975,7 @@ describe('Controller', () => { }); it('Receive zclData send default response fails should NOT attempt route discover when adapter does not support it', async () => { - const frame = ZclFrame.create(1, 1, false, 4476, 29, 1, 5, {groupid: 1, sceneid: 1, status: 0, transtime: 0, scenename: '', extensionfieldsets: []}); + const frame = Zcl.Frame.create(1, 1, false, 4476, 29, 1, 5, {groupid: 1, sceneid: 1, status: 0, transtime: 0, scenename: '', extensionfieldsets: []}); mockAdapterSupportsDiscoverRoute.mockReturnValueOnce(false); await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); @@ -2000,7 +1998,7 @@ describe('Controller', () => { }); it('Respond to genTime read', async () => { - const frame = ZclFrame.create(0, 0, true, null, 40, 0, 10, [{attrId: 0}, {attrId: 1}, {attrId: 7}, {attrId: 4}]); + const frame = Zcl.Frame.create(0, 0, true, null, 40, 0, 10, [{attrId: 0}, {attrId: 1}, {attrId: 7}, {attrId: 4}]); await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); mocksendZclFrameToEndpoint.mockClear(); @@ -2050,7 +2048,7 @@ describe('Controller', () => { const device = controller.getDeviceByIeeeAddr('0x129'); device.customReadResponse = jest.fn().mockReturnValue(true); - const frame = ZclFrame.create(0, 0, true, null, 40, 0, 10, [{attrId: 0}, {attrId: 1}, {attrId: 7}, {attrId: 9}]); + const frame = Zcl.Frame.create(0, 0, true, null, 40, 0, 10, [{attrId: 0}, {attrId: 1}, {attrId: 7}, {attrId: 9}]); const payload = { wasBroadcast: false, address: 129, @@ -2066,7 +2064,7 @@ describe('Controller', () => { expect(mocksendZclFrameToEndpoint).toHaveBeenCalledTimes(0); expect(device.customReadResponse).toHaveBeenCalledTimes(1); - expect(device.customReadResponse).toHaveBeenCalledWith(expect.any(ZclFrame), device.getEndpoint(1)) + expect(device.customReadResponse).toHaveBeenCalledWith(expect.any(Zcl.Frame), device.getEndpoint(1)) expect(device.customReadResponse.mock.calls[0][0].header).toBe(payload.header); }); @@ -2077,7 +2075,7 @@ describe('Controller', () => { const endpoint = device.getEndpoint(1); endpoint.saveClusterAttributeKeyValue('hvacThermostat', {systemMode: 3}); mocksendZclFrameToEndpoint.mockClear(); - const frame = ZclFrame.create(0, 0, true, null, 40, 0, 513, [{attrId: 28}, {attrId: 290}]); + const frame = Zcl.Frame.create(0, 0, true, null, 40, 0, 513, [{attrId: 28}, {attrId: 290}]); await mockAdapterEvents['zclPayload']({ wasBroadcast: false, address: 129, @@ -2102,7 +2100,7 @@ describe('Controller', () => { await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); mocksendZclFrameToEndpoint.mockClear(); mocksendZclFrameToEndpoint.mockRejectedValueOnce(new Error("")); - const frame = ZclFrame.create(0, 0, true, null, 40, 0, 10, [{attrId: 0}]); + const frame = Zcl.Frame.create(0, 0, true, null, 40, 0, 10, [{attrId: 0}]); await mockAdapterEvents['zclPayload']({ wasBroadcast: false, address: 129, @@ -2290,8 +2288,8 @@ describe('Controller', () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); const buffer = Buffer.from([28,95,17,3,10,5,0,66,21,108,117,109,105,46,115,101,110,115,111,114,95,119,108,101,97,107,46,97,113,49,1,255,66,34,1,33,213,12,3,40,33,4,33,168,19,5,33,43,0,6,36,0,0,5,0,0,8,33,4,2,10,33,0,0,100,16,0]); - const frame = ZclFrame.fromBuffer(Zcl.Utils.getCluster("genBasic").ID, ZclHeader.fromBuffer(buffer), buffer); - jest.spyOn(ZclFrame, 'fromBuffer').mockReturnValueOnce(frame); // Mock because no Buffalo write isn't supported for this payload + const frame = Zcl.Frame.fromBuffer(Zcl.Utils.getCluster("genBasic").ID, Zcl.Header.fromBuffer(buffer), buffer); + jest.spyOn(Zcl.Frame, 'fromBuffer').mockReturnValueOnce(frame); // Mock because no Buffalo write isn't supported for this payload await mockAdapterEvents['zclPayload']({ wasBroadcast: false, address: 129, @@ -2435,32 +2433,48 @@ describe('Controller', () => { expect(deepClone(events.message[0])).toStrictEqual(expected); }); - it('Should allow to specify custom attribute for existing', async () => { + 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'); - device.addCustomCluster('genBasic', {ID: 0, commands: {}, commandsResponse: {}, attributes: {customAttr: {ID: 256, type: Zcl.DataType.UINT8}}}); - const buffer = Buffer.from([24,169,10,0,1,24,3,0,0,24,1]); - const header = ZclHeader.fromBuffer(buffer); + device.addCustomCluster('genBasic', {ID: 0, commands: {}, commandsResponse: {}, attributes: { + customAttr: {ID: 256, type: Zcl.DataType.UINT8}, + aDifferentZclVersion: {ID: 0, type: Zcl.DataType.UINT8}, + }}); + const buffer = Buffer.from([24,169,10,0,1,24,3,0,0,24,1,2,0,24,1]); + const header = Zcl.Header.fromBuffer(buffer); await mockAdapterEvents['zclPayload']({wasBroadcast: false, address: 129, clusterID: 0, data: buffer, header, endpoint: 1, linkquality: 50, groupID: 1}); expect(events.message.length).toBe(1); - expect(events.message[0].data).toStrictEqual({customAttr: 3, zclVersion: 1}); + expect(events.message[0].data).toStrictEqual({customAttr: 3, aDifferentZclVersion: 1, stackVersion: 1}); expect(events.message[0].cluster).toBe('genBasic'); }); - it('Should allow to specific custom cluster', async () => { + it('Should allow to specify custom cluster', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); const device = controller.getDeviceByIeeeAddr('0x129'); device.addCustomCluster('myCustomCluster', {ID: 9123, commands: {}, commandsResponse: {}, attributes: {superAttribute: {ID: 0, type: Zcl.DataType.UINT8}}}); const buffer = Buffer.from([24,169,10,0,1,24,3,0,0,24,1]); - const header = ZclHeader.fromBuffer(buffer); + const header = Zcl.Header.fromBuffer(buffer); await mockAdapterEvents['zclPayload']({wasBroadcast: false, address: 129, clusterID: 9123, data: buffer, header, endpoint: 1, linkquality: 50, groupID: 1}); expect(events.message.length).toBe(1); expect(events.message[0].data).toStrictEqual({superAttribute: 1, '256': 3}); expect(events.message[0].cluster).toBe('myCustomCluster'); }); + 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'); + device.addCustomCluster('myCustomCluster', {ID: Zcl.Clusters.genBasic.ID, commands: {}, commandsResponse: {}, attributes: {customAttr: {ID: 256, type: Zcl.DataType.UINT8}}}); + const buffer = Buffer.from([24,169,10,0,1,24,3,0,0,24,1]); + const header = Zcl.Header.fromBuffer(buffer); + await mockAdapterEvents['zclPayload']({wasBroadcast: false, address: 129, clusterID: Zcl.Clusters.genBasic.ID, data: buffer, header, endpoint: 1, linkquality: 50, groupID: 1}); + expect(events.message.length).toBe(1); + expect(events.message[0].data).toStrictEqual({customAttr: 3, 0: 1/*zclVersion no longer recognized, cluster is overridden*/}); + expect(events.message[0].cluster).toBe('myCustomCluster'); + }); + it('Send zcl command to all no options', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); @@ -2674,7 +2688,7 @@ describe('Controller', () => { return {header: responseFrame.header, data: responseFrame.toBuffer(), clusterID: frame.cluster.ID}; }); mocksendZclFrameToEndpoint.mockImplementationOnce (() => jest.advanceTimersByTime(10)); - let frame = ZclFrame.create(Zcl.FrameType.SPECIFIC, Zcl.Direction.SERVER_TO_CLIENT, true, 1, 1, 'checkin', Zcl.Utils.getCluster("genPollCtrl").ID, {}, 0); + let frame = Zcl.Frame.create(Zcl.FrameType.SPECIFIC, Zcl.Direction.SERVER_TO_CLIENT, true, 1, 1, 'checkin', Zcl.Utils.getCluster("genPollCtrl").ID, {}, 0); await mockAdapterEvents['zclPayload']({ wasBroadcast: false, address: 174, @@ -2692,7 +2706,7 @@ describe('Controller', () => { device._checkinInterval = 50; mocksendZclFrameToEndpoint.mockClear(); - frame = ZclFrame.create(Zcl.FrameType.SPECIFIC, Zcl.Direction.SERVER_TO_CLIENT, true, 1, 1, 'checkin', Zcl.Utils.getCluster("genPollCtrl").ID, {}, 0); + frame = Zcl.Frame.create(Zcl.FrameType.SPECIFIC, Zcl.Direction.SERVER_TO_CLIENT, true, 1, 1, 'checkin', Zcl.Utils.getCluster("genPollCtrl").ID, {}, 0); await mockAdapterEvents['zclPayload']({ wasBroadcast: false, address: 174, @@ -3045,7 +3059,7 @@ describe('Controller', () => { catch (e) { error = e; } - expect(error instanceof Zcl.ZclStatusError).toBeTruthy(); + expect(error instanceof Zcl.StatusError).toBeTruthy(); expect(error.message).toStrictEqual(`ZCL command 0x129/1 genPowerCfg.configReport([{\"attribute\":\"mainsFrequency\",\"minimumReportInterval\":1,\"maximumReportInterval\":10,\"reportableChange\":1}], {\"timeout\":10000,\"disableResponse\":false,\"disableRecovery\":false,\"disableDefaultResponse\":true,\"direction\":0,\"srcEndpoint\":null,\"reservedBits\":0,\"manufacturerCode\":null,\"transactionSequenceNumber\":null,\"writeUndiv\":false}) failed (Status 'FAILURE')`); expect(error.code).toBe(1); }); @@ -3070,7 +3084,7 @@ describe('Controller', () => { catch (e) { error = e; } - expect(error instanceof Zcl.ZclStatusError).toBeTruthy(); + expect(error instanceof Zcl.StatusError).toBeTruthy(); expect(error.message).toStrictEqual(`ZCL command 0x129/1 genPowerCfg.configReport([{\"attribute\":\"mainsFrequency\",\"minimumReportInterval\":1,\"maximumReportInterval\":10,\"reportableChange\":1}], {\"timeout\":10000,\"disableResponse\":false,\"disableRecovery\":false,\"disableDefaultResponse\":true,\"direction\":0,\"srcEndpoint\":null,\"reservedBits\":0,\"manufacturerCode\":null,\"transactionSequenceNumber\":null,\"writeUndiv\":false}) failed (Status 'FAILURE')`); expect(error.code).toBe(1); }); @@ -3243,7 +3257,7 @@ describe('Controller', () => { const endpoint = device.getEndpoint(1); mocksendZclFrameToEndpoint.mockClear(); const buffer = Buffer.from([24,169,10,0,0,24,1]); - const frame = ZclFrame.fromBuffer(Zcl.Utils.getCluster("msOccupancySensing").ID, ZclHeader.fromBuffer(buffer), buffer); + const frame = Zcl.Frame.fromBuffer(Zcl.Utils.getCluster("msOccupancySensing").ID, Zcl.Header.fromBuffer(buffer), buffer); const promise = new Promise((resolve, reject) => resolve({clusterID: frame.cluster.ID, data: frame.toBuffer(), header: frame.header})) mockAdapterWaitFor.mockReturnValueOnce({promise, cancel: () => {}}); const result = endpoint.waitForCommand('genOta', 'upgradeEndRequest', 10, 20); @@ -3282,7 +3296,7 @@ describe('Controller', () => { await group.read('genBasic', ['modelId', 0x01], {}); expect(mocksendZclFrameToGroup).toBeCalledTimes(1); expect(mocksendZclFrameToGroup.mock.calls[0][0]).toBe(2); - expect(deepClone(mocksendZclFrameToGroup.mock.calls[0][1])).toStrictEqual(deepClone(ZclFrame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.CLIENT_TO_SERVER, true, null, 2, 'read', 0, [{"attrId": 5}, {"attrId": 1}]))); + expect(deepClone(mocksendZclFrameToGroup.mock.calls[0][1])).toStrictEqual(deepClone(Zcl.Frame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.CLIENT_TO_SERVER, true, null, 2, 'read', 0, [{"attrId": 5}, {"attrId": 1}]))); expect(mocksendZclFrameToGroup.mock.calls[0][2]).toBe(null); }); @@ -3303,7 +3317,7 @@ describe('Controller', () => { await group.write('genBasic', {0x0031: {value: 0x000B, type: 0x19}, deviceEnabled: true}, {}); expect(mocksendZclFrameToGroup).toBeCalledTimes(1); expect(mocksendZclFrameToGroup.mock.calls[0][0]).toBe(2); - expect(deepClone(mocksendZclFrameToGroup.mock.calls[0][1])).toStrictEqual(deepClone(ZclFrame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.CLIENT_TO_SERVER, true, null, 2, 'write', 0, [{"attrData": 11, "attrId": 49, "dataType": 25}, {"attrData": true, "attrId": 18, "dataType": 16}]))); + expect(deepClone(mocksendZclFrameToGroup.mock.calls[0][1])).toStrictEqual(deepClone(Zcl.Frame.create(Zcl.FrameType.GLOBAL, Zcl.Direction.CLIENT_TO_SERVER, true, null, 2, 'write', 0, [{"attrData": 11, "attrId": 49, "dataType": 25}, {"attrData": true, "attrId": 18, "dataType": 16}]))); expect(mocksendZclFrameToGroup.mock.calls[0][2]).toBe(null); }); @@ -3693,7 +3707,7 @@ describe('Controller', () => { let buffer = Buffer.from([24,169,10,0,0,24,1]); await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - let frame = ZclFrame.fromBuffer(Zcl.Utils.getCluster("msOccupancySensing").ID, ZclHeader.fromBuffer(buffer), buffer); + let frame = Zcl.Frame.fromBuffer(Zcl.Utils.getCluster("msOccupancySensing").ID, Zcl.Header.fromBuffer(buffer), buffer); await mockAdapterEvents['zclPayload']({ wasBroadcast: false, address: 129, @@ -3710,7 +3724,7 @@ describe('Controller', () => { expect(endpoint.getClusterAttributeValue('genBasic', 'modelId')).toBeNull(); buffer = Buffer.from([24,169,10,0,0,24,0]); - frame = ZclFrame.fromBuffer(Zcl.Utils.getCluster("msOccupancySensing").ID, ZclHeader.fromBuffer(buffer), buffer); + frame = Zcl.Frame.fromBuffer(Zcl.Utils.getCluster("msOccupancySensing").ID, Zcl.Header.fromBuffer(buffer), buffer); await mockAdapterEvents['zclPayload']({ wasBroadcast: false, address: 129, @@ -3906,7 +3920,7 @@ describe('Controller', () => { it('Emit read from device', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const frame = ZclFrame.create(0, 0, true, null, 40, 0, 1, [{attrId: 0}, {attrId: 9999}]); + const frame = Zcl.Frame.create(0, 0, true, null, 40, 0, 1, [{attrId: 0}, {attrId: 9999}]); await mockAdapterEvents['zclPayload']({ wasBroadcast: false, address: 129, @@ -4016,7 +4030,7 @@ describe('Controller', () => { it('Emit write from device', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const frame = ZclFrame.create(0, 0, true, null, 40, 2, 10, [{attrId:16389, dataType:32, attrData:3}]); + const frame = Zcl.Frame.create(0, 0, true, null, 40, 2, 10, [{attrId:16389, dataType:32, attrData:3}]); await mockAdapterEvents['zclPayload']({ wasBroadcast: false, address: 129, @@ -4332,7 +4346,7 @@ describe('Controller', () => { }, }; const frame = mockZclFrame.create(1, 0, true, null, 10, 'commissioningNotification', 33, data) - jest.spyOn(ZclFrame, 'fromBuffer').mockReturnValueOnce(frame); // Mock because no Buffalo write for 0xe0 is implemented + jest.spyOn(Zcl.Frame, 'fromBuffer').mockReturnValueOnce(frame); // Mock because no Buffalo write for 0xe0 is implemented await mockAdapterEvents['zclPayload']({ wasBroadcast: true, address: 0x46f4fe, @@ -4360,7 +4374,7 @@ describe('Controller', () => { expect(mocksendZclFrameToAll).toHaveBeenCalledTimes(1); // When joins again, shouldnt emit duplicate event - jest.spyOn(ZclFrame, 'fromBuffer').mockReturnValueOnce(frame); // Mock because no Buffalo write for 0xe0 is implemented + jest.spyOn(Zcl.Frame, 'fromBuffer').mockReturnValueOnce(frame); // Mock because no Buffalo write for 0xe0 is implemented await mockAdapterEvents['zclPayload']({ wasBroadcast: true, address: 0xf4fe, @@ -4392,7 +4406,7 @@ describe('Controller', () => { }, }; const frameToggle = mockZclFrame.create(1, 0, true, null, 10, 'notification', 33, dataToggle) - jest.spyOn(ZclFrame, 'fromBuffer').mockReturnValueOnce(frameToggle); // Mock because no Buffalo write for 0x22 is implemented + jest.spyOn(Zcl.Frame, 'fromBuffer').mockReturnValueOnce(frameToggle); // Mock because no Buffalo write for 0x22 is implemented await mockAdapterEvents['zclPayload']({ wasBroadcast: false, address: 0xf4fe, @@ -4414,7 +4428,7 @@ describe('Controller', () => { it('Should handle comissioning frame gracefully', async () => { await controller.start(); const buffer = Buffer.from([25,10,2,11,254,0]); - const frame = Zcl.ZclFrame.fromBuffer(Zcl.Clusters.greenPower.ID, Zcl.ZclHeader.fromBuffer(buffer)!, buffer, {}); + const frame = Zcl.Frame.fromBuffer(Zcl.Clusters.greenPower.ID, Zcl.Header.fromBuffer(buffer)!, buffer, {}); await mockAdapterEvents['zclPayload']({ wasBroadcast: true, address: 0x46f4fe, @@ -4436,7 +4450,7 @@ describe('Controller', () => { const device = controller.getDeviceByIeeeAddr('0x129'); device.addCustomCluster('myCustomCluster', {ID: 9123, commands: {}, commandsResponse: {}, attributes: {superAttribute: {ID: 0, type: Zcl.DataType.UINT8}}}); const buffer = Buffer.from([24,169,99,0,1,24,3,0,0,24,1]); - const header = ZclHeader.fromBuffer(buffer); + const header = Zcl.Header.fromBuffer(buffer); await mockAdapterEvents['zclPayload']({wasBroadcast: false, address: 129, clusterID: 33, data: buffer, header, endpoint: 1, linkquality: 50, groupID: 1}); expect(events.message.length).toBe(0); }); @@ -4458,7 +4472,7 @@ describe('Controller', () => { }, }; const frame = mockZclFrame.create(1, 0, true, null, 10, 'commissioningNotification', 33, data); - jest.spyOn(ZclFrame, 'fromBuffer').mockReturnValueOnce(frame); // Mock because no Buffalo write for 0xe3 is implemented + jest.spyOn(Zcl.Frame, 'fromBuffer').mockReturnValueOnce(frame); // Mock because no Buffalo write for 0xe3 is implemented await mockAdapterEvents['zclPayload']({ wasBroadcast: true, address: 0x46f4fe, @@ -4510,7 +4524,7 @@ describe('Controller', () => { }, }; const frame = mockZclFrame.create(1, 0, true, null, 10, 'commissioningNotification', 33, data); - jest.spyOn(ZclFrame, 'fromBuffer').mockReturnValueOnce(frame); // Mock because no Buffalo write for 0xe0 is implemented + jest.spyOn(Zcl.Frame, 'fromBuffer').mockReturnValueOnce(frame); // Mock because no Buffalo write for 0xe0 is implemented await mockAdapterEvents['zclPayload']({ wasBroadcast: true, address: 0x46f4fe, @@ -4617,10 +4631,10 @@ describe('Controller', () => { const expectedFrame = mockZclFrame.create(1, 0, true, null, 100, 'commissioningNotification', 33, data); const buffer = Buffer.from([0x11, 0x64, 0x04, 0x00, 0x08, 0xf8, 0x71, 0x71, 0x01, 0xf8, 0x00, 0x00, 0x00, 0xe0, 0x2e, 0x02, 0xc5, 0xf2, 0x21, 0x7f, 0x8c, 0xb2, 0x90, 0xd9, 0x90, 0x14, 0x15, 0xd0, 0x5c, 0xb1, 0x64, 0x7c, 0x44, 0x6c, 0xfa, 0x47, 0x05, 0xf8, 0xf8, 0x11, 0x00, 0x00, 0x04, 0x11, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x22, 0x60, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x81, 0x00, 0xd8]); - const receivedFrame = ZclFrame.fromBuffer(33, ZclHeader.fromBuffer(buffer), buffer); + const receivedFrame = Zcl.Frame.fromBuffer(33, Zcl.Header.fromBuffer(buffer), buffer); expect(deepClone(receivedFrame)).toStrictEqual(deepClone(expectedFrame)); - jest.spyOn(ZclFrame, 'fromBuffer').mockReturnValueOnce(expectedFrame); // Mock because no Buffalo write for 0xe0 is implemented + jest.spyOn(Zcl.Frame, 'fromBuffer').mockReturnValueOnce(expectedFrame); // Mock because no Buffalo write for 0xe0 is implemented await mockAdapterEvents['zclPayload']({ wasBroadcast: false, address: 129, @@ -4654,7 +4668,7 @@ describe('Controller', () => { expect(mocksendZclFrameToEndpoint.mock.calls[0][7]).toBe(242); // When joins again, shouldnt emit duplicate event - jest.spyOn(ZclFrame, 'fromBuffer').mockReturnValueOnce(expectedFrame); // Mock because no Buffalo write for 0xe0 is implemented + jest.spyOn(Zcl.Frame, 'fromBuffer').mockReturnValueOnce(expectedFrame); // Mock because no Buffalo write for 0xe0 is implemented await mockAdapterEvents['zclPayload']({ wasBroadcast: false, address: 129, @@ -4723,7 +4737,7 @@ describe('Controller', () => { expect(deepClone(Device.byIeeeAddr('0x00000000017171f8', true))).toStrictEqual({"ID":2,"_events":{},"_eventsCount":0,"_pendingRequestTimeout":0,"_skipDefaultResponse": false,"_customClusters":{},"_endpoints":[{"ID":242,"_binds":[],"_configuredReportings":[],"_events":{},"_eventsCount":0,"clusters":{},"deviceIeeeAddress":"0x00000000017171f8","deviceNetworkAddress":0x71f8,"inputClusters":[],"meta":{},"outputClusters":[],"pendingRequests": {"ID": 242,"deviceIeeeAddress": "0x00000000017171f8","sendInProgress": false}}],"_ieeeAddr":"0x00000000017171f8","_interviewCompleted":false,"_interviewing":false,"_lastSeen":150,"_linkquality":50,"_manufacturerID":null,"_modelID":"GreenPower_2","_networkAddress":0x71f8,"_type":"GreenPower","_deleted":true,"meta":{}}); // Re-add device - jest.spyOn(ZclFrame, 'fromBuffer').mockReturnValueOnce(expectedFrame); // Mock because no Buffalo write for 0xe0 is implemented + jest.spyOn(Zcl.Frame, 'fromBuffer').mockReturnValueOnce(expectedFrame); // Mock because no Buffalo write for 0xe0 is implemented await mockAdapterEvents['zclPayload']({ wasBroadcast: false, address: 129, @@ -4809,7 +4823,7 @@ describe('Controller', () => { expect(mocksendZclFrameToEndpoint).toHaveBeenCalledTimes(1); const buffer = Buffer.from([24,169,10,0,0,24,1]); - const frame = ZclFrame.fromBuffer(Zcl.Utils.getCluster("msOccupancySensing").ID, ZclHeader.fromBuffer(buffer), buffer); + const frame = Zcl.Frame.fromBuffer(Zcl.Utils.getCluster("msOccupancySensing").ID, Zcl.Header.fromBuffer(buffer), buffer); const data = { wasBroadcast: false, address: '0x129', @@ -4881,7 +4895,7 @@ describe('Controller', () => { await nextTick; expect(mocksendZclFrameToEndpoint).toHaveBeenCalledTimes(1); const buffer = Buffer.from([24,169,10,0,0,24,1]); - const frame = ZclFrame.fromBuffer(Zcl.Utils.getCluster("msOccupancySensing").ID, ZclHeader.fromBuffer(buffer), buffer); + const frame = Zcl.Frame.fromBuffer(Zcl.Utils.getCluster("msOccupancySensing").ID, Zcl.Header.fromBuffer(buffer), buffer); const data = { wasBroadcast: false, address: '0x129', @@ -5033,7 +5047,7 @@ describe('Controller', () => { // Implicit checkin, there are 5 ZclFrames and 2 other requests left in the queue: const buffer = Buffer.from([24,169,10,0,0,24,1]); - const frame = ZclFrame.fromBuffer(Zcl.Utils.getCluster("msOccupancySensing").ID, ZclHeader.fromBuffer(buffer), buffer); + const frame = Zcl.Frame.fromBuffer(Zcl.Utils.getCluster("msOccupancySensing").ID, Zcl.Header.fromBuffer(buffer), buffer); await mockAdapterEvents['zclPayload']({ wasBroadcast: false, address: '0x129', @@ -5075,7 +5089,7 @@ describe('Controller', () => { endpoint.pendingRequests.queue = async (req) => { const f = origQueueRequest.call(endpoint.pendingRequests, req); const buffer = Buffer.from([24,169,10,0,0,24,1]); - const frame = ZclFrame.fromBuffer(Zcl.Utils.getCluster("msOccupancySensing").ID, ZclHeader.fromBuffer(buffer), buffer); + const frame = Zcl.Frame.fromBuffer(Zcl.Utils.getCluster("msOccupancySensing").ID, Zcl.Header.fromBuffer(buffer), buffer); await mockAdapterEvents['zclPayload']({ wasBroadcast: false, address: 174, @@ -5154,7 +5168,7 @@ describe('Controller', () => { expect(mocksendZclFrameToEndpoint).toHaveBeenCalledTimes(0); const buffer = Buffer.from([24,169,10,0,0,24,1]); - let frame = ZclFrame.fromBuffer(Zcl.Utils.getCluster("msOccupancySensing").ID, ZclHeader.fromBuffer(buffer), buffer); + let frame = Zcl.Frame.fromBuffer(Zcl.Utils.getCluster("msOccupancySensing").ID, Zcl.Header.fromBuffer(buffer), buffer); await mockAdapterEvents['zclPayload']({ wasBroadcast: false, address: 174, @@ -5169,7 +5183,7 @@ describe('Controller', () => { expect(mocksendZclFrameToEndpoint).toHaveBeenCalledTimes(0); - frame = ZclFrame.create(Zcl.FrameType.SPECIFIC, Zcl.Direction.SERVER_TO_CLIENT, true, 1, 1, 'checkin', Zcl.Utils.getCluster("genPollCtrl").ID, {}, 0); + frame = Zcl.Frame.create(Zcl.FrameType.SPECIFIC, Zcl.Direction.SERVER_TO_CLIENT, true, 1, 1, 'checkin', Zcl.Utils.getCluster("genPollCtrl").ID, {}, 0); await mockAdapterEvents['zclPayload']({ wasBroadcast: false, address: 174, @@ -5215,7 +5229,7 @@ describe('Controller', () => { await mockAdapterEvents['deviceJoined']({networkAddress: 175, ieeeAddr: '0x175'}); await mockAdapterEvents['deviceJoined']({networkAddress: 171, ieeeAddr: '0x171'}); - const frame = ZclFrame.create(0, 0, true, null, 40, 0, 1, [{attrId: 0}, {attrId: 9999}]); + const frame = Zcl.Frame.create(0, 0, true, null, 40, 0, 1, [{attrId: 0}, {attrId: 9999}]); await mockAdapterEvents['zclPayload']({ wasBroadcast: false, address: 175, @@ -5255,7 +5269,7 @@ describe('Controller', () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const frame = ZclFrame.fromBuffer(Zcl.Utils.getCluster("closuresWindowCovering").ID, ZclHeader.fromBuffer(buffer), buffer); + const frame = Zcl.Frame.fromBuffer(Zcl.Utils.getCluster("closuresWindowCovering").ID, Zcl.Header.fromBuffer(buffer), buffer); await mockAdapterEvents['zclPayload']({ wasBroadcast: false, address: '0x129', @@ -5277,7 +5291,7 @@ describe('Controller', () => { const buffer = Buffer.from([28,33,16,13,1,2,240,0,48,4]); await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 177, ieeeAddr: '0x177'}); - const frame = ZclFrame.fromBuffer(Zcl.Utils.getCluster("closuresWindowCovering").ID, ZclHeader.fromBuffer(buffer), buffer); + const frame = Zcl.Frame.fromBuffer(Zcl.Utils.getCluster("closuresWindowCovering").ID, Zcl.Header.fromBuffer(buffer), buffer); await mockAdapterEvents['zclPayload']({ wasBroadcast: false, address: '0x177', @@ -5298,7 +5312,7 @@ describe('Controller', () => { const buffer = Buffer.from([24,242,10,2,240,48,4]); await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); - const frame = ZclFrame.fromBuffer(Zcl.Utils.getCluster("closuresWindowCovering").ID, ZclHeader.fromBuffer(buffer), buffer); + const frame = Zcl.Frame.fromBuffer(Zcl.Utils.getCluster("closuresWindowCovering").ID, Zcl.Header.fromBuffer(buffer), buffer); await mockAdapterEvents['zclPayload']({ wasBroadcast: false, address: '0x129', @@ -5319,7 +5333,7 @@ describe('Controller', () => { const buffer = Buffer.from([24,242,10,2,240,48,4]); await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 177, ieeeAddr: '0x177'}); - const frame = ZclFrame.fromBuffer(Zcl.Utils.getCluster("closuresWindowCovering").ID, ZclHeader.fromBuffer(buffer), buffer); + const frame = Zcl.Frame.fromBuffer(Zcl.Utils.getCluster("closuresWindowCovering").ID, Zcl.Header.fromBuffer(buffer), buffer); await mockAdapterEvents['zclPayload']({ wasBroadcast: false, address: '0x177', diff --git a/test/utils/math.ts b/test/utils/math.ts new file mode 100644 index 0000000000..f5830b13c0 --- /dev/null +++ b/test/utils/math.ts @@ -0,0 +1,7 @@ +export const uint16To8Array = (n: number): number[] => { + return [n & 0xFF, (n >> 8) & 0xFF]; +}; + +export const uint32To8Array = (n: number): number[] => { + return [n & 0xFF, (n >> 8) & 0xFF, (n >> 16) & 0xFF, (n >> 24) & 0xFF]; +}; diff --git a/test/zcl.test.ts b/test/zcl.test.ts index 4100ade015..cc82dc3ee7 100644 --- a/test/zcl.test.ts +++ b/test/zcl.test.ts @@ -1,10 +1,7 @@ import "regenerator-runtime/runtime"; -import * as Zcl from '../src/zcl'; -import {BuffaloZclDataType, DataType} from '../src/zcl/definition'; -import BuffaloZcl from '../src/zcl/buffaloZcl'; -import FrameType from "../src/zcl/definition/frameType"; -import Direction from "../src/zcl/definition/direction"; -import {StructuredIndicatorType} from "../src/zcl/tstype"; +import * as Zcl from '../src/zspec/zcl'; +import {BuffaloZclDataType, DataType, FrameType, Direction, StructuredIndicatorType} from '../src/zspec/zcl/definition/enums'; +import {BuffaloZcl} from '../src/zspec/zcl/buffaloZcl'; describe('Zcl', () => { @@ -97,21 +94,21 @@ describe('Zcl', () => { it('Get discrete or analog of unkown type', () => { expect(() => { - Zcl.Utils.IsDataTypeAnalogOrDiscrete(99999); + Zcl.Utils.getDataTypeClass(99999); }).toThrow("Don't know value type for 'undefined'") }); it('ZclFrame from buffer parse payload with unknown frame type', () => { expect(() => { // @ts-ignore - Zcl.ZclFrame.parsePayload({frameControl: {frameType: 9}}, null); + Zcl.Frame.parsePayload({frameControl: {frameType: 9}}, null); }).toThrow("Unsupported frameType '9'") }); it('ZclFrame from buffer report', () => { const buffer = Buffer.from([0x18, 0x4a, 0x0a, 0x55, 0x00, 0x39, 0x00, 0x00, 0x00, 0x00]); - const frame = Zcl.ZclFrame.fromBuffer(Zcl.Clusters.genAnalogInput.ID, Zcl.ZclHeader.fromBuffer(buffer)!, buffer, {}); - const header = new Zcl.ZclHeader({ + const frame = Zcl.Frame.fromBuffer(Zcl.Clusters.genAnalogInput.ID, Zcl.Header.fromBuffer(buffer)!, buffer, {}); + const header = new Zcl.Header({ reservedBits: 0, direction: 1, disableDefaultResponse: true, @@ -139,8 +136,8 @@ describe('Zcl', () => { it('ZclFrame from buffer tradfriArrowSingle', () => { const buffer = Buffer.from([0x05, 0x7c, 0x11, 0x1d, 0x07, 0x00, 0x01, 0x0d, 0x00]); - const frame = Zcl.ZclFrame.fromBuffer(Zcl.Clusters.genScenes.ID, Zcl.ZclHeader.fromBuffer(buffer)!, buffer, {}); - const header = new Zcl.ZclHeader({ + const frame = Zcl.Frame.fromBuffer(Zcl.Clusters.genScenes.ID, Zcl.Header.fromBuffer(buffer)!, buffer, {}); + const header = new Zcl.Header({ reservedBits: 0, direction: 0, disableDefaultResponse: false, @@ -162,8 +159,8 @@ describe('Zcl', () => { it('ZclFrame from buffer genGroups getMembership', () => { const buffer = Buffer.from([0x11, 0x7c, 0x02, 2, 10, 0, 20, 0]); - const frame = Zcl.ZclFrame.fromBuffer(Zcl.Clusters.genGroups.ID, Zcl.ZclHeader.fromBuffer(buffer)!, buffer, {}); - const header = new Zcl.ZclHeader({ + const frame = Zcl.Frame.fromBuffer(Zcl.Clusters.genGroups.ID, Zcl.Header.fromBuffer(buffer)!, buffer, {}); + const header = new Zcl.Header({ reservedBits: 0, direction: 0, disableDefaultResponse: true, @@ -183,8 +180,8 @@ describe('Zcl', () => { it('ZclFrame from buffer genGroups getMembership', () => { const buffer = Buffer.from([0x19, 0x7c, 0x03, 0, 10, 0]); - const frame = Zcl.ZclFrame.fromBuffer(Zcl.Clusters.genGroups.ID, Zcl.ZclHeader.fromBuffer(buffer)!, buffer, {}); - const header = new Zcl.ZclHeader({ + const frame = Zcl.Frame.fromBuffer(Zcl.Clusters.genGroups.ID, Zcl.Header.fromBuffer(buffer)!, buffer, {}); + const header = new Zcl.Header({ reservedBits: 0, direction: 1, disableDefaultResponse: true, @@ -204,8 +201,8 @@ describe('Zcl', () => { it('ZclFrame from buffer occupancy report', () => { const buffer = Buffer.from([24,169,10,0,0,24,1]); - const frame = Zcl.ZclFrame.fromBuffer(Zcl.Clusters.msOccupancySensing.ID, Zcl.ZclHeader.fromBuffer(buffer)!, buffer, {}); - const header = new Zcl.ZclHeader({ + const frame = Zcl.Frame.fromBuffer(Zcl.Clusters.msOccupancySensing.ID, Zcl.Header.fromBuffer(buffer)!, buffer, {}); + const header = new Zcl.Header({ reservedBits: 0, direction: 1, disableDefaultResponse: true, @@ -225,8 +222,8 @@ describe('Zcl', () => { it('ZclFrame from buffer configReportRsp - short', () => { const buffer = Buffer.from([0x08, 0x01, 0x07, 0x00]); - const frame = Zcl.ZclFrame.fromBuffer(Zcl.Clusters.genPowerCfg.ID, Zcl.ZclHeader.fromBuffer(buffer)!, buffer, {}); - const header = new Zcl.ZclHeader({ + const frame = Zcl.Frame.fromBuffer(Zcl.Clusters.genPowerCfg.ID, Zcl.Header.fromBuffer(buffer)!, buffer, {}); + const header = new Zcl.Header({ reservedBits: 0, direction: 1, disableDefaultResponse: false, @@ -246,8 +243,8 @@ describe('Zcl', () => { it('ZclFrame from buffer configReportRsp - long', () => { const buffer = Buffer.from([0x08, 0x01, 0x07, 0x00, 0x01, 0x34, 0x12, 0x01, 0x01, 0x35, 0x12]); - const frame = Zcl.ZclFrame.fromBuffer(Zcl.Clusters.genPowerCfg.ID, Zcl.ZclHeader.fromBuffer(buffer)!, buffer, {}); - const header = new Zcl.ZclHeader({ + const frame = Zcl.Frame.fromBuffer(Zcl.Clusters.genPowerCfg.ID, Zcl.Header.fromBuffer(buffer)!, buffer, {}); + const header = new Zcl.Header({ reservedBits: 0, direction: 1, disableDefaultResponse: false, @@ -267,8 +264,8 @@ describe('Zcl', () => { it('ZclFrame from buffer configReportRsp (hvacThermostat)', () => { const buffer = Buffer.from([0x18, 0x03, 0x07, 0x00, 0x00, 0x12, 0x00]); - const frame = Zcl.ZclFrame.fromBuffer(Zcl.Clusters.hvacThermostat.ID, Zcl.ZclHeader.fromBuffer(buffer)!, buffer, {}); - const header = new Zcl.ZclHeader({ + const frame = Zcl.Frame.fromBuffer(Zcl.Clusters.hvacThermostat.ID, Zcl.Header.fromBuffer(buffer)!, buffer, {}); + const header = new Zcl.Header({ reservedBits: 0, direction: 1, disableDefaultResponse: true, @@ -288,36 +285,36 @@ describe('Zcl', () => { it('ZclFrame from buffer getWeeklyScheduleRsp (hvacThermostat)', () => { const bufferHeat = Buffer.from([9, 7, 0, 6, 64, 1, 104, 1, 252, 8, 58, 2, 152, 8, 208, 2, 102, 8, 72, 3, 102, 8, 222, 3, 252, 8, 100, 5, 52, 8]); - const frameHeat = Zcl.ZclFrame.fromBuffer(Zcl.Clusters.hvacThermostat.ID, Zcl.ZclHeader.fromBuffer(bufferHeat)!, bufferHeat, {}); + const frameHeat = Zcl.Frame.fromBuffer(Zcl.Clusters.hvacThermostat.ID, Zcl.Header.fromBuffer(bufferHeat)!, bufferHeat, {}); expect(frameHeat.payload).toStrictEqual({numoftrans:6, dayofweek:64, mode:1, transitions: [{transitionTime:360,heatSetpoint:2300},{transitionTime:570,heatSetpoint:2200},{transitionTime:720,heatSetpoint:2150},{transitionTime:840,heatSetpoint:2150},{transitionTime:990,heatSetpoint:2300},{transitionTime:1380,heatSetpoint:2100}]}); const bufferCool = Buffer.from([9, 7, 0, 6, 64, 2, 104, 1, 252, 8, 58, 2, 152, 8, 208, 2, 102, 8, 72, 3, 102, 8, 222, 3, 252, 8, 100, 5, 52, 8]); - const frameCool = Zcl.ZclFrame.fromBuffer(Zcl.Clusters.hvacThermostat.ID, Zcl.ZclHeader.fromBuffer(bufferCool)!, bufferCool, {}); + const frameCool = Zcl.Frame.fromBuffer(Zcl.Clusters.hvacThermostat.ID, Zcl.Header.fromBuffer(bufferCool)!, bufferCool, {}); expect(frameCool.payload).toStrictEqual({numoftrans:6, dayofweek:64, mode:2, transitions: [{transitionTime:360,coolSetpoint:2300},{transitionTime:570,coolSetpoint:2200},{transitionTime:720,coolSetpoint:2150},{transitionTime:840,coolSetpoint:2150},{transitionTime:990,coolSetpoint:2300},{transitionTime:1380,coolSetpoint:2100}]}); const bufferHeatAndCool = Buffer.from([9, 7, 0, 1, 64, 3, 104, 1, 252, 8, 58, 2]); - const frameHeatAndCool = Zcl.ZclFrame.fromBuffer(Zcl.Clusters.hvacThermostat.ID, Zcl.ZclHeader.fromBuffer(bufferHeatAndCool)!, bufferHeatAndCool, {}); + const frameHeatAndCool = Zcl.Frame.fromBuffer(Zcl.Clusters.hvacThermostat.ID, Zcl.Header.fromBuffer(bufferHeatAndCool)!, bufferHeatAndCool, {}); expect(frameHeatAndCool.payload).toStrictEqual({numoftrans:1, dayofweek:64, mode:3, transitions: [{transitionTime:360,coolSetpoint:570, heatSetpoint: 2300}]}); }); it('ZclFrame to buffer setWeeklyScheduleRsp (hvacThermostat)', () => { const payloadHeat = {numoftrans:6, dayofweek:64, mode:1, transitions: [{transitionTime:360,heatSetpoint:23},{transitionTime:570,heatSetpoint:2200},{transitionTime:720,heatSetpoint:2150},{transitionTime:840,heatSetpoint:2150},{transitionTime:990,heatSetpoint:2300},{transitionTime:1380,heatSetpoint:2100}]}; - const frameHeat = Zcl.ZclFrame.create(FrameType.SPECIFIC, Direction.CLIENT_TO_SERVER, false, null, 8, 'setWeeklySchedule', 513, payloadHeat, {}); + const frameHeat = Zcl.Frame.create(FrameType.SPECIFIC, Direction.CLIENT_TO_SERVER, false, null, 8, 'setWeeklySchedule', 513, payloadHeat, {}); expect(frameHeat.toBuffer()).toStrictEqual(Buffer.from([1,8,1,6,64,1,104,1,23,0,58,2,152,8,208,2,102,8,72,3,102,8,222,3,252,8,100,5,52,8])); const payloadCool = {numoftrans:6, dayofweek:64, mode:2, transitions: [{transitionTime:360,coolSetpoint:2300},{transitionTime:570,coolSetpoint:2200},{transitionTime:720,coolSetpoint:2150},{transitionTime:840,coolSetpoint:2150},{transitionTime:990,coolSetpoint:2300},{transitionTime:1380,coolSetpoint:2100}]}; - const frameCool = Zcl.ZclFrame.create(FrameType.SPECIFIC, Direction.CLIENT_TO_SERVER, false, null, 8, 'setWeeklySchedule', 513, payloadCool, {}); + const frameCool = Zcl.Frame.create(FrameType.SPECIFIC, Direction.CLIENT_TO_SERVER, false, null, 8, 'setWeeklySchedule', 513, payloadCool, {}); expect(frameCool.toBuffer()).toStrictEqual(Buffer.from([1,8,1,6,64,2,104,1,252,8,58,2,152,8,208,2,102,8,72,3,102,8,222,3,252,8,100,5,52,8])); const payloadHeatAndCool = {numoftrans:6, dayofweek:64, mode:2, transitions: [{transitionTime:360,coolSetpoint:570, heatSetpoint: 2300}]}; - const frameHeatAndCool = Zcl.ZclFrame.create(FrameType.SPECIFIC, Direction.CLIENT_TO_SERVER, false, null, 8, 'setWeeklySchedule', 513, payloadHeatAndCool, {}); + const frameHeatAndCool = Zcl.Frame.create(FrameType.SPECIFIC, Direction.CLIENT_TO_SERVER, false, null, 8, 'setWeeklySchedule', 513, payloadHeatAndCool, {}); expect(frameHeatAndCool.toBuffer()).toStrictEqual(Buffer.from([1,8,1,6,64,2,104,1,252,8,58,2])); }); it('ZclFrame from buffer configReportRsp failed', () => { const buffer = Buffer.from([0x08, 0x01, 0x07, 0x02, 0x01, 0x01, 0x01]); - const frame = Zcl.ZclFrame.fromBuffer(Zcl.Clusters.genPowerCfg.ID, Zcl.ZclHeader.fromBuffer(buffer)!, buffer, {}); - const header = new Zcl.ZclHeader({ + const frame = Zcl.Frame.fromBuffer(Zcl.Clusters.genPowerCfg.ID, Zcl.Header.fromBuffer(buffer)!, buffer, {}); + const header = new Zcl.Header({ reservedBits: 0, direction: 1, disableDefaultResponse: false, @@ -337,8 +334,8 @@ describe('Zcl', () => { it('ZclFrame from buffer defaultRsp', () => { const buffer = Buffer.from([0x18, 0x04, 0x0b, 0x0c, 0x82]); - const frame = Zcl.ZclFrame.fromBuffer(Zcl.Clusters.genBasic.ID, Zcl.ZclHeader.fromBuffer(buffer)!, buffer, {}); - const header = new Zcl.ZclHeader({ + const frame = Zcl.Frame.fromBuffer(Zcl.Clusters.genBasic.ID, Zcl.Header.fromBuffer(buffer)!, buffer, {}); + const header = new Zcl.Header({ reservedBits: 0, direction: 1, disableDefaultResponse: true, @@ -358,8 +355,8 @@ describe('Zcl', () => { it('ZclFrame from buffer xiaomiStruct', () => { const buffer = Buffer.from([28,95,17,3,10,5,0,66,21,108,117,109,105,46,115,101,110,115,111,114,95,119,108,101,97,107,46,97,113,49,1,255,66,34,1,33,213,12,3,40,33,4,33,168,19,5,33,43,0,6,36,0,0,5,0,0,8,33,4,2,10,33,0,0,100,16,0]); - const frame = Zcl.ZclFrame.fromBuffer(Zcl.Clusters.genBasic.ID, Zcl.ZclHeader.fromBuffer(buffer)!, buffer, {}); - const header = new Zcl.ZclHeader({ + const frame = Zcl.Frame.fromBuffer(Zcl.Clusters.genBasic.ID, Zcl.Header.fromBuffer(buffer)!, buffer, {}); + const header = new Zcl.Header({ reservedBits: 0, direction: 1, disableDefaultResponse: true, @@ -379,8 +376,8 @@ describe('Zcl', () => { it('ZclFrame from buffer struct', () => { const buffer = Buffer.from([28,52,18,194,10,2,255,76,6,0,16,1,33,206,11,33,168,67,36,1,0,0,0,0,33,48,2,32,86]); - const frame = Zcl.ZclFrame.fromBuffer(Zcl.Clusters.genBasic.ID, Zcl.ZclHeader.fromBuffer(buffer)!, buffer, {}); - const header = new Zcl.ZclHeader({ + const frame = Zcl.Frame.fromBuffer(Zcl.Clusters.genBasic.ID, Zcl.Header.fromBuffer(buffer)!, buffer, {}); + const header = new Zcl.Header({ reservedBits: 0, direction: 1, disableDefaultResponse: true, @@ -422,8 +419,8 @@ describe('Zcl', () => { it('ZclFrame from buffer discoverRsp', () => { const buffer = Buffer.from([24,23,13,0,32,0,32,33,0,32,49,0,48,51,0,32,53,0,24]); - const frame = Zcl.ZclFrame.fromBuffer(Zcl.Clusters.genPowerCfg.ID, Zcl.ZclHeader.fromBuffer(buffer)!, buffer, {}); - const header = new Zcl.ZclHeader({ + const frame = Zcl.Frame.fromBuffer(Zcl.Clusters.genPowerCfg.ID, Zcl.Header.fromBuffer(buffer)!, buffer, {}); + const header = new Zcl.Header({ reservedBits: 0, direction: 1, disableDefaultResponse: true, @@ -446,14 +443,14 @@ describe('Zcl', () => { it('ZclFrame from buffer error on malformed', () => { const buffer = Buffer.from([0x08, 0x01]); expect(() => { - Zcl.ZclFrame.fromBuffer(Zcl.Clusters.genPowerCfg.ID, Zcl.ZclHeader.fromBuffer(buffer)!, buffer, {}); + Zcl.Frame.fromBuffer(Zcl.Clusters.genPowerCfg.ID, Zcl.Header.fromBuffer(buffer)!, buffer, {}); }).toThrow("Invalid ZclHeader"); }); it('ZclFrame from buffer readRsp failed', () => { const buffer = Buffer.from([8, 1, 1, 1, 0, 2]); - const frame = Zcl.ZclFrame.fromBuffer(Zcl.Clusters.genBasic.ID, Zcl.ZclHeader.fromBuffer(buffer)!, buffer, {}); - const header = new Zcl.ZclHeader({ + const frame = Zcl.Frame.fromBuffer(Zcl.Clusters.genBasic.ID, Zcl.Header.fromBuffer(buffer)!, buffer, {}); + const header = new Zcl.Header({ reservedBits: 0, direction: 1, disableDefaultResponse: false, @@ -473,8 +470,8 @@ describe('Zcl', () => { it('ZclFrame from buffer readRsp success', () => { const buffer = Buffer.from([8, 1, 1, 1, 0, 0, 32, 3]); - const frame = Zcl.ZclFrame.fromBuffer(Zcl.Clusters.genBasic.ID, Zcl.ZclHeader.fromBuffer(buffer)!, buffer, {}); - const header = new Zcl.ZclHeader({ + const frame = Zcl.Frame.fromBuffer(Zcl.Clusters.genBasic.ID, Zcl.Header.fromBuffer(buffer)!, buffer, {}); + const header = new Zcl.Header({ reservedBits: 0, direction: 1, disableDefaultResponse: false, @@ -494,8 +491,8 @@ describe('Zcl', () => { it('ZclFrame from buffer GDP commission', () => { const buffer = Buffer.from([0x11, 0x00, 0x04, 0x00, 0x00, 0xfe, 0xf4, 0x46, 0x00, 0xf9, 0x00, 0x00, 0x00, 0xe0, 0x1b, 0x02, 0x81, 0xf2, 0xf1, 0xec, 0x92, 0xab, 0xff, 0x8f, 0x13, 0x63, 0xe1, 0x46, 0xbe, 0xb5, 0x18, 0xc9, 0x0c, 0xab, 0xa4, 0x46, 0xd4, 0xd5, 0xf9, 0x01, 0x00, 0x00]); - const frame = Zcl.ZclFrame.fromBuffer(Zcl.Clusters.greenPower.ID, Zcl.ZclHeader.fromBuffer(buffer)!, buffer, {}); - const header = new Zcl.ZclHeader({ + const frame = Zcl.Frame.fromBuffer(Zcl.Clusters.greenPower.ID, Zcl.Header.fromBuffer(buffer)!, buffer, {}); + const header = new Zcl.Header({ reservedBits: 0, direction: 0, disableDefaultResponse: true, @@ -538,8 +535,8 @@ describe('Zcl', () => { it('ZclFrame from buffer GDP scene 0', () => { const buffer = Buffer.from([0x11, 0x00, 0x00, 0xa0, 0x14, 0xfe, 0xf4, 0x46, 0x00, 0xe5, 0x04, 0x00, 0x00, 0x10, 0xff]); - const frame = Zcl.ZclFrame.fromBuffer(Zcl.Clusters.greenPower.ID, Zcl.ZclHeader.fromBuffer(buffer)!, buffer, {}); - const header = new Zcl.ZclHeader({ + const frame = Zcl.Frame.fromBuffer(Zcl.Clusters.greenPower.ID, Zcl.Header.fromBuffer(buffer)!, buffer, {}); + const header = new Zcl.Header({ reservedBits: 0, direction: 0, disableDefaultResponse: true, @@ -566,8 +563,8 @@ describe('Zcl', () => { it('ZclFrame from buffer GDP with extra data', () => { const buffer = Buffer.from([0x11, 0x00, 0x00, 0xa0, 0x14, 0xfe, 0xf4, 0x46, 0x00, 0xe5, 0x04, 0x00, 0x00, 0x10, 0xff, 0x01]); - const frame = Zcl.ZclFrame.fromBuffer(Zcl.Clusters.greenPower.ID, Zcl.ZclHeader.fromBuffer(buffer)!, buffer, {}); - const header = new Zcl.ZclHeader({ + const frame = Zcl.Frame.fromBuffer(Zcl.Clusters.greenPower.ID, Zcl.Header.fromBuffer(buffer)!, buffer, {}); + const header = new Zcl.Header({ reservedBits: 0, direction: 0, disableDefaultResponse: true, @@ -594,8 +591,8 @@ describe('Zcl', () => { it('ZclFrame from buffer GDP pairing', () => { const buffer = Buffer.from([0x19, 0x17, 0x01, 0x68, 0xe5, 0x00, 0xf8, 0x71, 0x71, 0x01, 0x47, 0x65, 0xa1, 0x1c, 0x00, 0x4b, 0x12, 0x00, 0x00, 0x00, 0x02, 0x1c, 0x12, 0x00, 0x00, 0x09, 0x3c, 0xed, 0x1d, 0xbf, 0x25, 0x63, 0xf9, 0x29, 0x5c, 0x0d, 0x3d, 0x9f, 0xc5, 0x76, 0xe1,0,0,0,0,0,0]); - const frame = Zcl.ZclFrame.fromBuffer(Zcl.Clusters.greenPower.ID, Zcl.ZclHeader.fromBuffer(buffer)!, buffer, {}); - const header = new Zcl.ZclHeader({ + const frame = Zcl.Frame.fromBuffer(Zcl.Clusters.greenPower.ID, Zcl.Header.fromBuffer(buffer)!, buffer, {}); + const header = new Zcl.Header({ reservedBits: 0, direction: 1, disableDefaultResponse: true, @@ -623,8 +620,8 @@ describe('Zcl', () => { it('ZclFrame from buffer readRsp alias type', () => { const buffer = Buffer.from([8, 1, 1, 1, 0, 0, 8, 3]); - const frame = Zcl.ZclFrame.fromBuffer(Zcl.Clusters.genBasic.ID, Zcl.ZclHeader.fromBuffer(buffer)!, buffer, {}); - const header = new Zcl.ZclHeader({ + const frame = Zcl.Frame.fromBuffer(Zcl.Clusters.genBasic.ID, Zcl.Header.fromBuffer(buffer)!, buffer, {}); + const header = new Zcl.Header({ reservedBits: 0, direction: 1, disableDefaultResponse: false, @@ -644,8 +641,8 @@ describe('Zcl', () => { it('ZclFrame from buffer configReportRsp server to client', () => { const buffer = Buffer.from([8, 1, 6, 1, 1, 0, 10, 10]); - const frame = Zcl.ZclFrame.fromBuffer(Zcl.Clusters.genBasic.ID, Zcl.ZclHeader.fromBuffer(buffer)!, buffer, {}); - const header = new Zcl.ZclHeader({ + const frame = Zcl.Frame.fromBuffer(Zcl.Clusters.genBasic.ID, Zcl.Header.fromBuffer(buffer)!, buffer, {}); + const header = new Zcl.Header({ reservedBits: 0, direction: 1, disableDefaultResponse: false, @@ -665,8 +662,8 @@ describe('Zcl', () => { it('ZclFrame from buffer configReportRsp client to server analog', () => { const buffer = Buffer.from([8, 1, 6, 0, 0, 1, 32, 1, 0, 10, 0, 20]); - const frame = Zcl.ZclFrame.fromBuffer(Zcl.Clusters.genBasic.ID, Zcl.ZclHeader.fromBuffer(buffer)!, buffer, {}); - const header = new Zcl.ZclHeader({ + const frame = Zcl.Frame.fromBuffer(Zcl.Clusters.genBasic.ID, Zcl.Header.fromBuffer(buffer)!, buffer, {}); + const header = new Zcl.Header({ reservedBits: 0, direction: 1, disableDefaultResponse: false, @@ -686,8 +683,8 @@ describe('Zcl', () => { it('ZclFrame from buffer configReportRsp client to server analog', () => { const buffer = Buffer.from([8, 1, 6, 0, 0, 1, 8, 1, 0, 10, 0]); - const frame = Zcl.ZclFrame.fromBuffer(Zcl.Clusters.genBasic.ID, Zcl.ZclHeader.fromBuffer(buffer)!, buffer, {}); - const header = new Zcl.ZclHeader({ + const frame = Zcl.Frame.fromBuffer(Zcl.Clusters.genBasic.ID, Zcl.Header.fromBuffer(buffer)!, buffer, {}); + const header = new Zcl.Header({ reservedBits: 0, direction: 1, disableDefaultResponse: false, @@ -707,8 +704,8 @@ describe('Zcl', () => { it('ZclFrame from buffer readRsp', () => { const buffer = Buffer.from([24,7,1,5,0,0,66,30,84,82,65,68,70,82,73,32,98,117,108,98,32,69,50,55,32,87,83,32,111,112,97,108,32,57,56,48,108,109,6,0,0,66,8,50,48,49,55,48,51,51,49,7,0,0,48,1,10,0,0,65,15,76,69,68,49,53,52,53,71,49,50,69,50,55,69,85]); - const frame = Zcl.ZclFrame.fromBuffer(Zcl.Clusters.genBasic.ID, Zcl.ZclHeader.fromBuffer(buffer)!, buffer, {}); - const header = new Zcl.ZclHeader({ + const frame = Zcl.Frame.fromBuffer(Zcl.Clusters.genBasic.ID, Zcl.Header.fromBuffer(buffer)!, buffer, {}); + const header = new Zcl.Header({ reservedBits: 0, direction: 1, disableDefaultResponse: true, @@ -728,7 +725,7 @@ describe('Zcl', () => { it('ZclFrame with Assa (manufacturer specific) cluster create', () => { const payload = [{attrId: 0x0012, status: 0, attrData: 1, dataType: 32}]; - const frame = Zcl.ZclFrame.create( + const frame = Zcl.Frame.create( FrameType.GLOBAL, Direction.CLIENT_TO_SERVER, false, 0x101D, 8, 'readRsp', 0xfc00, payload, {}, ); @@ -737,7 +734,7 @@ describe('Zcl', () => { it('ZclFrame with Assa (manufacturer specific) cluster create with non Assamanufcode', () => { const payload = [{attrId: 0x0012, status: 0, attrData: 1, dataType: 32}]; - const frame = Zcl.ZclFrame.create( + const frame = Zcl.Frame.create( FrameType.GLOBAL, Direction.CLIENT_TO_SERVER, false, 0x10f3, 8, 'readRsp', 0xfc00, payload, {}, ); @@ -746,14 +743,14 @@ describe('Zcl', () => { it('ZclFrame with Assa (manufacturer specific) cluster fromBuffer', () => { const buffer = Buffer.from([0x04, 0xf2, 0x10, 0x08, 0x01, 0x00, 0x00, 0x00, 0x20, 0x01]) - const frame = Zcl.ZclFrame.fromBuffer(0xfc00, Zcl.ZclHeader.fromBuffer(buffer)!, buffer, {}); + const frame = Zcl.Frame.fromBuffer(0xfc00, Zcl.Header.fromBuffer(buffer)!, buffer, {}); expect(frame.cluster.name).toBe('manuSpecificAssaDoorLock'); }); it('ZclFrame to buffer with reservered bits', () => { const expected = Buffer.from([224,8,12,0,0,240]); const payload = {startAttrId: 0, maxAttrIds: 240}; - const frame = Zcl.ZclFrame.create( + const frame = Zcl.Frame.create( FrameType.GLOBAL, Direction.CLIENT_TO_SERVER, false, null, 8, 'discover', 0, payload, {}, 7, ); @@ -762,8 +759,8 @@ describe('Zcl', () => { it('ZclFrame from buffer with reservered bits', () => { const buffer = Buffer.from([224,8,12,0,0,240]); - const frame = Zcl.ZclFrame.fromBuffer(0, Zcl.ZclHeader.fromBuffer(buffer)!, buffer, {}); - const header = new Zcl.ZclHeader({ + const frame = Zcl.Frame.fromBuffer(0, Zcl.Header.fromBuffer(buffer)!, buffer, {}); + const header = new Zcl.Header({ reservedBits: 7, direction: 0, disableDefaultResponse: false, @@ -784,7 +781,7 @@ describe('Zcl', () => { it('ZclFrame to buffer discover', () => { const expected = Buffer.from([0,8,12,0,0,240]); const payload = {startAttrId: 0, maxAttrIds: 240}; - const frame = Zcl.ZclFrame.create( + const frame = Zcl.Frame.create( FrameType.GLOBAL, Direction.CLIENT_TO_SERVER, false, null, 8, 'discover', 0, payload, {}, ); @@ -794,7 +791,7 @@ describe('Zcl', () => { it('ZclFrame to buffer queryNextImageResponse with non zero status', () => { const expected = Buffer.from([9, 8, 2, 1]); const payload = {status: 1}; - const frame = Zcl.ZclFrame.create( + const frame = Zcl.Frame.create( FrameType.SPECIFIC, Direction.SERVER_TO_CLIENT, false, null, 8, 'queryNextImageResponse', 25, payload, {}, ); @@ -804,7 +801,7 @@ describe('Zcl', () => { it('ZclFrame to buffer queryNextImageResponse with zero status', () => { const expected = Buffer.from([9, 8, 2, 0, 1, 0, 3, 0, 5, 0, 0, 0, 6, 0, 0, 0]); const payload = {status: 0, manufacturerCode: 1, imageType: 3, fileVersion: 5, imageSize: 6}; - const frame = Zcl.ZclFrame.create( + const frame = Zcl.Frame.create( FrameType.SPECIFIC, Direction.SERVER_TO_CLIENT, false, null, 8, 'queryNextImageResponse', 25, payload, {}, ); @@ -814,7 +811,7 @@ describe('Zcl', () => { it('ZclFrame to buffer queryNextImageResponse with zero status and missing parameters', () => { const expected = Buffer.from([9, 8, 2, 1]); const payload = {status: 0}; - const frame = Zcl.ZclFrame.create( + const frame = Zcl.Frame.create( FrameType.SPECIFIC, Direction.SERVER_TO_CLIENT, false, null, 8, 'queryNextImageResponse', 25, payload, {}, ); @@ -826,7 +823,7 @@ describe('Zcl', () => { it('ZclFrame to buffer readRsp UTC', () => { const expected = Buffer.from([24,74,1,0,0,0,226,234,83,218,36]); const payload = [{attrId: 0, status: 0, attrData: 618288106, dataType: 226}]; - const frame = Zcl.ZclFrame.create( + const frame = Zcl.Frame.create( FrameType.GLOBAL, Direction.SERVER_TO_CLIENT, true, null, 74, 'readRsp', 0, payload, {}, ); @@ -837,14 +834,14 @@ describe('Zcl', () => { // Created as example for https://github.com/Koenkk/zigbee-herdsman/issues/127 const expectedOn = Buffer.from([0x1c ,0xd2 ,0x1a ,0xe9 ,0x02 ,0x01 ,0x00 ,0x01 ,0x01 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00, 0x00]); const payloadOn = [{attrId: 1, attrData: Buffer.from([1, 0, 0, 0, 0, 0, 0, 0]), dataType: 1}]; - const frameOn = Zcl.ZclFrame.create( + const frameOn = Zcl.Frame.create( FrameType.GLOBAL, Direction.SERVER_TO_CLIENT, true, 0x1ad2, 233, 'write', 0, payloadOn, {}, ); expect(frameOn.toBuffer()).toStrictEqual(expectedOn); const expectedOff = Buffer.from([0x1c ,0xd2 ,0x1a ,0xe9 ,0x02 ,0x01 ,0x00 ,0x01 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00, 0x00]); const payloadOff = [{attrId: 1, attrData: Buffer.from([0, 0, 0, 0, 0, 0, 0, 0]), dataType: 1}]; - const frameOff = Zcl.ZclFrame.create( + const frameOff = Zcl.Frame.create( FrameType.GLOBAL, Direction.SERVER_TO_CLIENT, true, 0x1ad2, 233, 'write', 0, payloadOff, {}, ); expect(frameOff.toBuffer()).toStrictEqual(expectedOff); @@ -852,7 +849,7 @@ describe('Zcl', () => { it('ZclFrame write request with string as bytes array', () => { const payload = [{attrId: 0x0401, attrData: [0x07, 0x00, 0x02, 0x01, 0x00, 0x00, 0x00, 0x14], dataType: 0x42}]; - const frame = Zcl.ZclFrame.create( + const frame = Zcl.Frame.create( FrameType.GLOBAL, Direction.CLIENT_TO_SERVER, true, 0x115f, 15, 'write', 0, payload, {}, ); @@ -862,7 +859,7 @@ describe('Zcl', () => { it('ZclFrame write rsp', () => { const payload = [{status: 0x11, attrId: 0x22}]; - const frame = Zcl.ZclFrame.create( + const frame = Zcl.Frame.create( FrameType.GLOBAL, Direction.CLIENT_TO_SERVER, true, 0x115f, 15, 'writeRsp', 0, payload, {}, ); const buffer = frame.toBuffer(); @@ -875,7 +872,7 @@ describe('Zcl', () => { it('ZclFrame to buffer readRsp success', () => { const expected = Buffer.from([8, 1, 1, 1, 0, 0, 32, 3]); const payload = [{status: Zcl.Status.SUCCESS, attrId: 1, dataType: Zcl.DataType.UINT8, attrData: 3}]; - const frame = Zcl.ZclFrame.create( + const frame = Zcl.Frame.create( FrameType.GLOBAL, Direction.SERVER_TO_CLIENT, false, null, 1, 1, 0, payload, {}, ); @@ -885,7 +882,7 @@ describe('Zcl', () => { it('ZclFrame to buffer defaultRsp success', () => { const expected = Buffer.from([0x18, 0x04, 0x0b, 0x0c, 0x82]); const payload = {cmdId: 12, statusCode: 130}; - const frame = Zcl.ZclFrame.create( + const frame = Zcl.Frame.create( FrameType.GLOBAL, Direction.SERVER_TO_CLIENT, true, null, 4, 11, 0, payload, {}, ); @@ -895,7 +892,7 @@ describe('Zcl', () => { it('ZclFrame to buffer readStructured single element', () => { const expected = Buffer.from([0x18, 0x02, 0x0E, 0x01, 0x00, 0x01, 0x02, 0x00]); const payload = [{attrId: 0x0001, selector: {indexes: [2]}}]; - const frame = Zcl.ZclFrame.create(FrameType.GLOBAL, Direction.SERVER_TO_CLIENT, true, null, 2, 0x0E, Zcl.Clusters.genBasic.ID, payload, {}); + const frame = Zcl.Frame.create(FrameType.GLOBAL, Direction.SERVER_TO_CLIENT, true, null, 2, 0x0E, Zcl.Clusters.genBasic.ID, payload, {}); expect(frame.toBuffer()).toStrictEqual(expected); }); @@ -906,7 +903,7 @@ describe('Zcl', () => { {attrId: 0x0002, selector: {indexes: [3, 4]}}, {attrId: 0x0005, selector: {indexes: [6, 7, 8]}} ]; - const frame = Zcl.ZclFrame.create(FrameType.GLOBAL, Direction.SERVER_TO_CLIENT, true, null, 2, 0x0E, Zcl.Clusters.genBasic.ID, payload, {}); + const frame = Zcl.Frame.create(FrameType.GLOBAL, Direction.SERVER_TO_CLIENT, true, null, 2, 0x0E, Zcl.Clusters.genBasic.ID, payload, {}); expect(frame.toBuffer()).toStrictEqual(expected); }); @@ -914,15 +911,15 @@ describe('Zcl', () => { it('ZclFrame to buffer readStructured whole attribute', () => { const expected = Buffer.from([0x18, 0x02, 0x0E, 0x09, 0x00, 0x00]); const payload = [{attrId: 0x0009, selector: {}}]; - const frame = Zcl.ZclFrame.create(FrameType.GLOBAL, Direction.SERVER_TO_CLIENT, true, null, 2, 0x0E, Zcl.Clusters.genBasic.ID, payload, {}); + const frame = Zcl.Frame.create(FrameType.GLOBAL, Direction.SERVER_TO_CLIENT, true, null, 2, 0x0E, Zcl.Clusters.genBasic.ID, payload, {}); expect(frame.toBuffer()).toStrictEqual(expected); }); it('ZclFrame from buffer readStructured single elements', () => { const buffer = Buffer.from([0x18, 0x02, 0x0E, 0x01, 0x00, 0x01, 0x02, 0x00]); - const frame = Zcl.ZclFrame.fromBuffer(Zcl.Clusters.genBasic.ID, Zcl.ZclHeader.fromBuffer(buffer)!, buffer, {}); - const header = new Zcl.ZclHeader({ + const frame = Zcl.Frame.fromBuffer(Zcl.Clusters.genBasic.ID, Zcl.Header.fromBuffer(buffer)!, buffer, {}); + const header = new Zcl.Header({ reservedBits: 0, direction: Direction.SERVER_TO_CLIENT, disableDefaultResponse: true, @@ -941,8 +938,8 @@ describe('Zcl', () => { it('ZclFrame from buffer readStructured multiple elements', () => { const buffer = Buffer.from([0x18, 0x02, 0x0E, 0x02, 0x00, 0x02, 0x03, 0x00, 0x04, 0x00, 0x05, 0x00, 0x03, 0x06, 0x00, 0x07, 0x00, 0x08, 0x00]); - const frame = Zcl.ZclFrame.fromBuffer(Zcl.Clusters.genBasic.ID, Zcl.ZclHeader.fromBuffer(buffer)!, buffer, {}); - const header = new Zcl.ZclHeader({ + const frame = Zcl.Frame.fromBuffer(Zcl.Clusters.genBasic.ID, Zcl.Header.fromBuffer(buffer)!, buffer, {}); + const header = new Zcl.Header({ reservedBits: 0, direction: Direction.SERVER_TO_CLIENT, disableDefaultResponse: true, @@ -964,8 +961,8 @@ describe('Zcl', () => { it('ZclFrame from buffer readStructured whole attribute', () => { const buffer = Buffer.from([0x18, 0x02, 0x0E, 0x09, 0x00, 0x00]); - const frame = Zcl.ZclFrame.fromBuffer(Zcl.Clusters.genBasic.ID, Zcl.ZclHeader.fromBuffer(buffer)!, buffer, {}); - const header = new Zcl.ZclHeader({ + const frame = Zcl.Frame.fromBuffer(Zcl.Clusters.genBasic.ID, Zcl.Header.fromBuffer(buffer)!, buffer, {}); + const header = new Zcl.Header({ reservedBits: 0, direction: Direction.SERVER_TO_CLIENT, disableDefaultResponse: true, @@ -985,7 +982,7 @@ describe('Zcl', () => { it('ZclFrame to buffer writeStructured single element', () => { const expected = Buffer.from([0x10, 0x02, 0x0f, 0x01, 0x00, 0x01, 0x02, 0x00, 0x20, 0x03]); const payload = [{attrId: 0x0001, selector: {indexes: [2]}, dataType: Zcl.DataType.UINT8, elementData: 3}]; - const frame = Zcl.ZclFrame.create(FrameType.GLOBAL, Direction.CLIENT_TO_SERVER, true, null, 2, 0x0f, Zcl.Clusters.genBasic.ID, payload, {}); + const frame = Zcl.Frame.create(FrameType.GLOBAL, Direction.CLIENT_TO_SERVER, true, null, 2, 0x0f, Zcl.Clusters.genBasic.ID, payload, {}); expect(frame.toBuffer()).toStrictEqual(expected); }); @@ -996,7 +993,7 @@ describe('Zcl', () => { {attrId: 0x0002, selector: {indexes: [3, 4]}, dataType: Zcl.DataType.CHAR_STR, elementData: 'foo'}, {attrId: 0x0005, selector: {indexes: [6, 7, 8]}, dataType: Zcl.DataType.CHAR_STR, elementData: 'bar'} ]; - const frame = Zcl.ZclFrame.create(FrameType.GLOBAL, Direction.CLIENT_TO_SERVER, true, null, 2, 0x0f, Zcl.Clusters.genBasic.ID, payload, {}); + const frame = Zcl.Frame.create(FrameType.GLOBAL, Direction.CLIENT_TO_SERVER, true, null, 2, 0x0f, Zcl.Clusters.genBasic.ID, payload, {}); expect(frame.toBuffer()).toStrictEqual(expected); }); @@ -1006,7 +1003,7 @@ describe('Zcl', () => { const payload = [ {attrId: 0x0009, selector: {}, dataType: Zcl.DataType.ARRAY, elementData: {elementType: Zcl.DataType.UINT8, elements: [10, 11, 12]}}, ]; - const frame = Zcl.ZclFrame.create(FrameType.GLOBAL, Direction.CLIENT_TO_SERVER, true, null, 2, 0x0f, Zcl.Clusters.genBasic.ID, payload, {}); + const frame = Zcl.Frame.create(FrameType.GLOBAL, Direction.CLIENT_TO_SERVER, true, null, 2, 0x0f, Zcl.Clusters.genBasic.ID, payload, {}); expect(frame.toBuffer()).toStrictEqual(expected); }); @@ -1016,7 +1013,7 @@ describe('Zcl', () => { const payload = [ {attrId: 0x000d, selector: {indicatorType: StructuredIndicatorType.WriteAdd}, dataType: Zcl.DataType.CHAR_STR, elementData: 'foo'}, ]; - const frame = Zcl.ZclFrame.create(FrameType.GLOBAL, Direction.CLIENT_TO_SERVER, true, null, 2, 0x0f, Zcl.Clusters.genBasic.ID, payload, {}); + const frame = Zcl.Frame.create(FrameType.GLOBAL, Direction.CLIENT_TO_SERVER, true, null, 2, 0x0f, Zcl.Clusters.genBasic.ID, payload, {}); expect(frame.toBuffer()).toStrictEqual(expected); }); @@ -1026,7 +1023,7 @@ describe('Zcl', () => { const payload = [ {attrId: 0x000e, selector: {indicatorType: StructuredIndicatorType.WriteRemove}, dataType: Zcl.DataType.CHAR_STR, elementData: 'bar'}, ]; - const frame = Zcl.ZclFrame.create(FrameType.GLOBAL, Direction.CLIENT_TO_SERVER, true, null, 2, 0x0f, Zcl.Clusters.genBasic.ID, payload, {}); + const frame = Zcl.Frame.create(FrameType.GLOBAL, Direction.CLIENT_TO_SERVER, true, null, 2, 0x0f, Zcl.Clusters.genBasic.ID, payload, {}); expect(frame.toBuffer()).toStrictEqual(expected); }); @@ -1036,7 +1033,7 @@ describe('Zcl', () => { const payload = [ {attrId: 0x0000, selector: null, elementData: [0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]}, ]; - const frame = Zcl.ZclFrame.create(FrameType.GLOBAL, Direction.SERVER_TO_CLIENT, true, 0x1ad2, 0xe9, 0x0f, Zcl.Clusters.genBasic.ID, payload, {}, 3); + const frame = Zcl.Frame.create(FrameType.GLOBAL, Direction.SERVER_TO_CLIENT, true, 0x1ad2, 0xe9, 0x0f, Zcl.Clusters.genBasic.ID, payload, {}, 3); expect(frame.toBuffer()).toStrictEqual(expected); }); @@ -1044,7 +1041,7 @@ describe('Zcl', () => { it('ZclFrame to buffer writeStructuredRsp success', () => { const expected = Buffer.from([8, 1, 0x10, 0]); const payload = [{status: Zcl.Status.SUCCESS}]; - const frame = Zcl.ZclFrame.create( + const frame = Zcl.Frame.create( FrameType.GLOBAL, Direction.SERVER_TO_CLIENT, false, null, 1, 0x10, Zcl.Clusters.genBasic.ID, payload, {}, ); @@ -1054,7 +1051,7 @@ describe('Zcl', () => { it('ZclFrame to buffer writeStructuredRsp failed selector unknown or none for type', () => { const expected = Buffer.from([8, 1, 0x10, 147, 1, 0, 0]); const payload = [{status: Zcl.Status.ACTION_DENIED, attrId: 1, selector: {indicatorType: StructuredIndicatorType.Whole}}]; - const frame = Zcl.ZclFrame.create( + const frame = Zcl.Frame.create( FrameType.GLOBAL, Direction.SERVER_TO_CLIENT, false, null, 1, 0x10, Zcl.Clusters.genBasic.ID, payload, {}, ); @@ -1064,7 +1061,7 @@ describe('Zcl', () => { it('ZclFrame to buffer writeStructuredRsp failed with selector', () => { const expected = Buffer.from([8, 1, 0x10, 147, 1, 0, 1, 254, 0]); const payload = [{status: Zcl.Status.ACTION_DENIED, attrId: 1, selector: {indexes: [254]}}]; - const frame = Zcl.ZclFrame.create( + const frame = Zcl.Frame.create( FrameType.GLOBAL, Direction.SERVER_TO_CLIENT, false, null, 1, 0x10, Zcl.Clusters.genBasic.ID, payload, {}, ); @@ -1077,7 +1074,7 @@ describe('Zcl', () => { {status: Zcl.Status.ACTION_DENIED, attrId: 16, selector: {indicatorType: StructuredIndicatorType.Whole}}, {status: Zcl.Status.ABORT, attrId: 14, selector: {indexes: [254, 32]}}, ]; - const frame = Zcl.ZclFrame.create( + const frame = Zcl.Frame.create( FrameType.GLOBAL, Direction.SERVER_TO_CLIENT, false, null, 1, 0x10, Zcl.Clusters.genBasic.ID, payload, {}, ); @@ -1086,8 +1083,8 @@ describe('Zcl', () => { it('ZclFrame from buffer writeStructuredRsp success', () => { const buffer = Buffer.from([8, 1, 0x10, 0]); - const frame = Zcl.ZclFrame.fromBuffer(Zcl.Clusters.genBasic.ID, Zcl.ZclHeader.fromBuffer(buffer)!, buffer, {}); - const header = new Zcl.ZclHeader({ + const frame = Zcl.Frame.fromBuffer(Zcl.Clusters.genBasic.ID, Zcl.Header.fromBuffer(buffer)!, buffer, {}); + const header = new Zcl.Header({ reservedBits: 0, direction: Direction.SERVER_TO_CLIENT, disableDefaultResponse: false, @@ -1106,8 +1103,8 @@ describe('Zcl', () => { it('ZclFrame from buffer writeStructuredRsp failed selector unknown or none for type', () => { const buffer = Buffer.from([8, 1, 0x10, 147, 1, 0, 0]); - const frame = Zcl.ZclFrame.fromBuffer(Zcl.Clusters.genBasic.ID, Zcl.ZclHeader.fromBuffer(buffer)!, buffer, {}); - const header = new Zcl.ZclHeader({ + const frame = Zcl.Frame.fromBuffer(Zcl.Clusters.genBasic.ID, Zcl.Header.fromBuffer(buffer)!, buffer, {}); + const header = new Zcl.Header({ reservedBits: 0, direction: Direction.SERVER_TO_CLIENT, disableDefaultResponse: false, @@ -1126,8 +1123,8 @@ describe('Zcl', () => { it('ZclFrame from buffer writeStructuredRsp failed with selector', () => { const buffer = Buffer.from([8, 1, 0x10, 147, 1, 0, 1, 254, 0]); - const frame = Zcl.ZclFrame.fromBuffer(Zcl.Clusters.genBasic.ID, Zcl.ZclHeader.fromBuffer(buffer)!, buffer, {}); - const header = new Zcl.ZclHeader({ + const frame = Zcl.Frame.fromBuffer(Zcl.Clusters.genBasic.ID, Zcl.Header.fromBuffer(buffer)!, buffer, {}); + const header = new Zcl.Header({ reservedBits: 0, direction: Direction.SERVER_TO_CLIENT, disableDefaultResponse: false, @@ -1146,8 +1143,8 @@ describe('Zcl', () => { it('ZclFrame from buffer writeStructuredRsp failed mixed selectors', () => { const buffer = Buffer.from([8, 1, 0x10, 147, 16, 0, 0, 149, 14, 0, 2, 254, 0, 32, 0]); - const frame = Zcl.ZclFrame.fromBuffer(Zcl.Clusters.genBasic.ID, Zcl.ZclHeader.fromBuffer(buffer)!, buffer, {}); - const header = new Zcl.ZclHeader({ + const frame = Zcl.Frame.fromBuffer(Zcl.Clusters.genBasic.ID, Zcl.Header.fromBuffer(buffer)!, buffer, {}); + const header = new Zcl.Header({ reservedBits: 0, direction: Direction.SERVER_TO_CLIENT, disableDefaultResponse: false, @@ -1169,8 +1166,8 @@ describe('Zcl', () => { it('ZclFrame from buffer ssIasAce arm command', () => { const buffer = Buffer.from([1,87,0,0,6,49,50,51,52,53,54,0]); - const frame = Zcl.ZclFrame.fromBuffer(Zcl.Clusters.ssIasAce.ID, Zcl.ZclHeader.fromBuffer(buffer)!, buffer, {}); - const header = new Zcl.ZclHeader({ + const frame = Zcl.Frame.fromBuffer(Zcl.Clusters.ssIasAce.ID, Zcl.Header.fromBuffer(buffer)!, buffer, {}); + const header = new Zcl.Header({ reservedBits: 0, direction: 0, disableDefaultResponse: false, @@ -1200,7 +1197,7 @@ describe('Zcl', () => { const expected = Buffer.from([24,23,13,0,32,0,32,33,0,32,49,0,48,51,0,32,53,0,24]); const payload = {"discComplete":0,"attrInfos":[{"attrId":32,"dataType":32},{"attrId":33,"dataType":32},{"attrId":49,"dataType":48},{"attrId":51,"dataType":32},{"attrId":53,"dataType":24}]}; - const frame = Zcl.ZclFrame.create( + const frame = Zcl.Frame.create( FrameType.GLOBAL, Direction.SERVER_TO_CLIENT, true, null, 23, 13, 0, payload, {}, ); @@ -1211,7 +1208,7 @@ describe('Zcl', () => { const expected = Buffer.from([0x05, 0x7c, 0x11, 0x1d, 0x07, 0x00, 0x01, 0x0d, 0x00]); const payload = {value: 256, value2: 13}; - const frame = Zcl.ZclFrame.create( + const frame = Zcl.Frame.create( FrameType.SPECIFIC, Direction.CLIENT_TO_SERVER, false, 4476, 29, 7, 5, payload, {}, ); @@ -1222,7 +1219,7 @@ describe('Zcl', () => { const expected = Buffer.from([8, 1, 1, 1, 0, 2]); const payload = [{status: 2, attrId: 1}]; - const frame = Zcl.ZclFrame.create( + const frame = Zcl.Frame.create( FrameType.GLOBAL, Direction.SERVER_TO_CLIENT, false, null, 1, 1, 0, payload, {}, ); @@ -1233,7 +1230,7 @@ describe('Zcl', () => { const expected = Buffer.from([0x1, 1, 64, 1, 0]); const payload = {effectid: 1, effectvariant: 0}; - const frame = Zcl.ZclFrame.create( + const frame = Zcl.Frame.create( FrameType.SPECIFIC, Direction.CLIENT_TO_SERVER, false, null, 1, 64, 6, payload, {}, ); @@ -1243,7 +1240,7 @@ describe('Zcl', () => { it('ZclFrame to buffer offWithEffect', () => { const expected = Buffer.from([9, 9, 0, 1]); - const frame = Zcl.ZclFrame.create( + const frame = Zcl.Frame.create( FrameType.SPECIFIC, Direction.SERVER_TO_CLIENT, false, null, 9, 'restartDeviceRsp', 21, {status: 1}, {}, ); @@ -1252,7 +1249,7 @@ describe('Zcl', () => { it('ZclFrame to buffer invalid frametype', () => { expect(() => { - Zcl.ZclFrame.create( + Zcl.Frame.create( 3, Direction.CLIENT_TO_SERVER, false, null, 1, 64, 6, {}, {}, ).toBuffer(); }).toThrow("Frametype '3' not valid"); @@ -1649,7 +1646,7 @@ describe('Zcl', () => { }); it('Zcl utils get cluster attributes manufacturerCode', () => { - const cluster = Zcl.Utils.getCluster('closuresWindowCovering', 0x1021); + const cluster = Zcl.Utils.getCluster('closuresWindowCovering', 0x1021, {}); const attribute = cluster.getAttribute(0xf004); expect(attribute).toStrictEqual({"ID": 0xf004, "manufacturerCode": 0x1021, "name": "stepPositionTilt", "type": 48}); }); @@ -1904,7 +1901,7 @@ describe('Zcl', () => { it('ZclFrame parse MiBoxer zone configuration command', () => { const zoneConfigPayload = Buffer.from([0x11, 0x01, 0xf0, 0x08, 0x84, 0x2b, 0x01, 0x98, 0x2b, 0x02, 0xac, 0x2b, 0x03, 0xc0, 0x2b, 0x04, 0xd4, 0x2b, 0x05, 0xe8, 0x2b, 0x06, 0xfc, 0x2b, 0x07, 0x10, 0x2c, 0x08]); - const zoneConfigFrame = Zcl.ZclFrame.fromBuffer(Zcl.Clusters.genGroups.ID, Zcl.ZclHeader.fromBuffer(zoneConfigPayload)!, zoneConfigPayload, {}); + const zoneConfigFrame = Zcl.Frame.fromBuffer(Zcl.Clusters.genGroups.ID, Zcl.Header.fromBuffer(zoneConfigPayload)!, zoneConfigPayload, {}); expect(zoneConfigFrame.payload.zones).toStrictEqual([ {zoneNum: 1, groupId: 0x2b84}, {zoneNum: 2, groupId: 0x2b98}, @@ -1927,7 +1924,7 @@ describe('Zcl', () => { {zoneNum: 7, groupId: 0x2bfc}, {zoneNum: 8, groupId: 0x2c10}, ]; - const zoneConfigFrame = Zcl.ZclFrame.create(FrameType.SPECIFIC, Direction.CLIENT_TO_SERVER, true, null, 1, 'miboxerSetZones', Zcl.Clusters.genGroups.ID, { zones: testZones }, {}); + const zoneConfigFrame = Zcl.Frame.create(FrameType.SPECIFIC, Direction.CLIENT_TO_SERVER, true, null, 1, 'miboxerSetZones', Zcl.Clusters.genGroups.ID, { zones: testZones }, {}); expect(zoneConfigFrame.toBuffer()).toStrictEqual(Buffer.from([0x11, 0x01, 0xf0, 0x08, 0x84, 0x2b, 0x01, 0x98, 0x2b, 0x02, 0xac, 0x2b, 0x03, 0xc0, 0x2b, 0x04, 0xd4, 0x2b, 0x05, 0xe8, 0x2b, 0x06, 0xfc, 0x2b, 0x07, 0x10, 0x2c, 0x08])); }); @@ -2057,9 +2054,9 @@ describe('Zcl', () => { expect(value).toStrictEqual(''); }); - it('BuffaloZcl read CHAR_STR non-value for attrId===65281', () => { + it('BuffaloZcl read MI_STRUCT non-value', () => { const buffalo = new BuffaloZcl(Buffer.from([0xFF])); - const value = buffalo.read(DataType.CHAR_STR, {attrId: 65281}); + const value = buffalo.read(BuffaloZclDataType.MI_STRUCT, {}); expect(value).toStrictEqual({}); }); @@ -2118,12 +2115,12 @@ describe('Zcl', () => { }); it('ZclHeader return undefined when too short', () => { - const header = Zcl.ZclHeader.fromBuffer(Buffer.from([0, 8])); + const header = Zcl.Header.fromBuffer(Buffer.from([0, 8])); expect(header).toStrictEqual(undefined); }); it('ZclHeader return undefined when too short manufacturer specific', () => { - const header = Zcl.ZclHeader.fromBuffer(Buffer.from([4, 8, 3])); + const header = Zcl.Header.fromBuffer(Buffer.from([4, 8, 3])); expect(header).toStrictEqual(undefined); }); @@ -2450,4 +2447,4 @@ describe('Zcl', () => { buffalo.read(BuffaloZclDataType.LIST_THERMO_TRANSITIONS, {payload: {numoftrans: 1}}); }).toThrow(`Cannot read LIST_THERMO_TRANSITIONS without required payload options specified`); }); -}); +}); \ No newline at end of file diff --git a/test/zspec/utils.test.ts b/test/zspec/utils.test.ts new file mode 100644 index 0000000000..7c5d0e147e --- /dev/null +++ b/test/zspec/utils.test.ts @@ -0,0 +1,19 @@ +import * as ZSpec from '../../src/zspec'; + +describe('ZSpec Utils', () => { + it('Converts channels number array to uint32 mask', () => { + expect(ZSpec.Utils.channelsToUInt32Mask(ZSpec.ALL_802_15_4_CHANNELS)).toStrictEqual(ZSpec.ALL_802_15_4_CHANNELS_MASK); + expect(ZSpec.Utils.channelsToUInt32Mask(ZSpec.PREFERRED_802_15_4_CHANNELS)).toStrictEqual(ZSpec.PREFERRED_802_15_4_CHANNELS_MASK); + }); + it('Converts channels uint32 mask to number array', () => { + expect(ZSpec.Utils.uint32MaskToChannels(ZSpec.ALL_802_15_4_CHANNELS_MASK)).toStrictEqual(ZSpec.ALL_802_15_4_CHANNELS); + expect(ZSpec.Utils.uint32MaskToChannels(ZSpec.PREFERRED_802_15_4_CHANNELS_MASK)).toStrictEqual(ZSpec.PREFERRED_802_15_4_CHANNELS); + }); + it('Checks if address is broadcast', () => { + expect(ZSpec.Utils.isBroadcastAddress(ZSpec.BroadcastAddress.DEFAULT)).toBeTruthy(); + expect(ZSpec.Utils.isBroadcastAddress(ZSpec.BroadcastAddress.RX_ON_WHEN_IDLE)).toBeTruthy(); + expect(ZSpec.Utils.isBroadcastAddress(ZSpec.BroadcastAddress.SLEEPY)).toBeTruthy(); + expect(ZSpec.Utils.isBroadcastAddress(ZSpec.BroadcastAddress.LOW_POWER_ROUTERS)).toBeTruthy(); + expect(ZSpec.Utils.isBroadcastAddress(0x0f30)).toBeFalsy(); + }); +}); diff --git a/test/zspec/zcl/buffalo.test.ts b/test/zspec/zcl/buffalo.test.ts new file mode 100644 index 0000000000..1757bc7de6 --- /dev/null +++ b/test/zspec/zcl/buffalo.test.ts @@ -0,0 +1,1155 @@ +import * as Zcl from '../../../src/zspec/zcl'; +import {BuffaloZcl} from '../../../src/zspec/zcl/buffaloZcl'; +import {uint16To8Array, uint32To8Array} from '../../utils/math'; + +describe('ZCL Buffalo', () => { + + it('Writes invalida data type as buffer if buffer-like', () => { + const value = [1, 2, 3]; + const buffer = Buffer.alloc(3); + const buffalo = new BuffaloZcl(buffer); + const writeSpy = jest.spyOn(buffalo, 'writeBuffer'); + // @ts-expect-error invalid on purpose + buffalo.write(99999, value, {}); + expect(writeSpy).toHaveBeenCalledWith(value, value.length); + expect(writeSpy).toHaveBeenCalledTimes(1); + + // @ts-expect-error private + buffalo.position = 0; + + // @ts-expect-error invalid on purpose + buffalo.write(99999, Buffer.from(value), {}); + expect(writeSpy).toHaveBeenLastCalledWith(Buffer.from(value), value.length); + expect(writeSpy).toHaveBeenCalledTimes(2); + }); + + it('Throws when write/read invalid data type, except for write if buffer-like', () => { + expect(() => { + const buffer = Buffer.alloc(1); + const buffalo = new BuffaloZcl(buffer); + // @ts-expect-error invalid on purpose + buffalo.write(99999, 127, {}); + }).toThrow(); + expect(() => { + const buffer = Buffer.alloc(1); + const buffalo = new BuffaloZcl(buffer); + // @ts-expect-error invalid on purpose + buffalo.read(99999, {}); + }).toThrow(); + }); + + it('Writes nothing', () => { + for (const type of [Zcl.DataType.NO_DATA, Zcl.DataType.UNKNOWN]) { + const buffer = Buffer.alloc(3); + const buffalo = new BuffaloZcl(buffer); + + buffalo.write(type, 123, {}); + expect(buffer).toStrictEqual(Buffer.from([0, 0, 0])); + expect(buffalo.getPosition()).toStrictEqual(0); + } + + { + const buffer = Buffer.alloc(3); + const buffalo = new BuffaloZcl(buffer); + + buffalo.write(Zcl.BuffaloZclDataType.STRUCTURED_SELECTOR, null, {}); + expect(buffer).toStrictEqual(Buffer.from([0, 0, 0])); + expect(buffalo.getPosition()).toStrictEqual(0); + } + }); + + it('Reads nothing', () => { + for (const type of [Zcl.DataType.NO_DATA, Zcl.DataType.UNKNOWN]) { + const buffer = Buffer.from([1, 2]); + const buffalo = new BuffaloZcl(buffer); + expect(buffalo.read(type, {})).toStrictEqual(undefined); + expect(buffalo.getPosition()).toStrictEqual(0); + } + }); + + it.each([ + [ + 'uint8-like', + {value: 250, types: [Zcl.DataType.DATA8, Zcl.DataType.BOOLEAN, Zcl.DataType.BITMAP8, Zcl.DataType.UINT8, Zcl.DataType.ENUM8]}, + {position: 1, write: 'writeUInt8', read: 'readUInt8'}, + ], + [ + 'uint16-like', + {value: 65530, types: [Zcl.DataType.DATA16, Zcl.DataType.BITMAP16, Zcl.DataType.UINT16, Zcl.DataType.ENUM16, Zcl.DataType.CLUSTER_ID, Zcl.DataType.ATTR_ID]}, + {position: 2, write: 'writeUInt16', read: 'readUInt16'}, + ], + [ + 'uint24-like', + {value: 16777210, types: [Zcl.DataType.DATA24, Zcl.DataType.BITMAP24, Zcl.DataType.UINT24]}, + {position: 3, write: 'writeUInt24', read: 'readUInt24'}, + ], + [ + 'uint32-like', + {value: 4294967290, types: [Zcl.DataType.DATA32, Zcl.DataType.BITMAP32, Zcl.DataType.UINT32, Zcl.DataType.UTC, Zcl.DataType.BAC_OID]}, + {position: 4, write: 'writeUInt32', read: 'readUInt32'}, + ], + [ + 'int8-like', + {value: -120, types: [Zcl.DataType.INT8]}, + {position: 1, write: 'writeInt8', read: 'readInt8'}, + ], + [ + 'int16-like', + {value: -32760, types: [Zcl.DataType.INT16]}, + {position: 2, write: 'writeInt16', read: 'readInt16'}, + ], + [ + 'int24-like', + {value: -8388600, types: [Zcl.DataType.INT24]}, + {position: 3, write: 'writeInt24', read: 'readInt24'}, + ], + [ + 'int32-like', + {value: -2147483640, types: [Zcl.DataType.INT32]}, + {position: 4, write: 'writeInt32', read: 'readInt32'}, + ], + [ + 'int48-like', + {value: -140737488355320, types: [Zcl.DataType.INT48]}, + {position: 6, write: 'writeInt48', read: 'readInt48'}, + ], + [ + 'float-like', + {value: 1.539989614439558e-36, types: [Zcl.DataType.SINGLE_PREC]}, + {position: 4, write: 'writeFloatLE', read: 'readFloatLE'}, + ], + [ + 'double-like', + {value: 5.447603722011605e-270, types: [Zcl.DataType.DOUBLE_PREC]}, + {position: 8, write: 'writeDoubleLE', read: 'readDoubleLE'}, + ], + [ + 'IEEE address', + {value: '0xfe1234abcd9876ff', types: [Zcl.DataType.IEEE_ADDR]}, + {position: 8, write: 'writeIeeeAddr', read: 'readIeeeAddr'}, + ], + [ + 'uint8-like list', + {value: [250, 25, 50], types: [Zcl.BuffaloZclDataType.LIST_UINT8]}, + {position: 3, write: 'writeListUInt8', read: 'readListUInt8'}, + ], + [ + 'uint16-like list', + {value: [65530, 6553, 5530], types: [Zcl.BuffaloZclDataType.LIST_UINT16]}, + {position: 6, write: 'writeListUInt16', read: 'readListUInt16'}, + ], + [ + 'uint24-like list', + {value: [16777210, 1677721, 6777210], types: [Zcl.BuffaloZclDataType.LIST_UINT24]}, + {position: 9, write: 'writeListUInt24', read: 'readListUInt24'}, + ], + [ + 'uint32-like list', + {value: [4294967290, 429496729, 294967290], types: [Zcl.BuffaloZclDataType.LIST_UINT32]}, + {position: 12, write: 'writeListUInt32', read: 'readListUInt32'}, + ], + [ + 'buffer', + {value: Buffer.from([1, 2, 3, 4]), types: [Zcl.BuffaloZclDataType.BUFFER]}, + {position: 4, write: 'writeBuffer', read: 'readBuffer'}, + ], + [ + 'security key', + {value: Buffer.from([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]), types: [Zcl.DataType.SEC_KEY]}, + {position: 16, write: 'writeBuffer', read: 'readBuffer'}, + ], + ])('Writes & Reads using base class for %s', (_name, payload, expected) => { + const readOptions: {length?: number} = {}; + + if (Array.isArray(payload.value) || payload.value instanceof Buffer) { + readOptions.length = payload.value.length; + } + + for (const type of payload.types) { + const buffer = Buffer.alloc(255); + const buffalo = new BuffaloZcl(buffer); + // @ts-expect-error dynamic + const writeSpy = jest.spyOn(buffalo, expected.write); + // @ts-expect-error dynamic + const readSpy = jest.spyOn(buffalo, expected.read); + + buffalo.write(type, payload.value, {}); + expect(writeSpy).toHaveBeenCalledTimes(1); + expect(buffalo.getPosition()).toStrictEqual(expected.position); + + // @ts-expect-error private + buffalo.position = 0; + + expect(buffalo.read(type, readOptions)).toStrictEqual(payload.value); + expect(readSpy).toHaveBeenCalledTimes(1); + expect(buffalo.getPosition()).toStrictEqual(expected.position); + } + }); + + it('Reads whole buffer without length option', () => { + const value = [1, 2, 3, 4]; + const buffer = Buffer.alloc(4); + const buffalo = new BuffaloZcl(buffer); + const writeSpy = jest.spyOn(buffalo, 'writeBuffer'); + const readSpy = jest.spyOn(buffalo, 'readBuffer'); + + buffalo.write(Zcl.BuffaloZclDataType.BUFFER, Buffer.from(value), {}); + expect(writeSpy).toHaveBeenCalledTimes(1); + // XXX: inconsistent with read, write is always "whole" + expect(writeSpy).toHaveBeenCalledWith(Buffer.from(value), value.length); + expect(buffalo.getPosition()).toStrictEqual(value.length); + + // @ts-expect-error private + buffalo.position = 0; + + expect(buffalo.read(Zcl.BuffaloZclDataType.BUFFER, {})).toStrictEqual(Buffer.from(value)); + expect(readSpy).toHaveBeenCalledTimes(1); + expect(readSpy).toHaveBeenCalledWith(value.length); + expect(buffalo.getPosition()).toStrictEqual(value.length); + }); + + it('Reads partial buffer with length option', () => { + const value = [1, 2, 3, 4]; + const length = 2; + const buffer = Buffer.alloc(255); + const buffalo = new BuffaloZcl(buffer); + const writeSpy = jest.spyOn(buffalo, 'writeBuffer'); + const readSpy = jest.spyOn(buffalo, 'readBuffer'); + + buffalo.write(Zcl.BuffaloZclDataType.BUFFER, Buffer.from(value), {length}); + expect(writeSpy).toHaveBeenCalledTimes(1); + // XXX: inconsistent with read, write is always "whole" + expect(writeSpy).toHaveBeenCalledWith(Buffer.from(value), value.length); + expect(buffalo.getPosition()).toStrictEqual(value.length); + + // @ts-expect-error private + buffalo.position = 0; + + expect(buffalo.read(Zcl.BuffaloZclDataType.BUFFER, {length})).toStrictEqual(Buffer.from([value[0], value[1]])); + expect(readSpy).toHaveBeenCalledTimes(1); + expect(readSpy).toHaveBeenCalledWith(length); + expect(buffalo.getPosition()).toStrictEqual(length); + }); + + it.each([ + [ + 'uint40-like', + {value: [250, 25635482], types: [Zcl.DataType.DATA40, Zcl.DataType.BITMAP40, Zcl.DataType.UINT40]}, + {position: 5, written: [...uint32To8Array(25635482), 250]}, + ], + [ + 'uint48-like', + {value: [65530, 65894582], types: [Zcl.DataType.DATA48, Zcl.DataType.BITMAP48, Zcl.DataType.UINT48]}, + {position: 6, written: [...uint32To8Array(65894582), ...uint16To8Array(65530)]}, + ], + [ + 'int40-like', + {value: [120, -2147483640], types: [Zcl.DataType.INT40]}, + {position: 5, written: [...uint32To8Array(-2147483640), 120]}, + ], + [ + 'int64-like', + {value: [-2147483640, 2147483640], types: [Zcl.DataType.INT64]}, + {position: 8, written: [...uint32To8Array(2147483640), ...uint32To8Array(-2147483640)]}, + ], + ])('Writes & Reads SB variant %s', (_name, payload, expected) => { + for (const type of payload.types) { + const buffer = Buffer.alloc(100); + const buffalo = new BuffaloZcl(buffer); + buffalo.write(type, payload.value, {}); + expect(buffalo.getPosition()).toStrictEqual(expected.position); + expect(buffalo.getWritten()).toStrictEqual(Buffer.from(expected.written)); + + // @ts-expect-error private + buffalo.position = 0; + + expect(buffalo.read(type, {})).toStrictEqual(payload.value); + expect(buffalo.getPosition()).toStrictEqual(expected.position); + } + }); + + it('Writes & Reads inconsistent SB variant uint56-like', () => { + // inconsistent with rest (write params != read return) + const lsb = 1294967290; + const msb = 123456789; + const expectedWritten = Buffer.from([...uint32To8Array(lsb), ...uint16To8Array(msb), (msb >> 16) & 0xFF]); + const expectedRead = [expectedWritten.readUInt8(6), expectedWritten.readUInt16LE(4), lsb]; + const expectedPosition = 7; + + for (const type of [Zcl.DataType.DATA56, Zcl.DataType.BITMAP56, Zcl.DataType.UINT56]) { + const buffer = Buffer.alloc(10); + const buffalo = new BuffaloZcl(buffer); + buffalo.write(type, [msb, lsb], {}); + expect(buffalo.getPosition()).toStrictEqual(expectedPosition); + expect(buffalo.getWritten()).toStrictEqual(expectedWritten); + + // @ts-expect-error private + buffalo.position = 0; + + expect(buffalo.read(type, {})).toStrictEqual(expectedRead); + expect(buffalo.getPosition()).toStrictEqual(expectedPosition); + } + }); + + it('Writes & Reads inconsistent SB variant uint64-like', () => { + // inconsistent with rest (string-based) + const ieee = '0xfe1234abcd9876ff'; + const expectedWritten = Buffer.from(ieee.substring(2).match(/.{2}/g)!.reverse().join(''), 'hex'); + const expectedPosition = 8; + + for (const type of [Zcl.DataType.DATA64, Zcl.DataType.BITMAP64, Zcl.DataType.UINT64]) { + const buffer = Buffer.alloc(10); + const buffalo = new BuffaloZcl(buffer); + buffalo.write(type, ieee, {}); + expect(buffalo.getPosition()).toStrictEqual(expectedPosition); + expect(buffalo.getWritten()).toStrictEqual(expectedWritten); + + // @ts-expect-error private + buffalo.position = 0; + + expect(buffalo.read(type, {})).toStrictEqual(ieee); + expect(buffalo.getPosition()).toStrictEqual(expectedPosition); + } + }); + + it('Writes & Reads inconsistent SB variant int56-like', () => { + // inconsistent with rest (write params != read return) + const lsb = -2147483640; + const msb = 123456789; + const expectedWritten = Buffer.from([...uint32To8Array(lsb), ...uint16To8Array(msb), (msb >> 16) & 0xFF]); + const expectedRead = [expectedWritten.readInt8(6), expectedWritten.readInt16LE(4), lsb]; + const expectedPosition = 7; + + for (const type of [Zcl.DataType.INT56]) { + const buffer = Buffer.alloc(10); + const buffalo = new BuffaloZcl(buffer); + buffalo.write(type, [msb, lsb], {}); + expect(buffalo.getPosition()).toStrictEqual(expectedPosition); + expect(buffalo.getWritten()).toStrictEqual(expectedWritten); + + // @ts-expect-error private + buffalo.position = 0; + + expect(buffalo.read(type, {})).toStrictEqual(expectedRead); + expect(buffalo.getPosition()).toStrictEqual(expectedPosition); + } + }); + + it('Writes & Reads octet str', () => { + const value = [0xFE, 0x01, 0xAB, 0x98]; + const expectedPosition = 5; + const buffer = Buffer.alloc(10); + const buffalo = new BuffaloZcl(buffer); + buffalo.write(Zcl.DataType.OCTET_STR, value, {}); + expect(buffalo.getPosition()).toStrictEqual(expectedPosition); + expect(buffalo.getWritten()).toStrictEqual(Buffer.from([value.length, ...value])); + + // @ts-expect-error private + buffalo.position = 0; + + expect(buffalo.read(Zcl.DataType.OCTET_STR, {})).toStrictEqual(Buffer.from(value)); + expect(buffalo.getPosition()).toStrictEqual(expectedPosition); + }); + + it('Writes & Reads long octet str', () => { + const value = [0xFE, 0x01, 0xAB, 0x98]; + const expectedPosition = 6; + const buffer = Buffer.alloc(10); + const buffalo = new BuffaloZcl(buffer); + buffalo.write(Zcl.DataType.LONG_OCTET_STR, value, {}); + expect(buffalo.getPosition()).toStrictEqual(expectedPosition); + expect(buffalo.getWritten()).toStrictEqual(Buffer.from([value.length, 0/*length uint16*/, ...value])); + + // @ts-expect-error private + buffalo.position = 0; + + expect(buffalo.read(Zcl.DataType.LONG_OCTET_STR, {})).toStrictEqual(Buffer.from(value)); + expect(buffalo.getPosition()).toStrictEqual(expectedPosition); + }); + + it('Writes char str from number array', () => { + const value = [0x61, 0x62, 0x63, 0x64]; + const expectedValue = 'abcd'; + const expectedPosition = 4;// value.length not written when number array given + const buffer = Buffer.alloc(10); + const buffalo = new BuffaloZcl(buffer); + buffalo.write(Zcl.DataType.CHAR_STR, value, {}); + expect(buffalo.getPosition()).toStrictEqual(expectedPosition); + expect(buffalo.getWritten()).toStrictEqual(Buffer.from(value));// see above comment + }); + + it('Writes & Reads char str from string', () => { + const value = 'abcd'; + const expectedValue = [value.length, 0x61, 0x62, 0x63, 0x64]; + const expectedPosition = 5; + const buffer = Buffer.alloc(10); + const buffalo = new BuffaloZcl(buffer); + buffalo.write(Zcl.DataType.CHAR_STR, value, {}); + expect(buffalo.getPosition()).toStrictEqual(expectedPosition); + expect(buffalo.getWritten()).toStrictEqual(Buffer.from(expectedValue)); + + // @ts-expect-error private + buffalo.position = 0; + + expect(buffalo.read(Zcl.DataType.CHAR_STR, {})).toStrictEqual(value); + expect(buffalo.getPosition()).toStrictEqual(expectedPosition); + }); + + it('[workaround] Reads char str as Mi struct for Xiaomi attridId=65281', () => { + const expectedValue = {"1": 3285, "3": 33, "4": 5032, "5": 43, "6": [0,327680], "8": 516, "10": 0, "100": 0}; + const buffer = Buffer.from([ + 34, + 1, Zcl.DataType.UINT16, ...uint16To8Array(3285), + 3, Zcl.DataType.INT8, 33, + 4, Zcl.DataType.UINT16, ...uint16To8Array(5032), + 5, Zcl.DataType.UINT16, ...uint16To8Array(43), + 6, Zcl.DataType.UINT40, ...uint32To8Array(327680), 0, + 8, Zcl.DataType.UINT16, ...uint16To8Array(516), + 10, Zcl.DataType.UINT16, ...uint16To8Array(0), + 100, Zcl.DataType.BOOLEAN, 0, + ]); + const buffalo = new BuffaloZcl(buffer); + + expect(buffalo.read(Zcl.BuffaloZclDataType.MI_STRUCT, {})).toStrictEqual(expectedValue); + expect(buffalo.getPosition()).toStrictEqual(buffer.length); + }); + + it('Writes & Reads long char str', () => { + const value = 'abcd'; + const expectedValue = [value.length, 0/*length uint16*/, 0x61, 0x62, 0x63, 0x64]; + const expectedPosition = 6; + const buffer = Buffer.alloc(10); + const buffalo = new BuffaloZcl(buffer); + buffalo.write(Zcl.DataType.LONG_CHAR_STR, value, {}); + expect(buffalo.getPosition()).toStrictEqual(expectedPosition); + expect(buffalo.getWritten()).toStrictEqual(Buffer.from(expectedValue)); + + // @ts-expect-error private + buffalo.position = 0; + + expect(buffalo.read(Zcl.DataType.LONG_CHAR_STR, {})).toStrictEqual(value); + expect(buffalo.getPosition()).toStrictEqual(expectedPosition); + }); + + it.each([ + [ + 'uint16', + {value: {elementType: Zcl.DataType.UINT16, elements: [256, 1, 65530, 0]}}, + {written: [Zcl.DataType.UINT16, 4, 0/*length uint16*/, ...uint16To8Array(256), ...uint16To8Array(1), ...uint16To8Array(65530), ...uint16To8Array(0)]}, + ], + [ + 'char str', + {value: {elementType: Zcl.DataType.CHAR_STR, elements: ['abcd', 'cd', 'a']}}, + {written: [Zcl.DataType.CHAR_STR, 3, 0/*length uint16*/, 4, 0x61, 0x62, 0x63, 0x64, 2, 0x63, 0x64, 1, 0x61]}, + ], + [ + 'uint16 with element type passed as string key of Zcl.DataType', + {value: {elementType: 'UINT16', elements: [256, 1, 65530, 0]}}, + {written: [Zcl.DataType.UINT16, 4, 0/*length uint16*/, ...uint16To8Array(256), ...uint16To8Array(1), ...uint16To8Array(65530), ...uint16To8Array(0)]}, + ], + ])('Writes & Reads array of %s', (_name, payload, expected) => { + const buffer = Buffer.alloc(50); + const buffalo = new BuffaloZcl(buffer); + buffalo.write(Zcl.DataType.ARRAY, payload.value, {}); + expect(buffalo.getPosition()).toStrictEqual(expected.written.length); + expect(buffalo.getWritten()).toStrictEqual(Buffer.from(expected.written)); + + // @ts-expect-error private + buffalo.position = 0; + + expect(buffalo.read(Zcl.DataType.ARRAY, {})).toStrictEqual(payload.value.elements); + expect(buffalo.getPosition()).toStrictEqual(expected.written.length); + }); + + it.each([ + [ + 'uint16', + {value: [ + {elmType: Zcl.DataType.UINT16, elmVal: 256}, + {elmType: Zcl.DataType.UINT16, elmVal: 1}, + {elmType: Zcl.DataType.UINT16, elmVal: 65530}, + {elmType: Zcl.DataType.UINT16, elmVal: 0}, + ]}, + {written: [ + 4, 0/*length uint16*/, + Zcl.DataType.UINT16, ...uint16To8Array(256), Zcl.DataType.UINT16, ...uint16To8Array(1), + Zcl.DataType.UINT16, ...uint16To8Array(65530), Zcl.DataType.UINT16, ...uint16To8Array(0) + ]}, + ], + [ + 'char str', + {value: [ + {elmType: Zcl.DataType.CHAR_STR, elmVal: 'abcd'}, + {elmType: Zcl.DataType.CHAR_STR, elmVal: 'cd'}, + {elmType: Zcl.DataType.CHAR_STR, elmVal: 'a'}, + ]}, + {written: [ + 3, 0/*length uint16*/, + Zcl.DataType.CHAR_STR, 4, 0x61, 0x62, 0x63, 0x64, Zcl.DataType.CHAR_STR, 2, 0x63, 0x64, Zcl.DataType.CHAR_STR, 1, 0x61 + ]}, + ], + [ + 'mixed', + {value: [ + {elmType: Zcl.DataType.UINT16, elmVal: 256}, + {elmType: Zcl.DataType.CHAR_STR, elmVal: 'abcd'}, + {elmType: Zcl.DataType.BITMAP8, elmVal: 3}, + ]}, + {written: [ + 3, 0/*length uint16*/, + Zcl.DataType.UINT16, ...uint16To8Array(256), Zcl.DataType.CHAR_STR, 4, 0x61, 0x62, 0x63, 0x64, Zcl.DataType.BITMAP8, 3 + ]}, + ], + ])('Writes & Reads struct of %s', (_name, payload, expected) => { + const buffer = Buffer.alloc(50); + const buffalo = new BuffaloZcl(buffer); + buffalo.write(Zcl.DataType.STRUCT, payload.value, {}); + expect(buffalo.getPosition()).toStrictEqual(expected.written.length); + expect(buffalo.getWritten()).toStrictEqual(Buffer.from(expected.written)); + + // @ts-expect-error private + buffalo.position = 0; + + expect(buffalo.read(Zcl.DataType.STRUCT, {})).toStrictEqual(payload.value); + expect(buffalo.getPosition()).toStrictEqual(expected.written.length); + }); + + it('Writes & Reads Time of Day', () => { + const value = {hours: 0, minutes: 59, seconds: 34, hundredths: 88}; + const expectedWritten = [0, 59, 34, 88]; + const buffer = Buffer.alloc(10); + const buffalo = new BuffaloZcl(buffer); + buffalo.write(Zcl.DataType.TOD, value, {}); + expect(buffalo.getPosition()).toStrictEqual(expectedWritten.length); + expect(buffalo.getWritten()).toStrictEqual(Buffer.from(expectedWritten)); + + // @ts-expect-error private + buffalo.position = 0; + + expect(buffalo.read(Zcl.DataType.TOD, {})).toStrictEqual(value); + expect(buffalo.getPosition()).toStrictEqual(expectedWritten.length); + }); + + it('Writes & Reads Date', () => { + const value = {year: 2000, month: 8, dayOfMonth: 31, dayOfWeek: 3}; + const expectedWritten = [100, 8, 31, 3]; + const buffer = Buffer.alloc(10); + const buffalo = new BuffaloZcl(buffer); + buffalo.write(Zcl.DataType.DATE, value, {}); + expect(buffalo.getPosition()).toStrictEqual(expectedWritten.length); + expect(buffalo.getWritten()).toStrictEqual(Buffer.from(expectedWritten)); + + // @ts-expect-error private + buffalo.position = 0; + + expect(buffalo.read(Zcl.DataType.DATE, {})).toStrictEqual(value); + expect(buffalo.getPosition()).toStrictEqual(expectedWritten.length); + }); + + it.each([ + ['octet str', {type: Zcl.DataType.OCTET_STR, position: 1, returned: Buffer.from([])}], + ['char str', {type: Zcl.DataType.CHAR_STR, position: 1, returned: ''}], + ['long octet str', {type: Zcl.DataType.LONG_OCTET_STR, position: 2, returned: Buffer.from([])}], + ['long char str', {type: Zcl.DataType.LONG_CHAR_STR, position: 2, returned: ''}], + ['array', {type: Zcl.DataType.ARRAY, position: 3, returned: []}], + ['struct', {type: Zcl.DataType.STRUCT, position: 2, returned: []}], + ['time of day', {type: Zcl.DataType.TOD, position: 4, returned: {hours: null, minutes: null, seconds: null, hundredths: null}}], + ['date', {type: Zcl.DataType.DATE, position: 4, returned: {year: null, month: null, dayOfMonth: null, dayOfWeek: null}}], + ['mi struct', {type: Zcl.BuffaloZclDataType.MI_STRUCT, position: 1, returned: {}}], + ])('Reads Non-Value for %s', (_name, payload) => { + const buffalo = new BuffaloZcl(Buffer.alloc(50, 0xFF)); + expect(buffalo.read(payload.type, {})).toStrictEqual(payload.returned); + expect(buffalo.getPosition()).toStrictEqual(payload.position); + }); + + it.each([ + // TODO: others not yet supported + ['time of day', {type: Zcl.DataType.TOD, position: 4, value: {hours: null, minutes: null, seconds: null, hundredths: null}, written: [0xFF, 0xFF, 0xFF, 0xFF]}], + ['date', {type: Zcl.DataType.DATE, position: 4, value: {year: null, month: null, dayOfMonth: null, dayOfWeek: null}, written: [0xFF, 0xFF, 0xFF, 0xFF]}], + ])('Writes Non-Value for %s', (_name, payload) => { + const buffer = Buffer.alloc(10); + const buffalo = new BuffaloZcl(buffer); + buffalo.write(payload.type, payload.value, {}); + expect(buffalo.getPosition()).toStrictEqual(payload.written.length); + expect(buffalo.getWritten()).toStrictEqual(Buffer.from(payload.written)); + }); + + it.each([ + ['time of day', {type: Zcl.DataType.TOD, value: {hours: 1, minutes: 2, seconds: null, hundredths: 3}, written: [1, 2, 0xFF, 3]}], + ['date', {type: Zcl.DataType.DATE, value: {year: 1901, month: 2, dayOfMonth: null, dayOfWeek: 3}, written: [1, 2, 0xFF, 3]}], + ])('Writes & Reads partial Non-Value for %s', (_name, payload) => { + const buffer = Buffer.alloc(10); + const buffalo = new BuffaloZcl(buffer); + buffalo.write(payload.type, payload.value, {}); + expect(buffalo.getPosition()).toStrictEqual(payload.written.length); + expect(buffalo.getWritten()).toStrictEqual(Buffer.from(payload.written)); + + // @ts-expect-error private + buffalo.position = 0; + + expect(buffalo.read(payload.type, {})).toStrictEqual(payload.value); + expect(buffalo.getPosition()).toStrictEqual(payload.written.length); + }); + + it.each([ + [ + 'uint32', + {type: Zcl.DataType.UINT32, value: 32902534}, + {position: 4, write: 'writeUInt32', read: 'readUInt32'}, + ], + [ + 'single prec', + {type: Zcl.DataType.SINGLE_PREC, value: 1.539989614439558e-36}, + {position: 4, write: 'writeFloatLE', read: 'readFloatLE'}, + ], + [ + 'IEEE address', + {type: Zcl.DataType.IEEE_ADDR, value: '0xfe1234abcd9876ff'}, + {position: 8, write: 'writeIeeeAddr', read: 'readIeeeAddr'}, + ], + ])('Writes & Reads Use Data Type for %s', (_name, payload, expected) => { + const buffer = Buffer.alloc(255); + const buffalo = new BuffaloZcl(buffer); + // @ts-expect-error dynamic + const writeSpy = jest.spyOn(buffalo, expected.write); + // @ts-expect-error dynamic + const readSpy = jest.spyOn(buffalo, expected.read); + + buffalo.write(Zcl.BuffaloZclDataType.USE_DATA_TYPE, payload.value, {dataType: payload.type}); + expect(writeSpy).toHaveBeenCalledTimes(1); + expect(buffalo.getPosition()).toStrictEqual(expected.position); + + // @ts-expect-error private + buffalo.position = 0; + + expect(buffalo.read(Zcl.BuffaloZclDataType.USE_DATA_TYPE, {dataType: payload.type})).toStrictEqual(payload.value); + expect(readSpy).toHaveBeenCalledTimes(1); + expect(buffalo.getPosition()).toStrictEqual(expected.position); + }); + + it('Writes & Reads Use Data Type as buffer when missing dataType option', () => { + const value = [12, 34]; + const buffer = Buffer.alloc(2); + const buffalo = new BuffaloZcl(buffer); + const writeSpy = jest.spyOn(buffalo, 'writeBuffer'); + const readSpy = jest.spyOn(buffalo, 'readBuffer'); + buffalo.write(Zcl.BuffaloZclDataType.USE_DATA_TYPE, value, {}); + expect(writeSpy).toHaveBeenCalledTimes(1); + expect(writeSpy).toHaveBeenCalledWith(value, value.length); + + // @ts-expect-error private + buffalo.position = 0; + + expect(buffalo.read(Zcl.BuffaloZclDataType.USE_DATA_TYPE, {})).toStrictEqual(Buffer.from(value)); + expect(readSpy).toHaveBeenCalledTimes(1); + expect(readSpy).toHaveBeenCalledWith(value.length); + + // @ts-expect-error private + buffalo.position = 0; + + expect(buffalo.read(Zcl.BuffaloZclDataType.USE_DATA_TYPE, {length: 1})).toStrictEqual(Buffer.from([value[0]])); + expect(readSpy).toHaveBeenCalledTimes(2); + expect(readSpy).toHaveBeenCalledWith(1); + }); + + it('Throws when write Use Data Type is missing dataType option and value isnt buffer or number array', () => { + expect(() => { + const payload = 1; + const buffalo = new BuffaloZcl(Buffer.alloc(2)); + buffalo.write(Zcl.BuffaloZclDataType.USE_DATA_TYPE, payload, {}); + }).toThrow(); + }); + + it.each([ + Zcl.BuffaloZclDataType.LIST_UINT8, Zcl.BuffaloZclDataType.LIST_UINT16, Zcl.BuffaloZclDataType.LIST_UINT24, + Zcl.BuffaloZclDataType.LIST_UINT32, Zcl.BuffaloZclDataType.LIST_ZONEINFO, + ])('Throws when read %s is missing required length option', (type) => { + expect(() => { + const buffalo = new BuffaloZcl(Buffer.alloc(1)); + buffalo.read(type, {}); + }).toThrow(); + }); + + it('Writes & Reads zone info list', () => { + const value = [{zoneID: 1, zoneStatus: 5}, {zoneID: 2, zoneStatus: 6}]; + const expectedWritten = [1, 5, 0/*uint16*/, 2, 6, 0/*uint16*/]; + const buffer = Buffer.alloc(10); + const buffalo = new BuffaloZcl(buffer); + buffalo.write(Zcl.BuffaloZclDataType.LIST_ZONEINFO, value, {}); + expect(buffalo.getPosition()).toStrictEqual(expectedWritten.length); + expect(buffalo.getWritten()).toStrictEqual(Buffer.from(expectedWritten)); + + // @ts-expect-error private + buffalo.position = 0; + + expect(buffalo.read(Zcl.BuffaloZclDataType.LIST_ZONEINFO, {length: value.length})).toStrictEqual(value); + expect(buffalo.getPosition()).toStrictEqual(expectedWritten.length); + }); + + it.each([ + [ + '6',/*uint8 x1*/ + {value: [{clstId: 6, len: 1, extField: [1]}]}, + {written: [...uint16To8Array(6), 1, 1]}, + ], + [ + '8',/*uint8 x1*/ + {value: [{clstId: 8, len: 1, extField: [2]}]}, + {written: [...uint16To8Array(8), 1, 2]}, + ], + [ + '258',/*uint8 x2*/ + {value: [{clstId: 258, len: 2, extField: [1, 2]}]}, + {written: [...uint16To8Array(258), 2, 1, 2]}, + ], + [ + '768',/*uint16 x3, uint8 x3, uint16 x2*/ + {value: [{clstId: 768, len: 13, extField: [1, 2, 3, 4, 5, 6, 7, 8]}]}, + {written: [ + ...uint16To8Array(768), 13, + ...uint16To8Array(1), ...uint16To8Array(2), ...uint16To8Array(3), + 4, 5, 6, + ...uint16To8Array(7), ...uint16To8Array(8), + ]}, + ], + ])('Writes & Reads Extension Field Sets for data type %s', (_name, payload, expected) => { + const buffer = Buffer.alloc(expected.written.length);// XXX: can't be arbitrary atm, see impl for identified issue + const buffalo = new BuffaloZcl(buffer); + buffalo.write(Zcl.BuffaloZclDataType.EXTENSION_FIELD_SETS, payload.value, {}); + expect(buffalo.getPosition()).toStrictEqual(expected.written.length); + expect(buffalo.getWritten()).toStrictEqual(Buffer.from(expected.written)); + + // @ts-expect-error private + buffalo.position = 0; + + expect(buffalo.read(Zcl.BuffaloZclDataType.EXTENSION_FIELD_SETS, {})).toStrictEqual(payload.value); + expect(buffalo.getPosition()).toStrictEqual(expected.written.length); + }); + + it.each([ + [ + 'single all options', + {value: [{transitionTime: 3, heatSetpoint: 20, coolSetpoint: 15}], readOptions: {payload: {mode: 0b11, numoftrans: 1}}}, + {written: [...uint16To8Array(3), ...uint16To8Array(20), ...uint16To8Array(15)]}, + ], + [ + 'single heat-only', + {value: [{transitionTime: 60, heatSetpoint: 25}], readOptions: {payload: {mode: 0b01, numoftrans: 1}}}, + {written: [...uint16To8Array(60), ...uint16To8Array(25)]}, + ], + [ + 'single cool-only', + {value: [{transitionTime: 256, coolSetpoint: 15}], readOptions: {payload: {mode: 0b10, numoftrans: 1}}}, + {written: [...uint16To8Array(256), ...uint16To8Array(15)]}, + ], + [ + 'multiple all options', + {value: [ + {transitionTime: 3, heatSetpoint: 20, coolSetpoint: 15}, + {transitionTime: 7, heatSetpoint: 8, coolSetpoint: 3}, + {transitionTime: 257, heatSetpoint: 256, coolSetpoint: 0}, + ], readOptions: {payload: {mode: 0b11, numoftrans: 3}}}, + {written: [ + ...uint16To8Array(3), ...uint16To8Array(20), ...uint16To8Array(15), + ...uint16To8Array(7), ...uint16To8Array(8), ...uint16To8Array(3), + ...uint16To8Array(257), ...uint16To8Array(256), ...uint16To8Array(0), + ]}, + ], + [ + 'multiple heat-only', + {value: [ + {transitionTime: 3, heatSetpoint: 20}, + {transitionTime: 70, heatSetpoint: 8}, + ], readOptions: {payload: {mode: 0b01, numoftrans: 2}}}, + {written: [ + ...uint16To8Array(3), ...uint16To8Array(20), + ...uint16To8Array(70), ...uint16To8Array(8), + ]}, + ], + [ + 'multiple cool-only', + {value: [ + {transitionTime: 3, coolSetpoint: 15}, + {transitionTime: 65000, coolSetpoint: 3}, + ], readOptions: {payload: {mode: 0b10, numoftrans: 2}}}, + {written: [ + ...uint16To8Array(3), ...uint16To8Array(15), + ...uint16To8Array(65000), ...uint16To8Array(3), + ]}, + ], + ])('Writes & Reads Thermo Transitions List', (_name, payload, expected) => { + const buffer = Buffer.alloc(50); + const buffalo = new BuffaloZcl(buffer); + buffalo.write(Zcl.BuffaloZclDataType.LIST_THERMO_TRANSITIONS, payload.value, {}); + expect(buffalo.getPosition()).toStrictEqual(expected.written.length); + expect(buffalo.getWritten()).toStrictEqual(Buffer.from(expected.written)); + + // @ts-expect-error private + buffalo.position = 0; + + expect(buffalo.read(Zcl.BuffaloZclDataType.LIST_THERMO_TRANSITIONS, payload.readOptions)).toStrictEqual(payload.value); + expect(buffalo.getPosition()).toStrictEqual(expected.written.length); + }); + + it('Throws when read Thermo Transitions List is missing required payload options', () => { + expect(() => { + const buffalo = new BuffaloZcl(Buffer.alloc(1)); + buffalo.read(Zcl.BuffaloZclDataType.LIST_THERMO_TRANSITIONS, {}); + }).toThrow(); + expect(() => { + const buffalo = new BuffaloZcl(Buffer.alloc(1)); + buffalo.read(Zcl.BuffaloZclDataType.LIST_THERMO_TRANSITIONS, {payload: {}}); + }).toThrow(); + expect(() => { + const buffalo = new BuffaloZcl(Buffer.alloc(1)); + buffalo.read(Zcl.BuffaloZclDataType.LIST_THERMO_TRANSITIONS, {payload: {mode: 1}}); + }).toThrow(); + expect(() => { + const buffalo = new BuffaloZcl(Buffer.alloc(1)); + buffalo.read(Zcl.BuffaloZclDataType.LIST_THERMO_TRANSITIONS, {payload: {numoftrans: 1}}); + }).toThrow(); + }); + + describe('GPD Frame', () => { + it('Reads unhandled command as object[raw] if buffer still has bytes to read', () => { + const value = [0xFF, 0x00]; + const buffalo = new BuffaloZcl(Buffer.from(value)); + + expect(buffalo.read(Zcl.BuffaloZclDataType.GDP_FRAME, {payload: {commandID: 0x1FF}})).toStrictEqual({raw: Buffer.from(value)}); + }); + + it('Reads unhandled command as empty object if buffer finished reading', () => { + const value = [0xFF, 0x00]; + const buffalo = new BuffaloZcl(Buffer.from(value), value.length/* pos at end*/); + + expect(buffalo.read(Zcl.BuffaloZclDataType.GDP_FRAME, {payload: {commandID: 0x1FF}})).toStrictEqual({}); + }); + + it('Writes commissioning', () => { + const expected = [1/*length*/, 0/*options*/]; + const buffalo = new BuffaloZcl(Buffer.alloc(2)); + + buffalo.write(Zcl.BuffaloZclDataType.GDP_FRAME, { + commandID: 0xF0, + options: 0, + panID: 0, + securityKey: Buffer.alloc(16), + keyMic: 0, + frameCounter: 0, + }, {}); + + expect(buffalo.getWritten()).toStrictEqual(Buffer.from(expected)); + }); + + it('Reads commissioning', () => { + const value = [0xFF/*device*/, 0x00/*options*/]; + const buffalo = new BuffaloZcl(Buffer.from(value)); + + expect(buffalo.read(Zcl.BuffaloZclDataType.GDP_FRAME, {payload: {commandID: 0xE0}})).toStrictEqual({ + deviceID: 0xFF, + options: 0x00, + extendedOptions: 0x00, + securityKey: Buffer.alloc(16), + keyMic: 0, + outgoingCounter: 0, + manufacturerID: 0, + modelID: 0, + numGdpCommands: 0, + gpdCommandIdList: Buffer.alloc(0), + numServerClusters: 0, + numClientClusters: 0, + gpdServerClusters: Buffer.alloc(0), + gpdClientClusters: Buffer.alloc(0), + applicationInfo: 0x00, + }); + }); + + it('Writes commissioning all options', () => { + const expected = [ + 27,// length + 0b11111,// options + 0xFF, 0xFF,// PAN ID + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,// security key + 0, 0, 0, 0,// key mic + 0, 0, 0, 0,// frame counter + ]; + const buffalo = new BuffaloZcl(Buffer.alloc(28)); + + buffalo.write(Zcl.BuffaloZclDataType.GDP_FRAME, { + commandID: 0xF0, + options: 0b11111, + panID: 0xFFFF, + securityKey: Buffer.alloc(16), + keyMic: 0, + frameCounter: 0, + }, {}); + + expect(buffalo.getWritten()).toStrictEqual(Buffer.from(expected)); + }); + + it('Reads commissioning all options', () => { + const value = [ + 0xFF,// device + 0x80 | 0x04,// options + 0x20 | 0x40 | 0x80,// extended options + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,// security key + 0, 0, 0, 0,// key mic + 0, 0, 0, 0,// outgoing counter + 0x01 | 0x02 | 0x04 | 0x08,// application info + 0, 0,// manufacturer ID + 0, 0,// model ID + 0,// num GDP commands + commands + 0,// clusters + ]; + const buffalo = new BuffaloZcl(Buffer.from(value)); + + expect(buffalo.read(Zcl.BuffaloZclDataType.GDP_FRAME, {payload: {commandID: 0xE0}})).toStrictEqual({ + deviceID: 0xFF, + options: 0x80 | 0x04, + extendedOptions: 0x20 | 0x40 | 0x80, + securityKey: Buffer.alloc(16), + keyMic: 0, + outgoingCounter: 0, + manufacturerID: 0, + modelID: 0, + numGdpCommands: 0, + gpdCommandIdList: Buffer.alloc(0), + numServerClusters: 0, + numClientClusters: 0, + gpdServerClusters: Buffer.alloc(0), + gpdClientClusters: Buffer.alloc(0), + applicationInfo: 0x01 | 0x02 | 0x04 | 0x08, + }); + }); + + it('Writes channel configuration', () => { + const expected = [1/*length*/, 0xF/*Channel 26*/]; + const buffalo = new BuffaloZcl(Buffer.alloc(2)); + buffalo.write(Zcl.BuffaloZclDataType.GDP_FRAME, { + commandID: 0xF3, + operationalChannel: 0xF, + basic: false, + }, {}); + + expect(buffalo.getWritten()).toStrictEqual(Buffer.from(expected)) + }); + + it('Writes channel configuration basic', () => { + const expected = [1/*length*/, 0x1F/*Channel 26 + Basic*/]; + const buffalo = new BuffaloZcl(Buffer.alloc(2)); + buffalo.write(Zcl.BuffaloZclDataType.GDP_FRAME, { + commandID: 0xF3, + operationalChannel: 0xF, + basic: true, + }, {}); + + expect(buffalo.getWritten()).toStrictEqual(Buffer.from(expected)) + }); + + it('Reads channel request', () => { + const value = [0xFA]; + const buffalo = new BuffaloZcl(Buffer.from(value)); + + expect(buffalo.read(Zcl.BuffaloZclDataType.GDP_FRAME, {payload: {commandID: 0xE3}})).toStrictEqual({ + nextChannel: 0xA, + nextNextChannel: 0xF, + }); + }); + + it('Reads attribute report', () => { + const value = [ + 0x12, 0x34,// Manufacturer ID + 0xFF, 0xFF,// Cluster ID + 0x00, 0x00,// Attribute ID + Zcl.DataType.UINT32,// Attribute Type + 0x00, 0x01, 0x02, 0x03, + 0x01, 0x00, + Zcl.DataType.CHAR_STR, + 0x06, 0x5a, 0x49, 0x47, 0x42, 0x45, 0x45, + 0x02, 0x00, + Zcl.DataType.BOOLEAN, + 0x01, + ]; + const buffalo = new BuffaloZcl(Buffer.from(value)); + + expect(buffalo.read(Zcl.BuffaloZclDataType.GDP_FRAME, {payload: {commandID: 0xA1, payloadSize: value.length}})).toStrictEqual({ + manufacturerCode: 13330, + clusterID: 65535, + attributes: {'0': 50462976, '1': 'ZIGBEE', '2': 1}, + }); + }); + + it('Writes custom reply', () => { + const expected = [ + 6,// length + 90, 73, 71, 66, 69, 69,// ZIGBEE + ]; + + const buffalo = new BuffaloZcl(Buffer.alloc(7)); + buffalo.write(Zcl.BuffaloZclDataType.GDP_FRAME, {commandID: 0xF4, buffer: Buffer.from("ZIGBEE")}, {}); + + expect(buffalo.getWritten()).toStrictEqual(Buffer.from(expected)) + }); + + it('Writes nothing for unhandled command', () => { + const buffalo = new BuffaloZcl(Buffer.alloc(7)); + buffalo.write(Zcl.BuffaloZclDataType.GDP_FRAME, {commandID: 0x1FF}, {}); + + expect(buffalo.getWritten()).toStrictEqual(Buffer.alloc(0)) + }); + + it('Throws when read is missing payload.payloadSize option when payload.commandID is 0xA1', () => { + expect(() => { + const buffalo = new BuffaloZcl(Buffer.alloc(1)); + buffalo.read(Zcl.BuffaloZclDataType.GDP_FRAME, {payload: {commandID: 0xA1}}); + }).toThrow(`Cannot read GDP_FRAME with commandID=0xA1 without payloadSize options specified`); + }); + }); + + it.each([ + [ + 'whole', + {value: {indicatorType: Zcl.StructuredIndicatorType.Whole}}, + {written: [Zcl.StructuredIndicatorType.Whole]}, + ], + [ + 'indexes only', + {value: {indexes: [3, 4, 5, 256]}}, + {written: [4, ...uint16To8Array(3), ...uint16To8Array(4), ...uint16To8Array(5), ...uint16To8Array(256)]}, + ], + ])('Writes & Reads Structured Selector for %s', (_name, payload, expected) => { + const buffer = Buffer.alloc(50); + const buffalo = new BuffaloZcl(buffer); + buffalo.write(Zcl.BuffaloZclDataType.STRUCTURED_SELECTOR, payload.value, {}); + expect(buffalo.getPosition()).toStrictEqual(expected.written.length); + expect(buffalo.getWritten()).toStrictEqual(Buffer.from(expected.written)); + + // @ts-expect-error private + buffalo.position = 0; + + expect(buffalo.read(Zcl.BuffaloZclDataType.STRUCTURED_SELECTOR, {})).toStrictEqual(payload.value); + expect(buffalo.getPosition()).toStrictEqual(expected.written.length); + }); + + it.each([ + [ + 'Add', + {value: {indicatorType: Zcl.StructuredIndicatorType.WriteAdd}}, + {written: [Zcl.StructuredIndicatorType.WriteAdd]}, + ], + [ + 'Remove', + {value: {indicatorType: Zcl.StructuredIndicatorType.WriteRemove}}, + {written: [Zcl.StructuredIndicatorType.WriteRemove]}, + ], + ])('Writes Structured Selector for %s', (_name, payload, expected) => { + const buffer = Buffer.alloc(50); + const buffalo = new BuffaloZcl(buffer); + buffalo.write(Zcl.BuffaloZclDataType.STRUCTURED_SELECTOR, payload.value, {}); + expect(buffalo.getPosition()).toStrictEqual(expected.written.length); + expect(buffalo.getWritten()).toStrictEqual(Buffer.from(expected.written)); + }); + + it('Throws when read Strctured Select indicator is outside range', () => { + expect(() => { + const buffer = Buffer.from([17]); + const buffalo = new BuffaloZcl(buffer); + buffalo.read(Zcl.BuffaloZclDataType.STRUCTURED_SELECTOR, {}); + }).toThrow(); + expect(() => { + const buffer = Buffer.from([17, 1, 2, 3]); + const buffalo = new BuffaloZcl(buffer); + buffalo.read(Zcl.BuffaloZclDataType.STRUCTURED_SELECTOR, {}); + }).toThrow(); + }); + + it.each([ + [ + 'single', + {value: [{dp: 254, datatype: 125, data: Buffer.from([1, 3, 5])}]}, + {written: [254, 125, ...uint16To8Array(3).reverse()/*BE*/, 1, 3, 5]}, + ], + [ + 'multiple', + {value: [ + {dp: 254, datatype: 125, data: Buffer.from([1, 3, 5])}, + {dp: 125, datatype: 254, data: Buffer.from([5, 0, 1, 5])}, + ]}, + {written: [ + 254, 125, ...uint16To8Array(3).reverse()/*BE*/, 1, 3, 5, + 125, 254, ...uint16To8Array(4).reverse()/*BE*/, 5, 0, 1, 5, + ]}, + ], + ])('Writes & Reads Tuya Data Point Values List %s', (_name, payload, expected) => { + const buffer = Buffer.alloc(expected.written.length);// XXX: can't be arbitrary atm, see impl for identified issue + const buffalo = new BuffaloZcl(buffer); + buffalo.write(Zcl.BuffaloZclDataType.LIST_TUYA_DATAPOINT_VALUES, payload.value, {}); + expect(buffalo.getPosition()).toStrictEqual(expected.written.length); + expect(buffalo.getWritten()).toStrictEqual(Buffer.from(expected.written)); + + // @ts-expect-error private + buffalo.position = 0; + + expect(buffalo.read(Zcl.BuffaloZclDataType.LIST_TUYA_DATAPOINT_VALUES, {})).toStrictEqual(payload.value); + expect(buffalo.getPosition()).toStrictEqual(expected.written.length); + }); + + it('Reads invalid Tuya Data Point Values List as empty array', () => { + // incomplete + const buffer = Buffer.from([254, 125]); + const buffalo = new BuffaloZcl(buffer); + expect(buffalo.read(Zcl.BuffaloZclDataType.LIST_TUYA_DATAPOINT_VALUES, {})).toStrictEqual([]); + }); + + it('Writes & Reads Mi Boxer Zones List', () => { + const value = [ + {zoneNum: 1, groupId: 0x2b84}, + {zoneNum: 2, groupId: 0x2b98}, + {zoneNum: 3, groupId: 0x2bac}, + {zoneNum: 4, groupId: 0x2bc0}, + {zoneNum: 5, groupId: 0x2bd4}, + {zoneNum: 6, groupId: 0x2be8}, + {zoneNum: 7, groupId: 0x2bfc}, + {zoneNum: 8, groupId: 0x2c10}, + ]; + const expectedWritten = [ + value.length, + ...uint16To8Array(value[0].groupId), value[0].zoneNum, + ...uint16To8Array(value[1].groupId), value[1].zoneNum, + ...uint16To8Array(value[2].groupId), value[2].zoneNum, + ...uint16To8Array(value[3].groupId), value[3].zoneNum, + ...uint16To8Array(value[4].groupId), value[4].zoneNum, + ...uint16To8Array(value[5].groupId), value[5].zoneNum, + ...uint16To8Array(value[6].groupId), value[6].zoneNum, + ...uint16To8Array(value[7].groupId), value[7].zoneNum, + ]; + const buffer = Buffer.alloc(50); + const buffalo = new BuffaloZcl(buffer); + buffalo.write(Zcl.BuffaloZclDataType.LIST_MIBOXER_ZONES, value, {}); + expect(buffalo.getPosition()).toStrictEqual(expectedWritten.length); + expect(buffalo.getWritten()).toStrictEqual(Buffer.from(expectedWritten)); + + // @ts-expect-error private + buffalo.position = 0; + + expect(buffalo.read(Zcl.BuffaloZclDataType.LIST_MIBOXER_ZONES, {length: value.length})).toStrictEqual(value); + expect(buffalo.getPosition()).toStrictEqual(expectedWritten.length); + }); + + it('Writes & Reads big endian uint24', () => { + const value = 16777200; + const expectedWritten = [0xFF, 0xFF, 0xF0]; + const expectedPosition = 3; + const buffer = Buffer.alloc(10); + const buffalo = new BuffaloZcl(buffer); + buffalo.write(Zcl.BuffaloZclDataType.BIG_ENDIAN_UINT24, value, {}); + expect(buffalo.getPosition()).toStrictEqual(expectedPosition); + expect(buffalo.getWritten()).toStrictEqual(Buffer.from(expectedWritten)); + + // @ts-expect-error private + buffalo.position = 0; + + expect(buffalo.read(Zcl.BuffaloZclDataType.BIG_ENDIAN_UINT24, {})).toStrictEqual(value); + expect(buffalo.getPosition()).toStrictEqual(expectedPosition); + }); +}); diff --git a/test/zspec/zcl/frame.test.ts b/test/zspec/zcl/frame.test.ts new file mode 100644 index 0000000000..f1248c1089 --- /dev/null +++ b/test/zspec/zcl/frame.test.ts @@ -0,0 +1,561 @@ +import * as Zcl from '../../../src/zspec/zcl'; +import {BuffaloZcl} from '../../../src/zspec/zcl/buffaloZcl'; +import {uint16To8Array, uint32To8Array} from '../../utils/math'; + +/** Header with Global frame type */ +const GLOBAL_HEADER = new Zcl.Header( + {frameType: Zcl.FrameType.GLOBAL, manufacturerSpecific: false, direction: Zcl.Direction.CLIENT_TO_SERVER, disableDefaultResponse: false, reservedBits: 0}, + null, 123, Zcl.Foundation.read.ID, +); +const GLOBAL_HEADER_BUFFER = Buffer.from([0, 123, Zcl.Foundation.read.ID]); + +/** Header with Global frame type and server to client direction */ +const GLOBAL_RSP_HEADER = new Zcl.Header( + {frameType: Zcl.FrameType.GLOBAL, manufacturerSpecific: false, direction: Zcl.Direction.SERVER_TO_CLIENT, disableDefaultResponse: false, reservedBits: 0}, + null, 78, Zcl.Foundation.readRsp.ID, +); +const GLOBAL_RSP_HEADER_BUFFER = Buffer.from([8, 78, Zcl.Foundation.readRsp.ID]); + +/** Header with Global frame type and server to client direction including condition-based parameters */ +const GLOBAL_CONDITION_HEADER = new Zcl.Header( + {frameType: Zcl.FrameType.GLOBAL, manufacturerSpecific: false, direction: Zcl.Direction.SERVER_TO_CLIENT, disableDefaultResponse: false, reservedBits: 0}, + null, 78, Zcl.Foundation.configReport.ID, +); +const GLOBAL_CONDITION_HEADER_BUFFER = Buffer.from([8, 78, Zcl.Foundation.configReport.ID]); + +/** Header with Specific frame type */ +const SPECIFIC_HEADER = new Zcl.Header( + {frameType: Zcl.FrameType.SPECIFIC, manufacturerSpecific: false, direction: Zcl.Direction.CLIENT_TO_SERVER, disableDefaultResponse: false, reservedBits: 0}, + null, 44, Zcl.Clusters.genAlarms.commands.getAlarm.ID, +); +const SPECIFIC_HEADER_BUFFER = Buffer.from([1, 44, Zcl.Clusters.genAlarms.commands.getAlarm.ID]); + +/** Header with Specific frame type including condition-based parameters */ +const SPECIFIC_CONDITION_HEADER = new Zcl.Header( + {frameType: Zcl.FrameType.SPECIFIC, manufacturerSpecific: false, direction: Zcl.Direction.SERVER_TO_CLIENT, disableDefaultResponse: false, reservedBits: 0}, + null, 45, Zcl.Clusters.genOta.commandsResponse.queryNextImageResponse.ID, +); +const SPECIFIC_CONDITION_HEADER_BUFFER = Buffer.from([9, 45, Zcl.Clusters.genOta.commandsResponse.queryNextImageResponse.ID]); + +/** Header with Specific frame type and server to client direction */ +const SPECIFIC_RSP_HEADER = new Zcl.Header( + {frameType: Zcl.FrameType.SPECIFIC, manufacturerSpecific: false, direction: Zcl.Direction.SERVER_TO_CLIENT, disableDefaultResponse: false, reservedBits: 0}, + null, 53, Zcl.Clusters.genAlarms.commandsResponse.alarm.ID, +); +const SPECIFIC_RSP_HEADER_BUFFER = Buffer.from([9, 53, Zcl.Clusters.genAlarms.commandsResponse.alarm.ID]); + +/** Header with manufacturer-specific */ +const MANUF_SPE_HEADER = new Zcl.Header( + {frameType: Zcl.FrameType.GLOBAL, manufacturerSpecific: true, direction: Zcl.Direction.CLIENT_TO_SERVER, disableDefaultResponse: false, reservedBits: 0}, + Zcl.ManufacturerCode.AAC_TECHNOLOGIES_HOLDING, 234, Zcl.Foundation.read.ID, +); +const MANUF_SPE_HEADER_BUFFER = Buffer.from([4, ...uint16To8Array(Zcl.ManufacturerCode.AAC_TECHNOLOGIES_HOLDING), 234, Zcl.Foundation.read.ID]); + +/** Frame of Global type */ +const GLOBAL_FRAME = Zcl.Frame.create( + GLOBAL_HEADER.frameControl.frameType, GLOBAL_HEADER.frameControl.direction, GLOBAL_HEADER.frameControl.disableDefaultResponse, + GLOBAL_HEADER.manufacturerCode, GLOBAL_HEADER.transactionSequenceNumber, GLOBAL_HEADER.commandIdentifier, + Zcl.Foundation.read.ID, [{attrId: 256}]/*payload*/, {}/*custom clusters*/, GLOBAL_HEADER.frameControl.reservedBits, +); +const GLOBAL_FRAME_BUFFER = Buffer.concat([GLOBAL_HEADER_BUFFER, Buffer.from(uint16To8Array(256))]); +const GLOBAL_FRAME_STRING = `{"header":{"frameControl":{"reservedBits":0,"frameType":0,"direction":0,"disableDefaultResponse":false,"manufacturerSpecific":false},"manufacturerCode":null,"transactionSequenceNumber":123,"commandIdentifier":0},"payload":[{"attrId":256}],"command":{"ID":0,"name":"read","parameters":[{"name":"attrId","type":33}],"response":1}}`; + +/** Frame of Global type and response command */ +const GLOBAL_RSP_FRAME = Zcl.Frame.create( + GLOBAL_RSP_HEADER.frameControl.frameType, GLOBAL_RSP_HEADER.frameControl.direction, GLOBAL_RSP_HEADER.frameControl.disableDefaultResponse, + GLOBAL_RSP_HEADER.manufacturerCode, GLOBAL_RSP_HEADER.transactionSequenceNumber, GLOBAL_RSP_HEADER.commandIdentifier, + Zcl.Foundation.readRsp.ID, [{attrId: 256, status: Zcl.Status.SUCCESS, dataType: Zcl.DataType.ENUM8, attrData: 127}]/*payload*/, + {}/*custom clusters*/, GLOBAL_RSP_HEADER.frameControl.reservedBits, +); +const GLOBAL_RSP_FRAME_BUFFER = Buffer.concat([GLOBAL_RSP_HEADER_BUFFER, Buffer.from([...uint16To8Array(256), Zcl.Status.SUCCESS, Zcl.DataType.ENUM8, 127])]); +const GLOBAL_RSP_FRAME_STRING = `{"header":{"frameControl":{"reservedBits":0,"frameType":0,"direction":1,"disableDefaultResponse":false,"manufacturerSpecific":false},"manufacturerCode":null,"transactionSequenceNumber":78,"commandIdentifier":1},"payload":[{"attrId":256,"status":0,"dataType":48,"attrData":127}],"command":{"ID":1,"name":"readRsp","parameters":[{"name":"attrId","type":33},{"name":"status","type":32},{"name":"dataType","type":32,"conditions":[{"type":"statusEquals","value":0}]},{"name":"attrData","type":1000,"conditions":[{"type":"statusEquals","value":0}]}]}}`; + +/** Frame of Global type with no payload */ +const GLOBAL_FRAME_NO_PAYLOAD = Zcl.Frame.create( + GLOBAL_HEADER.frameControl.frameType, GLOBAL_HEADER.frameControl.direction, GLOBAL_HEADER.frameControl.disableDefaultResponse, + GLOBAL_HEADER.manufacturerCode, GLOBAL_HEADER.transactionSequenceNumber, GLOBAL_HEADER.commandIdentifier, + Zcl.Foundation.read.ID, []/*payload*/, {}/*custom clusters*/, GLOBAL_HEADER.frameControl.reservedBits, +); +const GLOBAL_FRAME_NO_PAYLOAD_BUFFER = Buffer.concat([GLOBAL_HEADER_BUFFER]); +const GLOBAL_FRAME_NO_PAYLOAD_STRING = `{"header":{"frameControl":{"reservedBits":0,"frameType":0,"direction":0,"disableDefaultResponse":false,"manufacturerSpecific":false},"manufacturerCode":null,"transactionSequenceNumber":123,"commandIdentifier":0},"payload":[],"command":{"ID":0,"name":"read","parameters":[{"name":"attrId","type":33}],"response":1}}`; + +/** Frame of Global type with condition-based parameters */ +const GLOBAL_CONDITION_FRAME = Zcl.Frame.create( + GLOBAL_CONDITION_HEADER.frameControl.frameType, GLOBAL_CONDITION_HEADER.frameControl.direction, GLOBAL_CONDITION_HEADER.frameControl.disableDefaultResponse, + GLOBAL_CONDITION_HEADER.manufacturerCode, GLOBAL_CONDITION_HEADER.transactionSequenceNumber, GLOBAL_CONDITION_HEADER.commandIdentifier, + Zcl.Foundation.configReport.ID, [{direction: Zcl.Direction.SERVER_TO_CLIENT, attrId: 256, timeout: 10000}]/*payload*/, {}/*custom clusters*/, GLOBAL_CONDITION_HEADER.frameControl.reservedBits, +); +const GLOBAL_CONDITION_FRAME_BUFFER = Buffer.concat([GLOBAL_CONDITION_HEADER_BUFFER, Buffer.from([Zcl.Direction.SERVER_TO_CLIENT, ...uint16To8Array(256), ...uint16To8Array(10000)])]); +const GLOBAL_CONDITION_FRAME_STRING = `{"header":{"frameControl":{"reservedBits":0,"frameType":0,"direction":1,"disableDefaultResponse":false,"manufacturerSpecific":false},"manufacturerCode":null,"transactionSequenceNumber":78,"commandIdentifier":6},"payload":[{"direction":1,"attrId":256,"timeout":10000}],"command":{"ID":6,"name":"configReport","parameters":[{"name":"direction","type":32},{"name":"attrId","type":33},{"name":"dataType","type":32,"conditions":[{"type":"directionEquals","value":0}]},{"name":"minRepIntval","type":33,"conditions":[{"type":"directionEquals","value":0}]},{"name":"maxRepIntval","type":33,"conditions":[{"type":"directionEquals","value":0}]},{"name":"repChange","type":1000,"conditions":[{"type":"directionEquals","value":0},{"type":"dataTypeValueTypeEquals","value":"ANALOG"}]},{"name":"timeout","type":33,"conditions":[{"type":"directionEquals","value":1}]}],"response":7}}`; + +/** Frame of Specific type */ +const SPECIFIC_FRAME = Zcl.Frame.create( + SPECIFIC_HEADER.frameControl.frameType, SPECIFIC_HEADER.frameControl.direction, SPECIFIC_HEADER.frameControl.disableDefaultResponse, + SPECIFIC_HEADER.manufacturerCode, SPECIFIC_HEADER.transactionSequenceNumber, SPECIFIC_HEADER.commandIdentifier, + Zcl.Clusters.genAlarms.ID, {}/*payload*/, {}/*custom clusters*/, SPECIFIC_HEADER.frameControl.reservedBits, +); +const SPECIFIC_FRAME_BUFFER = Buffer.concat([SPECIFIC_HEADER_BUFFER]); +const SPECIFIC_FRAME_STRING = `{"header":{"frameControl":{"reservedBits":0,"frameType":1,"direction":0,"disableDefaultResponse":false,"manufacturerSpecific":false},"manufacturerCode":null,"transactionSequenceNumber":44,"commandIdentifier":2},"payload":{},"command":{"ID":2,"parameters":[],"name":"getAlarm"}}`; + +/** Frame of Specific type and response command */ +const SPECIFIC_RSP_FRAME = Zcl.Frame.create( + SPECIFIC_RSP_HEADER.frameControl.frameType, SPECIFIC_RSP_HEADER.frameControl.direction, SPECIFIC_RSP_HEADER.frameControl.disableDefaultResponse, + SPECIFIC_RSP_HEADER.manufacturerCode, SPECIFIC_RSP_HEADER.transactionSequenceNumber, SPECIFIC_RSP_HEADER.commandIdentifier, + Zcl.Clusters.genAlarms.ID, {alarmcode: 246, clusterid: 3456}/*payload*/, {}/*custom clusters*/, SPECIFIC_RSP_HEADER.frameControl.reservedBits, +); +const SPECIFIC_RSP_FRAME_BUFFER = Buffer.concat([SPECIFIC_RSP_HEADER_BUFFER, Buffer.from([246, ...uint16To8Array(3456)])]); +const SPECIFIC_RSP_FRAME_STRING = `{"header":{"frameControl":{"reservedBits":0,"frameType":1,"direction":1,"disableDefaultResponse":false,"manufacturerSpecific":false},"manufacturerCode":null,"transactionSequenceNumber":53,"commandIdentifier":0},"payload":{"alarmcode":246,"clusterid":3456},"command":{"ID":0,"parameters":[{"name":"alarmcode","type":32},{"name":"clusterid","type":33}],"name":"alarm"}}`; + +/** Frame of Specific type with condition-based parameters */ +const SPECIFIC_CONDITION_FRAME = Zcl.Frame.create( + SPECIFIC_CONDITION_HEADER.frameControl.frameType, SPECIFIC_CONDITION_HEADER.frameControl.direction, SPECIFIC_CONDITION_HEADER.frameControl.disableDefaultResponse, + SPECIFIC_CONDITION_HEADER.manufacturerCode, SPECIFIC_CONDITION_HEADER.transactionSequenceNumber, SPECIFIC_CONDITION_HEADER.commandIdentifier, + Zcl.Clusters.genOta.ID, {status: Zcl.Status.ABORT}/*payload*/, {}/*custom clusters*/, SPECIFIC_CONDITION_HEADER.frameControl.reservedBits, +); +const SPECIFIC_CONDITION_FRAME_BUFFER = Buffer.concat([SPECIFIC_CONDITION_HEADER_BUFFER, Buffer.from([149])]); +const SPECIFIC_CONDITION_FRAME_STRING = `{"header":{"frameControl":{"reservedBits":0,"frameType":1,"direction":1,"disableDefaultResponse":false,"manufacturerSpecific":false},"manufacturerCode":null,"transactionSequenceNumber":45,"commandIdentifier":2},"payload":{"status":149},"command":{"ID":2,"parameters":[{"name":"status","type":32},{"name":"manufacturerCode","type":33,"conditions":[{"type":"statusEquals","value":0}]},{"name":"imageType","type":33,"conditions":[{"type":"statusEquals","value":0}]},{"name":"fileVersion","type":35,"conditions":[{"type":"statusEquals","value":0}]},{"name":"imageSize","type":35,"conditions":[{"type":"statusEquals","value":0}]}],"name":"queryNextImageResponse"}}`; + +/** Frame manufacturer-specific */ +const MANUF_SPE_FRAME = Zcl.Frame.create( + MANUF_SPE_HEADER.frameControl.frameType, MANUF_SPE_HEADER.frameControl.direction, MANUF_SPE_HEADER.frameControl.disableDefaultResponse, + MANUF_SPE_HEADER.manufacturerCode, MANUF_SPE_HEADER.transactionSequenceNumber, MANUF_SPE_HEADER.commandIdentifier, + Zcl.Foundation.read.ID, [{attrId: 256}]/*payload*/, {}/*custom clusters*/, MANUF_SPE_HEADER.frameControl.reservedBits, +); +const MANUF_SPE_FRAME_BUFFER = Buffer.concat([MANUF_SPE_HEADER_BUFFER, Buffer.from(uint16To8Array(256))]); +const MANUF_SPE_FRAME_STRING = `{"header":{"frameControl":{"reservedBits":0,"frameType":0,"direction":0,"disableDefaultResponse":false,"manufacturerSpecific":true},"manufacturerCode":4344,"transactionSequenceNumber":234,"commandIdentifier":0},"payload":[{"attrId":256}],"command":{"ID":0,"name":"read","parameters":[{"name":"attrId","type":33}],"response":1}}`; + + +describe('ZCL Frame', () => { + describe('Validates Parameter Condition', () => { + it('STATUS_EQUAL', () => { + expect(Zcl.Frame.conditionsValid(Zcl.Foundation.readRsp.parameters[2], {status: 0}, null)).toBeTruthy(); + expect(Zcl.Frame.conditionsValid(Zcl.Foundation.readRsp.parameters[2], {status: 1}, null)).toBeFalsy(); + }); + + it('STATUS_NOT_EQUAL', () => { + expect(Zcl.Frame.conditionsValid(Zcl.Foundation.writeRsp.parameters[1], {status: 1}, null)).toBeTruthy(); + expect(Zcl.Frame.conditionsValid(Zcl.Foundation.writeRsp.parameters[1], {status: 0}, null)).toBeFalsy(); + }); + + it('MINIMUM_REMAINING_BUFFER_BYTES', () => { + expect(Zcl.Frame.conditionsValid(Zcl.Foundation.configReportRsp.parameters[1], {status: 1}, 3)).toBeTruthy(); + expect(Zcl.Frame.conditionsValid(Zcl.Foundation.configReportRsp.parameters[1], {status: 1}, 2)).toBeFalsy(); + }); + + it('DIRECTION_EQUAL', () => { + expect(Zcl.Frame.conditionsValid(Zcl.Foundation.configReport.parameters[2], {direction: Zcl.Direction.CLIENT_TO_SERVER}, null)).toBeTruthy(); + expect(Zcl.Frame.conditionsValid(Zcl.Foundation.configReport.parameters[2], {direction: Zcl.Direction.SERVER_TO_CLIENT}, null)).toBeFalsy(); + expect(Zcl.Frame.conditionsValid(Zcl.Foundation.configReport.parameters[6], {direction: Zcl.Direction.SERVER_TO_CLIENT}, null)).toBeTruthy(); + expect(Zcl.Frame.conditionsValid(Zcl.Foundation.configReport.parameters[6], {direction: Zcl.Direction.CLIENT_TO_SERVER}, null)).toBeFalsy(); + }); + + it('BITMASK_SET', () => { + expect(Zcl.Frame.conditionsValid(Zcl.Clusters.greenPower.commands.notification.parameters[8], {options: 0x4000}, null)).toBeTruthy(); + expect(Zcl.Frame.conditionsValid(Zcl.Clusters.greenPower.commands.notification.parameters[8], {options: 0x4150}, null)).toBeTruthy(); + expect(Zcl.Frame.conditionsValid(Zcl.Clusters.greenPower.commands.notification.parameters[8], {options: 0x0400}, null)).toBeFalsy(); + expect(Zcl.Frame.conditionsValid(Zcl.Clusters.greenPower.commands.notification.parameters[8], {options: 0x1400}, null)).toBeFalsy(); + }); + + it('BITFIELD_ENUM', () => { + // {param:'options', offset: 0, size: 3, value: 0b000} + expect(Zcl.Frame.conditionsValid(Zcl.Clusters.greenPower.commands.notification.parameters[1], {options: 0b000}, null)).toBeTruthy(); + expect(Zcl.Frame.conditionsValid(Zcl.Clusters.greenPower.commands.notification.parameters[1], {options: 0b1000}, null)).toBeTruthy(); + expect(Zcl.Frame.conditionsValid(Zcl.Clusters.greenPower.commands.notification.parameters[1], {options: 0b001}, null)).toBeFalsy(); + expect(Zcl.Frame.conditionsValid(Zcl.Clusters.greenPower.commands.notification.parameters[1], {options: 0b011}, null)).toBeFalsy(); + expect(Zcl.Frame.conditionsValid(Zcl.Clusters.greenPower.commands.notification.parameters[1], {options: 0b100}, null)).toBeFalsy(); + expect(Zcl.Frame.conditionsValid(Zcl.Clusters.greenPower.commands.notification.parameters[1], {options: 0b1010}, null)).toBeFalsy(); + }); + + it('multiple including DATA_TYPE_CLASS_EQUAL', () => { + expect(Zcl.Frame.conditionsValid( + Zcl.Foundation.configReport.parameters[5], + {direction: Zcl.Direction.CLIENT_TO_SERVER, dataType: Zcl.DataType.UINT8}, + null + )).toBeTruthy(); + expect(Zcl.Frame.conditionsValid( + Zcl.Foundation.configReport.parameters[5], + {direction: Zcl.Direction.CLIENT_TO_SERVER, dataType: Zcl.DataType.DATA8}, + null + )).toBeFalsy(); + expect(Zcl.Frame.conditionsValid( + Zcl.Foundation.configReport.parameters[5], + {direction: Zcl.Direction.SERVER_TO_CLIENT, dataType: Zcl.DataType.UINT8}, + null + )).toBeFalsy(); + expect(Zcl.Frame.conditionsValid( + Zcl.Foundation.configReport.parameters[5], + {direction: Zcl.Direction.SERVER_TO_CLIENT, dataType: Zcl.DataType.DATA8}, + null + )).toBeFalsy(); + }); + + }); + + describe('Header', () => { + it.each([ + [ + 'global', + { + frameControl: GLOBAL_HEADER.frameControl, manufacturerCode: GLOBAL_HEADER.manufacturerCode, + transactionSequenceNumber: GLOBAL_HEADER.transactionSequenceNumber, commandId: GLOBAL_HEADER.commandIdentifier, + }, + {written: GLOBAL_HEADER_BUFFER}, + ], + [ + 'global response', + { + frameControl: GLOBAL_RSP_HEADER.frameControl, manufacturerCode: GLOBAL_RSP_HEADER.manufacturerCode, + transactionSequenceNumber: GLOBAL_RSP_HEADER.transactionSequenceNumber, commandId: GLOBAL_RSP_HEADER.commandIdentifier, + }, + {written: GLOBAL_RSP_HEADER_BUFFER}, + ], + [ + 'specific', + { + frameControl: SPECIFIC_HEADER.frameControl, manufacturerCode: SPECIFIC_HEADER.manufacturerCode, + transactionSequenceNumber: SPECIFIC_HEADER.transactionSequenceNumber, commandId: SPECIFIC_HEADER.commandIdentifier, + }, + {written: SPECIFIC_HEADER_BUFFER}, + ], + [ + 'manufacturer-specific', + { + frameControl: MANUF_SPE_HEADER.frameControl, manufacturerCode: MANUF_SPE_HEADER.manufacturerCode, + transactionSequenceNumber: MANUF_SPE_HEADER.transactionSequenceNumber, commandId: MANUF_SPE_HEADER.commandIdentifier, + }, + {written: MANUF_SPE_HEADER_BUFFER}, + ], + [ + 'disable default response', + { + frameControl: { + frameType: Zcl.FrameType.GLOBAL, manufacturerSpecific: false, + direction: Zcl.Direction.CLIENT_TO_SERVER, disableDefaultResponse: true, reservedBits: 0, + }, + manufacturerCode: null, transactionSequenceNumber: 234, commandId: 1, + }, + {written: Buffer.from([16, 234, 1])}, + ], + [ + 'reserved bits - non-compliant use', + { + frameControl: { + frameType: Zcl.FrameType.GLOBAL, manufacturerSpecific: false, + direction: Zcl.Direction.CLIENT_TO_SERVER, disableDefaultResponse: false, reservedBits: 7, + }, + manufacturerCode: null, transactionSequenceNumber: 234, commandId: 1, + }, + {written: Buffer.from([224, 234, 1])}, + ], + [ + 'specific manufacturer-specific', + { + frameControl: { + frameType: Zcl.FrameType.SPECIFIC, manufacturerSpecific: true, + direction: Zcl.Direction.CLIENT_TO_SERVER, disableDefaultResponse: false, reservedBits: 0, + }, + manufacturerCode: Zcl.ManufacturerCode.S3C, transactionSequenceNumber: 234, commandId: 1, + }, + {written: Buffer.from([5, ...uint16To8Array(Zcl.ManufacturerCode.S3C), 234, 1])}, + ], + [ + 'all non-zero - non-compliant use of reservedBits', + { + frameControl: { + frameType: Zcl.FrameType.SPECIFIC, manufacturerSpecific: true, + direction: Zcl.Direction.SERVER_TO_CLIENT, disableDefaultResponse: true, reservedBits: 3, + }, + manufacturerCode: Zcl.ManufacturerCode.BOSCH_SECURITY_SYSTEMS_INC, transactionSequenceNumber: 234, commandId: 1, + }, + {written: Buffer.from([125, ...uint16To8Array(Zcl.ManufacturerCode.BOSCH_SECURITY_SYSTEMS_INC), 234, 1])}, + ], + ])('Reads & Writes %s', (_name, payload, expected) => { + // write + { + const header = new Zcl.Header(payload.frameControl, payload.manufacturerCode, payload.transactionSequenceNumber, payload.commandId); + const buffer = Buffer.alloc(10); + const buffalo = new BuffaloZcl(buffer); + header.write(buffalo); + + expect(buffer.subarray(0, header.length)).toStrictEqual(expected.written); + expect(header.length).toStrictEqual(expected.written.length); + expect(header.isGlobal).toStrictEqual(payload.frameControl.frameType === Zcl.FrameType.GLOBAL); + expect(header.isSpecific).toStrictEqual(payload.frameControl.frameType === Zcl.FrameType.SPECIFIC); + } + // read + { + const header = Zcl.Header.fromBuffer(expected.written)!; + + expect(header).toBeInstanceOf(Zcl.Header); + expect(header.length).toStrictEqual(expected.written.length); + expect(header.isGlobal).toStrictEqual(payload.frameControl.frameType === Zcl.FrameType.GLOBAL); + expect(header.isSpecific).toStrictEqual(payload.frameControl.frameType === Zcl.FrameType.SPECIFIC); + expect(header.frameControl).toStrictEqual(payload.frameControl); + expect(header.manufacturerCode).toStrictEqual(payload.manufacturerCode); + expect(header.transactionSequenceNumber).toStrictEqual(payload.transactionSequenceNumber); + expect(header.commandIdentifier).toStrictEqual(payload.commandId); + } + }); + + it.each([ + [ + 'basic', + {value: [0, 234]}, + ], + [ + 'manufacturer specific', + {value: [4, ...uint16To8Array(1234), 234]}, + ], + ])('Reads non-compliant header as undefined %s', (_name, payload) => { + expect(Zcl.Header.fromBuffer(Buffer.from(payload.value))).toStrictEqual(undefined); + }); + }); + + it.each([ + [ + 'global', + GLOBAL_FRAME, + {string: GLOBAL_FRAME_STRING, header: GLOBAL_HEADER, written: GLOBAL_FRAME_BUFFER}, + ], + [ + 'global response', + GLOBAL_RSP_FRAME, + {string: GLOBAL_RSP_FRAME_STRING, header: GLOBAL_RSP_HEADER, written: GLOBAL_RSP_FRAME_BUFFER}, + ], + [ + 'global no payload', + GLOBAL_FRAME_NO_PAYLOAD, + {string: GLOBAL_FRAME_NO_PAYLOAD_STRING, header: GLOBAL_HEADER, written: GLOBAL_FRAME_NO_PAYLOAD_BUFFER}, + ], + [ + 'global with condition-based parameters', + GLOBAL_CONDITION_FRAME, + {string: GLOBAL_CONDITION_FRAME_STRING, header: GLOBAL_CONDITION_HEADER, written: GLOBAL_CONDITION_FRAME_BUFFER}, + ], + [ + 'specific', + SPECIFIC_FRAME, + {string: SPECIFIC_FRAME_STRING, header: SPECIFIC_HEADER, written: SPECIFIC_FRAME_BUFFER}, + ], + [ + 'specific response', + SPECIFIC_RSP_FRAME, + {string: SPECIFIC_RSP_FRAME_STRING, header: SPECIFIC_RSP_HEADER, written: SPECIFIC_RSP_FRAME_BUFFER}, + ], + [ + 'specific with condition-based parameters', + SPECIFIC_CONDITION_FRAME, + {string: SPECIFIC_CONDITION_FRAME_STRING, header: SPECIFIC_CONDITION_HEADER, written: SPECIFIC_CONDITION_FRAME_BUFFER}, + ], + [ + 'manufacturer-specific', + MANUF_SPE_FRAME, + {string: MANUF_SPE_FRAME_STRING, header: MANUF_SPE_HEADER, written: MANUF_SPE_FRAME_BUFFER}, + ], + ])('Writes & Reads frame %s', (_name, frame, expected) => { + expect(frame).toBeDefined(); + expect(frame.toString()).toStrictEqual(expected.string); + expect(frame.header).toStrictEqual(expected.header); + + expect(frame.toBuffer()).toStrictEqual(expected.written); + expect(Zcl.Frame.fromBuffer(frame.cluster.ID, frame.header, expected.written, {}).toString()).toStrictEqual(expected.string); + }); + + it('Writes & Reads repetitive strategy', () => { + const expected = [{attrId: 256}, {attrId: 127}]; + const header = new Zcl.Header( + {frameType: Zcl.FrameType.GLOBAL, manufacturerSpecific: false, direction: Zcl.Direction.CLIENT_TO_SERVER, disableDefaultResponse: false, reservedBits: 0}, + null, 123, Zcl.Foundation.read.ID, + ); + const frame = Zcl.Frame.create( + header.frameControl.frameType, header.frameControl.direction, header.frameControl.disableDefaultResponse, + header.manufacturerCode, header.transactionSequenceNumber, header.commandIdentifier, + Zcl.Foundation.read.ID, expected/*payload*/, {}/*custom clusters*/, header.frameControl.reservedBits, + ); + + expect(frame.payload).toStrictEqual(expected); + + const buffer = frame.toBuffer(); + const readHeader = Zcl.Header.fromBuffer(buffer)!; + const readFrame = Zcl.Frame.fromBuffer(0/*Foundation*/, readHeader, buffer, {}/*custom clusters*/); + + expect(readFrame.payload).toStrictEqual(expected); + }); + + it('Writes & Reads flat strategy', () => { + const expected = {cmdId: 253, statusCode: 127}; + const header = new Zcl.Header( + {frameType: Zcl.FrameType.GLOBAL, manufacturerSpecific: false, direction: Zcl.Direction.CLIENT_TO_SERVER, disableDefaultResponse: false, reservedBits: 0}, + null, 123, Zcl.Foundation.defaultRsp.ID, + ); + const frame = Zcl.Frame.create( + header.frameControl.frameType, header.frameControl.direction, header.frameControl.disableDefaultResponse, + header.manufacturerCode, header.transactionSequenceNumber, header.commandIdentifier, + Zcl.Foundation.defaultRsp.ID, expected/*payload*/, {}/*custom clusters*/, header.frameControl.reservedBits, + ); + + expect(frame.payload).toStrictEqual(expected); + + const buffer = frame.toBuffer(); + const readHeader = Zcl.Header.fromBuffer(buffer)!; + const readFrame = Zcl.Frame.fromBuffer(0/*Foundation*/, readHeader, buffer, {}/*custom clusters*/); + + expect(readFrame.payload).toStrictEqual(expected); + }); + + it('Writes & Reads oneof strategy', () => { + const expected = { + discComplete: 123, + attrInfos: [{attrId: 32, dataType: Zcl.DataType.UINT16}, {attrId: 67, dataType: Zcl.DataType.ARRAY}] + }; + const header = new Zcl.Header( + {frameType: Zcl.FrameType.GLOBAL, manufacturerSpecific: false, direction: Zcl.Direction.CLIENT_TO_SERVER, disableDefaultResponse: false, reservedBits: 0}, + null, 123, Zcl.Foundation.discoverRsp.ID, + ); + const frame = Zcl.Frame.create( + header.frameControl.frameType, header.frameControl.direction, header.frameControl.disableDefaultResponse, + header.manufacturerCode, header.transactionSequenceNumber, header.commandIdentifier, + Zcl.Foundation.discoverRsp.ID, expected/*payload*/, {}/*custom clusters*/, header.frameControl.reservedBits, + ); + + expect(frame.payload).toStrictEqual(expected); + + const buffer = frame.toBuffer(); + const readHeader = Zcl.Header.fromBuffer(buffer)!; + const readFrame = Zcl.Frame.fromBuffer(0/*Foundation*/, readHeader, buffer, {}/*custom clusters*/); + + expect(readFrame.payload).toStrictEqual(expected); + }); + + it('Throws when writting invalid frame type', () => { + expect(() => { + const frame = Zcl.Frame.create( + // @ts-expect-error invalid on purpose + 3, + Zcl.Direction.CLIENT_TO_SERVER, false, null, 123, + Zcl.Clusters.genAlarms.commands.reset.ID, Zcl.Clusters.genAlarms.ID, + {}/*payload*/, {}/*custom clusters*/, 0/*reserved bits*/ + ); + + frame.toBuffer(); + }).toThrow(`Frametype '3' not valid`); + }); + + it('Throws when reading invalid frame type', () => { + expect(() => { + Zcl.Frame.fromBuffer( + Zcl.Clusters.genBasic.ID, + Zcl.Header.fromBuffer(Buffer.from([2, 123, 0]))!, + Buffer.from([])/*payload*/, {}/*custom clusters*/, + ); + }).toThrow(`Unsupported frameType '2'`); + }); + + it('Throws when reading frame without header', () => { + expect(() => { + Zcl.Frame.fromBuffer( + Zcl.Clusters.genBasic.ID, + // @ts-expect-error invalid of purpose + null, + Buffer.from([])/*payload*/, {}/*custom clusters*/, + ); + }).toThrow(`Invalid ZclHeader.`); + }); + + it('Throws when writting missing payload', () => { + expect(() => { + const frame = Zcl.Frame.create( + SPECIFIC_RSP_HEADER.frameControl.frameType, SPECIFIC_RSP_HEADER.frameControl.direction, SPECIFIC_RSP_HEADER.frameControl.disableDefaultResponse, + SPECIFIC_RSP_HEADER.manufacturerCode, SPECIFIC_RSP_HEADER.transactionSequenceNumber, SPECIFIC_RSP_HEADER.commandIdentifier, + Zcl.Clusters.genAlarms.ID, {alarmcode: 246, /*clusterid: 3456*/}/*payload*/, {}/*custom clusters*/, SPECIFIC_RSP_HEADER.frameControl.reservedBits, + ); + + frame.toBuffer(); + }).toThrow(`Parameter 'clusterid' is missing`); + }); + + it('Checks cluster match by name', () => { + expect(GLOBAL_FRAME.isCluster('genBasic')).toBeTruthy(); + expect(GLOBAL_FRAME.isCluster('discoverCommands')).toBeFalsy(); + // @ts-expect-error invalid on purpose + expect(GLOBAL_FRAME.isCluster('notacluster')).toBeFalsy(); + expect(SPECIFIC_FRAME.isCluster('genAlarms')).toBeTruthy(); + expect(SPECIFIC_FRAME.isCluster('genAnalogInput')).toBeFalsy(); + // @ts-expect-error invalid on purpose + expect(SPECIFIC_FRAME.isCluster('notacluster')).toBeFalsy(); + }); + + it('Checks command match by name', () => { + expect(GLOBAL_FRAME.isCommand('read')).toBeTruthy(); + expect(GLOBAL_FRAME.isCommand('discoverCommands')).toBeFalsy(); + // @ts-expect-error invalid on purpose + expect(GLOBAL_FRAME.isCommand('notacommand')).toBeFalsy(); + expect(SPECIFIC_FRAME.isCommand('getAlarm')).toBeTruthy(); + expect(SPECIFIC_FRAME.isCommand('enrollReq')).toBeFalsy(); + // @ts-expect-error invalid on purpose + expect(SPECIFIC_FRAME.isCommand('notacommand')).toBeFalsy(); + }); + + it('[workaround] Reads Foundation char str as Mi struct for Xiaomi attridId=65281', () => { + const expected = [ + {attrId: 5, dataType: Zcl.DataType.CHAR_STR, attrData: 'lumi.sensor_wleak.aq1'}, + {attrId: 65281, dataType: Zcl.DataType.CHAR_STR, attrData: {1: 3285, 3: 33, 4: 5032, 5: 43, 6: [0,327680], 8: 516, 10: 0, 100: 0}} + ]; + const buffer = Buffer.from([ + 28, ...uint16To8Array(Zcl.ManufacturerCode.LUMI_UNITED_TECHOLOGY_LTD_SHENZHEN), 3, 10,/*header*/ + // regular attr parsing + 5, 0, Zcl.DataType.CHAR_STR, 21, 108, 117, 109, 105, 46, 115, 101, 110, 115, 111, 114, 95, 119, 108, 101, 97, 107, 46, 97, 113, 49, + // workaround parsing + 1, 255, Zcl.DataType.CHAR_STR, + 34, + 1, Zcl.DataType.UINT16, ...uint16To8Array(3285), + 3, Zcl.DataType.INT8, 33, + 4, Zcl.DataType.UINT16, ...uint16To8Array(5032), + 5, Zcl.DataType.UINT16, ...uint16To8Array(43), + 6, Zcl.DataType.UINT40, ...uint32To8Array(327680), 0, + 8, Zcl.DataType.UINT16, ...uint16To8Array(516), + 10, Zcl.DataType.UINT16, ...uint16To8Array(0), + 100, Zcl.DataType.BOOLEAN, 0, + ]); + const header = Zcl.Header.fromBuffer(buffer)!; + const frame = Zcl.Frame.fromBuffer(0/*Foundation*/, header, buffer, {}); + + expect(frame.payload).toStrictEqual(expected); + }); + + it('[workaround] Reads Foundation struct with extra payload to match zcl-packet', () => { + const expected = [{ + attrId: 65282, dataType: Zcl.DataType.STRUCT, numElms: 6, + structElms: [ + {elmType: Zcl.DataType.BOOLEAN, elmVal: 1}, + {elmType: Zcl.DataType.UINT16, elmVal: 3022}, + {elmType: Zcl.DataType.UINT16, elmVal: 17320}, + {elmType: Zcl.DataType.UINT40, elmVal: [0,1]}, + {elmType: Zcl.DataType.UINT16, elmVal: 560}, + {elmType: Zcl.DataType.UINT8, elmVal: 86}, + ], + attrData: [ + {elmType: Zcl.DataType.BOOLEAN, elmVal: 1}, + {elmType: Zcl.DataType.UINT16, elmVal: 3022}, + {elmType: Zcl.DataType.UINT16, elmVal: 17320}, + {elmType: Zcl.DataType.UINT40, elmVal: [0,1]}, + {elmType: Zcl.DataType.UINT16, elmVal: 560}, + {elmType: Zcl.DataType.UINT8, elmVal: 86} + ], + }]; + const buffer = Buffer.from([ + 28, ...uint16To8Array(Zcl.ManufacturerCode.DSR_CORPORATION), 194, 10,/*header*/ + ...uint16To8Array(65282), Zcl.DataType.STRUCT, 6, 0, + Zcl.DataType.BOOLEAN, 1, + Zcl.DataType.UINT16, ...uint16To8Array(3022), + Zcl.DataType.UINT16, ...uint16To8Array(17320), + Zcl.DataType.UINT40, ...uint32To8Array(1), 0, + Zcl.DataType.UINT16, ...uint16To8Array(560), + Zcl.DataType.UINT8, 86, + ]); + const header = Zcl.Header.fromBuffer(buffer)!; + const frame = Zcl.Frame.fromBuffer(0/*Foundation*/, header, buffer, {}); + + expect(frame.payload).toStrictEqual(expected); + }); +}); diff --git a/test/zspec/zcl/utils.test.ts b/test/zspec/zcl/utils.test.ts new file mode 100644 index 0000000000..f72c3cc7b7 --- /dev/null +++ b/test/zspec/zcl/utils.test.ts @@ -0,0 +1,281 @@ +import * as Zcl from '../../../src/zspec/zcl'; +import {Command, CustomClusters} from '../../../src/zspec/zcl/definition/tstype'; + +const CUSTOM_CLUSTERS: CustomClusters = { + genBasic: {ID: Zcl.Clusters.genBasic.ID, commands: {}, commandsResponse: {}, attributes: {myCustomAttr: {ID: 65533, type: Zcl.DataType.UINT8}}}, + myCustomCluster: {ID: 65534, commands: {}, commandsResponse: {}, attributes: {myCustomAttr: {ID: 65533, type: Zcl.DataType.UINT8}}}, + myCustomClusterManuf: {ID: 65533, manufacturerCode: 65534, commands: {}, commandsResponse: {}, attributes: {myCustomAttr: {ID: 65533, type: Zcl.DataType.UINT8}}}, + manuSpecificBosch10NoManuf: {ID: Zcl.Clusters.manuSpecificBosch10.ID, commands: {}, commandsResponse: {}, attributes: {myCustomAttr: {ID: 65533, type: Zcl.DataType.UINT8}}} +}; + +describe('ZCL Utils', () => { + it('Creates status error', () => { + const zclError = new Zcl.StatusError(Zcl.Status.ABORT); + + expect(zclError).toBeInstanceOf(Zcl.StatusError); + expect(zclError.code).toStrictEqual(Zcl.Status.ABORT); + expect(zclError.message).toStrictEqual(`Status '${Zcl.Status[Zcl.Status.ABORT]}'`); + }); + + it('Gets data type class', () => { + expect(Zcl.Utils.getDataTypeClass(Zcl.DataType.UINT16)).toStrictEqual(Zcl.DataTypeClass.ANALOG); + expect(Zcl.Utils.getDataTypeClass(Zcl.DataType.DATA16)).toStrictEqual(Zcl.DataTypeClass.DISCRETE); + expect(() => { + Zcl.Utils.getDataTypeClass(Zcl.DataType.NO_DATA); + }).toThrow(); + }); + + it.each([ + [ + 'by ID', + {key: Zcl.Clusters.genBasic.ID, manufacturerCode: null, customClusters: {}}, + {cluster: Zcl.Clusters.genBasic, name: 'genBasic'}, + ], + [ + 'by name', + {key: 'genAlarms', manufacturerCode: null, customClusters: {}}, + {cluster: Zcl.Clusters.genAlarms, name: 'genAlarms'}, + ], + [ + 'by ID with no manufacturer code', + {key: Zcl.Clusters.genAlarms.ID, manufacturerCode: 123, customClusters: {}}, + {cluster: Zcl.Clusters.genAlarms, name: 'genAlarms'}, + ], + [ + 'by ID with non-matching manufacturer code', + {key: Zcl.Clusters.sprutDevice.ID, manufacturerCode: 123, customClusters: {}}, + {cluster: Zcl.Clusters.sprutDevice, name: 'sprutDevice'}, + ], + [ + 'by ID with matching manufacturer code', + {key: Zcl.Clusters.sprutDevice.ID, manufacturerCode: Zcl.Clusters.sprutDevice.manufacturerCode!, customClusters: {}}, + {cluster: Zcl.Clusters.sprutDevice, name: 'sprutDevice'}, + ], + [ + 'custom by ID', + {key: CUSTOM_CLUSTERS.myCustomCluster.ID, manufacturerCode: null, customClusters: CUSTOM_CLUSTERS}, + {cluster: CUSTOM_CLUSTERS.myCustomCluster, name: 'myCustomCluster'}, + ], + [ + 'custom by name', + {key: 'myCustomCluster', manufacturerCode: null, customClusters: CUSTOM_CLUSTERS}, + {cluster: CUSTOM_CLUSTERS.myCustomCluster, name: 'myCustomCluster'}, + ], + [ + 'custom by ID with no manufacturer code', + {key: CUSTOM_CLUSTERS.myCustomCluster.ID, manufacturerCode: 123, customClusters: CUSTOM_CLUSTERS}, + {cluster: CUSTOM_CLUSTERS.myCustomCluster, name: 'myCustomCluster'}, + ], + [ + 'custom by ID with non-matching manufacturer code', + {key: CUSTOM_CLUSTERS.myCustomClusterManuf.ID, manufacturerCode: 123, customClusters: CUSTOM_CLUSTERS}, + {cluster: CUSTOM_CLUSTERS.myCustomClusterManuf, name: 'myCustomClusterManuf'}, + ], + [ + 'custom by ID with matching manufacturer code', + { + key: CUSTOM_CLUSTERS.myCustomClusterManuf.ID, manufacturerCode: CUSTOM_CLUSTERS.myCustomClusterManuf.manufacturerCode!, + customClusters: CUSTOM_CLUSTERS, + }, + {cluster: CUSTOM_CLUSTERS.myCustomClusterManuf, name: 'myCustomClusterManuf'}, + ], + [ + 'custom by ID overriding same Zcl ID entirely', + {key: CUSTOM_CLUSTERS.genBasic.ID, manufacturerCode: null, customClusters: CUSTOM_CLUSTERS}, + {cluster: CUSTOM_CLUSTERS.genBasic, name: 'genBasic'}, + ], + [ + 'by ID ignoring same custom ID if Zcl is better match with manufacture code', + { + key: CUSTOM_CLUSTERS.manuSpecificBosch10NoManuf.ID, manufacturerCode: Zcl.ManufacturerCode.ROBERT_BOSCH_GMBH, + customClusters: CUSTOM_CLUSTERS + }, + {cluster: Zcl.Clusters.manuSpecificBosch10, name: 'manuSpecificBosch10'}, + ], + ])('Gets cluster %s', (_name, payload, expected) => { + const cluster = Zcl.Utils.getCluster(payload.key, payload.manufacturerCode, payload.customClusters); + + expect(cluster.ID).toStrictEqual(expected.cluster.ID); + expect(cluster.name).toStrictEqual(expected.name); + + for (const k in expected.cluster.attributes) { + expect(cluster.attributes[k]).toBeDefined(); + expect(cluster.attributes[k].name).toStrictEqual(k); + } + + expect(cluster.manufacturerCode).toStrictEqual(expected.cluster.manufacturerCode); + expect(cluster.getAttribute).toBeInstanceOf(Function); + expect(cluster.getCommand).toBeInstanceOf(Function); + expect(cluster.getCommandResponse).toBeInstanceOf(Function); + expect(cluster.hasAttribute).toBeInstanceOf(Function); + }); + + it('Creates empty cluster when getting by invalid ID', () => { + const cluster = Zcl.Utils.getCluster(99999, null, {}); + expect(cluster.ID).toStrictEqual(99999); + expect(cluster.name).toStrictEqual('99999'); + expect(cluster.manufacturerCode).toStrictEqual(null); + expect(cluster.attributes).toStrictEqual({}); + expect(cluster.commands).toStrictEqual({}); + expect(cluster.commandsResponse).toStrictEqual({}); + expect(cluster.getAttribute).toBeInstanceOf(Function); + expect(cluster.getCommand).toBeInstanceOf(Function); + expect(cluster.getCommandResponse).toBeInstanceOf(Function); + expect(cluster.hasAttribute).toBeInstanceOf(Function); + }); + + it('Throws when getting invalid cluster name', () => { + expect(() => { + Zcl.Utils.getCluster('invalid', null, {}); + }).toThrow(); + }); + + it.each([ + [ + 'by ID', + {key: Zcl.Clusters.genBasic.attributes.zclVersion.ID, manufacturerCode: null, customClusters: {}}, + {cluster: Zcl.Clusters.genBasic, name: 'zclVersion'}, + ], + [ + 'by name', + {key: 'alarmCount', manufacturerCode: null, customClusters: {}}, + {cluster: Zcl.Clusters.genAlarms, name: 'alarmCount'}, + ], + [ + 'by ID with no manufacturer code', + {key: Zcl.Clusters.genAlarms.attributes.alarmCount.ID, manufacturerCode: 123, customClusters: {}}, + {cluster: Zcl.Clusters.genAlarms, name: 'alarmCount'}, + ], + [ + 'by ID with matching manufacturer code', + {key: Zcl.Clusters.haDiagnostic.attributes.danfossSystemStatusCode.ID, manufacturerCode: Zcl.ManufacturerCode.DANFOSS_A_S, customClusters: {}}, + {cluster: Zcl.Clusters.haDiagnostic, name: 'danfossSystemStatusCode'}, + ], + [ + 'custom by ID', + {key: CUSTOM_CLUSTERS.genBasic.attributes.myCustomAttr.ID, manufacturerCode: null, customClusters: CUSTOM_CLUSTERS}, + {cluster: Zcl.Clusters.genBasic, name: 'myCustomAttr'}, + ], + [ + 'custom by name', + {key: 'myCustomAttr', manufacturerCode: null, customClusters: CUSTOM_CLUSTERS}, + {cluster: Zcl.Clusters.genBasic, name: 'myCustomAttr'}, + ], + ])('Gets and checks cluster attribute %s', (_name, payload, expected) => { + const cluster = Zcl.Utils.getCluster(expected.cluster.ID, payload.manufacturerCode, payload.customClusters); + const attribute = cluster.getAttribute(payload.key); + expect(cluster.hasAttribute(payload.key)).toBeTruthy(); + expect(attribute).toStrictEqual(cluster.attributes[expected.name]); + }); + + it('Throws when getting invalid attribute', () => { + const cluster = Zcl.Utils.getCluster(Zcl.Clusters.genAlarms.ID, null, {}); + expect(() => { + cluster.getAttribute('abcd'); + }).toThrow(); + expect(() => { + cluster.getAttribute(99999); + }).toThrow(); + }); + + it('Throws when getting attribute with invalid manufacturer code', () => { + const cluster = Zcl.Utils.getCluster(Zcl.Clusters.haDiagnostic.ID, 123, {}); + expect(() => { + cluster.getAttribute(Zcl.Clusters.haDiagnostic.attributes.danfossSystemStatusCode.ID); + }).toThrow(); + }); + + it.each([ + [ + 'by ID', + {key: Zcl.Clusters.genBasic.commands.resetFactDefault.ID}, + {cluster: Zcl.Clusters.genBasic, name: 'resetFactDefault'}, + ], + [ + 'by name', + {key: 'resetAll'}, + {cluster: Zcl.Clusters.genAlarms, name: 'resetAll'}, + ], + ])('Gets cluster command %s', (_name, payload, expected) => { + const cluster = Zcl.Utils.getCluster(expected.cluster.ID, null, {}); + const command = cluster.getCommand(payload.key); + expect(command).toStrictEqual(cluster.commands[expected.name]); + }); + + it('Throws when getting invalid command', () => { + const cluster = Zcl.Utils.getCluster(Zcl.Clusters.genAlarms.ID, null, {}); + expect(() => { + cluster.getCommand('abcd'); + }).toThrow(); + expect(() => { + cluster.getCommand(99999); + }).toThrow(); + }); + + it.each([ + [ + 'by ID', + {key: Zcl.Clusters.genIdentify.commandsResponse.identifyQueryRsp.ID}, + {cluster: Zcl.Clusters.genIdentify, name: 'identifyQueryRsp'}, + ], + [ + 'by name', + {key: 'getEventLog'}, + {cluster: Zcl.Clusters.genAlarms, name: 'getEventLog'}, + ], + ])('Gets cluster command response %s', (_name, payload, expected) => { + const cluster = Zcl.Utils.getCluster(expected.cluster.ID, null, {}); + const commandResponse = cluster.getCommandResponse(payload.key); + expect(commandResponse).toStrictEqual(cluster.commandsResponse[expected.name]); + }); + + it('Throws when getting invalid command response', () => { + const cluster = Zcl.Utils.getCluster(Zcl.Clusters.genAlarms.ID, null, {}); + expect(() => { + cluster.getCommandResponse('abcd'); + }).toThrow(); + expect(() => { + cluster.getCommandResponse(99999); + }).toThrow(); + }); + + it.each([ + [ + 'by ID', + {key: Zcl.Foundation.writeUndiv.ID}, + {cluster: Zcl.Foundation.writeUndiv, name: 'writeUndiv'}, + ], + [ + 'by name', + {key: 'read'}, + {cluster: Zcl.Foundation.read, name: 'read'}, + ], + ])('Gets global command %s', (_name, payload, expected) => { + let command: Command = { + ID: expected.cluster.ID, + name: expected.name, + parameters: expected.cluster.parameters, + }; + + if (expected.cluster.response) { + command.response = expected.cluster.response; + } + + expect(Zcl.Utils.getGlobalCommand(payload.key)).toStrictEqual(command); + }); + + it('Throws when getting invalid global command', () => { + expect(() => { + Zcl.Utils.getGlobalCommand(99999); + }).toThrow(); + expect(() => { + Zcl.Utils.getGlobalCommand('abcd'); + }).toThrow(); + }); + + it('Checks cluster name', () => { + expect(Zcl.Utils.isClusterName('genBasic')).toBeTruthy(); + expect(Zcl.Utils.isClusterName('genAlarms')).toBeTruthy(); + expect(Zcl.Utils.isClusterName('invalid')).toBeFalsy(); + }); +}); diff --git a/test/zspec/zdo/buffalo.test.ts b/test/zspec/zdo/buffalo.test.ts new file mode 100644 index 0000000000..12bb516f68 --- /dev/null +++ b/test/zspec/zdo/buffalo.test.ts @@ -0,0 +1,1312 @@ +import * as ZSpec from '../../../src/zspec'; +import {ClusterId, EUI64, ExtendedPanId, NodeId} from '../../../src/zspec/tstypes'; +import * as Zcl from '../../../src/zspec/zcl'; +import * as Zdo from '../../../src/zspec/zdo'; +import {BuffaloZdo} from '../../../src/zspec/zdo/buffaloZdo'; +import { + APSFrameCounterChallengeTLV, + APSFrameCounterResponseTLV, + ActiveEndpointsResponse, + AuthenticationTokenIdTLV, + BeaconAppendixEncapsulationGlobalTLV, + BeaconSurveyConfigurationTLV, + BeaconSurveyResultsTLV, + BindingTableResponse, + ClearAllBindingsReqEUI64TLV, + ConfigurationParametersGlobalTLV, + Curve25519PublicPointTLV, + DeviceAuthenticationLevelTLV, + DeviceCapabilityExtensionGlobalTLV, + DeviceEUI64ListTLV, + EndDeviceAnnounce, + FragmentationParametersGlobalTLV, + IEEEAddressResponse, + JoinerEncapsulationGlobalTLV, + LQITableResponse, + ManufacturerSpecificGlobalTLV, + MatchDescriptorsResponse, + NetworkAddressResponse, + NextChannelChangeGlobalTLV, + NextPanIdChangeGlobalTLV, + NodeDescriptorResponse, + NwkBeaconSurveyResponse, + NwkEnhancedUpdateResponse, + NwkIEEEJoiningListResponse, + NwkUnsolicitedEnhancedUpdateResponse, + NwkUpdateResponse, + PanIdConflictReportGlobalTLV, + PotentialParentsTLV, + PowerDescriptorResponse, + ProcessingStatusTLV, + RouterInformationGlobalTLV, + RoutingTableResponse, + SelectedKeyNegotiationMethodTLV, + ServerMask, + SimpleDescriptorResponse, + SupportedKeyNegotiationMethodsGlobalTLV, + SymmetricPassphraseGlobalTLV, + SystemServerDiscoveryResponse, + TargetIEEEAddressTLV +} from '../../../src/zspec/zdo/definition/tstypes'; +import {uint16To8Array, uint32To8Array} from '../../utils/math'; + +const IEEE_ADDRESS1: EUI64 = `0xfe34ac2385ff8311`; +const IEEE_ADDRESS1_BYTES = [0x11, 0x83, 0xff, 0x85, 0x23, 0xac, 0x34, 0xfe]; +const IEEE_ADDRESS2: EUI64 = `0x28373fecd834ba37`; +const IEEE_ADDRESS2_BYTES = [0x37, 0xba, 0x34, 0xd8, 0xec, 0x3f, 0x37, 0x28]; +const NODE_ID1: NodeId = 0xfe32; +const NODE_ID1_BYTES = uint16To8Array(NODE_ID1); +const NODE_ID2: NodeId = 0xab39; +const NODE_ID2_BYTES = uint16To8Array(NODE_ID2); +const EXT_PAN_ID1: ExtendedPanId = [3, 43, 56, 23, 65, 23, 67, 23]; +const EXT_PAN_ID2: ExtendedPanId = [253, 231, 21, 3, 0, 44, 24, 46]; +const CLUSTER_LIST1: ClusterId[] = [Zcl.Clusters.genAlarms.ID, Zcl.Clusters.seMetering.ID, Zcl.Clusters.haApplianceStatistics.ID]; +const CLUSTER_LIST1_BYTES = [...uint16To8Array(CLUSTER_LIST1[0]), ...uint16To8Array(CLUSTER_LIST1[1]), ...uint16To8Array(CLUSTER_LIST1[2])]; +const CLUSTER_LIST2: ClusterId[] = [Zcl.Clusters.genOnOff.ID, Zcl.Clusters.genBasic.ID, Zcl.Clusters.ssIasZone.ID, Zcl.Clusters.genLevelCtrl.ID]; +const CLUSTER_LIST2_BYTES = [...uint16To8Array(CLUSTER_LIST2[0]), ...uint16To8Array(CLUSTER_LIST2[1]), ...uint16To8Array(CLUSTER_LIST2[2]), ...uint16To8Array(CLUSTER_LIST2[3])]; +const SERVER_MASK_R22: ServerMask = { + primaryTrustCenter: 1, + backupTrustCenter: 1, + deprecated1: 0, + deprecated2: 0, + deprecated3: 0, + deprecated4: 0, + networkManager: 1, + reserved1: 0, + reserved2: 0, + stackComplianceResivion: 22, +}; +const SERVER_MASK_R22_BYTE = Zdo.Utils.createServerMask(SERVER_MASK_R22); + +const SERVER_MASK_R23: ServerMask = { + primaryTrustCenter: 1, + backupTrustCenter: 1, + deprecated1: 0, + deprecated2: 0, + deprecated3: 0, + deprecated4: 0, + networkManager: 1, + reserved1: 0, + reserved2: 0, + stackComplianceResivion: 23, +}; +const SERVER_MASK_R23_BYTE = Zdo.Utils.createServerMask(SERVER_MASK_R23); + +describe('ZDO Buffalo', () => { + it('Sets & Gets position', () => { + const buffalo = new BuffaloZdo(Buffer.alloc(3)); + expect(buffalo.getPosition()).toStrictEqual(0); + buffalo.setPosition(3); + expect(buffalo.getPosition()).toStrictEqual(3); + buffalo.setPosition(1); + expect(buffalo.getPosition()).toStrictEqual(1); + buffalo.setPosition(0); + expect(buffalo.getPosition()).toStrictEqual(0); + }); + + it('Sets & Gets bytes without changing internal position', () => { + const buffalo = new BuffaloZdo(Buffer.from([1, 2, 3, 255])); + expect(buffalo.getByte(0)).toStrictEqual(1); + expect(buffalo.getByte(1)).toStrictEqual(2); + expect(buffalo.getByte(2)).toStrictEqual(3); + expect(buffalo.getByte(3)).toStrictEqual(255); + expect(buffalo.getPosition()).toStrictEqual(0); + buffalo.setByte(3, 127); + expect(buffalo.getByte(3)).toStrictEqual(127); + buffalo.setByte(0, 7); + expect(buffalo.getByte(0)).toStrictEqual(7); + }); + + it('Checks if more available to by amount', () => { + const buffalo = new BuffaloZdo(Buffer.from([1, 2, 3, 255])); + buffalo.setPosition(4); + expect(buffalo.isMoreBy(0)).toBeTruthy(); + expect(buffalo.isMoreBy(1)).toBeFalsy(); + buffalo.setPosition(1); + expect(buffalo.isMoreBy(0)).toBeTruthy(); + expect(buffalo.isMoreBy(3)).toBeTruthy(); + expect(buffalo.isMoreBy(4)).toBeFalsy(); + }); + + it('Throws when duplicate TLV tag found and not valid', () => { + expect(() => { + new BuffaloZdo(Buffer.from([ + Zdo.GlobalTLV.FRAGMENTATION_PARAMETERS, 5 - 1, ...uint16To8Array(NODE_ID1), 1, ...uint16To8Array(213), + Zdo.GlobalTLV.FRAGMENTATION_PARAMETERS, 5 - 1, ...uint16To8Array(NODE_ID2), 0, ...uint16To8Array(344) + ])).readTLVs(); + }).toThrow(`Duplicate tag. Cannot have more than one of tagId=${Zdo.GlobalTLV.FRAGMENTATION_PARAMETERS}.`); + expect(() => { + new BuffaloZdo(Buffer.from([ + Zdo.GlobalTLV.MANUFACTURER_SPECIFIC, 2 - 1, ...uint16To8Array(Zcl.ManufacturerCode.ABB), + Zdo.GlobalTLV.MANUFACTURER_SPECIFIC, 2 - 1, ...uint16To8Array(Zcl.ManufacturerCode.ABB_GENWAY_XIAMEN_ELECTRICAL_EQUIPMENT_CO_LTD), + ])).readTLVs(); + }).not.toThrow(); + }); + + it('Throws when encapsulated TLV tag found inside encapsulated', () => { + expect(() => { + new BuffaloZdo(Buffer.from([ + Zdo.GlobalTLV.BEACON_APPENDIX_ENCAPSULATION, 4 - 1, Zdo.GlobalTLV.JOINER_ENCAPSULATION, 2 - 1, 123, 456 + ])).readTLVs(); + }).toThrow(`Invalid nested encapsulation for tagId=${Zdo.GlobalTLV.JOINER_ENCAPSULATION}.`); + }); + + it('Throws when not enough bytes to read in TLV', () => { + expect(() => { + new BuffaloZdo(Buffer.from([ + Zdo.GlobalTLV.MANUFACTURER_SPECIFIC, 6 - 1, ...uint16To8Array(Zcl.ManufacturerCode.ABB), + ])).readTLVs(); + }).toThrow(`Malformed TLV. Invalid data length for tagId=${Zdo.GlobalTLV.MANUFACTURER_SPECIFIC}, expected ${6}.`); + }); + + it('Ignores invalid TLV tag and reads next TLV', () => { + const buffalo = new BuffaloZdo(Buffer.from([ + 0xFF, 5 - 1, ...uint16To8Array(Zcl.ManufacturerCode.ABB), 4, 5, 6, + Zdo.GlobalTLV.MANUFACTURER_SPECIFIC, 5 - 1, ...uint16To8Array(Zcl.ManufacturerCode.ABB), 1, 2, 3 + ])) + const tlvs = buffalo.readTLVs(); + + expect(tlvs).toStrictEqual([{ + tagId: Zdo.GlobalTLV.MANUFACTURER_SPECIFIC, length: 5, tlv: { + zigbeeManufacturerId: Zcl.ManufacturerCode.ABB, + additionalData: Buffer.from([1, 2, 3]), + } as ManufacturerSpecificGlobalTLV + }]); + }); + + it('Throws when writing invalid TLV tag', () => { + const buffalo = new BuffaloZdo(Buffer.alloc(3)); + + expect(() => { + buffalo.writeGlobalTLV({tagId: 0xFE, length: 2, tlv: {nwkPanIdConflictCount: NODE_ID2} as PanIdConflictReportGlobalTLV}); + }).toThrow(new Zdo.StatusError(Zdo.Status.NOT_SUPPORTED)); + }) + + it.each([ + ['readProcessingStatusTLV', {length: 6, error: `Malformed TLV. Invalid length '6', expected 5.`, bytes: [2]}], + ['readDeviceAuthenticationLevelTLV', {length: 11, error: `Malformed TLV. Invalid length '11', expected 10.`, bytes: []}], + ['readPotentialParentsTLV', {length: 3, error: `Malformed TLV. Invalid length '3', expected at least 4.`, bytes: []}], + ['readPotentialParentsTLV', {length: 11, error: `Malformed TLV. Invalid length '11', expected 13.`, bytes: [1, 2, 230, 3]}], + ['readBeaconSurveyResultsTLV', {length: 6, error: `Malformed TLV. Invalid length '6', expected 4.`, bytes: []}], + ['readAPSFrameCounterResponseTLV', {length: 36, error: `Malformed TLV. Invalid length '36', expected 32.`, bytes: []}], + // ['readDeviceEUI64ListTLV', {length: 36, error: `Malformed TLV. Invalid length '36', expected 41.`, bytes: [5]}], + // ['readSelectedKeyNegotiationMethodTLV', {length: 11, error: `Malformed TLV. Invalid length '11', expected 10.`, bytes: []}], + // ['readTargetIEEEAddressTLV', {length: 11, error: `Malformed TLV. Invalid length '11', expected 8.`, bytes: []}], + ['readCurve25519PublicPointTLV', {length: 11, error: `Malformed TLV. Invalid length '11', expected 40.`, bytes: []}], + // ['readBeaconSurveyConfigurationTLV', {length: 3, error: `Malformed TLV. Invalid length '3', expected 6.`, bytes: [1]}], + ['readConfigurationParametersGlobalTLV', {length: 1, error: `Malformed TLV. Invalid length '1', expected at least 2.`, bytes: []}], + ['readBeaconAppendixEncapsulationGlobalTLV', {length: 1, error: `Malformed TLV. Invalid length '1', expected at least 2.`, bytes: []}], + ['readJoinerEncapsulationGlobalTLV', {length: 1, error: `Malformed TLV. Invalid length '1', expected at least 2.`, bytes: []}], + ['readFragmentationParametersGlobalTLV', {length: 1, error: `Malformed TLV. Invalid length '1', expected at least 2.`, bytes: []}], + ['readRouterInformationGlobalTLV', {length: 1, error: `Malformed TLV. Invalid length '1', expected at least 2.`, bytes: []}], + ['readSymmetricPassphraseGlobalTLV', {length: 13, error: `Malformed TLV. Invalid length '13', expected at least 16.`, bytes: []}], + ['readNextChannelChangeGlobalTLV', {length: 3, error: `Malformed TLV. Invalid length '3', expected at least 4.`, bytes: []}], + ['readNextPanIdChangeGlobalTLV', {length: 1, error: `Malformed TLV. Invalid length '1', expected at least 2.`, bytes: []}], + ['readPanIdConflictReportGlobalTLV', {length: 1, error: `Malformed TLV. Invalid length '1', expected at least 2.`, bytes: []}], + ['readSupportedKeyNegotiationMethodsGlobalTLV', {length: 1, error: `Malformed TLV. Invalid length '1', expected at least 2.`, bytes: []}], + ['readManufacturerSpecificGlobalTLV', {length: 1, error: `Malformed TLV. Invalid length '1', expected at least 2.`, bytes: []}], + ])('Throws when reading invalid length TLV %s', (func, payload) => { + expect(() => { + const buffalo = new BuffaloZdo(Buffer.from(payload.bytes)); + + buffalo[func](payload.length); + }).toThrow(payload.error); + }) + + it.each([ + [ + 'MANUFACTURER_SPECIFIC', + [Zdo.GlobalTLV.MANUFACTURER_SPECIFIC, 3 - 1, ...uint16To8Array(256), 123], + [{tagId: Zdo.GlobalTLV.MANUFACTURER_SPECIFIC, length: 3, tlv: {zigbeeManufacturerId: 256, additionalData: Buffer.from([123])} as ManufacturerSpecificGlobalTLV}] + ], + [ + 'SUPPORTED_KEY_NEGOTIATION_METHODS', + [Zdo.GlobalTLV.SUPPORTED_KEY_NEGOTIATION_METHODS, 2 - 1, 1, 2], + [{tagId: Zdo.GlobalTLV.SUPPORTED_KEY_NEGOTIATION_METHODS, length: 2, tlv: {keyNegotiationProtocolsBitmask: 1, preSharedSecretsBitmask: 2, sourceDeviceEui64: undefined} as SupportedKeyNegotiationMethodsGlobalTLV}] + ], + [ + 'SUPPORTED_KEY_NEGOTIATION_METHODS with IEEE', + [Zdo.GlobalTLV.SUPPORTED_KEY_NEGOTIATION_METHODS, 10 - 1, 1, 2, ...IEEE_ADDRESS1_BYTES], + [{tagId: Zdo.GlobalTLV.SUPPORTED_KEY_NEGOTIATION_METHODS, length: 10, tlv: {keyNegotiationProtocolsBitmask: 1, preSharedSecretsBitmask: 2, sourceDeviceEui64: IEEE_ADDRESS1} as SupportedKeyNegotiationMethodsGlobalTLV}] + ], + [ + 'PAN_ID_CONFLICT_REPORT', + [Zdo.GlobalTLV.PAN_ID_CONFLICT_REPORT, 2 - 1, ...NODE_ID2_BYTES], + [{tagId: Zdo.GlobalTLV.PAN_ID_CONFLICT_REPORT, length: 2, tlv: {nwkPanIdConflictCount: NODE_ID2} as PanIdConflictReportGlobalTLV}] + ], + [ + 'NEXT_PAN_ID_CHANGE', + [Zdo.GlobalTLV.NEXT_PAN_ID_CHANGE, 2 - 1, ...uint16To8Array(0xff00)], + [{tagId: Zdo.GlobalTLV.NEXT_PAN_ID_CHANGE, length: 2, tlv: {panId: 0xff00} as NextPanIdChangeGlobalTLV}] + ], + [ + 'NEXT_CHANNEL_CHANGE', + [Zdo.GlobalTLV.NEXT_CHANNEL_CHANGE, 4 - 1, ...uint32To8Array(423432)], + [{tagId: Zdo.GlobalTLV.NEXT_CHANNEL_CHANGE, length: 4, tlv: {channel: 423432} as NextChannelChangeGlobalTLV}] + ], + [ + 'SYMMETRIC_PASSPHRASE', + [Zdo.GlobalTLV.SYMMETRIC_PASSPHRASE, ZSpec.DEFAULT_ENCRYPTION_KEY_SIZE - 1, ...Buffer.alloc(ZSpec.DEFAULT_ENCRYPTION_KEY_SIZE).fill(0xca)], + [{tagId: Zdo.GlobalTLV.SYMMETRIC_PASSPHRASE, length: ZSpec.DEFAULT_ENCRYPTION_KEY_SIZE, tlv: {passphrase: Buffer.alloc(ZSpec.DEFAULT_ENCRYPTION_KEY_SIZE).fill(0xca)} as SymmetricPassphraseGlobalTLV}] + ], + [ + 'ROUTER_INFORMATION', + [Zdo.GlobalTLV.ROUTER_INFORMATION, 2 - 1, ...uint16To8Array(4396)], + [{tagId: Zdo.GlobalTLV.ROUTER_INFORMATION, length: 2, tlv: {bitmask: 4396} as RouterInformationGlobalTLV}] + ], + [ + 'FRAGMENTATION_PARAMETERS', + [Zdo.GlobalTLV.FRAGMENTATION_PARAMETERS, 5 - 1, ...NODE_ID1_BYTES, 5, ...uint16To8Array(32456)], + [{tagId: Zdo.GlobalTLV.FRAGMENTATION_PARAMETERS, length: 5, tlv: {nwkAddress: NODE_ID1, fragmentationOptions: 5, maxIncomingTransferUnit: 32456} as FragmentationParametersGlobalTLV}] + ], + [ + 'JOINER_ENCAPSULATION', + [Zdo.GlobalTLV.JOINER_ENCAPSULATION, 4 - 1, Zdo.GlobalTLV.FRAGMENTATION_PARAMETERS, 2 - 1, ...NODE_ID1_BYTES], + [{tagId: Zdo.GlobalTLV.JOINER_ENCAPSULATION, length: 4, tlv: {additionalTLVs: [{tagId: Zdo.GlobalTLV.FRAGMENTATION_PARAMETERS, length: 2, tlv: {nwkAddress: NODE_ID1, fragmentationOptions: undefined, maxIncomingTransferUnit: undefined}}]} as JoinerEncapsulationGlobalTLV}] + ], + [ + 'BEACON_APPENDIX_ENCAPSULATION', + [Zdo.GlobalTLV.BEACON_APPENDIX_ENCAPSULATION, 4 - 1, Zdo.GlobalTLV.FRAGMENTATION_PARAMETERS, 2 - 1, ...NODE_ID1_BYTES], + [{tagId: Zdo.GlobalTLV.BEACON_APPENDIX_ENCAPSULATION, length: 4, tlv: {additionalTLVs: [{tagId: Zdo.GlobalTLV.FRAGMENTATION_PARAMETERS, length: 2, tlv: {nwkAddress: NODE_ID1, fragmentationOptions: undefined, maxIncomingTransferUnit: undefined}}]} as BeaconAppendixEncapsulationGlobalTLV}] + ], + [ + 'CONFIGURATION_PARAMETERS', + [Zdo.GlobalTLV.CONFIGURATION_PARAMETERS, 2 - 1, ...uint16To8Array(47593)], + [{tagId: Zdo.GlobalTLV.CONFIGURATION_PARAMETERS, length: 2, tlv: {configurationParameters: 47593} as ConfigurationParametersGlobalTLV}] + ], + [ + 'DEVICE_CAPABILITY_EXTENSION', + [Zdo.GlobalTLV.DEVICE_CAPABILITY_EXTENSION, 3 - 1, 3, 1, 2], + [{tagId: Zdo.GlobalTLV.DEVICE_CAPABILITY_EXTENSION, length: 3, tlv: {data: Buffer.from([3, 1, 2])} as DeviceCapabilityExtensionGlobalTLV}] + ], + [ + 'invalid', + [0xfe, 0, 0], + [] + ], + ])('Reads & Writes global TLV %s', (_name, bytes, expected) => { + const readBuffalo = new BuffaloZdo(Buffer.from(bytes)); + expect(readBuffalo.readTLVs()).toStrictEqual(expected); + + const writeBuffer = new BuffaloZdo(Buffer.alloc(255)); + writeBuffer.writeGlobalTLVs(expected); + expect(writeBuffer.getWritten()).toStrictEqual(Buffer.from(expected.length ? bytes : [])); + }) + + it('buildNetworkAddressRequest', () => { + expect(BuffaloZdo.buildNetworkAddressRequest(IEEE_ADDRESS1, false, 1)).toStrictEqual(Buffer.from([0, ...IEEE_ADDRESS1_BYTES, 0, 1])); + expect(BuffaloZdo.buildNetworkAddressRequest(IEEE_ADDRESS2, true, 3)).toStrictEqual(Buffer.from([0, ...IEEE_ADDRESS2_BYTES, 1, 3])) + }); + + it('buildIeeeAddressRequest', () => { + expect(BuffaloZdo.buildIeeeAddressRequest(NODE_ID1, false, 1)).toStrictEqual(Buffer.from([0, ...NODE_ID1_BYTES, 0, 1])); + expect(BuffaloZdo.buildIeeeAddressRequest(NODE_ID1, true, 3)).toStrictEqual(Buffer.from([0, ...NODE_ID1_BYTES, 1, 3])) + }); + + it('buildNodeDescriptorRequest', () => { + expect(BuffaloZdo.buildNodeDescriptorRequest(NODE_ID1)).toStrictEqual(Buffer.from([0, ...NODE_ID1_BYTES])); + + const tlv: FragmentationParametersGlobalTLV = { + nwkAddress: NODE_ID1, + /*fragmentationOptions: undefined,*/ + /*maxIncomingTransferUnit: undefined,*/ + }; + expect(BuffaloZdo.buildNodeDescriptorRequest(NODE_ID1, tlv)).toStrictEqual( + Buffer.from([0, ...NODE_ID1_BYTES, Zdo.GlobalTLV.FRAGMENTATION_PARAMETERS, 1, ...uint16To8Array(tlv.nwkAddress)]) + ); + + const tlv2: FragmentationParametersGlobalTLV = { + nwkAddress: NODE_ID1, + fragmentationOptions: 1, + /*maxIncomingTransferUnit: undefined,*/ + }; + expect(BuffaloZdo.buildNodeDescriptorRequest(NODE_ID1, tlv2)).toStrictEqual( + Buffer.from([0, ...NODE_ID1_BYTES, Zdo.GlobalTLV.FRAGMENTATION_PARAMETERS, 2, ...uint16To8Array(tlv2.nwkAddress), tlv2.fragmentationOptions!]) + ); + + const tlv3: FragmentationParametersGlobalTLV = { + nwkAddress: NODE_ID1, + /*fragmentationOptions: undefined,*/ + maxIncomingTransferUnit: 256, + }; + expect(BuffaloZdo.buildNodeDescriptorRequest(NODE_ID1, tlv3)).toStrictEqual( + Buffer.from([0, ...NODE_ID1_BYTES, Zdo.GlobalTLV.FRAGMENTATION_PARAMETERS, 3, ...uint16To8Array(tlv3.nwkAddress), ...uint16To8Array(tlv3.maxIncomingTransferUnit!)]) + ); + + const tlv4: FragmentationParametersGlobalTLV = { + nwkAddress: NODE_ID1, + fragmentationOptions: 1, + maxIncomingTransferUnit: 65352, + }; + expect(BuffaloZdo.buildNodeDescriptorRequest(NODE_ID1, tlv4)).toStrictEqual( + Buffer.from([0, ...NODE_ID1_BYTES, Zdo.GlobalTLV.FRAGMENTATION_PARAMETERS, 4, ...uint16To8Array(tlv4.nwkAddress), tlv4.fragmentationOptions!, ...uint16To8Array(tlv4.maxIncomingTransferUnit!)]) + ); + }); + + it('buildPowerDescriptorRequest', () => { + expect(BuffaloZdo.buildPowerDescriptorRequest(NODE_ID1)).toStrictEqual(Buffer.from([0, ...NODE_ID1_BYTES])); + }); + + it('buildSimpleDescriptorRequest', () => { + expect(BuffaloZdo.buildSimpleDescriptorRequest(NODE_ID1, 3)).toStrictEqual(Buffer.from([0, ...NODE_ID1_BYTES, 3])); + }); + + it('buildActiveEndpointsRequest', () => { + expect(BuffaloZdo.buildActiveEndpointsRequest(NODE_ID1)).toStrictEqual(Buffer.from([0, ...NODE_ID1_BYTES])); + }); + + it('buildMatchDescriptorRequest', () => { + expect( + BuffaloZdo.buildMatchDescriptorRequest(NODE_ID1, ZSpec.HA_PROFILE_ID, CLUSTER_LIST1, CLUSTER_LIST2) + ).toStrictEqual( + Buffer.from([0, ...NODE_ID1_BYTES, ...uint16To8Array(ZSpec.HA_PROFILE_ID), CLUSTER_LIST1.length, ...CLUSTER_LIST1_BYTES, CLUSTER_LIST2.length, ...CLUSTER_LIST2_BYTES]) + ); + expect( + BuffaloZdo.buildMatchDescriptorRequest(NODE_ID1, ZSpec.HA_PROFILE_ID, CLUSTER_LIST1, []) + ).toStrictEqual( + Buffer.from([0, ...NODE_ID1_BYTES, ...uint16To8Array(ZSpec.HA_PROFILE_ID), CLUSTER_LIST1.length, ...CLUSTER_LIST1_BYTES, 0]) + ); + }); + + it('buildSystemServiceDiscoveryRequest', () => { + expect(BuffaloZdo.buildSystemServiceDiscoveryRequest({ + primaryTrustCenter: 1, backupTrustCenter: 0, deprecated1: 0, deprecated2: 0, deprecated3: 0, deprecated4: 0, + networkManager: 0, reserved1: 0, reserved2: 0, stackComplianceResivion: 0 + })).toStrictEqual(Buffer.from([0, ...uint16To8Array(0b0000000000000001)])); + expect(BuffaloZdo.buildSystemServiceDiscoveryRequest({ + primaryTrustCenter: 1, backupTrustCenter: 0, deprecated1: 0, deprecated2: 0, deprecated3: 0, deprecated4: 0, + networkManager: 1, reserved1: 0, reserved2: 0, stackComplianceResivion: 23 + })).toStrictEqual(Buffer.from([0, ...uint16To8Array(0b0010111001000001)])); + }); + + it('buildParentAnnounce', () => { + const children = [IEEE_ADDRESS1, IEEE_ADDRESS2]; + expect(BuffaloZdo.buildParentAnnounce(children)).toStrictEqual(Buffer.from([0, children.length, ...IEEE_ADDRESS1_BYTES, ...IEEE_ADDRESS2_BYTES])); + }); + + it('buildBindRequest', () => { + expect( + BuffaloZdo.buildBindRequest(IEEE_ADDRESS1, 2, Zcl.Clusters.seMetering.ID, Zdo.UNICAST_BINDING, IEEE_ADDRESS2, 123, 64) + ).toStrictEqual(Buffer.from([0, ...IEEE_ADDRESS1_BYTES, 2, ...uint16To8Array(Zcl.Clusters.seMetering.ID), Zdo.UNICAST_BINDING, ...IEEE_ADDRESS2_BYTES, 64])); + expect( + BuffaloZdo.buildBindRequest(IEEE_ADDRESS1, 3, Zcl.Clusters.seMetering.ID, Zdo.MULTICAST_BINDING, IEEE_ADDRESS2, 123, 64) + ).toStrictEqual(Buffer.from([0, ...IEEE_ADDRESS1_BYTES, 3, ...uint16To8Array(Zcl.Clusters.seMetering.ID), Zdo.MULTICAST_BINDING, ...uint16To8Array(123)])); + }); + + it('buildUnbindRequest', () => { + expect( + BuffaloZdo.buildUnbindRequest(IEEE_ADDRESS1, 2, Zcl.Clusters.seMetering.ID, Zdo.UNICAST_BINDING, IEEE_ADDRESS2, 123, 64) + ).toStrictEqual(Buffer.from([0, ...IEEE_ADDRESS1_BYTES, 2, ...uint16To8Array(Zcl.Clusters.seMetering.ID), Zdo.UNICAST_BINDING, ...IEEE_ADDRESS2_BYTES, 64])); + expect( + BuffaloZdo.buildUnbindRequest(IEEE_ADDRESS1, 3, Zcl.Clusters.seMetering.ID, Zdo.MULTICAST_BINDING, IEEE_ADDRESS2, 123, 64) + ).toStrictEqual(Buffer.from([0, ...IEEE_ADDRESS1_BYTES, 3, ...uint16To8Array(Zcl.Clusters.seMetering.ID), Zdo.MULTICAST_BINDING, ...uint16To8Array(123)])); + }); + + it('Throws when buildBindRequest/buildUnbindRequest invalid type', () => { + expect(() => { + BuffaloZdo.buildBindRequest(IEEE_ADDRESS1, 2, Zcl.Clusters.seMetering.ID, 99, IEEE_ADDRESS2, 123, 64) + }).toThrow(`Status 'NOT_SUPPORTED'`); + expect(() => { + BuffaloZdo.buildUnbindRequest(IEEE_ADDRESS1, 2, Zcl.Clusters.seMetering.ID, 99, IEEE_ADDRESS2, 123, 64) + }).toThrow(`Status 'NOT_SUPPORTED'`); + }); + + it('buildClearAllBindingsRequest', () => { + const eui64List = [IEEE_ADDRESS1, IEEE_ADDRESS2]; + expect( + BuffaloZdo.buildClearAllBindingsRequest({eui64List} as ClearAllBindingsReqEUI64TLV) + ).toStrictEqual( + Buffer.from([0, 0, ZSpec.EUI64_SIZE * eui64List.length + 1 - 1, eui64List.length, ...IEEE_ADDRESS1_BYTES, ...IEEE_ADDRESS2_BYTES]) + ); + expect( + BuffaloZdo.buildClearAllBindingsRequest({eui64List: []} as ClearAllBindingsReqEUI64TLV) + ).toStrictEqual( + Buffer.from([0, 0, 0, 0]) + ); + }); + + it('buildLqiTableRequest', () => { + expect(BuffaloZdo.buildLqiTableRequest(1)).toStrictEqual(Buffer.from([0, 1])); + expect(BuffaloZdo.buildLqiTableRequest(254)).toStrictEqual(Buffer.from([0, 254])); + }); + + it('buildRoutingTableRequest', () => { + expect(BuffaloZdo.buildRoutingTableRequest(1)).toStrictEqual(Buffer.from([0, 1])); + expect(BuffaloZdo.buildRoutingTableRequest(254)).toStrictEqual(Buffer.from([0, 254])); + }); + + it('buildBindingTableRequest', () => { + expect(BuffaloZdo.buildBindingTableRequest(1)).toStrictEqual(Buffer.from([0, 1])); + expect(BuffaloZdo.buildBindingTableRequest(254)).toStrictEqual(Buffer.from([0, 254])); + }); + + it('buildLeaveRequest', () => { + expect( + BuffaloZdo.buildLeaveRequest(IEEE_ADDRESS2, Zdo.LeaveRequestFlags.WITHOUT_REJOIN) + ).toStrictEqual(Buffer.from([0, ...IEEE_ADDRESS2_BYTES, Zdo.LeaveRequestFlags.WITHOUT_REJOIN])); + expect( + BuffaloZdo.buildLeaveRequest(IEEE_ADDRESS2, Zdo.LeaveRequestFlags.AND_REJOIN) + ).toStrictEqual(Buffer.from([0, ...IEEE_ADDRESS2_BYTES, Zdo.LeaveRequestFlags.AND_REJOIN])); + }); + + it('buildPermitJoining', () => { + expect(BuffaloZdo.buildPermitJoining(254, 1, [])).toStrictEqual(Buffer.from([0, 254, 1])); + + const tlvs = [{ + tagId: Zdo.GlobalTLV.BEACON_APPENDIX_ENCAPSULATION, + length: 4, + tlv: { + additionalTLVs: [{tagId: Zdo.GlobalTLV.FRAGMENTATION_PARAMETERS, length: 2, tlv: {nwkAddress: NODE_ID1}}] + }, + }]; + expect( + BuffaloZdo.buildPermitJoining(255, 1, tlvs) + ).toStrictEqual(Buffer.from([0, 255, 1, Zdo.GlobalTLV.BEACON_APPENDIX_ENCAPSULATION, 4 - 1, Zdo.GlobalTLV.FRAGMENTATION_PARAMETERS, 2 - 1, ...NODE_ID1_BYTES])); + }); + + it('buildScanChannelsRequest', () => { + expect( + BuffaloZdo.buildScanChannelsRequest(ZSpec.ALL_802_15_4_CHANNELS, 3, 3) + ).toStrictEqual(Buffer.from([0, ...uint32To8Array(ZSpec.ALL_802_15_4_CHANNELS_MASK), 3, 3])); + expect( + BuffaloZdo.buildScanChannelsRequest(ZSpec.ALL_802_15_4_CHANNELS, 64, 3) + ).toStrictEqual(Buffer.from([0, ...uint32To8Array(ZSpec.ALL_802_15_4_CHANNELS_MASK), 64/*, 3*/])); + }); + + it('buildChannelChangeRequest', () => { + expect(BuffaloZdo.buildChannelChangeRequest(15, 1)).toStrictEqual(Buffer.from([0, ...uint32To8Array(ZSpec.Utils.channelsToUInt32Mask([15])), 0xFE, 1])); + }); + + it('buildSetActiveChannelsAndNwkManagerIdRequest', () => { + expect( + BuffaloZdo.buildSetActiveChannelsAndNwkManagerIdRequest(ZSpec.PREFERRED_802_15_4_CHANNELS, 3, 123) + ).toStrictEqual(Buffer.from([0, ...uint32To8Array(ZSpec.PREFERRED_802_15_4_CHANNELS_MASK), 0xFF, 3, ...uint16To8Array(123)])); + }); + + it('buildEnhancedScanChannelsRequest', () => { + const channelPages = [123, 54394, 29344]; + expect( + BuffaloZdo.buildEnhancedScanChannelsRequest(channelPages, 5, 3, 1) + ).toStrictEqual( + Buffer.from([ + 0, channelPages.length, + ...uint32To8Array(channelPages[0]), ...uint32To8Array(channelPages[1]), ...uint32To8Array(channelPages[2]), + 5, 3, 1 + ]) + ); + expect( + BuffaloZdo.buildEnhancedScanChannelsRequest(channelPages, 6, 3, 1) + ).toStrictEqual( + Buffer.from([ + 0, channelPages.length, + ...uint32To8Array(channelPages[0]), ...uint32To8Array(channelPages[1]), ...uint32To8Array(channelPages[2]), + 6/*, 3*/, 1 + ]) + ); + }); + + it('buildEnhancedChannelChangeRequest', () => { + const channelPage = 54394; + expect( + BuffaloZdo.buildEnhancedChannelChangeRequest(channelPage, 3, 1) + ).toStrictEqual(Buffer.from([0, 1, ...uint32To8Array(channelPage), 0xFE, 3, 1])); + }); + + it('buildEnhancedSetActiveChannelsAndNwkManagerIdRequest', () => { + const channelPages = [123, 54394, 29344]; + const nwkManagerAddr = 0xfe01; + expect( + BuffaloZdo.buildEnhancedSetActiveChannelsAndNwkManagerIdRequest(channelPages, 2, nwkManagerAddr, 1) + ).toStrictEqual( + Buffer.from([ + 0, channelPages.length, + ...uint32To8Array(channelPages[0]), ...uint32To8Array(channelPages[1]), ...uint32To8Array(channelPages[2]), + 0xFF, 2, ...uint16To8Array(nwkManagerAddr), 1 + ]) + ); + }); + + it('buildNwkIEEEJoiningListRequest', () => { + expect(BuffaloZdo.buildNwkIEEEJoiningListRequest(3)).toStrictEqual(Buffer.from([0, 3])); + + }); + + it('buildNwkBeaconSurveyRequest', () => { + const tlv: BeaconSurveyConfigurationTLV = { + scanChannelList: [], + configurationBitmask: 0 + }; + expect(BuffaloZdo.buildNwkBeaconSurveyRequest(tlv)).toStrictEqual(Buffer.from([0, 0, 2 - 1, 0, 0])); + const tlv2: BeaconSurveyConfigurationTLV = { + scanChannelList: [34252], + configurationBitmask: 1 + }; + expect(BuffaloZdo.buildNwkBeaconSurveyRequest(tlv2)).toStrictEqual(Buffer.from([0, 0, 6 - 1, 1, ...uint32To8Array(tlv2.scanChannelList[0]), 1])); + const tlv3: BeaconSurveyConfigurationTLV = { + scanChannelList: [34252, 123], + configurationBitmask: 1 + }; + expect( + BuffaloZdo.buildNwkBeaconSurveyRequest(tlv3) + ).toStrictEqual( + Buffer.from([0, 0, 10 - 1, 2, ...uint32To8Array(tlv3.scanChannelList[0]), ...uint32To8Array(tlv3.scanChannelList[1]), 1]) + ); + }); + + it('buildStartKeyNegotiationRequest', () => { + const tlv: Curve25519PublicPointTLV = { + eui64: IEEE_ADDRESS1, + publicPoint: Buffer.alloc(Zdo.CURVE_PUBLIC_POINT_SIZE).fill(0xCD), + }; + expect( + BuffaloZdo.buildStartKeyNegotiationRequest(tlv) + ).toStrictEqual( + Buffer.from([0, 0, ZSpec.EUI64_SIZE + Zdo.CURVE_PUBLIC_POINT_SIZE - 1, ...IEEE_ADDRESS1_BYTES, ...tlv.publicPoint] + )); + const tlv2: Curve25519PublicPointTLV = { + eui64: IEEE_ADDRESS2, + publicPoint: Buffer.alloc(Zdo.CURVE_PUBLIC_POINT_SIZE).fill(0x3C), + }; + expect( + BuffaloZdo.buildStartKeyNegotiationRequest(tlv2) + ).toStrictEqual( + Buffer.from([0, 0, ZSpec.EUI64_SIZE + Zdo.CURVE_PUBLIC_POINT_SIZE - 1, ...IEEE_ADDRESS2_BYTES, ...tlv2.publicPoint] + )); + }); + + it('buildRetrieveAuthenticationTokenRequest', () => { + const tlv: AuthenticationTokenIdTLV = {tlvTypeTagId: 0}; + expect(BuffaloZdo.buildRetrieveAuthenticationTokenRequest(tlv)).toStrictEqual(Buffer.from([0, 0, 1 - 1, 0])); + const tlv2: AuthenticationTokenIdTLV = {tlvTypeTagId: 31}; + expect(BuffaloZdo.buildRetrieveAuthenticationTokenRequest(tlv2)).toStrictEqual(Buffer.from([0, 0, 1 - 1, 31])); + }); + + it('buildGetAuthenticationLevelRequest', () => { + const tlv: TargetIEEEAddressTLV = {ieee: IEEE_ADDRESS2}; + expect(BuffaloZdo.buildGetAuthenticationLevelRequest(tlv)).toStrictEqual(Buffer.from([0, 0, ZSpec.EUI64_SIZE - 1, ...IEEE_ADDRESS2_BYTES])); + }); + + it.each([ + {panId: 0xFEEF, channel: 0, configurationParameters: 0}, + {panId: 0x1234, channel: 15, configurationParameters: 0}, + {panId: 0x1234, channel: 0, configurationParameters: 1}, + {panId: 0x6543, channel: 45, configurationParameters: 1}, + ])('buildSetConfigurationRequest', ({panId, channel, configurationParameters}) => { + expect( + BuffaloZdo.buildSetConfigurationRequest( + {panId} as NextPanIdChangeGlobalTLV, + {channel} as NextChannelChangeGlobalTLV, + {configurationParameters} as ConfigurationParametersGlobalTLV + ) + ).toStrictEqual( + Buffer.from([ + 0, + Zdo.GlobalTLV.NEXT_PAN_ID_CHANGE, ZSpec.PAN_ID_SIZE - 1, ...uint16To8Array(panId), + Zdo.GlobalTLV.NEXT_CHANNEL_CHANGE, 4 - 1, ...uint32To8Array(channel), + Zdo.GlobalTLV.CONFIGURATION_PARAMETERS, 2 - 1, ...uint16To8Array(configurationParameters) + ]) + ); + }); + + it('buildGetConfigurationRequest', () => { + expect(BuffaloZdo.buildGetConfigurationRequest([84])).toStrictEqual(Buffer.from([0, 1, 84])); + expect(BuffaloZdo.buildGetConfigurationRequest([67, 71])).toStrictEqual(Buffer.from([0, 2, 67, 71])); + }); + + it('buildStartKeyUpdateRequest', () => { + const method: SelectedKeyNegotiationMethodTLV = { + protocol: Zdo.SelectedKeyNegotiationProtocol.SPEKE_CURVE25519_SHA256, + presharedSecret: Zdo.SelectedPreSharedSecret.BASIC_AUTHORIZATION_KEY, + sendingDeviceEui64: IEEE_ADDRESS2, + }; + const params: FragmentationParametersGlobalTLV = { + nwkAddress: NODE_ID1, + fragmentationOptions: 1, + maxIncomingTransferUnit: 2345 + }; + expect( + BuffaloZdo.buildStartKeyUpdateRequest(method, params) + ).toStrictEqual( + Buffer.from([ + 0, + 0, ZSpec.EUI64_SIZE + 2 - 1, method.protocol, method.presharedSecret, ...IEEE_ADDRESS2_BYTES, + Zdo.GlobalTLV.FRAGMENTATION_PARAMETERS, 5 - 1, ...NODE_ID1_BYTES, params.fragmentationOptions!, ...uint16To8Array(params.maxIncomingTransferUnit!) + ]) + ); + }); + + it('buildDecommissionRequest', () => { + const tlv: DeviceEUI64ListTLV = { + eui64List: [IEEE_ADDRESS1] + }; + expect( + BuffaloZdo.buildDecommissionRequest(tlv) + ).toStrictEqual( + Buffer.from([0, 0, ZSpec.EUI64_SIZE * tlv.eui64List.length + 1 - 1, tlv.eui64List.length, ...IEEE_ADDRESS1_BYTES]) + ); + + const tlv2: DeviceEUI64ListTLV = { + eui64List: [IEEE_ADDRESS2, IEEE_ADDRESS1] + }; + expect( + BuffaloZdo.buildDecommissionRequest(tlv2) + ).toStrictEqual( + Buffer.from([0, 0, ZSpec.EUI64_SIZE * tlv2.eui64List.length + 1 - 1, tlv2.eui64List.length, ...IEEE_ADDRESS2_BYTES, ...IEEE_ADDRESS1_BYTES]) + ); + + const tlv3: DeviceEUI64ListTLV = { + eui64List: [] + }; + expect( + BuffaloZdo.buildDecommissionRequest(tlv3) + ).toStrictEqual( + Buffer.from([0, 0, ZSpec.EUI64_SIZE * tlv3.eui64List.length + 1 - 1, tlv3.eui64List.length]) + ); + }); + + it('buildChallengeRequest', () => { + const tlv: APSFrameCounterChallengeTLV = { + senderEui64: IEEE_ADDRESS2, + challengeValue: Buffer.alloc(Zdo.CHALLENGE_VALUE_SIZE).fill(0xFE), + }; + expect( + BuffaloZdo.buildChallengeRequest(tlv) + ).toStrictEqual( + Buffer.from([0, 0, ZSpec.EUI64_SIZE + Zdo.CHALLENGE_VALUE_SIZE - 1, ...IEEE_ADDRESS2_BYTES, ...tlv.challengeValue]) + ); + + const tlv2: APSFrameCounterChallengeTLV = { + senderEui64: IEEE_ADDRESS1, + challengeValue: Buffer.from([0xFE, 0xAC, 0x12, 0x23, 0x85, 0x8C, 0x7C, 0xA3]), + }; + expect( + BuffaloZdo.buildChallengeRequest(tlv2) + ).toStrictEqual( + Buffer.from([0, 0, ZSpec.EUI64_SIZE + Zdo.CHALLENGE_VALUE_SIZE - 1, ...IEEE_ADDRESS1_BYTES, ...tlv2.challengeValue]) + ); + }); + + it.each([ + 'readNetworkAddressResponse', + 'readIEEEAddressResponse', + 'readNodeDescriptorResponse', + 'readPowerDescriptorResponse', + 'readSimpleDescriptorResponse', + 'readActiveEndpointsResponse', + 'readMatchDescriptorsResponse', + 'readSystemServerDiscoveryResponse', + 'readParentAnnounceResponse', + 'readBindResponse', + 'readUnbindResponse', + 'readClearAllBindingsResponse', + 'readLQITableResponse', + 'readRoutingTableResponse', + 'readBindingTableResponse', + 'readLeaveResponse', + 'readPermitJoiningResponse', + 'readNwkUpdateResponse', + 'readNwkEnhancedUpdateResponse', + 'readNwkIEEEJoiningListResponse', + 'readNwkUnsolicitedEnhancedUpdateResponse', + 'readNwkBeaconSurveyResponse', + 'readStartKeyNegotiationResponse', + 'readRetrieveAuthenticationTokenResponse', + 'readGetAuthenticationLevelResponse', + 'readSetConfigurationResponse', + 'readGetConfigurationResponse', + 'readStartKeyUpdateResponse', + 'readDecommissionResponse', + 'readChallengeResponse', + ])('Throws status error when reading unsuccessful response for %s', (func) => { + const buffalo = new BuffaloZdo(Buffer.from([Zdo.Status.INV_REQUESTTYPE, 1, 2, 3])); + expect(() => { + buffalo[func](); + }).toThrow(new Zdo.StatusError(Zdo.Status.INV_REQUESTTYPE)); + }); + + it.each([ + [Zdo.ClusterId.NETWORK_ADDRESS_RESPONSE, 'readNetworkAddressResponse'], + [Zdo.ClusterId.IEEE_ADDRESS_RESPONSE, 'readIEEEAddressResponse'], + [Zdo.ClusterId.NODE_DESCRIPTOR_RESPONSE, 'readNodeDescriptorResponse'], + [Zdo.ClusterId.POWER_DESCRIPTOR_RESPONSE, 'readPowerDescriptorResponse'], + [Zdo.ClusterId.SIMPLE_DESCRIPTOR_RESPONSE, 'readSimpleDescriptorResponse'], + [Zdo.ClusterId.ACTIVE_ENDPOINTS_RESPONSE, 'readActiveEndpointsResponse'], + [Zdo.ClusterId.MATCH_DESCRIPTORS_RESPONSE, 'readMatchDescriptorsResponse'], + [Zdo.ClusterId.END_DEVICE_ANNOUNCE, 'readEndDeviceAnnounce'], + [Zdo.ClusterId.SYSTEM_SERVER_DISCOVERY_RESPONSE, 'readSystemServerDiscoveryResponse'], + [Zdo.ClusterId.PARENT_ANNOUNCE_RESPONSE, 'readParentAnnounceResponse'], + [Zdo.ClusterId.BIND_RESPONSE, 'readBindResponse'], + [Zdo.ClusterId.UNBIND_RESPONSE, 'readUnbindResponse'], + [Zdo.ClusterId.CLEAR_ALL_BINDINGS_RESPONSE, 'readClearAllBindingsResponse'], + [Zdo.ClusterId.LQI_TABLE_RESPONSE, 'readLQITableResponse'], + [Zdo.ClusterId.ROUTING_TABLE_RESPONSE, 'readRoutingTableResponse'], + [Zdo.ClusterId.BINDING_TABLE_RESPONSE, 'readBindingTableResponse'], + [Zdo.ClusterId.LEAVE_RESPONSE, 'readLeaveResponse'], + [Zdo.ClusterId.PERMIT_JOINING_RESPONSE, 'readPermitJoiningResponse'], + [Zdo.ClusterId.NWK_UPDATE_RESPONSE, 'readNwkUpdateResponse'], + [Zdo.ClusterId.NWK_ENHANCED_UPDATE_RESPONSE, 'readNwkEnhancedUpdateResponse'], + [Zdo.ClusterId.NWK_IEEE_JOINING_LIST_REPONSE, 'readNwkIEEEJoiningListResponse'], + [Zdo.ClusterId.NWK_UNSOLICITED_ENHANCED_UPDATE_RESPONSE, 'readNwkUnsolicitedEnhancedUpdateResponse'], + [Zdo.ClusterId.NWK_BEACON_SURVEY_RESPONSE, 'readNwkBeaconSurveyResponse'], + [Zdo.ClusterId.START_KEY_NEGOTIATION_RESPONSE, 'readStartKeyNegotiationResponse'], + [Zdo.ClusterId.RETRIEVE_AUTHENTICATION_TOKEN_RESPONSE, 'readRetrieveAuthenticationTokenResponse'], + [Zdo.ClusterId.GET_AUTHENTICATION_LEVEL_RESPONSE, 'readGetAuthenticationLevelResponse'], + [Zdo.ClusterId.SET_CONFIGURATION_RESPONSE, 'readSetConfigurationResponse'], + [Zdo.ClusterId.GET_CONFIGURATION_RESPONSE, 'readGetConfigurationResponse'], + [Zdo.ClusterId.START_KEY_UPDATE_RESPONSE, 'readStartKeyUpdateResponse'], + [Zdo.ClusterId.DECOMMISSION_RESPONSE, 'readDecommissionResponse'], + [Zdo.ClusterId.CHALLENGE_RESPONSE, 'readChallengeResponse'], + ])('Reads response by cluster ID %s', (clusterId, func) => { + // @ts-expect-error TS typing is lost here ;( + const readSpy = jest.spyOn(BuffaloZdo.prototype, func).mockImplementationOnce(jest.fn());// passing bogus data, don't want to actually call it + BuffaloZdo.readResponse(clusterId, Buffer.from([123])); + expect(readSpy).toHaveBeenCalledTimes(1); + }); + + it('Throws when reading unknown cluster ID', () => { + const clusterId = Zdo.ClusterId.ACTIVE_ENDPOINTS_REQUEST; + + expect(() => { + BuffaloZdo.readResponse(clusterId, Buffer.from([123])); + }).toThrow(`Unsupported response reading for cluster ID '${clusterId}'.`); + }) + + it('readNetworkAddressResponse', () => { + const buffer = Buffer.from([Zdo.Status.SUCCESS, ...IEEE_ADDRESS1_BYTES, ...NODE_ID1_BYTES]); + expect(new BuffaloZdo(buffer).readNetworkAddressResponse()).toStrictEqual({ + eui64: IEEE_ADDRESS1, + nwkAddress: NODE_ID1, + startIndex: 0, + assocDevList: [], + } as NetworkAddressResponse); + const bufferWAssoc = Buffer.from([Zdo.Status.SUCCESS, ...IEEE_ADDRESS2_BYTES, ...NODE_ID1_BYTES, 2, 3, ...uint16To8Array(123), ...uint16To8Array(52523)]); + expect(new BuffaloZdo(bufferWAssoc).readNetworkAddressResponse()).toStrictEqual({ + eui64: IEEE_ADDRESS2, + nwkAddress: NODE_ID1, + startIndex: 3, + assocDevList: [123, 52523], + } as NetworkAddressResponse); + }); + + it('readIEEEAddressResponse', () => { + const buffer = Buffer.from([Zdo.Status.SUCCESS, ...IEEE_ADDRESS1_BYTES, ...NODE_ID1_BYTES]); + expect(new BuffaloZdo(buffer).readIEEEAddressResponse()).toStrictEqual({ + eui64: IEEE_ADDRESS1, + nwkAddress: NODE_ID1, + startIndex: 0, + assocDevList: [], + } as IEEEAddressResponse); + const bufferWAssoc = Buffer.from([Zdo.Status.SUCCESS, ...IEEE_ADDRESS2_BYTES, ...NODE_ID1_BYTES, 2, 3, ...uint16To8Array(123), ...uint16To8Array(52523)]); + expect(new BuffaloZdo(bufferWAssoc).readIEEEAddressResponse()).toStrictEqual({ + eui64: IEEE_ADDRESS2, + nwkAddress: NODE_ID1, + startIndex: 3, + assocDevList: [123, 52523], + } as IEEEAddressResponse); + }); + + it('readNodeDescriptorResponse', () => { + const buffer = Buffer.from([ + Zdo.Status.SUCCESS, ...NODE_ID1_BYTES, + 0b00100010, 0b00100000, 0b00001110, + ...uint16To8Array(Zcl.ManufacturerCode.BARACODA_SA), 0x7c, ...uint16To8Array(0x7eff), + ...uint16To8Array(SERVER_MASK_R22_BYTE), ...uint16To8Array(0x3cff), 0 + ]); + expect(new BuffaloZdo(buffer).readNodeDescriptorResponse()).toStrictEqual({ + nwkAddress: NODE_ID1, + logicalType: 0b010, + fragmentationSupported: null, + apsFlags: 0, + frequencyBand: 0b00100, + capabilities: { + alternatePANCoordinator: 0, + deviceType: 1, + powerSource: 1, + rxOnWhenIdle: 1, + reserved1: 0, + reserved2: 0, + securityCapability: 0, + allocateAddress: 0, + }, + manufacturerCode: Zcl.ManufacturerCode.BARACODA_SA, + maxBufSize: 0x7c, + maxIncTxSize: 0x7eff, + serverMask: SERVER_MASK_R22, + maxOutTxSize: 0x3cff, + deprecated1: 0, + tlvs: [], + } as NodeDescriptorResponse); + + const tlv: FragmentationParametersGlobalTLV = { + nwkAddress: NODE_ID1, + fragmentationOptions: 1, + maxIncomingTransferUnit: 65352, + }; + const buffer2 = Buffer.from([ + Zdo.Status.SUCCESS, ...NODE_ID1_BYTES, + 0b00100010, 0b00100000, 0b00001110, + ...uint16To8Array(Zcl.ManufacturerCode.BEIJING_RUYING_TECH_LIMITED), 0x3a, ...uint16To8Array(0x7cff), + ...uint16To8Array(SERVER_MASK_R23_BYTE), ...uint16To8Array(0x11ff), 0, + Zdo.GlobalTLV.FRAGMENTATION_PARAMETERS, 5 - 1, ...uint16To8Array(tlv.nwkAddress), tlv.fragmentationOptions!, ...uint16To8Array(tlv.maxIncomingTransferUnit!) + ]); + expect(new BuffaloZdo(buffer2).readNodeDescriptorResponse()).toStrictEqual({ + nwkAddress: NODE_ID1, + logicalType: 0b010, + fragmentationSupported: true, + apsFlags: 0, + frequencyBand: 0b00100, + capabilities: { + alternatePANCoordinator: 0, + deviceType: 1, + powerSource: 1, + rxOnWhenIdle: 1, + reserved1: 0, + reserved2: 0, + securityCapability: 0, + allocateAddress: 0, + }, + manufacturerCode: Zcl.ManufacturerCode.BEIJING_RUYING_TECH_LIMITED, + maxBufSize: 0x3a, + maxIncTxSize: 0x7cff, + serverMask: SERVER_MASK_R23, + maxOutTxSize: 0x11ff, + deprecated1: 0, + tlvs: [{tagId: Zdo.GlobalTLV.FRAGMENTATION_PARAMETERS, length: 5, tlv}], + } as NodeDescriptorResponse); + }); + + it('readPowerDescriptorResponse', () => { + const buffer = Buffer.from([ + Zdo.Status.SUCCESS, ...NODE_ID1_BYTES, 0b10100100, 0b11110100 + ]); + expect(new BuffaloZdo(buffer).readPowerDescriptorResponse()).toStrictEqual({ + nwkAddress: NODE_ID1, + currentPowerMode: 0b0100, + availPowerSources: 0b1010, + currentPowerSource: 0b0100, + currentPowerSourceLevel: 0b1111, + } as PowerDescriptorResponse); + }); + + it('readSimpleDescriptorResponse', () => { + const buffer = Buffer.from([ + Zdo.Status.SUCCESS, ...NODE_ID1_BYTES, 8, + ZSpec.HA_ENDPOINT, ...uint16To8Array(ZSpec.HA_PROFILE_ID), ...uint16To8Array(123), 0, + 2, ...uint16To8Array(7653), ...uint16To8Array(624), 1, ...uint16To8Array(5322) + ]); + expect(new BuffaloZdo(Buffer.from(buffer)).readSimpleDescriptorResponse()).toStrictEqual({ + nwkAddress: NODE_ID1, + endpoint: ZSpec.HA_ENDPOINT, + profileId: ZSpec.HA_PROFILE_ID, + deviceId: 123, + deviceVersion: 0, + inClusterList: [7653, 624], + outClusterList: [5322], + } as SimpleDescriptorResponse); + const bufferEmpty = Buffer.from([ + Zdo.Status.SUCCESS, ...NODE_ID1_BYTES, 8, + ZSpec.HA_ENDPOINT, ...uint16To8Array(ZSpec.HA_PROFILE_ID), ...uint16To8Array(123), 0, 0, 0 + ]); + expect(new BuffaloZdo(Buffer.from(bufferEmpty)).readSimpleDescriptorResponse()).toStrictEqual({ + nwkAddress: NODE_ID1, + endpoint: ZSpec.HA_ENDPOINT, + profileId: ZSpec.HA_PROFILE_ID, + deviceId: 123, + deviceVersion: 0, + inClusterList: [], + outClusterList: [], + } as SimpleDescriptorResponse); + }); + + it('readActiveEndpointsResponse', () => { + const buffer = Buffer.from([Zdo.Status.SUCCESS, ...NODE_ID1_BYTES, 2, 0xEF, 0x87]); + expect(new BuffaloZdo(Buffer.from(buffer)).readActiveEndpointsResponse()).toStrictEqual({ + nwkAddress: NODE_ID1, + endpointList: [0xEF, 0x87], + } as ActiveEndpointsResponse); + const bufferEmpty = Buffer.from([Zdo.Status.SUCCESS, ...NODE_ID1_BYTES, 0]); + expect(new BuffaloZdo(Buffer.from(bufferEmpty)).readActiveEndpointsResponse()).toStrictEqual({ + nwkAddress: NODE_ID1, + endpointList: [], + } as ActiveEndpointsResponse); + }); + + it('readMatchDescriptorsResponse', () => { + const buffer = Buffer.from([Zdo.Status.SUCCESS, ...NODE_ID1_BYTES, 2, 0xEF, 0x87]); + expect(new BuffaloZdo(Buffer.from(buffer)).readMatchDescriptorsResponse()).toStrictEqual({ + nwkAddress: NODE_ID1, + endpointList: [0xEF, 0x87], + } as MatchDescriptorsResponse); + const bufferEmpty = Buffer.from([Zdo.Status.SUCCESS, ...NODE_ID1_BYTES, 0]); + expect(new BuffaloZdo(Buffer.from(bufferEmpty)).readMatchDescriptorsResponse()).toStrictEqual({ + nwkAddress: NODE_ID1, + endpointList: [], + } as MatchDescriptorsResponse); + }); + + it('readEndDeviceAnnounce', () => { + const buffer = Buffer.from([...NODE_ID1_BYTES, ...IEEE_ADDRESS2_BYTES, 0b01000000]); + expect(new BuffaloZdo(Buffer.from(buffer)).readEndDeviceAnnounce()).toStrictEqual({ + nwkAddress: NODE_ID1, + eui64: IEEE_ADDRESS2, + capabilities: { + alternatePANCoordinator: 0, + deviceType: 0, + powerSource: 0, + rxOnWhenIdle: 0, + reserved1: 0, + reserved2: 0, + securityCapability: 1, + allocateAddress: 0, + }, + } as EndDeviceAnnounce); + }); + + it('readSystemServerDiscoveryResponse', () => { + const buffer = Buffer.from([Zdo.Status.SUCCESS, ...uint16To8Array(SERVER_MASK_R23_BYTE)]); + expect(new BuffaloZdo(Buffer.from(buffer)).readSystemServerDiscoveryResponse()).toStrictEqual({ + serverMask: SERVER_MASK_R23, + } as SystemServerDiscoveryResponse); + }); + + it('readParentAnnounceResponse', () => { + const buffer = Buffer.from([Zdo.Status.SUCCESS, 2, ...IEEE_ADDRESS2_BYTES, ...IEEE_ADDRESS1_BYTES]); + expect(new BuffaloZdo(Buffer.from(buffer)).readParentAnnounceResponse()).toStrictEqual({ + children: [IEEE_ADDRESS2, IEEE_ADDRESS1], + }); + const bufferEmpty = Buffer.from([Zdo.Status.SUCCESS, 0]); + expect(new BuffaloZdo(Buffer.from(bufferEmpty)).readParentAnnounceResponse()).toStrictEqual({ + children: [], + }); + }); + + it('readLQITableResponse', () => { + const buffer = Buffer.from([Zdo.Status.SUCCESS, 16, 3, 2, + ...EXT_PAN_ID2, ...IEEE_ADDRESS1_BYTES, ...NODE_ID2_BYTES, 0b00100101, 0b00000001, 1, 235, + ...EXT_PAN_ID1, ...IEEE_ADDRESS2_BYTES, ...NODE_ID1_BYTES, 0b01000010, 0b00000000, 1, 179 + ]); + expect(new BuffaloZdo(Buffer.from(buffer)).readLQITableResponse()).toStrictEqual({ + neighborTableEntries: 16, + startIndex: 3, + entryList: [ + { + extendedPanId: EXT_PAN_ID2, + eui64: IEEE_ADDRESS1, + nwkAddress: NODE_ID2, + deviceType: 1, + rxOnWhenIdle: 1, + relationship: 2, + reserved1: 0, + permitJoining: 1, + reserved2: 0, + depth: 1, + lqi: 235, + }, + { + extendedPanId: EXT_PAN_ID1, + eui64: IEEE_ADDRESS2, + nwkAddress: NODE_ID1, + deviceType: 2, + rxOnWhenIdle: 0, + relationship: 4, + reserved1: 0, + permitJoining: 0, + reserved2: 0, + depth: 1, + lqi: 179, + }, + ], + } as LQITableResponse); + + const bufferEmpty = Buffer.from([Zdo.Status.SUCCESS, 5, 4, 0]); + expect(new BuffaloZdo(Buffer.from(bufferEmpty)).readLQITableResponse()).toStrictEqual({ + neighborTableEntries: 5, + startIndex: 4, + entryList: [], + } as LQITableResponse); + }); + + it('readRoutingTableResponse', () => { + const buffer = Buffer.from([Zdo.Status.SUCCESS, 4, 3, 1, ...NODE_ID2_BYTES, 0b00101000, ...NODE_ID1_BYTES]); + expect(new BuffaloZdo(Buffer.from(buffer)).readRoutingTableResponse()).toStrictEqual({ + routingTableEntries: 4, + startIndex: 3, + entryList: [ + { + destinationAddress: NODE_ID2, + status: 0, + memoryConstrained: 1, + manyToOne: 0, + routeRecordRequired: 1, + reserved1: 0, + nextHopAddress: NODE_ID1, + }, + ], + } as RoutingTableResponse); + + const bufferEmpty = Buffer.from([Zdo.Status.SUCCESS, 0, 0, 0]); + expect(new BuffaloZdo(Buffer.from(bufferEmpty)).readRoutingTableResponse()).toStrictEqual({ + routingTableEntries: 0, + startIndex: 0, + entryList: [], + } as RoutingTableResponse); + }); + + it('readBindingTableResponse', () => { + const buffer = Buffer.from([ + Zdo.Status.SUCCESS, 1, 0, 3, + ...IEEE_ADDRESS1_BYTES, 0xf0, ...uint16To8Array(Zcl.Clusters.barrierControl.ID), 0x03, ...IEEE_ADDRESS2_BYTES, ZSpec.GP_ENDPOINT, + ...IEEE_ADDRESS2_BYTES, 0x34, ...uint16To8Array(Zcl.Clusters.closuresShadeCfg.ID), 0x01, ...NODE_ID2_BYTES, + ...IEEE_ADDRESS2_BYTES, 0xf4, ...uint16To8Array(Zcl.Clusters.genAnalogInput.ID), 0x02 + ]); + expect(new BuffaloZdo(Buffer.from(buffer)).readBindingTableResponse()).toStrictEqual({ + bindingTableEntries: 1, + startIndex: 0, + entryList: [ + { + sourceEui64: IEEE_ADDRESS1, + sourceEndpoint: 0xf0, + clusterId: Zcl.Clusters.barrierControl.ID, + destAddrMode: 0x03, + dest: IEEE_ADDRESS2, + destEndpoint: ZSpec.GP_ENDPOINT, + }, + { + sourceEui64: IEEE_ADDRESS2, + sourceEndpoint: 0x34, + clusterId: Zcl.Clusters.closuresShadeCfg.ID, + destAddrMode: 0x01, + dest: NODE_ID2, + destEndpoint: null, + }, + { + sourceEui64: IEEE_ADDRESS2, + sourceEndpoint: 0xf4, + clusterId: Zcl.Clusters.genAnalogInput.ID, + destAddrMode: 0x02, + dest: null, + destEndpoint: null, + }, + ], + } as BindingTableResponse); + + const bufferEmpty = Buffer.from([Zdo.Status.SUCCESS, 30, 2, 0]); + expect(new BuffaloZdo(Buffer.from(bufferEmpty)).readBindingTableResponse()).toStrictEqual({ + bindingTableEntries: 30, + startIndex: 2, + entryList: [], + } as BindingTableResponse); + }); + + it('readNwkUpdateResponse', () => { + const buffer = Buffer.from([Zdo.Status.SUCCESS, ...uint32To8Array(34732495), ...uint16To8Array(445), ...uint16To8Array(34), 3, 0x43, 0xff, 0x6f]); + expect(new BuffaloZdo(Buffer.from(buffer)).readNwkUpdateResponse()).toStrictEqual({ + scannedChannels: 34732495, + totalTransmissions: 445, + totalFailures: 34, + entryList: [0x43, 0xff, 0x6f], + } as NwkUpdateResponse); + }); + + it('readNwkEnhancedUpdateResponse', () => { + const buffer = Buffer.from([Zdo.Status.SUCCESS, ...uint32To8Array(34732495), ...uint16To8Array(445), ...uint16To8Array(34), 3, 0x43, 0xff, 0x6f]); + expect(new BuffaloZdo(Buffer.from(buffer)).readNwkEnhancedUpdateResponse()).toStrictEqual({ + scannedChannels: 34732495, + totalTransmissions: 445, + totalFailures: 34, + entryList: [0x43, 0xff, 0x6f], + } as NwkEnhancedUpdateResponse); + }); + + it('readNwkIEEEJoiningListResponse', () => { + const buffer = Buffer.from([Zdo.Status.SUCCESS, 3, Zdo.JoiningPolicy.IEEELIST_JOIN, 4, 0, 2, ...IEEE_ADDRESS2_BYTES, ...IEEE_ADDRESS1_BYTES]); + expect(new BuffaloZdo(Buffer.from(buffer)).readNwkIEEEJoiningListResponse()).toStrictEqual({ + updateId: 3, + joiningPolicy: Zdo.JoiningPolicy.IEEELIST_JOIN, + entryListTotal: 4, + startIndex: 0, + entryList: [IEEE_ADDRESS2, IEEE_ADDRESS1], + } as NwkIEEEJoiningListResponse); + const bufferEmpty = Buffer.from([Zdo.Status.SUCCESS, 0xff, Zdo.JoiningPolicy.ALL_JOIN, 0]); + expect(new BuffaloZdo(Buffer.from(bufferEmpty)).readNwkIEEEJoiningListResponse()).toStrictEqual({ + updateId: 0xff, + joiningPolicy: Zdo.JoiningPolicy.ALL_JOIN, + entryListTotal: 0, + startIndex: undefined, + entryList: undefined, + } as NwkIEEEJoiningListResponse); + }); + + it('readNwkUnsolicitedEnhancedUpdateResponse', () => { + const buffer = Buffer.from([ + Zdo.Status.SUCCESS, ...uint32To8Array(9023342), + ...uint16To8Array(3454), ...uint16To8Array(435), ...uint16To8Array(1239), 32 + ]); + expect(new BuffaloZdo(Buffer.from(buffer)).readNwkUnsolicitedEnhancedUpdateResponse()).toStrictEqual({ + channelInUse: 9023342, + macTxUCastTotal: 3454, + macTxUCastFailures: 435, + macTxUCastRetries: 1239, + timePeriod: 32, + } as NwkUnsolicitedEnhancedUpdateResponse); + }); + + it('readNwkBeaconSurveyResponse', () => { + const buffer = Buffer.from([ + Zdo.Status.SUCCESS, + 0x01, 4 - 1, 14, 7, 5, 2, + 0x02, 7 - 1, ...NODE_ID1_BYTES, 234, 1, ...NODE_ID2_BYTES, 223 + ]); + expect(new BuffaloZdo(Buffer.from(buffer)).readNwkBeaconSurveyResponse()).toStrictEqual({ + tlvs: [ + {tagId: 0x01, length: 4, tlv: { + totalBeaconsReceived: 14, + onNetworkBeacons: 7, + potentialParentBeacons: 5, + otherNetworkBeacons: 2, + } as BeaconSurveyResultsTLV}, + {tagId: 0x02, length: 7, tlv: { + currentParentNwkAddress: NODE_ID1, + currentParentLQA: 234, + entryCount: 1, + potentialParents: [{nwkAddress: NODE_ID2, lqa: 223}], + } as PotentialParentsTLV}, + ], + } as NwkBeaconSurveyResponse); + + const bufferEmpty = Buffer.from([Zdo.Status.SUCCESS]); + expect(new BuffaloZdo(Buffer.from(bufferEmpty)).readNwkBeaconSurveyResponse()).toStrictEqual({ + tlvs: [] + } as NwkBeaconSurveyResponse); + }); + + it('readStartKeyNegotiationResponse', () => { + const buffer = Buffer.from([ + Zdo.Status.SUCCESS, 0x00, ZSpec.EUI64_SIZE + Zdo.CURVE_PUBLIC_POINT_SIZE - 1, + ...IEEE_ADDRESS2_BYTES, ...Buffer.alloc(Zdo.CURVE_PUBLIC_POINT_SIZE).fill(0xAB) + ]); + expect(new BuffaloZdo(Buffer.from(buffer)).readStartKeyNegotiationResponse()).toStrictEqual({ + tlvs: [ + {tagId: 0x00, length: ZSpec.EUI64_SIZE + Zdo.CURVE_PUBLIC_POINT_SIZE, tlv: { + eui64: IEEE_ADDRESS2, + publicPoint: Buffer.alloc(Zdo.CURVE_PUBLIC_POINT_SIZE).fill(0xAB), + } as Curve25519PublicPointTLV}, + ], + }); + }); + + it('readRetrieveAuthenticationTokenResponse', () => { + // this one has no local TLV, so test with a global one + const tlv: FragmentationParametersGlobalTLV = { + nwkAddress: NODE_ID1, + fragmentationOptions: 1, + maxIncomingTransferUnit: 65352, + }; + const buffer = Buffer.from([ + Zdo.Status.SUCCESS, + Zdo.GlobalTLV.FRAGMENTATION_PARAMETERS, 5 - 1, ...uint16To8Array(tlv.nwkAddress), tlv.fragmentationOptions!, ...uint16To8Array(tlv.maxIncomingTransferUnit!) + ]); + expect(new BuffaloZdo(Buffer.from(buffer)).readRetrieveAuthenticationTokenResponse()).toStrictEqual({ + tlvs: [{tagId: Zdo.GlobalTLV.FRAGMENTATION_PARAMETERS, length: 5, tlv}], + }); + + const bufferEmpty = Buffer.from([Zdo.Status.SUCCESS]); + expect(new BuffaloZdo(Buffer.from(bufferEmpty)).readRetrieveAuthenticationTokenResponse()).toStrictEqual({ + tlvs: [], + }); + }); + + it('readGetAuthenticationLevelResponse', () => { + const buffer = Buffer.from([ + Zdo.Status.SUCCESS, + 0x00, 10 - 1, ...IEEE_ADDRESS2_BYTES, Zdo.InitialJoinMethod.INSTALL_CODE_KEY, Zdo.ActiveLinkKeyType.AUTHENTICATED_KEY_NEGOTIATION + ]); + expect(new BuffaloZdo(Buffer.from(buffer)).readGetAuthenticationLevelResponse()).toStrictEqual({ + tlvs: [ + {tagId: 0x00, length: 10, tlv: { + remoteNodeIeee: IEEE_ADDRESS2, + initialJoinMethod: Zdo.InitialJoinMethod.INSTALL_CODE_KEY, + activeLinkKeyType: Zdo.ActiveLinkKeyType.AUTHENTICATED_KEY_NEGOTIATION, + } as DeviceAuthenticationLevelTLV}, + ], + }); + }); + + it('readSetConfigurationResponse', () => { + const buffer = Buffer.from([ + Zdo.Status.SUCCESS, + 0x00, 7 - 1, 3, + 0x00, Zdo.Status.SUCCESS, + Zdo.GlobalTLV.CONFIGURATION_PARAMETERS, Zdo.Status.SUCCESS, + Zdo.GlobalTLV.ROUTER_INFORMATION, Zdo.Status.INV_REQUESTTYPE, + ]); + expect(new BuffaloZdo(Buffer.from(buffer)).readSetConfigurationResponse()).toStrictEqual({ + tlvs: [ + {tagId: 0x00, length: 7, tlv: { + count: 3, + tlvs: [ + {tagId: 0x00, processingStatus: Zdo.Status.SUCCESS}, + {tagId: Zdo.GlobalTLV.CONFIGURATION_PARAMETERS, processingStatus: Zdo.Status.SUCCESS}, + {tagId: Zdo.GlobalTLV.ROUTER_INFORMATION, processingStatus: Zdo.Status.INV_REQUESTTYPE}, + ], + } as ProcessingStatusTLV}, + ], + }); + + const bufferEmpty = Buffer.from([Zdo.Status.SUCCESS, 0x00, 1 - 1, 0]); + expect(new BuffaloZdo(Buffer.from(bufferEmpty)).readSetConfigurationResponse()).toStrictEqual({ + tlvs: [ + {tagId: 0x00, length: 1, tlv: { + count: 0, + tlvs: [], + } as ProcessingStatusTLV}, + ], + }); + }); + + it('readGetConfigurationResponse', () => { + // this one has no local TLV, so test with a global one + const tlv: FragmentationParametersGlobalTLV = { + nwkAddress: NODE_ID1, + fragmentationOptions: 1, + maxIncomingTransferUnit: 65352, + }; + const buffer = Buffer.from([ + Zdo.Status.SUCCESS, + Zdo.GlobalTLV.FRAGMENTATION_PARAMETERS, 5 - 1, ...uint16To8Array(tlv.nwkAddress), tlv.fragmentationOptions!, ...uint16To8Array(tlv.maxIncomingTransferUnit!) + ]); + expect(new BuffaloZdo(Buffer.from(buffer)).readGetConfigurationResponse()).toStrictEqual({ + tlvs: [{tagId: Zdo.GlobalTLV.FRAGMENTATION_PARAMETERS, length: 5, tlv}], + }); + + const bufferEmpty = Buffer.from([Zdo.Status.SUCCESS, ]); + expect(new BuffaloZdo(Buffer.from(bufferEmpty)).readGetConfigurationResponse()).toStrictEqual({ + tlvs: [], + }); + }); + + it('readChallengeResponse', () => { + const buffer = Buffer.from([ + Zdo.Status.SUCCESS, + 0x00, 32 - 1, ...IEEE_ADDRESS1_BYTES, ...Buffer.alloc(Zdo.CHALLENGE_VALUE_SIZE).fill(0x39), + ...uint32To8Array(4302952), ...uint32To8Array(12435682), ...Buffer.from([0xff, 0xfe, 0x34, 0x04, 0x49, 0x9f, 0x03, 0xbc]) + ]); + expect(new BuffaloZdo(Buffer.from(buffer)).readChallengeResponse()).toStrictEqual({ + tlvs: [ + {tagId: 0x00, length: 32, tlv: { + responderEui64: IEEE_ADDRESS1, + receivedChallengeValue: Buffer.alloc(Zdo.CHALLENGE_VALUE_SIZE).fill(0x39), + apsFrameCounter: 4302952, + challengeSecurityFrameCounter: 12435682, + mic: Buffer.from([0xff, 0xfe, 0x34, 0x04, 0x49, 0x9f, 0x03, 0xbc]), + } as APSFrameCounterResponseTLV} + ] + }); + + const bufferEmpty = Buffer.from([Zdo.Status.SUCCESS]); + expect(new BuffaloZdo(Buffer.from(bufferEmpty)).readChallengeResponse()).toStrictEqual({ + tlvs: [] + }); + }); + +}); diff --git a/test/zspec/zdo/utils.test.ts b/test/zspec/zdo/utils.test.ts new file mode 100644 index 0000000000..63d749a664 --- /dev/null +++ b/test/zspec/zdo/utils.test.ts @@ -0,0 +1,64 @@ +import * as Zdo from '../../../src/zspec/zdo'; + +describe('ZDO Utils', () => { + it('Creates status error', () => { + const zdoError = new Zdo.StatusError(Zdo.Status.INVALID_INDEX); + + expect(zdoError).toBeInstanceOf(Zdo.StatusError); + expect(zdoError.code).toStrictEqual(Zdo.Status.INVALID_INDEX); + expect(zdoError.message).toStrictEqual(`Status '${Zdo.Status[Zdo.Status.INVALID_INDEX]}'`); + }); + + it.each([ + [Zdo.ClusterId.ACTIVE_ENDPOINTS_REQUEST, Zdo.ClusterId.ACTIVE_ENDPOINTS_RESPONSE], + [Zdo.ClusterId.BINDING_TABLE_REQUEST, Zdo.ClusterId.BINDING_TABLE_RESPONSE], + [Zdo.ClusterId.CHALLENGE_REQUEST, Zdo.ClusterId.CHALLENGE_RESPONSE], + [Zdo.ClusterId.NODE_DESCRIPTOR_REQUEST, Zdo.ClusterId.NODE_DESCRIPTOR_RESPONSE], + [Zdo.ClusterId.NETWORK_ADDRESS_REQUEST, Zdo.ClusterId.NETWORK_ADDRESS_RESPONSE], + [Zdo.ClusterId.END_DEVICE_ANNOUNCE, null], + [Zdo.ClusterId.NWK_UNSOLICITED_ENHANCED_UPDATE_RESPONSE, null], + [Zdo.ClusterId.ACTIVE_ENDPOINTS_RESPONSE, null], + [Zdo.ClusterId.CHALLENGE_RESPONSE, null], + [0x7999, null], + ])('Gets response cluster ID for request %s', (request, response) => { + expect(Zdo.Utils.getResponseClusterId(request)).toStrictEqual(response); + }); + + it.each([ + [0, {alternatePANCoordinator: 0, deviceType: 0, powerSource: 0, rxOnWhenIdle: 0, reserved1: 0, reserved2: 0, securityCapability: 0, allocateAddress: 0}], + [0b00000001, {alternatePANCoordinator: 1, deviceType: 0, powerSource: 0, rxOnWhenIdle: 0, reserved1: 0, reserved2: 0, securityCapability: 0, allocateAddress: 0}], + [0b00000010, {alternatePANCoordinator: 0, deviceType: 1, powerSource: 0, rxOnWhenIdle: 0, reserved1: 0, reserved2: 0, securityCapability: 0, allocateAddress: 0}], + [0b00000100, {alternatePANCoordinator: 0, deviceType: 0, powerSource: 1, rxOnWhenIdle: 0, reserved1: 0, reserved2: 0, securityCapability: 0, allocateAddress: 0}], + [0b00001000, {alternatePANCoordinator: 0, deviceType: 0, powerSource: 0, rxOnWhenIdle: 1, reserved1: 0, reserved2: 0, securityCapability: 0, allocateAddress: 0}], + [0b00010000, {alternatePANCoordinator: 0, deviceType: 0, powerSource: 0, rxOnWhenIdle: 0, reserved1: 1, reserved2: 0, securityCapability: 0, allocateAddress: 0}], + [0b00100000, {alternatePANCoordinator: 0, deviceType: 0, powerSource: 0, rxOnWhenIdle: 0, reserved1: 0, reserved2: 1, securityCapability: 0, allocateAddress: 0}], + [0b01000000, {alternatePANCoordinator: 0, deviceType: 0, powerSource: 0, rxOnWhenIdle: 0, reserved1: 0, reserved2: 0, securityCapability: 1, allocateAddress: 0}], + [0b10000000, {alternatePANCoordinator: 0, deviceType: 0, powerSource: 0, rxOnWhenIdle: 0, reserved1: 0, reserved2: 0, securityCapability: 0, allocateAddress: 1}], + [0b10101011, {alternatePANCoordinator: 1, deviceType: 1, powerSource: 0, rxOnWhenIdle: 1, reserved1: 0, reserved2: 1, securityCapability: 0, allocateAddress: 1}], + [0b11111111, {alternatePANCoordinator: 1, deviceType: 1, powerSource: 1, rxOnWhenIdle: 1, reserved1: 1, reserved2: 1, securityCapability: 1, allocateAddress: 1}], + ])('Gets MAC capabilities flags for %s', (value, expected) => { + expect(Zdo.Utils.getMacCapFlags(value)).toStrictEqual(expected); + }); + + it.each([ + [0, { + primaryTrustCenter: 0, backupTrustCenter: 0, deprecated1: 0, deprecated2: 0, deprecated3: 0, deprecated4: 0, + networkManager: 0, reserved1: 0, reserved2: 0, stackComplianceResivion: 0 + }], + [0b0000000000000001, { + primaryTrustCenter: 1, backupTrustCenter: 0, deprecated1: 0, deprecated2: 0, deprecated3: 0, deprecated4: 0, + networkManager: 0, reserved1: 0, reserved2: 0, stackComplianceResivion: 0 + }], + [0b0010111001000001, { + primaryTrustCenter: 1, backupTrustCenter: 0, deprecated1: 0, deprecated2: 0, deprecated3: 0, deprecated4: 0, + networkManager: 1, reserved1: 0, reserved2: 0, stackComplianceResivion: 23 + }], + [0b0010101001000010, { + primaryTrustCenter: 0, backupTrustCenter: 1, deprecated1: 0, deprecated2: 0, deprecated3: 0, deprecated4: 0, + networkManager: 1, reserved1: 0, reserved2: 0, stackComplianceResivion: 21 + }], + ])('Gets & Creates server mask for %s', (value, expected) => { + expect(Zdo.Utils.getServerMask(value)).toStrictEqual(expected); + expect(Zdo.Utils.createServerMask(expected)).toStrictEqual(value); + }); +}); \ No newline at end of file