Skip to content

Instantly share code, notes, and snippets.

@Koenkk
Created February 4, 2020 19:27
Show Gist options
  • Save Koenkk/c4bdd09dec17f69e4702bff87b24399b to your computer and use it in GitHub Desktop.
Save Koenkk/c4bdd09dec17f69e4702bff87b24399b to your computer and use it in GitHub Desktop.
import {
NetworkOptions, SerialPortOptions, Coordinator, CoordinatorVersion, NodeDescriptor,
DeviceType, ActiveEndpoints, SimpleDescriptor, LQI, RoutingTable, Backup as BackupType, NetworkParameters,
StartResult, LQINeighbor, RoutingTableEntry,
} from '../../tstype';
import {ZnpVersion} from './tstype';
import * as Events from '../../events';
import Adapter from '../../adapter';
import {Znp, ZpiObject} from '../znp';
import StartZnp from './startZnp';
import {Constants as UnpiConstants} from '../unpi';
import {ZclFrame, FrameType, Direction, Foundation} from '../../../zcl';
import {Queue, Waitress, Wait} from '../../../utils';
import * as Constants from '../constants';
import Debug from "debug";
import {Backup} from './backup';
const debug = Debug("zigbee-herdsman:adapter:zStack:adapter");
const Subsystem = UnpiConstants.Subsystem;
const Type = UnpiConstants.Type;
const DataConfirmErrorCodeLookup: {[k: number]: string} = {
183: 'APS no ack',
205: 'No network route',
225: 'MAC channel access failure',
233: 'MAC no ack',
240: 'MAC transaction expired',
};
interface WaitFor {
ID: number;
promise: Promise<Events.ZclDataPayload>;
}
interface WaitressMatcher {
address: number | string;
endpoint: number;
transactionSequenceNumber: number;
frameType: FrameType;
clusterID: number;
commandIdentifier: number;
direction: number;
};
class DataConfirmError extends Error {
public code: number;
constructor (code: number) {
const message = `Data request failed with error: '${DataConfirmErrorCodeLookup[code]}' (${code})`;
super(message);
this.code = code;
}
}
class ZStackAdapter extends Adapter {
private znp: Znp;
private transactionID: number;
private version: {
product: number; transportrev: number; majorrel: number; minorrel: number; maintrel: number; revision: string;
};
private closing: boolean;
private queue: Queue;
private waitress: Waitress<Events.ZclDataPayload, WaitressMatcher>;
public constructor(networkOptions: NetworkOptions, serialPortOptions: SerialPortOptions, backupPath: string) {
super(networkOptions, serialPortOptions, backupPath);
this.znp = new Znp(this.serialPortOptions.path, this.serialPortOptions.baudRate, this.serialPortOptions.rtscts);
this.transactionID = 0;
this.closing = false;
this.queue = new Queue(2);
this.waitress = new Waitress<Events.ZclDataPayload, WaitressMatcher>(
this.waitressValidator, this.waitressTimeoutFormatter
);
this.znp.on('received', this.onZnpRecieved.bind(this));
this.znp.on('close', this.onZnpClose.bind(this));
}
/**
* Adapter methods
*/
public async start(): Promise<StartResult> {
await this.znp.open();
// Old firmware did not support version, assume it's Z-Stack 1.2 for now.
try {
this.version = (await this.znp.request(Subsystem.SYS, 'version', {})).payload;
} catch (e) {
this.version = {"transportrev":2, "product":0, "majorrel":2, "minorrel":0, "maintrel":0, "revision":""};
}
debug(`Detected znp version '${ZnpVersion[this.version.product]}' (${JSON.stringify(this.version)})`);
return StartZnp(this.znp, this.version.product, this.networkOptions, this.backupPath);
}
public async stop(): Promise<void> {
this.closing = true;
await this.znp.close();
}
public static async isValidPath(path: string): Promise<boolean> {
return Znp.isValidPath(path);
}
public static async autoDetectPath(): Promise<string> {
return Znp.autoDetectPath();
}
public async getCoordinator(): Promise<Coordinator> {
return this.queue.execute<Coordinator>(async () => {
const activeEpRsp = this.znp.waitFor(UnpiConstants.Type.AREQ, Subsystem.ZDO, 'activeEpRsp');
await this.znp.request(Subsystem.ZDO, 'activeEpReq', {dstaddr: 0, nwkaddrofinterest: 0});
const activeEp = await activeEpRsp.promise;
const deviceInfo = await this.znp.request(Subsystem.UTIL, 'getDeviceInfo', {});
const endpoints = [];
for (const endpoint of activeEp.payload.activeeplist) {
const simpleDescRsp = this.znp.waitFor(
UnpiConstants.Type.AREQ, Subsystem.ZDO, 'simpleDescRsp', {endpoint}
);
this.znp.request(Subsystem.ZDO, 'simpleDescReq', {dstaddr: 0, nwkaddrofinterest: 0, endpoint});
const simpleDesc = await simpleDescRsp.promise;
endpoints.push({
ID: simpleDesc.payload.endpoint,
profileID: simpleDesc.payload.profileid,
deviceID: simpleDesc.payload.deviceid,
inputClusters: simpleDesc.payload.inclusterlist,
outputClusters: simpleDesc.payload.outclusterlist,
});
}
return {
networkAddress: 0,
manufacturerID: 0,
ieeeAddr: deviceInfo.payload.ieeeaddr,
endpoints,
};
});
}
public async permitJoin(seconds: number): Promise<void> {
await this.queue.execute<void>(async () => {
const payload = {addrmode: 0x0F, dstaddr: 0xFFFC , duration: seconds, tcsignificance: 0};
await this.znp.request(Subsystem.ZDO, 'mgmtPermitJoinReq', payload);
});
}
public async getCoordinatorVersion(): Promise<CoordinatorVersion> {
return {type: ZnpVersion[this.version.product], meta: this.version};
}
public async reset(type: 'soft' | 'hard'): Promise<void> {
if (type === 'soft') {
await this.znp.request(Subsystem.SYS, 'resetReq', {type: Constants.SYS.resetType.SOFT});
} else {
await this.znp.request(Subsystem.SYS, 'resetReq', {type: Constants.SYS.resetType.HARD});
}
}
public async supportsLED(): Promise<boolean> {
return this.version.product !== ZnpVersion.zStack3x0;
}
public async setLED(enabled: boolean): Promise<void> {
await this.znp.request(Subsystem.UTIL, 'ledControl', {ledid: 3, mode: enabled ? 1 : 0});
}
public async supportsDiscoverRoute(): Promise<boolean> {
return this.version.product !== ZnpVersion.zStack12;
}
public async discoverRoute(networkAddress: number): Promise<void> {
const payload = {dstAddr: networkAddress, options: 2, radius: Constants.AF.DEFAULT_RADIUS};
await this.znp.request(Subsystem.ZDO, 'extRouteDisc', payload);
}
public async nodeDescriptor(networkAddress: number): Promise<NodeDescriptor> {
return this.queue.execute<NodeDescriptor>(async () => {
const response = this.znp.waitFor(Type.AREQ, Subsystem.ZDO, 'nodeDescRsp', {nwkaddr: networkAddress});
const payload = {dstaddr: networkAddress, nwkaddrofinterest: networkAddress};
this.znp.request(Subsystem.ZDO, 'nodeDescReq', payload);
const descriptor = await response.promise;
let type: DeviceType = 'Unknown';
const logicalType = descriptor.payload.logicaltype_cmplxdescavai_userdescavai & 0x07;
for (const [key, value] of Object.entries(Constants.ZDO.deviceLogicalType)) {
if (value === logicalType) {
if (key === 'COORDINATOR') type = 'Coordinator';
else if (key === 'ROUTER') type = 'Router';
else if (key === 'ENDDEVICE') type = 'EndDevice';
break;
}
}
return {manufacturerCode: descriptor.payload.manufacturercode, type};
}, networkAddress);
}
public async activeEndpoints(networkAddress: number): Promise<ActiveEndpoints> {
return this.queue.execute<ActiveEndpoints>(async () => {
const response = this.znp.waitFor(Type.AREQ, Subsystem.ZDO, 'activeEpRsp', {nwkaddr: networkAddress});
const payload = {dstaddr: networkAddress, nwkaddrofinterest: networkAddress};
this.znp.request(Subsystem.ZDO, 'activeEpReq', payload);
const activeEp = await response.promise;
return {endpoints: activeEp.payload.activeeplist};
}, networkAddress);
}
public async simpleDescriptor(networkAddress: number, endpointID: number): Promise<SimpleDescriptor> {
return this.queue.execute<SimpleDescriptor>(async () => {
const responsePayload = {nwkaddr: networkAddress, endpoint: endpointID};
const response = this.znp.waitFor(Type.AREQ, Subsystem.ZDO, 'simpleDescRsp', responsePayload);
const payload = {dstaddr: networkAddress, nwkaddrofinterest: networkAddress, endpoint: endpointID};
this.znp.request(Subsystem.ZDO, 'simpleDescReq', payload);
const descriptor = await response.promise;
return {
profileID: descriptor.payload.profileid,
endpointID: descriptor.payload.endpoint,
deviceID: descriptor.payload.deviceid,
inputClusters: descriptor.payload.inclusterlist,
outputClusters: descriptor.payload.outclusterlist,
};
}, networkAddress);
}
public async sendZclFrameNetworkAddressWithResponse(
networkAddress: number, endpoint: number, zclFrame: ZclFrame, timeout: number, defaultResponseTimeout: number,
): Promise<Events.ZclDataPayload> {
return this.queue.execute<Events.ZclDataPayload>(async () => {
const command = zclFrame.getCommand();
if (!command.hasOwnProperty('response')) {
throw new Error(`Command '${command.name}' has no response, cannot wait for response`);
}
const defaultResponse = !zclFrame.Header.frameControl.disableDefaultResponse ?
this.waitDefaultResponse(networkAddress, endpoint, zclFrame, defaultResponseTimeout) : null;
const responsePayload = {
address: networkAddress, endpoint, transactionSequenceNumber: zclFrame.Header.transactionSequenceNumber,
clusterID: zclFrame.Cluster.ID, frameType: zclFrame.Header.frameControl.frameType,
direction: Direction.SERVER_TO_CLIENT, commandIdentifier: command.response,
};
const response = this.waitress.waitFor(responsePayload, timeout);
try {
await this.dataRequest(
networkAddress, endpoint, 1, zclFrame.Cluster.ID, Constants.AF.DEFAULT_RADIUS, zclFrame.toBuffer(),
timeout, 0
);
} catch (error) {
if (defaultResponse) {
this.waitress.remove(defaultResponse.ID);
}
this.waitress.remove(response.ID);
throw error;
}
if (defaultResponse) {
const result = await Promise.all([response.promise, defaultResponse.promise]);
return result[0];
} else {
return response.promise;
}
}, networkAddress);
}
public async sendZclFrameNetworkAddress(
networkAddress: number, endpoint: number, zclFrame: ZclFrame, timeout: number, defaultResponseTimeout: number,
): Promise<void> {
return this.queue.execute<void>(async () => {
const defaultResponse = !zclFrame.Header.frameControl.disableDefaultResponse ?
this.waitDefaultResponse(networkAddress, endpoint, zclFrame, defaultResponseTimeout) : null;
try {
await this.dataRequest(
networkAddress, endpoint, 1, zclFrame.Cluster.ID, Constants.AF.DEFAULT_RADIUS, zclFrame.toBuffer(),
timeout, 0,
);
} catch (error) {
if (defaultResponse) {
this.waitress.remove(defaultResponse.ID);
}
throw error;
}
if (defaultResponse) {
await defaultResponse.promise;
}
}, networkAddress);
}
public async sendZclFrameGroup(groupID: number, zclFrame: ZclFrame, timeout: number): Promise<void> {
return this.queue.execute<void>(async () => {
await this.dataRequestExtended(
Constants.COMMON.addressMode.ADDR_GROUP, groupID, 0xFF, 0, 1, zclFrame.Cluster.ID,
Constants.AF.DEFAULT_RADIUS, zclFrame.toBuffer(), timeout, 0, true
);
/**
* As a group command is not confirmed and thus immidiately returns
* (contrary to network address requests) we will give the
* command some time to 'settle' in the network.
*/
await Wait(200);
});
}
public async lqi(networkAddress: number): Promise<LQI> {
return this.queue.execute<LQI>(async (): Promise<LQI> => {
const neighbors: LQINeighbor[] = [];
// eslint-disable-next-line
const request = async (startIndex: number): Promise<any> => {
const response = this.znp.waitFor(Type.AREQ, Subsystem.ZDO, 'mgmtLqiRsp', {srcaddr: networkAddress});
this.znp.request(Subsystem.ZDO, 'mgmtLqiReq', {dstaddr: networkAddress, startindex: startIndex});
const result = await response.promise;
if (result.payload.status !== 0) {
throw new Error(`LQI for '${networkAddress}' failed`);
}
return result;
};
// eslint-disable-next-line
const add = (list: any) => {
for (const entry of list) {
neighbors.push({
linkquality: entry.lqi,
networkAddress: entry.nwkAddr,
ieeeAddr: entry.extAddr,
relationship: entry.relationship,
depth: entry.depth,
});
}
};
let response = await request(0);
add(response.payload.neighborlqilist);
const size = response.payload.neighbortableentries;
let nextStartIndex = response.payload.neighborlqilist.length;
while (neighbors.length < size) {
response = await request(nextStartIndex);
add(response.payload.neighborlqilist);
nextStartIndex += response.payload.neighborlqilist.length;
}
return {neighbors};
}, networkAddress);
}
public async routingTable(networkAddress: number): Promise<RoutingTable> {
return this.queue.execute<RoutingTable>(async (): Promise<RoutingTable> => {
const table: RoutingTableEntry[] = [];
// eslint-disable-next-line
const request = async (startIndex: number): Promise<any> => {
const response = this.znp.waitFor(Type.AREQ, Subsystem.ZDO, 'mgmtRtgRsp', {srcaddr: networkAddress});
this.znp.request(Subsystem.ZDO, 'mgmtRtgReq', {dstaddr: networkAddress, startindex: startIndex});
const result = await response.promise;
if (result.payload.status !== 0) {
throw new Error(`Routing table for '${networkAddress}' failed`);
}
return result;
};
// eslint-disable-next-line
const add = (list: any) => {
for (const entry of list) {
table.push({
destinationAddress: entry.destNwkAddr,
status: entry.routeStatus,
nextHop: entry.nextHopNwkAddr,
});
}
};
let response = await request(0);
add(response.payload.routingtablelist);
const size = response.payload.routingtableentries;
let nextStartIndex = response.payload.routingtablelist.length;
while (table.length < size) {
response = await request(nextStartIndex);
add(response.payload.routingtablelist);
nextStartIndex += response.payload.routingtablelist.length;
}
return {table};
}, networkAddress);
}
public async bind(
destinationNetworkAddress: number, sourceIeeeAddress: string, sourceEndpoint: number,
clusterID: number, destinationAddressOrGroup: string | number, type: 'endpoint' | 'group',
destinationEndpoint?: number
): Promise<void> {
return this.queue.execute<void>(async () => {
const responsePayload = {srcaddr: destinationNetworkAddress};
const response = this.znp.waitFor(Type.AREQ, Subsystem.ZDO, 'bindRsp', responsePayload);
const payload = {
dstaddr: destinationNetworkAddress,
srcaddr: sourceIeeeAddress,
srcendpoint: sourceEndpoint,
clusterid: clusterID,
dstaddrmode: type === 'group' ?
Constants.COMMON.addressMode.ADDR_GROUP : Constants.COMMON.addressMode.ADDR_64BIT,
dstaddress: this.toAddressString(destinationAddressOrGroup),
dstendpoint: type === 'group' ? 0xFF : destinationEndpoint,
};
this.znp.request(Subsystem.ZDO, 'bindReq', payload);
await response.promise;
}, destinationNetworkAddress);
}
public async unbind(
destinationNetworkAddress: number, sourceIeeeAddress: string, sourceEndpoint: number,
clusterID: number, destinationAddressOrGroup: string | number, type: 'endpoint' | 'group',
destinationEndpoint: number
): Promise<void> {
return this.queue.execute<void>(async () => {
const response = this.znp.waitFor(
Type.AREQ, Subsystem.ZDO, 'unbindRsp', {srcaddr: destinationNetworkAddress}
);
const payload = {
dstaddr: destinationNetworkAddress,
srcaddr: sourceIeeeAddress,
srcendpoint: sourceEndpoint,
clusterid: clusterID,
dstaddrmode: type === 'group' ?
Constants.COMMON.addressMode.ADDR_GROUP : Constants.COMMON.addressMode.ADDR_64BIT,
dstaddress: this.toAddressString(destinationAddressOrGroup),
dstendpoint: type === 'group' ? 0xFF : destinationEndpoint,
};
this.znp.request(Subsystem.ZDO, 'unbindReq', payload);
await response.promise;
}, destinationNetworkAddress);
}
public removeDevice(networkAddress: number, ieeeAddr: string): Promise<void> {
return this.queue.execute<void>(async () => {
const response = this.znp.waitFor(
UnpiConstants.Type.AREQ, Subsystem.ZDO, 'mgmtLeaveRsp', {srcaddr: networkAddress}
);
const payload = {
dstaddr: networkAddress,
deviceaddress: ieeeAddr,
removechildrenRejoin: 0,
};
this.znp.request(Subsystem.ZDO, 'mgmtLeaveReq', payload);
await response.promise;
}, networkAddress);
}
/**
* Event handlers
*/
public onZnpClose(): void {
if (!this.closing) {
this.emit(Events.Events.disconnected);
}
}
public onZnpRecieved(object: ZpiObject): void {
if (object.type !== UnpiConstants.Type.AREQ) {
return;
}
if (object.subsystem === Subsystem.ZDO) {
if (object.command === 'tcDeviceInd') {
const payload: Events.DeviceJoinedPayload = {
networkAddress: object.payload.nwkaddr,
ieeeAddr: object.payload.extaddr,
};
this.emit(Events.Events.deviceJoined, payload);
} else if (object.command === 'endDeviceAnnceInd') {
const payload: Events.DeviceAnnouncePayload = {
networkAddress: object.payload.nwkaddr,
ieeeAddr: object.payload.ieeeaddr,
};
this.emit(Events.Events.deviceAnnounce, payload);
} else {
/* istanbul ignore else */
if (object.command === 'leaveInd') {
const payload: Events.DeviceLeavePayload = {
networkAddress: object.payload.srcaddr,
ieeeAddr: object.payload.extaddr,
};
this.emit(Events.Events.deviceLeave, payload);
}
}
} else {
/* istanbul ignore else */
if (object.subsystem === Subsystem.AF) {
/* istanbul ignore else */
if (object.command === 'incomingMsg' || object.command === 'incomingMsgExt') {
try {
const payload: Events.ZclDataPayload = {
frame: ZclFrame.fromBuffer(object.payload.clusterid, object.payload.data),
address: object.payload.srcaddr,
endpoint: object.payload.srcendpoint,
linkquality: object.payload.linkquality,
groupID: object.payload.groupid,
};
this.waitress.resolve(payload);
this.emit(Events.Events.zclData, payload);
} catch (error) {
const payload: Events.RawDataPayload = {
clusterID: object.payload.clusterid,
data: object.payload.data,
address: object.payload.srcaddr,
endpoint: object.payload.srcendpoint,
linkquality: object.payload.linkquality,
groupID: object.payload.groupid,
};
this.emit(Events.Events.rawData, payload);
}
}
}
}
}
public async getNetworkParameters(): Promise<NetworkParameters> {
const result = await this.znp.request(Subsystem.ZDO, 'extNwkInfo', {});
return {
panID: result.payload.panid, extendedPanID: result.payload.extendedpanid,
channel: result.payload.channel
};
}
public async supportsBackup(): Promise<boolean> {
return this.version.product !== ZnpVersion.zStack12;
}
public async backup(): Promise<BackupType> {
return Backup(this.znp);
}
public async setChannelInterPAN(channel: number): Promise<void> {
return this.queue.execute<void>(async () => {
await this.znp.request(Subsystem.AF, 'interPanCtl', {cmd: 1, data: [channel]});
// Make sure that endpoint 12 is registered to proxy the InterPAN messages.
await this.znp.request(Subsystem.AF, 'interPanCtl', {cmd: 2, data: [12]});
});
}
public async sendZclFrameInterPANIeeeAddr(zclFrame: ZclFrame, ieeeAddr: string): Promise<void> {
return this.queue.execute<void>(async () => {
await this.dataRequestExtended(
Constants.COMMON.addressMode.ADDR_64BIT, ieeeAddr, 0xFE, 0xFFFF,
12, zclFrame.Cluster.ID, 30, zclFrame.toBuffer(), 10000, 0, false
);
});
}
public async sendZclFrameInterPANBroadcastWithResponse(
zclFrame: ZclFrame, timeout: number
): Promise<Events.ZclDataPayload> {
return this.queue.execute<Events.ZclDataPayload>(async () => {
const command = zclFrame.getCommand();
if (!command.hasOwnProperty('response')) {
throw new Error(`Command '${command.name}' has no response, cannot wait for response`);
}
const responsePayload: WaitressMatcher = {
address: null, endpoint: 0xFE, transactionSequenceNumber: null,
clusterID: zclFrame.Cluster.ID, frameType: zclFrame.Header.frameControl.frameType,
direction: Direction.SERVER_TO_CLIENT, commandIdentifier: command.response,
};
const response = this.waitress.waitFor(responsePayload, timeout);
try {
await this.dataRequestExtended(
Constants.COMMON.addressMode.ADDR_16BIT, 0xFFFF, 0xFE, 0xFFFF,
12, zclFrame.Cluster.ID, 30, zclFrame.toBuffer(), 10000, 0, false
);
} catch (error) {
this.waitress.remove(response.ID);
throw error;
}
return response.promise;
});
}
public async restoreChannelInterPAN(): Promise<void> {
return this.queue.execute<void>(async () => {
await this.znp.request(Subsystem.AF, 'interPanCtl', {cmd: 0, data: []});
// Give adapter some time to restore, otherwise stuff crashes
await Wait(1000);
});
}
public async setTransmitPower(value: number): Promise<void> {
return this.queue.execute<void>(async () => {
await this.znp.request(Subsystem.SYS, 'stackTune', {operation: 0, value});
});
}
/**
* Private methods
*/
private async dataRequest(
destinationAddress: number, destinationEndpoint: number, sourceEndpoint: number, clusterID: number,
radius: number, data: Buffer, timeout: number, attempt: number,
): Promise<ZpiObject> {
const transactionID = this.nextTransactionID();
const response = this.znp.waitFor(Type.AREQ, Subsystem.AF, 'dataConfirm', {transid: transactionID}, timeout);
try {
await this.znp.request(Subsystem.AF, 'dataRequest', {
dstaddr: destinationAddress,
destendpoint: destinationEndpoint,
srcendpoint: sourceEndpoint,
clusterid: clusterID,
transid: transactionID,
options: 0, // TODO: why was this here? Constants.AF.options.ACK_REQUEST | DISCV_ROUTE,
radius: radius,
len: data.length,
data: data,
});
} catch (error) {
this.znp.removeWaitFor(response.ID);
throw error;
}
const dataConfirm = await response.promise;
if (dataConfirm.payload.status !== 0) {
if ([225, 233, 240].includes(dataConfirm.payload.status) && attempt <= 3) {
/**
* When many commands at once are executed we can end up in a MAC channel access failure
* error (225). This is because there is too much traffic on the network.
* Retry this command once after a cooling down period.
*/
await Wait(1000);
return this.dataRequest(
destinationAddress, destinationEndpoint, sourceEndpoint, clusterID, radius, data, timeout,
attempt + 1
);
} else {
throw new DataConfirmError(dataConfirm.payload.status);
}
}
return dataConfirm;
};
private async dataRequestExtended(
addressMode: number, destinationAddressOrGroupID: number | string, destinationEndpoint: number, panID: number,
sourceEndpoint: number, clusterID: number, radius: number, data: Buffer, timeout: number, attempt: number,
confirmation: boolean
): Promise<ZpiObject> {
const transactionID = this.nextTransactionID();
const response = confirmation ?
this.znp.waitFor(Type.AREQ, Subsystem.AF, 'dataConfirm', {transid: transactionID}, timeout) : null;
try {
await this.znp.request(Subsystem.AF, 'dataRequestExt', {
dstaddrmode: addressMode,
dstaddr: this.toAddressString(destinationAddressOrGroupID),
destendpoint: destinationEndpoint,
dstpanid: panID,
srcendpoint: sourceEndpoint,
clusterid: clusterID,
transid: transactionID,
options: 0, // TODO: why was this here? Constants.AF.options.DISCV_ROUTE,
radius,
len: data.length,
data: data,
});
} catch (error) {
if (confirmation) {
this.znp.removeWaitFor(response.ID);
}
throw error;
}
if (confirmation) {
const dataConfirm = await response.promise;
if (dataConfirm.payload.status !== 0) {
if ([225, 233, 240].includes(dataConfirm.payload.status) && attempt <= 3) {
/**
* When many commands at once are executed we can end up in a MAC channel access failure
* error (225). This is because there is too much traffic on the network.
* Retry this command once after a cooling down period.
*/
await Wait(1000);
return this.dataRequestExtended(
addressMode, destinationAddressOrGroupID, destinationEndpoint, panID, sourceEndpoint, clusterID,
radius, data, timeout, attempt + 1, confirmation,
);
} else {
throw new DataConfirmError(dataConfirm.payload.status);
}
}
return dataConfirm;
}
};
private nextTransactionID(): number {
this.transactionID++;
if (this.transactionID > 255) {
this.transactionID = 1;
}
return this.transactionID;
}
private toAddressString(address: number | string): string {
if (typeof address === 'number') {
let addressString = address.toString(16);
for (let i = addressString.length; i < 16; i++) {
addressString = '0' + addressString;
}
return `0x${addressString}`;
} else {
return address.toString();
}
}
private waitDefaultResponse(
networkAddress: number, endpoint: number, zclFrame: ZclFrame, timeout: number,
): WaitFor {
const payload = {
address: networkAddress, endpoint, transactionSequenceNumber: zclFrame.Header.transactionSequenceNumber,
clusterID: zclFrame.Cluster.ID, frameType: FrameType.GLOBAL, direction: Direction.SERVER_TO_CLIENT,
commandIdentifier: Foundation.defaultRsp.ID,
};
return this.waitress.waitFor(payload, timeout);
}
private waitressTimeoutFormatter(matcher: WaitressMatcher, timeout: number): string {
return `Timeout - ${matcher.address} - ${matcher.endpoint}` +
` - ${matcher.transactionSequenceNumber} - ${matcher.clusterID}` +
` - ${matcher.commandIdentifier} after ${timeout}ms`;
}
private waitressValidator(payload: Events.ZclDataPayload, matcher: WaitressMatcher): boolean {
const transactionSequenceNumber = payload.frame.Header.transactionSequenceNumber;
return (!matcher.address || payload.address === matcher.address) &&
payload.endpoint === matcher.endpoint &&
(!matcher.transactionSequenceNumber || transactionSequenceNumber === matcher.transactionSequenceNumber) &&
payload.frame.Cluster.ID === matcher.clusterID &&
matcher.frameType === payload.frame.Header.frameControl.frameType &&
matcher.commandIdentifier === payload.frame.Header.commandIdentifier &&
matcher.direction === payload.frame.Header.frameControl.direction;
}
}
export default ZStackAdapter;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment