UNPKG

zigbee-herdsman

Version:

An open source ZigBee gateway solution with node.js.

1,273 lines (1,071 loc) 88 kB
import Buffalo from "../../buffalo/buffalo"; import {logger} from "../../utils/logger"; import {DEFAULT_ENCRYPTION_KEY_SIZE, EUI64_SIZE, EXTENDED_PAN_ID_SIZE, PAN_ID_SIZE} from "../consts"; import type {ClusterId, Eui64, NodeId, ProfileId} from "../tstypes"; import * as ZSpecUtils from "../utils"; import {ClusterId as ZdoClusterId} from "./definition/clusters"; import {CHALLENGE_VALUE_SIZE, CURVE_PUBLIC_POINT_SIZE, MULTICAST_BINDING, UNICAST_BINDING, ZDO_MESSAGE_OVERHEAD} from "./definition/consts"; import {GlobalTLV, type LeaveRequestFlags, RoutingTableStatus} from "./definition/enums"; import {Status} from "./definition/status"; import type { APSFrameCounterChallengeTLV, APSFrameCounterResponseTLV, ActiveEndpointsResponse, AuthenticationTokenIdTLV, BeaconAppendixEncapsulationGlobalTLV, BeaconSurveyConfigurationTLV, BeaconSurveyResultsTLV, BindingTableEntry, BindingTableResponse, ChallengeResponse, ClearAllBindingsReqEUI64TLV, ConfigurationParametersGlobalTLV, Curve25519PublicPointTLV, DeviceAuthenticationLevelTLV, DeviceCapabilityExtensionGlobalTLV, DeviceEUI64ListTLV, FragmentationParametersGlobalTLV, GetAuthenticationLevelResponse, GetConfigurationResponse, IEEEAddressResponse, JoinerEncapsulationGlobalTLV, LQITableEntry, LQITableResponse, LocalTLVReader, ManufacturerSpecificGlobalTLV, MatchDescriptorsResponse, NetworkAddressResponse, NextChannelChangeGlobalTLV, NextPanIdChangeGlobalTLV, NodeDescriptorResponse, NwkBeaconSurveyResponse, NwkEnhancedUpdateResponse, NwkIEEEJoiningListResponse, NwkUnsolicitedEnhancedUpdateResponse, NwkUpdateResponse, PanIdConflictReportGlobalTLV, ParentAnnounceResponse, PotentialParentsTLV, PowerDescriptorResponse, ProcessingStatusTLV, RequestMap, ResponseMap, RetrieveAuthenticationTokenResponse, RouterInformationGlobalTLV, RoutingTableEntry, RoutingTableResponse, SelectedKeyNegotiationMethodTLV, ServerMask, SetConfigurationResponse, SimpleDescriptorResponse, StartKeyNegotiationResponse, SupportedKeyNegotiationMethodsGlobalTLV, SymmetricPassphraseGlobalTLV, SystemServerDiscoveryResponse, TargetIEEEAddressTLV, Tlv, ValidResponseMap, } from "./definition/tstypes"; import * as Utils from "./utils"; import {ZdoStatusError} from "./zdoStatusError"; 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: SupportedKeyNegotiationMethodsGlobalTLV["sourceDeviceEui64"]; 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: FragmentationParametersGlobalTLV["fragmentationOptions"]; let maxIncomingTransferUnit: FragmentationParametersGlobalTLV["maxIncomingTransferUnit"]; 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(undefined, 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(undefined, 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"] | undefined { 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 undefined; } } } 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(); // 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); // 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); // 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(); // 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<number, LocalTLVReader>, encapsulated = 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...) // 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; // undefined == unknown tag let tlv: Tlv["tlv"] | undefined; if (tagId < GlobalTLV.MANUFACTURER_SPECIFIC) { if (localTLVReaders) { const localTLVReader = localTLVReaders.get(tagId); if (localTLVReader) { tlv = localTLVReader.call(this, length); /* v8 ignore start */ } else { logger.debug(`Local TLV found tagId=${tagId} but no reader given for it. Ignoring it.`, NS); } /* v8 ignore stop */ /* v8 ignore start */ } else { logger.debug(`Local TLV found tagId=${tagId} but no reader available. Ignoring it.`, NS); } /* v8 ignore stop */ } else { tlv = this.readGlobalTLV(tagId, length); } // validation: unknown tag shall be ignored if (tlv) { 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 public static buildRequest<K extends keyof RequestMap>(hasZdoMessageOverhead: boolean, clusterId: K, ...args: RequestMap[K]): Buffer { const buffalo = new BuffaloZdo(Buffer.alloc(MAX_BUFFER_SIZE), hasZdoMessageOverhead ? ZDO_MESSAGE_OVERHEAD : 0); switch (clusterId) { case ZdoClusterId.NETWORK_ADDRESS_REQUEST: { return buffalo.buildNetworkAddressRequest(...(args as RequestMap[ZdoClusterId.NETWORK_ADDRESS_REQUEST])); } case ZdoClusterId.IEEE_ADDRESS_REQUEST: { return buffalo.buildIeeeAddressRequest(...(args as RequestMap[ZdoClusterId.IEEE_ADDRESS_REQUEST])); } case ZdoClusterId.NODE_DESCRIPTOR_REQUEST: { return buffalo.buildNodeDescriptorRequest(...(args as RequestMap[ZdoClusterId.NODE_DESCRIPTOR_REQUEST])); } case ZdoClusterId.POWER_DESCRIPTOR_REQUEST: { return buffalo.buildPowerDescriptorRequest(...(args as RequestMap[ZdoClusterId.POWER_DESCRIPTOR_REQUEST])); } case ZdoClusterId.SIMPLE_DESCRIPTOR_REQUEST: { return buffalo.buildSimpleDescriptorRequest(...(args as RequestMap[ZdoClusterId.SIMPLE_DESCRIPTOR_REQUEST])); } case ZdoClusterId.ACTIVE_ENDPOINTS_REQUEST: { return buffalo.buildActiveEndpointsRequest(...(args as RequestMap[ZdoClusterId.ACTIVE_ENDPOINTS_REQUEST])); } case ZdoClusterId.MATCH_DESCRIPTORS_REQUEST: { return buffalo.buildMatchDescriptorRequest(...(args as RequestMap[ZdoClusterId.MATCH_DESCRIPTORS_REQUEST])); } case ZdoClusterId.SYSTEM_SERVER_DISCOVERY_REQUEST: { return buffalo.buildSystemServiceDiscoveryRequest(...(args as RequestMap[ZdoClusterId.SYSTEM_SERVER_DISCOVERY_REQUEST])); } case ZdoClusterId.PARENT_ANNOUNCE: { return buffalo.buildParentAnnounce(...(args as RequestMap[ZdoClusterId.PARENT_ANNOUNCE])); } case ZdoClusterId.BIND_REQUEST: { return buffalo.buildBindRequest(...(args as RequestMap[ZdoClusterId.BIND_REQUEST])); } case ZdoClusterId.UNBIND_REQUEST: { return buffalo.buildUnbindRequest(...(args as RequestMap[ZdoClusterId.UNBIND_REQUEST])); } case ZdoClusterId.CLEAR_ALL_BINDINGS_REQUEST: { return buffalo.buildClearAllBindingsRequest(...(args as RequestMap[ZdoClusterId.CLEAR_ALL_BINDINGS_REQUEST])); } case ZdoClusterId.LQI_TABLE_REQUEST: { return buffalo.buildLqiTableRequest(...(args as RequestMap[ZdoClusterId.LQI_TABLE_REQUEST])); } case ZdoClusterId.ROUTING_TABLE_REQUEST: { return buffalo.buildRoutingTableRequest(...(args as RequestMap[ZdoClusterId.ROUTING_TABLE_REQUEST])); } case ZdoClusterId.BINDING_TABLE_REQUEST: { return buffalo.buildBindingTableRequest(...(args as RequestMap[ZdoClusterId.BINDING_TABLE_REQUEST])); } case ZdoClusterId.LEAVE_REQUEST: { return buffalo.buildLeaveRequest(...(args as RequestMap[ZdoClusterId.LEAVE_REQUEST])); } case ZdoClusterId.PERMIT_JOINING_REQUEST: { return buffalo.buildPermitJoining(...(args as RequestMap[ZdoClusterId.PERMIT_JOINING_REQUEST])); } case ZdoClusterId.NWK_UPDATE_REQUEST: { return buffalo.buildNwkUpdateRequest(...(args as RequestMap[ZdoClusterId.NWK_UPDATE_REQUEST])); } case ZdoClusterId.NWK_ENHANCED_UPDATE_REQUEST: { return buffalo.buildNwkEnhancedUpdateRequest(...(args as RequestMap[ZdoClusterId.NWK_ENHANCED_UPDATE_REQUEST])); } case ZdoClusterId.NWK_IEEE_JOINING_LIST_REQUEST: { return buffalo.buildNwkIEEEJoiningListRequest(...(args as RequestMap[ZdoClusterId.NWK_IEEE_JOINING_LIST_REQUEST])); } case ZdoClusterId.NWK_BEACON_SURVEY_REQUEST: { return buffalo.buildNwkBeaconSurveyRequest(...(args as RequestMap[ZdoClusterId.NWK_BEACON_SURVEY_REQUEST])); } case ZdoClusterId.START_KEY_NEGOTIATION_REQUEST: { return buffalo.buildStartKeyNegotiationRequest(...(args as RequestMap[ZdoClusterId.START_KEY_NEGOTIATION_REQUEST])); } case ZdoClusterId.RETRIEVE_AUTHENTICATION_TOKEN_REQUEST: { return buffalo.buildRetrieveAuthenticationTokenRequest(...(args as RequestMap[ZdoClusterId.RETRIEVE_AUTHENTICATION_TOKEN_REQUEST])); } case ZdoClusterId.GET_AUTHENTICATION_LEVEL_REQUEST: { return buffalo.buildGetAuthenticationLevelRequest(...(args as RequestMap[ZdoClusterId.GET_AUTHENTICATION_LEVEL_REQUEST])); } case ZdoClusterId.SET_CONFIGURATION_REQUEST: { return buffalo.buildSetConfigurationRequest(...(args as RequestMap[ZdoClusterId.SET_CONFIGURATION_REQUEST])); } case ZdoClusterId.GET_CONFIGURATION_REQUEST: { return buffalo.buildGetConfigurationRequest(...(args as RequestMap[ZdoClusterId.GET_CONFIGURATION_REQUEST])); } case ZdoClusterId.START_KEY_UPDATE_REQUEST: { return buffalo.buildStartKeyUpdateRequest(...(args as RequestMap[ZdoClusterId.START_KEY_UPDATE_REQUEST])); } case ZdoClusterId.DECOMMISSION_REQUEST: { return buffalo.buildDecommissionRequest(...(args as RequestMap[ZdoClusterId.DECOMMISSION_REQUEST])); } case ZdoClusterId.CHALLENGE_REQUEST: { return buffalo.buildChallengeRequest(...(args as RequestMap[ZdoClusterId.CHALLENGE_REQUEST])); } default: { throw new Error(`Unsupported request building for cluster ID '${clusterId}'.`); } } } /** * @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. */ private buildNetworkAddressRequest(target: Eui64, reportKids: boolean, childStartIndex: number): Buffer { this.writeIeeeAddr(target); this.writeUInt8(reportKids ? 1 : 0); this.writeUInt8(childStartIndex); return this.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. */ private buildIeeeAddressRequest(target: NodeId, reportKids: boolean, childStartIndex: number): Buffer { this.writeUInt16(target); this.writeUInt8(reportKids ? 1 : 0); this.writeUInt8(childStartIndex); return this.getWritten(); } /** * @see ClusterId.NODE_DESCRIPTOR_REQUEST * @param target NWK address for the request */ private buildNodeDescriptorRequest(target: NodeId, fragmentationParameters?: FragmentationParametersGlobalTLV): Buffer { this.writeUInt16(target); if (fragmentationParameters) { let length = 2; if (fragmentationParameters.fragmentationOptions) { length += 1; } if (fragmentationParameters.maxIncomingTransferUnit) { length += 2; } this.writeGlobalTLV({tagId: GlobalTLV.FRAGMENTATION_PARAMETERS, length, tlv: fragmentationParameters}); } return this.getWritten(); } /** * @see ClusterId.POWER_DESCRIPTOR_REQUEST * @param target NWK address for the request */ private buildPowerDescriptorRequest(target: NodeId): Buffer { this.writeUInt16(target); return this.getWritten(); } /** * @see ClusterId.SIMPLE_DESCRIPTOR_REQUEST * @param target NWK address for the request * @param targetEndpoint The endpoint on the destination */ private buildSimpleDescriptorRequest(target: NodeId, targetEndpoint: number): Buffer { this.writeUInt16(target); this.writeUInt8(targetEndpoint); return this.getWritten(); } /** * @see ClusterId.ACTIVE_ENDPOINTS_REQUEST * @param target NWK address for the request */ private buildActiveEndpointsRequest(target: NodeId): Buffer { this.writeUInt16(target); return this.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 */ private buildMatchDescriptorRequest(target: NodeId, profileId: ProfileId, inClusterList: ClusterId[], outClusterList: ClusterId[]): Buffer { this.writeUInt16(target); this.writeUInt16(profileId); this.writeUInt8(inClusterList.length); this.writeListUInt16(inClusterList); this.writeUInt8(outClusterList.length); this.writeListUInt16(outClusterList); return this.getWritten(); } /** * @see ClusterId.SYSTEM_SERVER_DISCOVERY_REQUEST * @param serverMask See Table 2-34 for bit assignments. */ private buildSystemServiceDiscoveryRequest(serverMask: ServerMask): Buffer { this.writeUInt16(Utils.createServerMask(serverMask)); return this.getWritten(); } /** * @see ClusterId.PARENT_ANNOUNCE * @param children The IEEE addresses of the children bound to the parent. */ private buildParentAnnounce(children: Eui64[]): Buffer { this.writeUInt8(children.length); for (const child of children) { this.writeIeeeAddr(child); } return this.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. */ private buildBindRequest( source: Eui64, sourceEndpoint: number, clusterId: ClusterId, type: number, destination: Eui64, groupAddress: number, destinationEndpoint: number, ): Buffer { this.writeIeeeAddr(source); this.writeUInt8(sourceEndpoint); this.writeUInt16(clusterId); this.writeUInt8(type); switch (type) { case UNICAST_BINDING: { this.writeIeeeAddr(destination); this.writeUInt8(destinationEndpoint); break; } case MULTICAST_BINDING: { this.writeUInt16(groupAddress); break; } default: throw new ZdoStatusError(Status.NOT_SUPPORTED); } return this.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. */ private buildUnbindRequest( source: Eui64, sourceEndpoint: number, clusterId: ClusterId, type: number, destination: Eui64, groupAddress: number, destinationEndpoint: number, ): Buffer { this.writeIeeeAddr(source); this.writeUInt8(sourceEndpoint); this.writeUInt16(clusterId); this.writeUInt8(type); switch (type) { case UNICAST_BINDING: { this.writeIeeeAddr(destination); this.writeUInt8(destinationEndpoint); break; } case MULTICAST_BINDING: { this.writeUInt16(groupAddress); break; } default: throw new ZdoStatusError(Status.NOT_SUPPORTED); } return this.getWritten(); } /** * @see ClusterId.CLEAR_ALL_BINDINGS_REQUEST */ private buildClearAllBindingsRequest(tlv: ClearAllBindingsReqEUI64TLV): Buffer { // ClearAllBindingsReqEUI64TLV: Local: ID: 0x00 this.writeUInt8(0x00); this.writeUInt8(tlv.eui64List.length * EUI64_SIZE + 1 - 1); this.writeUInt8(tlv.eui64List.length); for (const entry of tlv.eui64List) { this.writeIeeeAddr(entry); } return this.getWritten(); } /** * @see ClusterId.LQI_TABLE_REQUEST * @param startIndex Starting Index for the requested elements of the Neighbor Table. */ private buildLqiTableRequest(startIndex: number): Buffer { this.writeUInt8(startIndex); return this.getWritten(); } /** * @see ClusterId.ROUTING_TABLE_REQUEST * @param startIndex Starting Index for the requested elements of the Neighbor Table. */ private buildRoutingTableRequest(startIndex: number): Buffer { this.writeUInt8(startIndex); return this.getWritten(); } /** * @see ClusterId.BINDING_TABLE_REQUEST * @param startIndex Starting Index for the requested elements of the Neighbor Table. */ private buildBindingTableRequest(startIndex: number): Buffer { this.writeUInt8(startIndex); return this.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. */ private buildLeaveRequest(deviceAddress: Eui64, leaveRequestFlags: LeaveRequestFlags): Buffer { this.writeIeeeAddr(deviceAddress); this.writeUInt8(leaveRequestFlags); return this.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. */ private buildPermitJoining(duration: number, authentication: number, tlvs: Tlv[]): Buffer { this.writeUInt8(duration); this.writeUInt8(authentication); // BeaconAppendixEncapsulationGlobalTLV // - SupportedKeyNegotiationMethodsGlobalTLV // - FragmentationParametersGlobalTLV this.writeGlobalTLVs(tlvs); return this.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 buildNwkUpdateRequest( channels: number[], duration: number, count: number | undefined, nwkUpdateId: number | undefined, nwkManagerAddr: number | undefined, ): Buffer { this.writeUInt32(ZSpecUtils.channelsToUInt32Mask(channels)); this.writeUInt8(duration); if (count !== undefined && duration >= 0x00 && duration <= 0x05) { this.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 undefined, otherwise should throw if undefined and duration passes below conditions (see NwkEnhancedUpdateRequest) if (nwkUpdateId !== undefined && (duration === 0xfe || duration === 0xff)) { this.writeUInt8(nwkUpdateId); } if (nwkManagerAddr !== undefined && duration === 0xff) { this.writeUInt16(nwkManagerAddr); } return this.getWritten(); } // /** // * Shortcut for @see BuffaloZdo.buildNwkUpdateRequest // */ // private buildScanChannelsRequest(scanChannels: number[], duration: number, count: number): Buffer { // return this.buildNwkUpdateRequest(scanChannels, duration, count, undefined, undefined); // } // /** // * Shortcut for @see BuffaloZdo.buildNwkUpdateRequest // */ // private buildChannelChangeRequest(channel: number, nwkUpdateId: number | undefined): Buffer { // return this.buildNwkUpdateRequest([channel], 0xfe, undefined, nwkUpdateId, undefined); // } // /** // * Shortcut for @see BuffaloZdo.buildNwkUpdateRequest // */ // private buildSetActiveChannelsAndNwkManagerIdRequest(channels: number[], nwkUpdateId: number | undefined, nwkManagerAddr: NodeId): Buffer { // return this.buildNwkUpdateRequest(channels, 0xff, undefined, 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 buildNwkEnhancedUpdateRequest( channelPages: number[], duration: number, count: number | undefined, nwkUpdateId: number | undefined, nwkManagerAddr: NodeId | undefined, configurationBitmask: number | undefined, ): Buffer { this.writeUInt8(channelPages.length); for (const channelPage of channelPages) { this.writeUInt32(ch