diff --git a/.npmignore b/.npmignore index 7392f01cbf..34e044dd26 100644 --- a/.npmignore +++ b/.npmignore @@ -14,7 +14,8 @@ coverage #editor settings .vscode +.idea #src src -docs \ No newline at end of file +docs diff --git a/src/adapter/adapter.ts b/src/adapter/adapter.ts index 0bda8c9cc4..a84e03a004 100644 --- a/src/adapter/adapter.ts +++ b/src/adapter/adapter.ts @@ -36,10 +36,11 @@ abstract class Adapter extends events.EventEmitter { ): Promise { const {ZStackAdapter} = await import('./z-stack/adapter'); const {DeconzAdapter} = await import('./deconz/adapter'); - type AdapterImplementation = typeof ZStackAdapter | typeof DeconzAdapter; + const {ZiGateAdapter} = await import('./zigate/adapter'); + type AdapterImplementation = typeof ZStackAdapter | typeof DeconzAdapter | typeof ZiGateAdapter; let adapters: AdapterImplementation[]; - const adapterLookup = {zstack: ZStackAdapter, deconz: DeconzAdapter}; + const adapterLookup = {zstack: ZStackAdapter, deconz: DeconzAdapter, zigate: ZiGateAdapter}; if (serialPortOptions.adapter) { if (adapterLookup.hasOwnProperty(serialPortOptions.adapter)) { adapters = [adapterLookup[serialPortOptions.adapter]]; diff --git a/src/adapter/tstype.ts b/src/adapter/tstype.ts index b1a2fcfa5e..2e3e790b93 100644 --- a/src/adapter/tstype.ts +++ b/src/adapter/tstype.ts @@ -10,7 +10,7 @@ interface SerialPortOptions { baudRate?: number; rtscts?: boolean; path?: string; - adapter?: 'zstack' | 'deconz'; + adapter?: 'zstack' | 'deconz' | 'zigate'; } interface AdapterOptions { @@ -96,4 +96,4 @@ export { SerialPortOptions, NetworkOptions, Coordinator, CoordinatorVersion, NodeDescriptor, DeviceType, ActiveEndpoints, SimpleDescriptor, LQI, LQINeighbor, RoutingTable, Backup, NetworkParameters, StartResult, RoutingTableEntry, AdapterOptions, -}; \ No newline at end of file +}; diff --git a/src/adapter/zigate/adapter/index.ts b/src/adapter/zigate/adapter/index.ts new file mode 100644 index 0000000000..0805fc80bb --- /dev/null +++ b/src/adapter/zigate/adapter/index.ts @@ -0,0 +1,7 @@ +/* istanbul ignore file */ +/* eslint-disable */ +import ZiGateAdapter from './zigateAdapter'; + +export { + ZiGateAdapter +}; \ No newline at end of file diff --git a/src/adapter/zigate/adapter/zigateAdapter.ts b/src/adapter/zigate/adapter/zigateAdapter.ts new file mode 100644 index 0000000000..b8d683d5bf --- /dev/null +++ b/src/adapter/zigate/adapter/zigateAdapter.ts @@ -0,0 +1,708 @@ +/* istanbul ignore file */ +/* eslint-disable */ +import * as TsType from '../../tstype'; +import {DeviceType, LQINeighbor} from '../../tstype'; +import * as Events from '../../events'; +import Adapter from '../../adapter'; +import {Direction, FrameType, ZclFrame} from '../../../zcl'; +import {Waitress} from '../../../utils'; +import Driver from '../driver/zigate'; +import {Debug} from "../debug"; +import { + coordinatorEndpoints, + DEVICE_TYPE, + ZiGateCommandCode, + ZiGateMessageCode, + ZPSNwkKeyState +} from "../driver/constants"; +import {RawAPSDataRequestPayload} from "../driver/commandType"; +import ZiGateObject from "../driver/ziGateObject"; +import BuffaloZiGate from "../driver/buffaloZiGate"; + +const debug = Debug('adapter'); + +interface WaitressMatcher { + address: number | string; + endpoint: number; + transactionSequenceNumber?: number; + frameType: FrameType; + clusterID: number; + commandIdentifier: number; + direction: number; +} + +const channelsToMask = (channels: number[]): number => + channels.map((x) => 2 ** x).reduce( + (acc, x) => acc + x, 0); + + +class ZiGateAdapter extends Adapter { + private driver: Driver; + private joinPermitted: boolean; + private waitress: Waitress; + private closing: boolean; + + public constructor(networkOptions: TsType.NetworkOptions, + serialPortOptions: TsType.SerialPortOptions, + backupPath: string, + adapterOptions: TsType.AdapterOptions + ) { + + super(networkOptions, serialPortOptions, backupPath, adapterOptions); + + debug.log('construct', arguments); + + this.joinPermitted = false; + this.driver = new Driver(serialPortOptions.path, serialPortOptions); + this.waitress = new Waitress( + this.waitressValidator, this.waitressTimeoutFormatter + ); + + this.driver.on('received', (data: any) => { + if (data.zclFrame instanceof ZclFrame) { + const payload: Events.ZclDataPayload = { + address: data.ziGateObject.payload.sourceAddress, + frame: data.zclFrame, + endpoint: data.ziGateObject.payload.sourceEndpoint, + linkquality: data.ziGateObject.frame.readRSSI(), + groupID: null, + }; + this.emit(Events.Events.zclData, payload) + } else { + debug.error('msg not zclFrame', data.zclFrame); + } + }); + + this.driver.on('receivedRaw', (data: any) => { + const payload: Events.RawDataPayload = { + clusterID: data.ziGateObject.payload.clusterID, + data: data.ziGateObject.payload.payload, + address: data.ziGateObject.payload.sourceAddress, + endpoint: data.ziGateObject.payload.sourceEndpoint, + linkquality: data.ziGateObject.frame.readRSSI(), + groupID: null + }; + + this.emit(Events.Events.rawData, payload); + }); + + this.driver.on('LeaveIndication', (data: any) => { + debug.log('LeaveIndication %o', data); + const payload: Events.DeviceLeavePayload = { + networkAddress: data.ziGateObject.payload.extendedAddress, + ieeeAddr: data.ziGateObject.payload.extendedAddress + }; + this.emit(Events.Events.deviceLeave, payload) + }); + + this.driver.on('DeviceAnnounce', (data: any) => { + const payload: Events.DeviceAnnouncePayload = { + networkAddress: data.ziGateObject.payload.shortAddress, + ieeeAddr: data.ziGateObject.payload.ieee + }; + + debug.log('DeviceAnnounce join permit(%s) : %o', this.joinPermitted, data.ziGateObject.payload); + if (this.joinPermitted === true) { + this.emit(Events.Events.deviceJoined, payload) + } else { + this.emit(Events.Events.deviceAnnounce, payload) + } + }); + + } + + /** + * Adapter methods + */ + public async start(): Promise { + debug.log('start', arguments) + let startResult: TsType.StartResult = 'resumed'; + try { + debug.log("connected to zigate adapter successfully.", arguments); + await this.driver.sendCommand(ZiGateCommandCode.SetDeviceType, {deviceType: 0}); + + const resetResponse = await this.driver.sendCommand(ZiGateCommandCode.Reset, {}, 5000) + if (resetResponse.code === ZiGateMessageCode.RestartNonFactoryNew) { + startResult = 'resumed'; + } else if (resetResponse.code === ZiGateMessageCode.RestartFactoryNew) { + startResult = 'reset'; + } + + await this.driver.sendCommand(ZiGateCommandCode.SetDeviceType, { + deviceType: DEVICE_TYPE.coordinator + }); + await this.driver.sendCommand(ZiGateCommandCode.RawMode, {enabled: 0x01}); + await this.initNetwork(); + } catch(error) { + throw new Error("failed to connect to zigate adapter " + error.message); + } + + return startResult; // 'resumed' | 'reset' | 'restored' + } + + public async getCoordinator(): Promise { + debug.log('getCoordinator', arguments) + const networkResponse: any = await this.driver.sendCommand(ZiGateCommandCode.GetNetworkState); + + // @TODO deal hardcoded endpoints, made by analogy with deconz + // polling the coordinator on some firmware went into a memory leak, so we don't ask this info + const response: TsType.Coordinator = { + networkAddress: 0, + manufacturerID: 0, + ieeeAddr: networkResponse.payload.extendedAddress, + endpoints: coordinatorEndpoints + }; + debug.log('getCoordinator %o', response) + return response; + }; + + public async stop(): Promise { + debug.log('stop', arguments) + this.closing = true; + await this.driver.close(); + } + + public async getCoordinatorVersion(): Promise { + debug.log('getCoordinatorVersion'); + + return this.driver.sendCommand(ZiGateCommandCode.GetVersion, {}) + .then((result) => { + const formattedVersion = parseInt(result.payload.installerVersion).toString(16); + const version: TsType.CoordinatorVersion = { + type: 'zigate', + meta: { + 'major': formattedVersion + } + }; + return Promise.resolve(version) + }).catch(() => Promise.reject()); + }; + + public async getNetworkParameters(): Promise { + debug.log('getNetworkParameters, %o ', arguments) + + return this.driver.sendCommand(ZiGateCommandCode.GetNetworkState, {}, 10000) + .then((NetworkStateResponse) => { + const resultPayload: TsType.NetworkParameters = { + panID: NetworkStateResponse.payload.PANID, + extendedPanID: NetworkStateResponse.payload.ExtPANID, + channel: NetworkStateResponse.payload.Channel + } + return Promise.resolve(resultPayload) + }).catch(() => Promise.reject()); + }; + + public async reset(type: 'soft' | 'hard'): Promise { + debug.log('reset', type, arguments) + + if (type === 'soft') { + await this.driver.sendCommand(ZiGateCommandCode.Reset, {}, 5000); + return Promise.resolve(); + } else if (type === 'hard') { + await this.driver.sendCommand(ZiGateCommandCode.ErasePersistentData, {}, 5000); + return Promise.resolve(); + } + }; + + public supportsLED(): Promise { + return Promise.reject(); + }; + + public setLED(enabled: boolean): Promise { + return Promise.reject(); + }; + + /** + * https://zigate.fr/documentation/deplacer-le-pdm-de-la-zigate/ + * pdm from host + */ + public async supportsBackup(): Promise { + return false; + }; + + public async backup(): Promise { + return Promise.reject(); + }; + + public async setTransmitPower(value: number): Promise { + debug.log('setTransmitPower, %o', arguments); + return this.driver.sendCommand(ZiGateCommandCode.SetTXpower, {value: value}) + .then(() => Promise.resolve()).catch(() => Promise.reject()); + }; + + public async permitJoin(seconds: number, networkAddress: number): Promise { + await this.driver.sendCommand(ZiGateCommandCode.PermitJoin, { + targetShortAddress: networkAddress || 0, + interval: seconds, + TCsignificance: 0 + }); + + const result = await this.driver.sendCommand(ZiGateCommandCode.PermitJoinStatus, {}); + this.joinPermitted = result.payload.status === 1; + }; + + // @TODO + public async lqi(networkAddress: number): Promise { + debug.log('lqi, %o', arguments) + + const neighbors: LQINeighbor[] = []; + + const add = (list: any) => { + for (const entry of list) { + const relationByte = entry.readUInt8(18); + const extAddr: Buffer = entry.slice(8, 16); + neighbors.push({ + linkquality: entry.readUInt8(21), + networkAddress: entry.readUInt16LE(16), + ieeeAddr: BuffaloZiGate.addressBufferToStringBE(extAddr), + relationship: (relationByte >> 1) & ((1 << 3) - 1), + depth: entry.readUInt8(20) + }); + } + }; + + const request = async (startIndex: number): Promise => { + + + try { + const resultPayload = await this.driver.sendCommand(ZiGateCommandCode.ManagementLQI, + {targetAddress: networkAddress, startIndex: startIndex} + ); + const data = resultPayload.payload.payload; + + if (data[1] !== 0) { // status + throw new Error(`LQI for '${networkAddress}' failed`); + } + const tableList: Buffer[] = []; + const response = { + status: data[1], + tableEntrys: data[2], + startIndex: data[3], + tableListCount: data[4], + tableList: tableList + } + + let tableEntry: number[] = []; + let counter = 0; + + for (let i = 5; i < ((response.tableListCount * 22) + 5); i++) { // one tableentry = 22 bytes + tableEntry.push(data[i]); + counter++; + if (counter === 22) { + response.tableList.push(Buffer.from(tableEntry)); + tableEntry = []; + counter = 0; + } + } + + debug.log("LQI RESPONSE - addr: " + networkAddress.toString(16) + " status: " + + response.status + " read " + (response.tableListCount + response.startIndex) + + "/" + response.tableEntrys + " entrys"); + return response; + } catch (error) { + debug.log("LQI REQUEST FAILED - addr: 0x" + networkAddress.toString(16) + " " + error); + return Promise.reject(); + } + }; + + let response = await request(0); + add(response.tableList); + let nextStartIndex = response.tableListCount; + + while (neighbors.length < response.tableEntrys) { + response = await request(nextStartIndex); + add(response.tableList); + nextStartIndex += response.tableListCount; + } + + return {neighbors}; + }; + + // @TODO + public routingTable(networkAddress: number): Promise { + debug.log('RoutingTable, %o', arguments) + return Promise.reject(); + }; + + public async nodeDescriptor(networkAddress: number): Promise { + debug.log('nodeDescriptor, \n %o', arguments) + + try { + const nodeDescriptorResponse = await this.driver.sendCommand( + ZiGateCommandCode.NodeDescriptor, { + targetShortAddress: networkAddress + } + ); + + const data: Buffer = nodeDescriptorResponse.payload.payload; + const buf = data; + const logicaltype = (data[4] & 7); + let type: DeviceType = 'Unknown'; + switch (logicaltype) { + case 1: + type = 'Router'; + break; + case 2: + type = 'EndDevice'; + break; + case 0: + type = 'Coordinator'; + break; + + } + const manufacturer = buf.readUInt16LE(7); + + debug.log("RECEIVING NODE_DESCRIPTOR - addr: 0x" + networkAddress.toString(16) + + " type: " + type + " manufacturer: 0x" + manufacturer.toString(16)); + + return {manufacturerCode: manufacturer, type}; + } catch (error) { + debug.error("RECEIVING NODE_DESCRIPTOR FAILED - addr: 0x" + + networkAddress.toString(16) + " " + error); + return Promise.reject(); + } + }; + + public async activeEndpoints(networkAddress: number): Promise { + debug.log('ActiveEndpoints request: %o', arguments); + const payload = { + targetShortAddress: networkAddress + } + + try { + const result = await this.driver.sendCommand(ZiGateCommandCode.ActiveEndpoint, payload); + + const zclFrame = ZiGateObject.fromBufer( + ZiGateMessageCode.ActiveEndpointResponse, result.payload.payload); + const payloadAE: TsType.ActiveEndpoints = { + endpoints: zclFrame.payload.endpoints + } + + debug.log('ActiveEndpoints response: %o', payloadAE); + return payloadAE; + + } catch (error) { + debug.error("RECEIVING ActiveEndpoints FAILED, %o", error); + return Promise.reject(); + } + }; + + public async simpleDescriptor(networkAddress: number, endpointID: number): Promise { + debug.log('SimpleDescriptor request: %o', arguments) + + try { + const payload = { + targetShortAddress: networkAddress, + endpoint: endpointID + } + const result = await this.driver.sendCommand(ZiGateCommandCode.SimpleDescriptor, payload); + + const buf: Buffer = result.payload.payload; + + if (buf.length > 11) { + + const inCount = buf.readUInt8(11); + const inClusters = []; + let cIndex = 12; + for (let i = 0; i < inCount; i++) { + inClusters[i] = buf.readUInt16LE(cIndex); + cIndex += 2; + } + const outCount = buf.readUInt8(12 + (inCount * 2)); + const outClusters = []; + cIndex = 13 + (inCount * 2); + for (let l = 0; l < outCount; l++) { + outClusters[l] = buf.readUInt16LE(cIndex); + cIndex += 2; + } + + + const resultPayload: TsType.SimpleDescriptor = { + profileID: buf.readUInt16LE(6), + endpointID: buf.readUInt8(5), + deviceID: buf.readUInt16LE(8), + inputClusters: inClusters, + outputClusters: outClusters + } + + debug.log(resultPayload); + return resultPayload; + } + } catch (error) { + debug.error("RECEIVING SIMPLE_DESCRIPTOR FAILED - addr: 0x" + networkAddress.toString(16) + + " EP:" + endpointID + " " + error); + return Promise.reject(); + } + }; + + public async bind( + destinationNetworkAddress: number, sourceIeeeAddress: string, sourceEndpoint: number, + clusterID: number, destinationAddressOrGroup: string | number, type: 'endpoint' | 'group', + destinationEndpoint?: number + ): Promise { + debug.error('bind', arguments); + let payload = { + targetExtendedAddress: sourceIeeeAddress, + targetEndpoint: sourceEndpoint, + clusterID: clusterID, + destinationAddressMode: (type === 'group') ? 0x01 : 0x03, + destinationAddress: destinationAddressOrGroup, + }; + + if (typeof destinationEndpoint !== undefined) { + // @ts-ignore + payload['destinationEndpoint'] = destinationEndpoint + } + const result = await this.driver.sendCommand(ZiGateCommandCode.Bind, payload, + null, {destinationNetworkAddress} + ); + + let data = result.payload.payload; + if (data[1] === 0) { + debug.log('Bind %s success', sourceIeeeAddress); + return Promise.resolve(); + } else { + debug.error('Bind %s failed', sourceIeeeAddress); + return Promise.reject(); + } + }; + + public async unbind( + destinationNetworkAddress: number, sourceIeeeAddress: string, sourceEndpoint: number, + clusterID: number, destinationAddressOrGroup: string | number, type: 'endpoint' | 'group', + destinationEndpoint: number + ): Promise { + debug.error('unbind', arguments); + let payload = { + targetExtendedAddress: sourceIeeeAddress, + targetEndpoint: sourceEndpoint, + clusterID: clusterID, + destinationAddressMode: (type === 'group') ? 0x01 : 0x03, + destinationAddress: destinationAddressOrGroup, + }; + + if (typeof destinationEndpoint !== undefined) { + // @ts-ignore + payload['destinationEndpoint'] = destinationEndpoint + } + const result = await this.driver.sendCommand(ZiGateCommandCode.UnBind, payload, + null, + {destinationNetworkAddress}); + + + let data = result.payload.payload; + if (data[1] === 0) { + debug.log('Unbind %s success', sourceIeeeAddress); + return Promise.resolve(); + } else { + debug.error('Unbind %s failed', sourceIeeeAddress); + return Promise.reject(); + } + }; + + public removeDevice(networkAddress: number, ieeeAddr: string): Promise { + const payload = { + targetShortAddress: networkAddress, + extendedAddress: ieeeAddr + }; + + // @TODO test + return this.driver.sendCommand(ZiGateCommandCode.RemoveDevice, payload) + .then(() => Promise.resolve()).catch(() => Promise.reject()); + }; + + /** + * ZCL + */ + public async sendZclFrameToEndpoint( + ieeeAddr: string, networkAddress: number, endpoint: number, zclFrame: ZclFrame, timeout: number, + disableResponse: boolean, disableRecovery: boolean, sourceEndpoint?: number, + ): Promise { + const data = zclFrame.toBuffer(); + + // @TODO deal with hardcoded parameters + const payload: RawAPSDataRequestPayload = { + addressMode: 0x02, //nwk + targetShortAddress: networkAddress, + sourceEndpoint: sourceEndpoint || 0x01, + destinationEndpoint: endpoint, + profileID: 0x0104, + clusterID: zclFrame.Cluster.ID, + securityMode: 0x02, + radius: 30, + dataLength: data.length, + data: data, + } + debug.log('sendZclFrameToEndpoint: \n %O', payload) + + const extraParameters = { + transactionSequenceNumber: zclFrame.Header.transactionSequenceNumber + }; + try { + const result = await this.driver.sendCommand( + ZiGateCommandCode.RawAPSDataRequest, payload, + undefined, extraParameters + ); + + if (result !== null) { + const frame: ZclFrame = ZclFrame.fromBuffer(zclFrame.Cluster.ID, result.payload.payload); + + const resultPayload: Events.ZclDataPayload = { + address: result.payload.sourceAddress, + frame: frame, + endpoint: result.payload.sourceEndpoint, + linkquality: result.frame.readRSSI(), + groupID: 0 + } + return resultPayload; + } else { + debug.error('no response') + return null; + } + + } catch (e) { + return Promise.reject(e); + } + }; + + public sendZclFrameToAll(endpoint: number, zclFrame: ZclFrame, sourceEndpoint: number): Promise { + debug.log('sendZclFrameToAll: \n %o', arguments) + + // @TODO ?? fix zigate GP 242 not supported + if (sourceEndpoint !== 0x01 && sourceEndpoint !== 0x0A) { + debug.error('source endpoint %d, not supported', sourceEndpoint); + return; + } + + const data = zclFrame.toBuffer(); + const payload: RawAPSDataRequestPayload = { + addressMode: 2, //nwk + targetShortAddress: 0xFFFD, + sourceEndpoint: sourceEndpoint, + destinationEndpoint: endpoint, + profileID: 0x0104, + clusterID: zclFrame.Cluster.ID, + securityMode: 0x02, + radius: 30, + dataLength: data.length, + data: data, + } + debug.log('sendZclFrameToAll', payload) + + return this.driver.sendCommand(ZiGateCommandCode.RawAPSDataRequest, payload) + .then(() => Promise.resolve()).catch(() => Promise.reject()); + }; + + public sendZclFrameToGroup(groupID: number, zclFrame: ZclFrame, sourceEndpoint?: number): Promise { + debug.log('sendZclFrameToGroup', arguments) + return + }; + + /** + * InterPAN + */ + public async setChannelInterPAN(channel: number): Promise { + debug.log('setChannelInterPAN', arguments) + return Promise.reject(); + }; + + public async sendZclFrameInterPANToIeeeAddr(zclFrame: ZclFrame, ieeeAddress: string): Promise { + debug.log('sendZclFrameInterPANToIeeeAddr', arguments) + return Promise.reject(); + }; + + public async sendZclFrameInterPANBroadcast( + zclFrame: ZclFrame, timeout: number + ): Promise { + debug.log('sendZclFrameInterPANBroadcast', arguments) + return Promise.reject(); + }; + + /** + * Supplementary functions + */ + private async initNetwork(): Promise { + debug.log(`Set channel mask ${this.networkOptions.channelList} key`); + await this.driver.sendCommand( + ZiGateCommandCode.SetChannelMask, + {channelMask: channelsToMask(this.networkOptions.channelList)}, + ); + debug.log(`Set security key`); + + await this.driver.sendCommand( + ZiGateCommandCode.SetSecurityStateKey, + { + keyType: this.networkOptions.networkKeyDistribute ? + ZPSNwkKeyState.ZPS_ZDO_DISTRIBUTED_LINK_KEY : + ZPSNwkKeyState.ZPS_ZDO_PRECONFIGURED_LINK_KEY, + key: this.networkOptions.networkKey, + }, + ); + + + // @TODO + try { + // set EPID from config + debug.log('Set EPanID %h', this.networkOptions.extendedPanID); + await this.driver.sendCommand(ZiGateCommandCode.SetExtendedPANID, { + panId: this.networkOptions.extendedPanID, + }); + + await this.driver.sendCommand(ZiGateCommandCode.StartNetwork, {}); + }catch (e) { + // @TODO + debug.error(e); + } + + return Promise.resolve(); + } + + public restoreChannelInterPAN(): Promise { + debug.log('restoreChannelInterPAN', arguments) + return Promise.reject(); + }; + + public waitFor( + networkAddress: number, endpoint: number, frameType: FrameType, direction: Direction, + transactionSequenceNumber: number, clusterID: number, commandIdentifier: number, timeout: number, + ): { promise: Promise; cancel: () => void } { + debug.log('waitFor', arguments) + const payload = { + address: networkAddress, endpoint, clusterID, commandIdentifier, frameType, direction, + transactionSequenceNumber, + }; + const waiter = this.waitress.waitFor(payload, timeout); + const cancel = (): void => this.waitress.remove(waiter.ID); + return {promise: waiter.start().promise, cancel}; + }; + + private waitressTimeoutFormatter(matcher: WaitressMatcher, timeout: number): string { + debug.log('waitressTimeoutFormatter', arguments) + return `Timeout - ${matcher.address} - ${matcher.endpoint}` + + ` - ${matcher.transactionSequenceNumber} - ${matcher.clusterID}` + + ` - ${matcher.commandIdentifier} after ${timeout}ms`; + } + + private waitressValidator(payload: Events.ZclDataPayload, matcher: WaitressMatcher): boolean { + debug.log('waitressValidator', arguments) + 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; + } + + public static async isValidPath(path: string): Promise { + return Driver.isValidPath(path); + } + + public static async autoDetectPath(): Promise { + return Driver.autoDetectPath(); + } + +} + +export default ZiGateAdapter; diff --git a/src/adapter/zigate/debug.ts b/src/adapter/zigate/debug.ts new file mode 100644 index 0000000000..fb32629343 --- /dev/null +++ b/src/adapter/zigate/debug.ts @@ -0,0 +1,19 @@ +/* istanbul ignore file */ +/* eslint-disable */ +import debug from 'debug'; + +debug.formatters.h = (v): string => { + return v.toString('hex'); +}; +const adapterDebug = debug('zigbee-herdsman:adapter:zigate'); + +const Debug = (suffix: string): { log: debug.Debugger, error: debug.Debugger , info: debug.Debugger} => { + const extendDebug = adapterDebug.extend(suffix); + return { + log: extendDebug.extend('log'), + info: extendDebug.extend('info'), + error: extendDebug.extend('error'), + }; +}; + +export {Debug}; diff --git a/src/adapter/zigate/driver/LICENSE b/src/adapter/zigate/driver/LICENSE new file mode 100644 index 0000000000..e845d25e71 --- /dev/null +++ b/src/adapter/zigate/driver/LICENSE @@ -0,0 +1,17 @@ +When writing the adapter, the first tests and code implementation examples were taken from +https://github.com/nouknouk/node-zigate +https://github.com/Neonox31/zigate + + +The zigate frame parsing is mostly inherited from Neonox31/zigate +driver/frame.ts + +Copyright 2017 WEBER Logan + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + diff --git a/src/adapter/zigate/driver/buffaloZiGate.ts b/src/adapter/zigate/driver/buffaloZiGate.ts new file mode 100644 index 0000000000..88c6d18733 --- /dev/null +++ b/src/adapter/zigate/driver/buffaloZiGate.ts @@ -0,0 +1,126 @@ +/* istanbul ignore file */ +/* eslint-disable */ +import {Buffalo, TsType as BuffaloTsType, TsType} from '../../../buffalo'; +import {Options, Value} from "../../../buffalo/tstype"; +import {IsNumberArray} from "../../../utils"; + +export interface BuffaloZiGateOptions extends BuffaloTsType.Options { + startIndex?: number; +} + +class BuffaloZiGate extends Buffalo { + public write(type: string, value: Value, options: Options): void { + if (type === 'RAW') { + this.buffer.set(value, this.position); + this.position++; + } else if (type === 'UINT16BE') { + this.writeUInt16BE(value); + } else if (type === 'UINT32BE') { + this.writeUInt32BE(value); + } else if (type === 'IEEADDR') { + return this.readIeeeAddr(); + }else if (type === 'ADDRESS_WITH_TYPE_DEPENDENCY') { + const addressMode = this.buffer.readUInt8(this.position - 1); + return addressMode == 3 ? this.writeIeeeAddr(value) : this.writeUInt16BE(value); + } else if (type === 'BUFFER' && (Buffer.isBuffer(value) || IsNumberArray(value))) { + this.writeBuffer(value, value.length); + } else { + super.write(type, value, options); + } + } + + public static addressBufferToStringBE(buffer: Buffer): string { + let address = '0x'; + for (let i = 0; i < buffer.length; i++) { + const value = buffer.readUInt8(i); + if (value <= 15) { + address += '0' + value.toString(16); + } else { + address += value.toString(16); + } + } + + return address; + } + + public read(type: string, options: BuffaloZiGateOptions): TsType.Value { + + if (type === 'MACCAPABILITY') { + const result: { [k: string]: boolean | number } = {}; + const mac = this.readUInt8(); + // + result.alternatePanCoordinator = !!(mac.mac & 0b00000001); + // bit 0: Alternative PAN Coordinator, always 0 + result.fullFunctionDevice = !!(mac.mac & 0b00000010); + // bit 1: Device Type, 1 = FFD , 0 = RFD ; cf. https://fr.wikipedia.org/wiki/IEEE_802.15.4 + result.mainsPowerSource = !!(mac.mac & 0b00000100); + // bit 2: Power Source, 1 = mains power, 0 = other + result.receiverOnWhenIdle = !!(mac.mac & 0b00001000); + // bit 3: Receiver on when Idle, 1 = non-sleepy, 0 = sleepy + result.reserved = (mac.mac & 0b00110000) >> 4; + // bit 4&5: Reserved + result.securityCapability = !!(mac.mac & 0b01000000); + // bit 6: Security capacity, always 0 (standard security) + result.allocateAddress = !!(mac.mac & 0b10000000); + // bit 7: 1 = joining device must be issued network address + + return result; + } else if (type === 'UINT16BE') { + return this.readUInt16BE(); + } else if (type === 'UINT32BE') { + return this.readUInt32BE(); + } else if (type === 'IEEADDR') { + return this.readIeeeAddr(); + } else if (type === 'ADDRESS_WITH_TYPE_DEPENDENCY') { + // rep.addressSourceMode = Enum.ADDRESS_MODE(reader.nextUInt8()); + // rep.addressSource = rep.addressSourceMode.name === 'short' ? + // reader.nextUInt16BE() : reader.nextBuffer(8).toString('hex'); + // + const addressMode = this.buffer.readUInt8(this.position - 1); + return addressMode == 3 ? this.readIeeeAddr() : this.readUInt16BE(); + } else if (type === 'BUFFER_RAW') { + const buffer = this.buffer.slice(this.position); + this.position += buffer.length; + return buffer; + } else { + return super.read(type, options); + } + } + + public writeIeeeAddr(value: string): void { + this.writeUInt32BE(parseInt(value.slice(2, 10), 16)); + this.writeUInt32BE(parseInt(value.slice(10), 16)); + } + + public readIeeeAddr(): Value { + const length = 8; + const value = this.buffer.slice(this.position, this.position + length); + this.position += length; + return BuffaloZiGate.addressBufferToStringBE(value); + } + + public readUInt16BE(): Value { + const value = this.buffer.readUInt16BE(this.position); + this.position += 2; + return value; + } + + public readUInt32BE(): Value { + const value = this.buffer.readUInt32BE(this.position); + this.position += 4; + return value; + } + + public writeUInt16BE(value: number): void { + this.buffer.writeUInt16BE(value, this.position); + this.position += 2; + } + + public writeUInt32BE(value: number): void { + this.buffer.writeUInt32BE(value, this.position); + this.position += 4; + } + +} + +export default BuffaloZiGate; diff --git a/src/adapter/zigate/driver/commandType.ts b/src/adapter/zigate/driver/commandType.ts new file mode 100644 index 0000000000..5271df2bdd --- /dev/null +++ b/src/adapter/zigate/driver/commandType.ts @@ -0,0 +1,424 @@ +/* istanbul ignore file */ +/* eslint-disable */ +import {ZiGateCommandCode, ZiGateMessageCode, ZiGateObjectPayload} from "./constants"; + + +export interface PermitJoinPayload extends ZiGateObjectPayload { + targetShortAddress: number + interval: number + TCsignificance?: number +} + +export interface RawAPSDataRequestPayload extends ZiGateObjectPayload { + addressMode: number + targetShortAddress: number + sourceEndpoint: number + destinationEndpoint: number + profileID: number + clusterID: number + securityMode: number + radius: number + dataLength: number + data: Buffer, +} + +export interface ZiGateCommandParameter { + name: string; + parameterType: string; +} + +export interface ZiGateCommandType { + request: ZiGateCommandParameter[]; + response?: ZiGateResponseMatcher[]; + waitStatus?: boolean; +} + +export interface ZiGateResponseMatcherRule { + receivedProperty: string; + matcher: (expected: string | number | ZiGateMessageCode, + received: string | number | ZiGateMessageCode) => boolean; + expectedProperty?: string; // or + expectedExtraParameter?: string; // or + value?: string | number | ZiGateMessageCode; + +} + +export function equal( + expected: string | number | ZiGateMessageCode, + received: string | number | ZiGateMessageCode): boolean { + + return expected === received; +} + +export function notEqual( + expected: string | number | ZiGateMessageCode, + received: string | number | ZiGateMessageCode): boolean { + + return expected !== received; +} + +export type ZiGateResponseMatcher = ZiGateResponseMatcherRule[]; + + +export const ZiGateCommand: { [key: string]: ZiGateCommandType } = { + [ZiGateCommandCode.SetDeviceType]: { // 0x0023 + request: [ + {name: 'deviceType', parameterType: 'UINT8'} // + ], + }, + [ZiGateCommandCode.StartNetwork]: { // 0x0024 + request: [], + response: [ + [ + {receivedProperty: 'code', matcher: equal, value: ZiGateMessageCode.NetworkJoined} + ], + ] + }, + [ZiGateCommandCode.StartNetworkScan]: { + request: [], + }, + [ZiGateCommandCode.GetNetworkState]: { // 0x0009 + request: [], + response: [ + [ + {receivedProperty: 'code', matcher: equal, value: ZiGateMessageCode.NetworkState}, + ], + ] + }, + [ZiGateCommandCode.GetTimeServer]: { // 0x0017 + request: [] + }, + [ZiGateCommandCode.ErasePersistentData]: { // 0x0012 + request: [], + response: [ + [ + { + receivedProperty: 'code', + matcher: equal, + value: ZiGateMessageCode.RestartFactoryNew + }, + ] + ], + waitStatus: false + }, + [ZiGateCommandCode.Reset]: { // 0x0011 + request: [], + response: [ + [ + { + receivedProperty: 'code', + matcher: equal, + value: ZiGateMessageCode.RestartNonFactoryNew + }, + ], + [ + { + receivedProperty: 'code', + matcher: equal, + value: ZiGateMessageCode.RestartFactoryNew + }, + ], + ], + waitStatus: false + }, + [ZiGateCommandCode.SetTXpower]: { // SetTXpower + request: [ + {name: 'value', parameterType: 'UINT8'} + ] + }, + [ZiGateCommandCode.ManagementLQI]: { // 0x004E + request: [ + {name: 'targetAddress', parameterType: 'UINT16BE'}, // Status + {name: 'startIndex', parameterType: 'UINT8'}, // + + ], + response: [ + [ + { + receivedProperty: 'code', + matcher: equal, + value: ZiGateMessageCode.DataIndication + }, + { + receivedProperty: 'payload.sourceAddress', + matcher: equal, + expectedProperty: 'payload.targetAddress' + }, + { + receivedProperty: 'payload.clusterID', + matcher: equal, + value: 0x8031 + }, + ], + ] + }, + [ZiGateCommandCode.SetSecurityStateKey]: { // 0x0022 + request: [ + {name: 'keyType', parameterType: 'UINT8'}, // + {name: 'key', parameterType: 'BUFFER'}, // + + ], + }, + [ZiGateCommandCode.GetVersion]: { + request: [], + response: [ + [ + {receivedProperty: 'code', matcher: equal, value: ZiGateMessageCode.VersionList} + ], + ] + }, + [ZiGateCommandCode.RawMode]: { + request: [ + {name: 'enabled', parameterType: 'INT8'}, + ] + }, + [ZiGateCommandCode.SetExtendedPANID]: { + request: [ + {name: 'panId', parameterType: 'BUFFER'}, //<64-bit Extended PAN ID:uint64_t> + ] + }, + [ZiGateCommandCode.SetChannelMask]: { + request: [ + {name: 'channelMask', parameterType: 'UINT32BE'}, // + ] + }, + [ZiGateCommandCode.RemoveDevice]: { + request: [ + {name: 'targetShortAddress', parameterType: 'UINT16BE'}, // + {name: 'extendedAddress', parameterType: 'IEEEADDR'}, // + ], + response: [ + [ + { + receivedProperty: 'code', + matcher: equal, + value: ZiGateMessageCode.LeaveIndication + }, + { + receivedProperty: 'payload.extendedAddress', matcher: equal, + expectedProperty: 'payload.extendedAddress' + }, + ], + ] + }, + [ZiGateCommandCode.PermitJoin]: { + request: [ + {name: 'targetShortAddress', parameterType: 'UINT16BE'}, // - + // broadcast 0xfffc + {name: 'interval', parameterType: 'UINT8'}, // + // 0 = Disable Joining + // 1 – 254 = Time in seconds to allow joins + // 255 = Allow all joins + // {name: 'TCsignificance', parameterType: 'UINT8'}, // + // 0 = No change in authentication + // 1 = Authentication policy as spec + ] + }, + [ZiGateCommandCode.PermitJoinStatus]: { + request: [ + {name: 'targetShortAddress', parameterType: 'UINT16BE'}, // - + // broadcast 0xfffc + {name: 'interval', parameterType: 'UINT8'}, // + // 0 = Disable Joining + // 1 – 254 = Time in seconds to allow joins + // 255 = Allow all joins + {name: 'TCsignificance', parameterType: 'UINT8'}, // + // 0 = No change in authentication + // 1 = Authentication policy as spec + ], + response: [ + [ + {receivedProperty: 'code', matcher: equal, value: ZiGateMessageCode.PermitJoinStatus} + ], + ] + }, + [ZiGateCommandCode.RawAPSDataRequest]: { + request: [ + {name: 'addressMode', parameterType: 'UINT8'}, //
+ {name: 'targetShortAddress', parameterType: 'UINT16BE'}, // + {name: 'sourceEndpoint', parameterType: 'UINT8'}, // + {name: 'destinationEndpoint', parameterType: 'UINT8'}, // + {name: 'clusterID', parameterType: 'UINT16BE'}, // + {name: 'profileID', parameterType: 'UINT16BE'}, // + {name: 'securityMode', parameterType: 'UINT8'}, // + {name: 'radius', parameterType: 'UINT8'}, // + {name: 'dataLength', parameterType: 'UINT8'}, // + {name: 'data', parameterType: 'BUFFER'}, // + ], + response: [ + [ + {receivedProperty: 'code', matcher: equal, value: ZiGateMessageCode.DataIndication}, + { + receivedProperty: 'payload.sourceAddress', + matcher: equal, + expectedProperty: 'payload.targetShortAddress' + }, + { + receivedProperty: 'payload.clusterID', + matcher: equal, + expectedProperty: 'payload.clusterID' + }, + { + receivedProperty: 'payload.sourceEndpoint', + matcher: equal, + expectedProperty: 'payload.sourceEndpoint' + }, + { + receivedProperty: 'payload.destinationEndpoint', + matcher: equal, + expectedProperty: 'payload.destinationEndpoint' + }, + { + receivedProperty: 'payload.profileID', + matcher: equal, + expectedProperty: 'payload.profileID' + }, + ], + ] + }, + [ZiGateCommandCode.NodeDescriptor]: { + request: [ + {name: 'targetShortAddress', parameterType: 'UINT16BE'}, // + ], + response: [ + [ + { + receivedProperty: 'code', + matcher: equal, + value: ZiGateMessageCode.DataIndication + }, + { + receivedProperty: 'payload.sourceAddress', + matcher: equal, + expectedProperty: 'payload.targetShortAddress' + }, + { + receivedProperty: 'payload.clusterID', + matcher: equal, + value: 0x8002 + } + ], + ] + }, + [ZiGateCommandCode.ActiveEndpoint]: { + request: [ + {name: 'targetShortAddress', parameterType: 'UINT16BE'}, // + ], + response: [ + [ + { + receivedProperty: 'code', + matcher: equal, + value: ZiGateMessageCode.DataIndication + }, + { + receivedProperty: 'payload.sourceAddress', + matcher: equal, + expectedProperty: 'payload.targetShortAddress' + }, + { + receivedProperty: 'payload.clusterID', + matcher: equal, + value: ZiGateMessageCode.ActiveEndpointResponse + } + ], + ] + }, + [ZiGateCommandCode.SimpleDescriptor]: { + request: [ + {name: 'targetShortAddress', parameterType: 'UINT16BE'}, // + {name: 'endpoint', parameterType: 'UINT8'}, // + ], + response: [ + [ + {receivedProperty: 'code', matcher: equal, value: ZiGateMessageCode.DataIndication}, + { + receivedProperty: 'payload.sourceAddress', + matcher: equal, + expectedProperty: 'payload.targetShortAddress' + }, + { + receivedProperty: 'payload.clusterID', + matcher: equal, + value: 0x8004 + } + ], + ] + }, + [ZiGateCommandCode.Bind]: { + request: [ + {name: 'targetExtendedAddress', parameterType: 'IEEEADDR'}, // + {name: 'targetEndpoint', parameterType: 'UINT8'}, // + {name: 'clusterID', parameterType: 'UINT16BE'}, // + {name: 'destinationAddressMode', parameterType: 'UINT8'}, // + { + name: 'destinationAddress', + parameterType: 'ADDRESS_WITH_TYPE_DEPENDENCY' + }, // + {name: 'destinationEndpoint', parameterType: 'UINT8'}, // + ], + response: [ + [ + { + receivedProperty: 'code', + matcher: equal, + value: ZiGateMessageCode.DataIndication + }, + { + receivedProperty: 'payload.sourceAddress', + matcher: equal, + expectedExtraParameter: 'destinationNetworkAddress' + }, + { + receivedProperty: 'payload.clusterID', + matcher: equal, + value: 0x8021 + }, + { + receivedProperty: 'payload.profileID', + matcher: equal, + value: 0x0000 + }, + ], + ] + }, + [ZiGateCommandCode.UnBind]: { + request: [ + {name: 'targetExtendedAddress', parameterType: 'IEEEADDR'}, // + {name: 'targetEndpoint', parameterType: 'UINT8'}, // + {name: 'clusterID', parameterType: 'UINT16BE'}, // + {name: 'destinationAddressMode', parameterType: 'UINT8'}, // + { + name: 'destinationAddress', + parameterType: 'ADDRESS_WITH_TYPE_DEPENDENCY' + }, // + {name: 'destinationEndpoint', parameterType: 'UINT8'}, // + ], + response: [ + [ + { + receivedProperty: 'code', + matcher: equal, + value: ZiGateMessageCode.DataIndication + }, + { + receivedProperty: 'payload.sourceAddress', + matcher: equal, + expectedExtraParameter: 'destinationNetworkAddress' + }, + { + receivedProperty: 'payload.clusterID', + matcher: equal, + value: 0x8022 + }, + { + receivedProperty: 'payload.profileID', + matcher: equal, + value: 0x0000 + }, + ], + ] + }, +}; diff --git a/src/adapter/zigate/driver/constants.ts b/src/adapter/zigate/driver/constants.ts new file mode 100644 index 0000000000..805e84bfb7 --- /dev/null +++ b/src/adapter/zigate/driver/constants.ts @@ -0,0 +1,362 @@ +/* istanbul ignore file */ +/* eslint-disable */ + +export enum ADDRESS_MODE { + bound = 0, //Use one or more bound nodes/endpoints, with acknowledgements + group = 1, //Use a pre-defined group address, with acknowledgements + short = 2, //Use a 16-bit network address, with acknowledgements + ieee = 3, //Use a 64-bit IEEE/MAC address, with acknowledgements + broadcast = 4, //Perform a broadcast + no_transmit = 5, //Do not transmit + bound_no_ack = 6, //Perform a bound transmission, with no acknowledgements + short_no_ack = 7, //Perform a transmission using a 16-bit network address, with no acknowledgements + ieee_no_ack = 8, //Perform a transmission using a 64-bit IEEE/MAC address, with no acknowledgements + bound_non_blocking = 9, //Perform a non-blocking bound transmission, with acknowledgements + bound_non_blocking_no_ack = 10, //Perform a non-blocking bound transmission, with no acknowledgements +} + +export enum DEVICE_TYPE { + coordinator = 0, + router = 1, + legacy_router = 2, +} + +export enum BOOLEAN { + false = 0x00, + true = 0x01, +} + +export enum NODE_LOGICAL_TYPE { + coordinator = 0x00, + router = 0x01, + end_device = 0x02, +} + +export enum STATUS { + success = 0, + invalid_params = 1, + unhandled_command = 2, + command_failed = 3, + busy = 4, //Node is carrying out a lengthy operation and is currently unable to handle the incoming command + stack_already_started = 5, //Stack already started (no new configuration accepted) +} + +export enum PERMIT_JOIN_STATUS { + on = 1, // devices are allowed to join network + off = 0, // devices are not allowed join the network +} + +export enum NETWORK_JOIN_STATUS { + joined_existing_network = 0, + formed_new_network = 1, + failed_128 = 128, //network join failed (error 0x80) + failed_129 = 129, //network join failed (error 0x81) + failed_130 = 130, //network join failed (error 0x82) + failed_131 = 131, //network join failed (error 0x83) + failed_132 = 132, //network join failed (error 0x84) + failed_133 = 133, //network join failed (error 0x85) + failed_134 = 134, //network join failed (error 0x86) + failed_135 = 135, //network join failed (error 0x87) + failed_136 = 136, //network join failed (error 0x88) + failed_137 = 137, //network join failed (error 0x89) + failed_138 = 138, //network join failed (error 0x8a) + failed_139 = 139, //network join failed (error 0x8b) + failed_140 = 140, //network join failed (error 0x8c) + failed_141 = 141, //network join failed (error 0x8d) + failed_142 = 142, //network join failed (error 0x8e) + failed_143 = 143, //network join failed (error 0x8f) + failed_144 = 144, //network join failed (error 0x90) + failed_145 = 145, //network join failed (error 0x91) + failed_146 = 146, //network join failed (error 0x92) + failed_147 = 147, //network join failed (error 0x93) + failed_148 = 148, //network join failed (error 0x94) + failed_149 = 149, //network join failed (error 0x95) + failed_150 = 150, //network join failed (error 0x96) + failed_151 = 151, //network join failed (error 0x97) + failed_152 = 152, //network join failed (error 0x98) + failed_153 = 153, //network join failed (error 0x99) + failed_154 = 154, //network join failed (error 0x9a) + failed_155 = 155, //network join failed (error 0x9b) + failed_156 = 156, //network join failed (error 0x9c) + failed_157 = 157, //network join failed (error 0x9d) + failed_158 = 158, //network join failed (error 0x9e) + failed_159 = 159, //network join failed (error 0x9f) + failed_160 = 160, //network join failed (error 0xa0) + failed_161 = 161, //network join failed (error 0xa1) + failed_162 = 162, //network join failed (error 0xa2) + failed_163 = 163, //network join failed (error 0xa3) + failed_164 = 164, //network join failed (error 0xa4) + failed_165 = 165, //network join failed (error 0xa5) + failed_166 = 166, //network join failed (error 0xa6) + failed_167 = 167, //network join failed (error 0xa7) + failed_168 = 168, //network join failed (error 0xa8) + failed_169 = 169, //network join failed (error 0xa9) + failed_170 = 170, //network join failed (error 0xaa) + failed_171 = 171, //network join failed (error 0xab) + failed_172 = 172, //network join failed (error 0xac) + failed_173 = 173, //network join failed (error 0xad) + failed_174 = 174, //network join failed (error 0xae) + failed_175 = 175, //network join failed (error 0xaf) + failed_176 = 176, //network join failed (error 0xb0) + failed_177 = 177, //network join failed (error 0xb1) + failed_178 = 178, //network join failed (error 0xb2) + failed_179 = 179, //network join failed (error 0xb3) + failed_180 = 180, //network join failed (error 0xb4) + failed_181 = 181, //network join failed (error 0xb5) + failed_182 = 182, //network join failed (error 0xb6) + failed_183 = 183, //network join failed (error 0xb7) + failed_184 = 184, //network join failed (error 0xb8) + failed_185 = 185, //network join failed (error 0xb9) + failed_186 = 186, //network join failed (error 0xba) + failed_187 = 187, //network join failed (error 0xbb) + failed_188 = 188, //network join failed (error 0xbc) + failed_189 = 189, //network join failed (error 0xbd) + failed_190 = 190, //network join failed (error 0xbe) + failed_191 = 191, //network join failed (error 0xbf) + failed_192 = 192, //network join failed (error 0xc0) + failed_193 = 193, //network join failed (error 0xc1) + failed_194 = 194, //network join failed (error 0xc2) + failed_195 = 195, //network join failed (error 0xc3) + failed_196 = 196, //network join failed (error 0xc4) + failed_197 = 197, //network join failed (error 0xc5) + failed_198 = 198, //network join failed (error 0xc6) + failed_199 = 199, //network join failed (error 0xc7) + failed_200 = 200, //network join failed (error 0xc8) + failed_201 = 201, //network join failed (error 0xc9) + failed_202 = 202, //network join failed (error 0xca) + failed_203 = 203, //network join failed (error 0xcb) + failed_204 = 204, //network join failed (error 0xcc) + failed_205 = 205, //network join failed (error 0xcd) + failed_206 = 206, //network join failed (error 0xce) + failed_207 = 207, //network join failed (error 0xcf) + failed_208 = 208, //network join failed (error 0xd0) + failed_209 = 209, //network join failed (error 0xd1) + failed_210 = 210, //network join failed (error 0xd2) + failed_211 = 211, //network join failed (error 0xd3) + failed_212 = 212, //network join failed (error 0xd4) + failed_213 = 213, //network join failed (error 0xd5) + failed_214 = 214, //network join failed (error 0xd6) + failed_215 = 215, //network join failed (error 0xd7) + failed_216 = 216, //network join failed (error 0xd8) + failed_217 = 217, //network join failed (error 0xd9) + failed_218 = 218, //network join failed (error 0xda) + failed_219 = 219, //network join failed (error 0xdb) + failed_220 = 220, //network join failed (error 0xdc) + failed_221 = 221, //network join failed (error 0xdd) + failed_222 = 222, //network join failed (error 0xde) + failed_223 = 223, //network join failed (error 0xdf) + failed_224 = 224, //network join failed (error 0xe0) + failed_225 = 225, //network join failed (error 0xe1) + failed_226 = 226, //network join failed (error 0xe2) + failed_227 = 227, //network join failed (error 0xe3) + failed_228 = 228, //network join failed (error 0xe4) + failed_229 = 229, //network join failed (error 0xe5) + failed_230 = 230, //network join failed (error 0xe6) + failed_231 = 231, //network join failed (error 0xe7) + failed_232 = 232, //network join failed (error 0xe8) + failed_233 = 233, //network join failed (error 0xe9) + failed_234 = 234, //network join failed (error 0xea) + failed_235 = 235, //network join failed (error 0xeb) + failed_236 = 236, //network join failed (error 0xec) + failed_237 = 237, //network join failed (error 0xed) + failed_238 = 238, //network join failed (error 0xee) + failed_239 = 239, //network join failed (error 0xef) + failed_240 = 240, //network join failed (error 0xf0) + failed_241 = 241, //network join failed (error 0xf1) + failed_242 = 242, //network join failed (error 0xf2) + failed_243 = 243, //network join failed (error 0xf3) + failed_244 = 244, //network join failed (error 0xf4) +} + +export enum ON_OFF_STATUS { + on = 1, + off = 0, +} + +export enum RESTART_STATUS { + startup = 0, + nfn_start = 2, + running = 6, +} + +export enum ZiGateCommandCode { + GetNetworkState = 0x0009, + RawMode = 0x0002, + SetExtendedPANID = 0x0020, + SetChannelMask = 0x0021, + GetVersion = 0x0010, + Reset = 0x0011, + ErasePersistentData = 0x0012, + RemoveDevice = 0x0026, + PermitJoin = 0x0049, + RawAPSDataRequest = 0x0530, + GetTimeServer = 0x0017, + SetTimeServer = 0x0016, + PermitJoinStatus = 0x0014, + GetDevicesList = 0x0015, + + StartNetwork = 0x0024, + StartNetworkScan = 0x0025, + SetCertification = 0x0019, + Bind = 0x0030, + UnBind = 0x0031, + + // ResetFactoryNew = 0x0013, + OnOff = 0x0092, + OnOffTimed = 0x0093, + ActiveEndpoint = 0x0045, + AttributeDiscovery = 0x0140, + AttributeRead = 0x0100, + AttributeWrite = 0x0110, + DescriptorComplex = 0x0531, + NodeDescriptor = 0x0042, + PowerDescriptor = 0x0044, + SimpleDescriptor = 0x0043, + SetDeviceType = 0x0023, + IEEEAddress = 0x0041, + LED = 0x0018, + SetTXpower = 0x0806, + ManagementLQI = 0x004E, + SetSecurityStateKey = 0x0022, +} + + +export enum ZiGateMessageCode { + DeviceAnnounce = 0x004D, + Status = 0x8000, + DataIndication = 0x8002, + ActiveEndpointResponse = 0x8005, + NetworkState = 0x8009, + VersionList = 0x8010, + APSDataConfirm = 0x8011, // 0x8012 ?? + NetworkJoined = 0x8024, + LeaveIndication = 0x8048, + RouterDiscoveryConfirm = 0x8701, + APSDataConfirmFail = 0x8702, + PermitJoinStatus = 0x8014, + GetTimeServer = 0x8017, + ManagementLQIResponse = 0x804E, + PDMEvent = 0x8035, + RestartNonFactoryNew = 0x8006, + RestartFactoryNew = 0x8007, +} + +interface ZiGateOpjectDefaultPayload { + [key: string]: string | number | number[] | boolean | Buffer +} + +export type ZiGateObjectPayload = ZiGateOpjectDefaultPayload; + + +export enum ZPSNwkKeyState { + ZPS_ZDO_NO_NETWORK_KEY, + ZPS_ZDO_PRECONFIGURED_LINK_KEY, + ZPS_ZDO_DISTRIBUTED_LINK_KEY, + ZPS_ZDO_PRECONFIGURED_INSTALLATION_CODE, +} + +export enum ZPSNwkKeyType { + ZPS_APS_UNIQUE_LINK_KEY, /*Initial key*/ + ZPS_APS_GLOBAL_LINK_KEY, +} + +export enum PDMEventType { + E_PDM_SYSTEM_EVENT_WEAR_COUNT_TRIGGER_VALUE_REACHED=0, + E_PDM_SYSTEM_EVENT_DESCRIPTOR_SAVE_FAILED, + E_PDM_SYSTEM_EVENT_PDM_NOT_ENOUGH_SPACE, + E_PDM_SYSTEM_EVENT_LARGEST_RECORD_FULL_SAVE_NO_LONGER_POSSIBLE, + E_PDM_SYSTEM_EVENT_SEGMENT_DATA_CHECKSUM_FAIL, + E_PDM_SYSTEM_EVENT_SEGMENT_SAVE_OK, + E_PDM_SYSTEM_EVENT_EEPROM_SEGMENT_HEADER_REPAIRED, + E_PDM_SYSTEM_EVENT_SYSTEM_INTERNAL_BUFFER_WEAR_COUNT_SWAP, + E_PDM_SYSTEM_EVENT_SYSTEM_DUPLICATE_FILE_SEGMENT_DETECTED, + E_PDM_SYSTEM_EVENT_SYSTEM_ERROR, + E_PDM_SYSTEM_EVENT_SEGMENT_PREWRITE, + E_PDM_SYSTEM_EVENT_SEGMENT_POSTWRITE, + E_PDM_SYSTEM_EVENT_SEQUENCE_DUPLICATE_DETECTED, + E_PDM_SYSTEM_EVENT_SEQUENCE_VERIFY_FAIL, + E_PDM_SYSTEM_EVENT_PDM_SMART_SAVE, + E_PDM_SYSTEM_EVENT_PDM_FULL_SAVE +} + + +const coordinatorEndpoints: any = [ + { + ID: 0x01, + profileID: 0x0104, + deviceID: 0x0840, + inputClusters: [ + 0x0000, + 0x0003, + 0x0019, + 0x0204, + 0x000F, + ], + outputClusters: [ + 0x0B03, + 0x0000, + 0x0300, + 0x0004, + 0x0003, + 0x0008, + 0x0006, + 0x0005, + 0x0101, + 0x0702, + 0x0500, + 0x0019, + 0x0201, + 0x0401, + 0x0400, + 0x0406, + 0x0403, + 0x0405, + 0x0402, + 0x0204, + 0x0001, + 0x0B05, + 0x1000 + ] + }, + { + ID: 0x0A, + profileID: 0x0104, + deviceID: 0x0840, + inputClusters: [ + 0x0000, + 0x0003, + 0x0019, + 0x0204, + 0x000F, + ], + outputClusters: [ + 0x0B03, + 0x0000, + 0x0300, + 0x0004, + 0x0003, + 0x0008, + 0x0006, + 0x0005, + 0x0101, + 0x0702, + 0x0500, + 0x0019, + 0x0201, + 0x0401, + 0x0400, + 0x0406, + 0x0403, + 0x0405, + 0x0402, + 0x0204, + 0x0001, + 0x0B05, + 0x1000 + ] + } +]; + +export {coordinatorEndpoints}; + + diff --git a/src/adapter/zigate/driver/frame.ts b/src/adapter/zigate/driver/frame.ts new file mode 100644 index 0000000000..2a18bad347 --- /dev/null +++ b/src/adapter/zigate/driver/frame.ts @@ -0,0 +1,211 @@ +/* istanbul ignore file */ +/* eslint-disable */ +import {Debug} from '../debug'; + +const debug = Debug('driver:frame'); + +enum ZiGateFrameChunkSize { + UInt8 = 1, + UInt16, + UInt32, + UInt64 +} + +const hasStartByte = (startByte: number, frame: Buffer): boolean => { + return frame.indexOf(startByte, 0) === 0; +}; + +const hasStopByte = (stopByte: number, frame: Buffer): boolean => { + return frame.indexOf(stopByte, frame.length - 1) === frame.length - 1; +}; + +const combineBytes = (byte: number, idx: number, frame: number[]): [number, number] => { + const nextByte = frame[idx + 1]; + + return [byte, nextByte]; +}; +// maybe any +const removeDuplicate = (_: unknown, idx: number, frame: number[][]): boolean => { + if (idx === 0) { + return true; + } + + const [first] = frame[idx - 1]; + + return first !== 0x2; +}; + +const decodeBytes = (bytesPair: [number, number]): number => { + return bytesPair[0] === 0x2 ? bytesPair[1] ^ 0x10 : bytesPair[0]; +}; + +const readBytes = (bytes: Buffer): number => { + return bytes.readUIntBE(0, bytes.length); +}; + +const writeBytes = (bytes: Buffer, val: number): void => { + bytes.writeUIntBE(val, 0, bytes.length); +}; + +const xor = (checksum: number, byte: number): number => { + return checksum ^ byte; +}; + +const decodeFrame = (frame: Buffer): Buffer => { + const arrFrame = Array.from(frame) + .map(combineBytes) + .filter(removeDuplicate) + .map(decodeBytes); + + return Buffer.from(arrFrame); +}; + +const getFrameChunk = (frame: Buffer, pos: number, size: ZiGateFrameChunkSize): Buffer => { + return frame.slice(pos, pos + size); +}; + +export default class ZiGateFrame { + static readonly START_BYTE = 0x1; + static readonly STOP_BYTE = 0x3; + + msgCodeBytes: Buffer = Buffer.alloc(ZiGateFrameChunkSize.UInt16); + msgLengthBytes: Buffer = Buffer.alloc(ZiGateFrameChunkSize.UInt16); + checksumBytes: Buffer = Buffer.alloc(ZiGateFrameChunkSize.UInt8); + msgPayloadBytes: Buffer = Buffer.alloc(0); + rssiBytes: Buffer = Buffer.alloc(0); + + msgLengthOffset = 0; + + constructor(frame?: Buffer) { + if (frame !== undefined) { + const decodedFrame = decodeFrame(frame); + debug.log(`decoded frame >>> %o`, decodedFrame); + // Due to ZiGate incoming frames with erroneous msg length + this.msgLengthOffset = -1; + + if (!ZiGateFrame.isValid(frame)) { + debug.error('Provided frame is not a valid ZiGate frame.'); + return; + } + + this.buildChunks(decodedFrame); + + try { + debug.log(`%o`, this); + } catch (e) { + debug.error(e) + } + + if (this.readChecksum() !== this.calcChecksum()) { + debug.error(`Provided frame has an invalid checksum.`); + return; + } + } + } + + static isValid(frame: Buffer): boolean { + return hasStartByte(ZiGateFrame.START_BYTE, frame) && hasStopByte(ZiGateFrame.STOP_BYTE, frame); + } + + buildChunks(frame: Buffer): void { + this.msgCodeBytes = getFrameChunk(frame, 1, this.msgCodeBytes.length); + this.msgLengthBytes = getFrameChunk(frame, 3, this.msgLengthBytes.length); + this.checksumBytes = getFrameChunk(frame, 5, this.checksumBytes.length); + this.msgPayloadBytes = getFrameChunk(frame, 6, this.readMsgLength()); + this.rssiBytes = getFrameChunk(frame, 6 + this.readMsgLength(), ZiGateFrameChunkSize.UInt8); + } + + toBuffer(): Buffer { + const length = 5 + this.readMsgLength(); + + const escapedData = this.escapeData(Buffer.concat( + [ + this.msgCodeBytes, + this.msgLengthBytes, + this.checksumBytes, + this.msgPayloadBytes, + ], + length, + )); + + return Buffer.concat( + [ + Uint8Array.from([ZiGateFrame.START_BYTE]), + escapedData, + Uint8Array.from([ZiGateFrame.STOP_BYTE]), + ] + ); + } + + escapeData(data: Buffer): Buffer { + let encodedLength = 0; + const encodedData = Buffer.alloc(data.length * 2); + const FRAME_ESCAPE_XOR = 0x10; + const FRAME_ESCAPE = 0x02; + for (const b of data) { + if (b <= FRAME_ESCAPE_XOR) { + encodedData[encodedLength++] = FRAME_ESCAPE; + encodedData[encodedLength++] = b ^ FRAME_ESCAPE_XOR; + } else { + encodedData[encodedLength++] = b; + } + } + return encodedData.slice(0, encodedLength); + } + + readMsgCode(): number { + return readBytes(this.msgCodeBytes); + } + + writeMsgCode(msgCode: number): ZiGateFrame { + writeBytes(this.msgCodeBytes, msgCode); + this.writeChecksum(); + return this; + } + + readMsgLength(): number { + return readBytes(this.msgLengthBytes) + this.msgLengthOffset; + } + + writeMsgLength(msgLength: number): ZiGateFrame { + writeBytes(this.msgLengthBytes, msgLength); + return this; + } + + readChecksum(): number { + return readBytes(this.checksumBytes); + } + + writeMsgPayload(msgPayload: Buffer): ZiGateFrame { + this.msgPayloadBytes = Buffer.from(msgPayload); + this.writeMsgLength(msgPayload.length); + this.writeChecksum(); + return this; + } + + readRSSI(): number { + return readBytes(this.rssiBytes); + } + + writeRSSI(rssi: number): ZiGateFrame { + this.rssiBytes = Buffer.from([rssi]); + this.writeChecksum(); + return this; + } + + calcChecksum(): number { + let checksum = 0x00; + + checksum = this.msgCodeBytes.reduce(xor, checksum); + checksum = this.msgLengthBytes.reduce(xor, checksum); + checksum = this.rssiBytes.reduce(xor, checksum); + checksum = this.msgPayloadBytes.reduce(xor, checksum); + + return checksum; + } + + writeChecksum(): this { + this.checksumBytes = Buffer.from([this.calcChecksum()]); + return this; + } +} diff --git a/src/adapter/zigate/driver/messageType.ts b/src/adapter/zigate/driver/messageType.ts new file mode 100644 index 0000000000..04aa04a180 --- /dev/null +++ b/src/adapter/zigate/driver/messageType.ts @@ -0,0 +1,213 @@ +/* istanbul ignore file */ +/* eslint-disable */ +import {ZiGateMessageCode} from "./constants"; + +export interface ZiGateMessageParameter { + name: string; + parameterType: string; + options?: object; +} + +export interface ZiGateMessageType { + response: ZiGateMessageParameter[]; +} + +export const ZiGateMessage: { [k: number]: ZiGateMessageType } = { + [ZiGateMessageCode.GetTimeServer]: { + response: [ + {name: 'timestampUTC', parameterType:'UINT32'}, // from 2000-01-01 00:00:00 + ] + }, + [ZiGateMessageCode.DeviceAnnounce]: { + response: [ + {name: 'shortAddress', parameterType:'UINT16BE'}, + {name: 'ieee', parameterType:'IEEEADDR'}, + {name: 'MACcapability', parameterType:'MACCAPABILITY'}, + // MAC capability + // Bit 0 – Alternate PAN Coordinator + // Bit 1 – Device Type + // Bit 2 – Power source + // Bit 3 – Receiver On when Idle + // Bit 4,5 – Reserved + // Bit 6 – Security capability + // Bit 7 – Allocate Address + {name: 'rejoin', parameterType:'UINT8'}, + ] + }, + [ZiGateMessageCode.Status]: { + response: [ + {name: 'status', parameterType:'UINT8'}, // + // 0 = Success + // 1 = Incorrect parameters + // 2 = Unhandled command + // 3 = Command failed + // eslint-disable-next-line max-len + // 4 = Busy (Node is carrying out a lengthy operation and is currently unable to handle the incoming command) + // 5 = Stack already started (no new configuration accepted) + // 128 – 244 = Failed (ZigBee event codes) + // Packet Type: The value of the initiating command request. + {name: 'sequence', parameterType: 'UINT8'}, // + {name: 'packetType', parameterType: 'UINT16BE'}, // + {name: 'requestSent', parameterType: 'UINT8'},// - 1 if a request been sent to + // a device(aps ack/nack 8011 should be expected) , 0 otherwise + {name: 'seqApsNum', parameterType: 'UINT8'},// - sqn of the APS layer - used to + // check sqn sent back in aps ack + ] + }, + [ZiGateMessageCode.PermitJoinStatus]: { + response: [ + {name: 'status', parameterType:'UINT8'}, // + ] + }, + [ZiGateMessageCode.DataIndication]: { + response: [ + {name: 'status', parameterType:'UINT8'}, // + {name: 'profileID', parameterType:'UINT16BE'}, // + {name: 'clusterID', parameterType: 'UINT16BE'}, // + {name: 'sourceEndpoint', parameterType: 'UINT8'}, // + {name: 'destinationEndpoint', parameterType: 'UINT8'}, // + {name: 'sourceAddressMode', parameterType: 'UINT8'}, // + {name: 'sourceAddress', parameterType: 'ADDRESS_WITH_TYPE_DEPENDENCY'}, + // + {name: 'destinationAddressMode', parameterType: 'UINT8'}, + // + {name: 'destinationAddress', parameterType: 'ADDRESS_WITH_TYPE_DEPENDENCY'}, + // + // {name: 'payloadSize', parameterType:'UINT8'}, // + {name: 'payload', parameterType: 'BUFFER_RAW'}, // + ] + }, + [ZiGateMessageCode.APSDataConfirm]: { + response: [ + {name: 'status', parameterType:'UINT8'}, // + // {name: 'sourceEndpoint', parameterType:'UINT8'}, // + // {name: 'destinationAddressMode', parameterType:'UINT8'}, + // // + {name: 'destinationAddress', parameterType:'UINT16BE'}, + {name: 'destinationEndpoint', parameterType:'UINT8'}, // + {name: 'clusterID', parameterType:'UINT16BE'}, + // // + {name: 'seqNumber', parameterType:'UINT8'}, // + ] + }, + [ZiGateMessageCode.NetworkState]: { + response: [ + {name: 'shortAddress', parameterType:'UINT16BE'}, // + {name: 'extendedAddress', parameterType:'IEEEADDR'}, // + {name: 'PANID', parameterType:'UINT16BE'}, // + {name: 'ExtPANID', parameterType:'IEEEADDR'}, // + {name: 'Channel', parameterType:'UINT8'}, // + ] + }, + [ZiGateMessageCode.VersionList]: { + response: [ + {name: 'majorVersion', parameterType: 'UINT16BE'}, // + {name: 'installerVersion', parameterType: 'UINT16BE'}, // + ] + }, + [ZiGateMessageCode.NetworkJoined]: { + response: [ + {name: 'status', parameterType:'UINT8'}, // + // Status: + // 0 = Joined existing network + // 1 = Formed new network + // 128 – 244 = Failed (ZigBee event codes) + {name: 'shortAddress', parameterType:'UINT16BE'}, // + {name: 'extendedAddress', parameterType:'IEEEADDR'}, // + {name: 'channel', parameterType:'UINT8'}, // + ] + }, + [ZiGateMessageCode.LeaveIndication]: { + response: [ + {name: 'extendedAddress', parameterType:'IEEEADDR'}, // + {name: 'rejoin', parameterType:'UINT8'}, // + ] + }, + [ZiGateMessageCode.RouterDiscoveryConfirm]: { + response: [ + {name: 'status', parameterType:'UINT8'}, // + {name: 'nwkStatus', parameterType:'UINT8'}, // + ] + }, + [ZiGateMessageCode.APSDataConfirmFail]: { + response: [ + {name: 'status', parameterType:'UINT8'}, // + {name: 'sourceEndpoint', parameterType:'UINT8'}, // + {name: 'destinationEndpoint', parameterType:'UINT8'}, // + {name: 'destinationAddressMode', parameterType:'UINT8'}, // + {name: 'destinationAddress', parameterType:'ADDRESS_WITH_TYPE_DEPENDENCY'}, + // + {name: 'seqNumber', parameterType:'UINT8'}, // + ] + }, + [ZiGateMessageCode.ActiveEndpointResponse]: { + response: [ + {name: 'sequence', parameterType:'UINT8'}, // + {name: 'status', parameterType:'UINT8'}, // + {name: 'nwkAddr', parameterType:'UINT16BE'}, + {name: 'endpointCount', parameterType:'UINT8'}, + {name: 'endpoints', parameterType: 'LIST_UINT8'}, + ] + }, + // [ZiGateMessageCode.SimpleDescriptorResponse]: { + // response: [ + // {name: 'sourceEndpoint', parameterType:'UINT8'}, // + // {name: 'profile ID', parameterType:'UINT16BE'}, // + // {name: 'clusterID', parameterType:'UINT16BE'}, // + // {name: 'attributeList', parameterType:'LIST_UINT16BE'}, // + // ] + // }, + [ZiGateMessageCode.ManagementLQIResponse]: { + response: [ + {name: 'sequence', parameterType:'UINT8'}, // + {name: 'status', parameterType:'UINT8'}, // + {name: 'neighbourTableEntries', parameterType:'UINT8'}, // + {name: 'neighbourTableListCount', parameterType:'UINT8'}, // + {name: 'startIndex', parameterType:'UINT8'}, // + // @TODO list TYPE + // + // Note: If Neighbour Table list count is 0, there are no elements in the list. + {name: 'NWKAddress', parameterType:'UINT16BE'}, // NWK Address : uint16_t + {name: 'Extended PAN ID', parameterType:'UINT64'}, // Extended PAN ID : uint64_t + {name: 'IEEE Address', parameterType:'IEEEADR'}, // IEEE Address : uint64_t + {name: 'Depth', parameterType:'UINT8'}, // Depth : uint_t + {name: 'linkQuality', parameterType:'UINT8'}, // Link Quality : uint8_t + {name: 'bitMap', parameterType:'UINT8'}, // Bit map of attributes Described below: uint8_t + // bit 0-1 Device Type + // (0-Coordinator 1-Router 2-End Device) + // bit 2-3 Permit Join status + // (1- On 0-Off) + // bit 4-5 Relationship + // (0-Parent 1-Child 2-Sibling) + // bit 6-7 Rx On When Idle status + // (1-On 0-Off) + {name: 'srcAddress', parameterType: 'UINT16BE'}, // ( only from v3.1a) + ] + }, + + [ZiGateMessageCode.PDMEvent]: { + response: [ + {name: 'eventStatus', parameterType: 'UINT8'}, // + {name: 'recordID', parameterType: 'UINT32BE'}, // + + ] + }, + [ZiGateMessageCode.RestartNonFactoryNew]: { // Non “Factory new” Restart + response: [ + {name: 'status', parameterType: 'UINT8'}, // + // 0 – STARTUP + // 1 – RUNNING + // 2 – NFN_START + ] + }, + [ZiGateMessageCode.RestartFactoryNew]: { // “Factory New” Restart + response: [ + {name: 'status', parameterType: 'UINT8'}, // + // 0 – STARTUP + // 2 – NFN_START + // 6 – RUNNING + // The node is not yet provisioned. + ] + } +}; diff --git a/src/adapter/zigate/driver/parameterType.ts b/src/adapter/zigate/driver/parameterType.ts new file mode 100644 index 0000000000..8013cb9155 --- /dev/null +++ b/src/adapter/zigate/driver/parameterType.ts @@ -0,0 +1,24 @@ +enum ParameterType { + UINT8 = 0, + UINT16 = 1, + UINT32 = 2, + IEEEADDR = 3, + + BUFFER = 4, + BUFFER8 = 5, + BUFFER16 = 6, + BUFFER18 = 7, + BUFFER32 = 8, + BUFFER42 = 9, + BUFFER100 = 10, + + LIST_UINT8 = 11, + LIST_UINT16 = 12, + + INT8 = 18, + MACCAPABILITY = 100, + ADDRESS_WITH_TYPE_DEPENDENCY = 101, + RAW = 102, +} + +export default ParameterType; diff --git a/src/adapter/zigate/driver/ziGateObject.ts b/src/adapter/zigate/driver/ziGateObject.ts new file mode 100755 index 0000000000..22841110ef --- /dev/null +++ b/src/adapter/zigate/driver/ziGateObject.ts @@ -0,0 +1,144 @@ +/* istanbul ignore file */ +/* eslint-disable */ +import ZiGateFrame from './frame'; +import BuffaloZiGate, {BuffaloZiGateOptions} from './buffaloZiGate'; +import {ZiGateCommandCode, ZiGateMessageCode, ZiGateObjectPayload} from "./constants"; +import {ZiGateMessage, ZiGateMessageParameter} from "./messageType"; +import {ZiGateCommand, ZiGateCommandParameter, ZiGateCommandType} from "./commandType"; +import {Debug} from '../debug'; + +type ZiGateCode = ZiGateCommandCode | ZiGateMessageCode; +type ZiGateParameter = ZiGateCommandParameter | ZiGateMessageParameter; + + +const debug = Debug('driver:ziGateObject'); + +const BufferAndListTypes = [ + 'BUFFER', 'BUFFER8', 'BUFFER16', + 'BUFFER18', 'BUFFER32', 'BUFFER42', + 'BUFFER100', 'LIST_UINT16', 'LIST_ROUTING_TABLE', + 'LIST_BIND_TABLE', 'LIST_NEIGHBOR_LQI', 'LIST_NETWORK', + 'LIST_ASSOC_DEV', 'LIST_UINT8', +]; + +class ZiGateObject { + private readonly _code: ZiGateCode; + private readonly _payload: ZiGateObjectPayload; + private readonly _parameters: ZiGateParameter[]; + private readonly _frame: ZiGateFrame; + + private constructor( + code: ZiGateCode, + payload: ZiGateObjectPayload, + parameters: ZiGateParameter[], + frame?: ZiGateFrame + ) { + this._code = code; + this._payload = payload; + this._parameters = parameters; + this._frame = frame; + } + + get code(): ZiGateCode { + return this._code; + } + + get frame(): ZiGateFrame { + return this._frame; + } + + get payload(): ZiGateObjectPayload { + return this._payload; + } + + get command(): ZiGateCommandType { + return ZiGateCommand[this._code]; + } + + public static createRequest( + commandCode: ZiGateCommandCode, + payload: ZiGateObjectPayload + ): ZiGateObject { + const cmd = ZiGateCommand[commandCode]; + + if (!cmd) { + throw new Error(`Command '${commandCode}' not found`); + } + + return new ZiGateObject(commandCode, payload, cmd.request); + } + + public static fromZiGateFrame(frame: ZiGateFrame): ZiGateObject { + const code = frame.readMsgCode(); + return ZiGateObject.fromBufer(code, frame.msgPayloadBytes, frame); + } + + public static fromBufer(code: number, buffer: Buffer, frame?: ZiGateFrame): ZiGateObject { + const msg = ZiGateMessage[code]; + + if (!msg) { + throw new Error(`Message '${code.toString(16)}' not found`); + } + + const parameters = msg.response; + if (parameters === undefined) { + throw new Error(`Message '${code.toString(16)}' cannot be a response`); + } + + const payload = this.readParameters(buffer, parameters); + + if (code !== ZiGateMessageCode.Status || payload.status !== 0) { + debug.info(`--> frame to object %o`, payload); + } + + return new ZiGateObject(code, payload, parameters, frame); + } + + private static readParameters(buffer: Buffer, parameters: ZiGateParameter[]): ZiGateObjectPayload { + const buffalo = new BuffaloZiGate(buffer); + const result: ZiGateObjectPayload = {}; + + for (const parameter of parameters) { + const options: BuffaloZiGateOptions = {}; + + if (BufferAndListTypes.includes(parameter.parameterType)) { + // When reading a buffer, assume that the previous parsed parameter contains + // the length of the buffer + const lengthParameter = parameters[parameters.indexOf(parameter) - 1]; + const length = result[lengthParameter.name]; + + if (typeof length === 'number') { + options.length = length; + } + } + + try { + result[parameter.name] = buffalo.read(parameter.parameterType, options); + } catch (e) { + debug.error(e.stack); + } + } + return result; + } + + public toZiGateFrame(): ZiGateFrame { + const buffer = this.createPayloadBuffer(); + const frame = new ZiGateFrame(); + frame.writeMsgCode(this._code as number); + frame.writeMsgPayload(buffer); + return frame; + } + + private createPayloadBuffer(): Buffer { + const buffalo = new BuffaloZiGate(Buffer.alloc(256)); // hardcode @todo + + for (const parameter of this._parameters) { + const value = this._payload[parameter.name]; + buffalo.write(parameter.parameterType, value, {}); + } + return buffalo.getBuffer().slice(0, buffalo.getPosition()); + } + +} + +export default ZiGateObject; diff --git a/src/adapter/zigate/driver/zigate.ts b/src/adapter/zigate/driver/zigate.ts new file mode 100644 index 0000000000..c0d55fd645 --- /dev/null +++ b/src/adapter/zigate/driver/zigate.ts @@ -0,0 +1,377 @@ +/* istanbul ignore file */ +/* eslint-disable */ + +import SerialPort from 'serialport'; +import {EventEmitter} from 'events'; +import {Debug} from '../debug'; +import SerialPortUtils from "../../serialPortUtils"; +import SocketPortUtils from "../../socketPortUtils"; +import net from "net"; +import {Queue} from "../../../utils"; +import {SerialPortOptions} from "../../tstype"; +import {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"; + +const debug = Debug('driver'); + +const autoDetectDefinitions = [ + {manufacturer: 'zigate_PL2303', vendorId: '067b', productId: '2303'}, + {manufacturer: 'zigate_cp2102', vendorId: '10c4', productId: 'ea60'}, +]; + +const timeouts = { + reset: 30000, + default: 10000, +}; + +type WaitressMatcher = { + ziGateObject: ZiGateObject, + rules: ZiGateResponseMatcher, + extraParameters?: object +}; + +function zeroPad(number: number, size?: number): string { + return (number).toString(16).padStart(size || 4, '0'); +} + +function resolve(path: string | [], obj: { [k: string]: any }, separator = '.'): any { + const properties = Array.isArray(path) ? path : path.split(separator); + return properties.reduce((prev, curr) => prev && prev[curr], obj); +} + +export default class ZiGate extends EventEmitter { + private path: string; + private baudRate: number; + private rtscts: boolean; + private initialized: boolean; + // private timeoutResetTimeout: any; + // private apsRequestFreeSlots: number; + + private parser: EventEmitter; + private serialPort: SerialPort; + private seqNumber: number; + private portType: 'serial' | 'socket'; + private socketPort: net.Socket; + private queue: Queue; + + public portWrite: SerialPort | net.Socket; + private waitress: Waitress; + + public constructor(path: string, serialPortOptions: SerialPortOptions) { + super(); + this.path = path; + this.baudRate = typeof serialPortOptions.baudRate === 'number' ? serialPortOptions.baudRate : 1000000; + this.rtscts = typeof serialPortOptions.rtscts === 'boolean' ? serialPortOptions.rtscts : false; + this.portType = SocketPortUtils.isTcpPath(path) ? 'socket' : 'serial'; + this.initialized = false; + this.queue = new Queue(1); + + this.waitress = new Waitress( + this.waitressValidator, this.waitressTimeoutFormatter); + + } + + public async sendCommand( + code: ZiGateCommandCode, + payload?: ZiGateObjectPayload, + timeout?: number, + extraParameters?: object + ): Promise { + + const waiters: Promise[] = []; + const statusResponse: ZiGateObject | void = await this.queue.execute(async () => { + try { + debug.log( + 'Send command \x1b[42m>>>> ' + + ZiGateCommandCode[code] + + ' 0x' + zeroPad(code) + + ' <<<<\x1b[0m \nPayload: %o', + payload + ); + const ziGateObject = ZiGateObject.createRequest(code, payload); + const frame = ziGateObject.toZiGateFrame(); + debug.log('%o', frame); + + const sendBuffer = frame.toBuffer(); + debug.log('<-- send command ', sendBuffer); + + + if (Array.isArray(ziGateObject.command.response)) { + ziGateObject.command.response.forEach((rules) => { + waiters.push( + this.waitress.waitFor( + {ziGateObject, rules, extraParameters}, + timeout || timeouts.default + ).start().promise + ); + }); + } + + let resultPromise: Promise; + if (ziGateObject.command.waitStatus !== false) { + const ruleStatus: ZiGateResponseMatcher = [ + {receivedProperty: 'code', matcher: equal, value: ZiGateMessageCode.Status}, + {receivedProperty: 'payload.packetType', matcher: equal, value: ziGateObject.code}, + ]; + + const statusWaiter = this.waitress.waitFor( + {ziGateObject, rules: ruleStatus}, + timeout || timeouts.default + ).start(); + resultPromise = statusWaiter.promise; + } + + // @ts-ignore + this.portWrite.write(sendBuffer); + + return resultPromise || Promise.resolve(); + } catch (e) { + debug.error('sendCommand error:', e); + return Promise.reject(); + } + }); + + if (statusResponse && statusResponse.payload.status !== 0) { + // else + return Promise.reject(statusResponse); + } else { + if (typeof statusResponse === "undefined" + || statusResponse.payload.status === 0) { + + if (waiters.length > 0) { + return Promise.race(waiters); + } else { + return Promise.resolve(statusResponse); + } + } + } + } + + public static async isValidPath(path: string): Promise { + return SerialPortUtils.is(path, autoDetectDefinitions); + } + + public static async autoDetectPath(): Promise { + const paths = await SerialPortUtils.find(autoDetectDefinitions); + return paths.length > 0 ? paths[0] : null; + } + + public open(): Promise { + return this.portType === 'serial' ? this.openSerialPort() : this.openSocketPort(); + } + + public close(): Promise { + debug.info('close'); + return new Promise((resolve, reject) => { + if (this.initialized) { + this.initialized = false; + this.portWrite = null; + if (this.portType === 'serial') { + this.serialPort.flush((): void => { + this.serialPort.close((error): void => { + this.serialPort = null; + error == null ? + resolve() : + reject(new Error(`Error while closing serialPort '${error}'`)); + this.emit('close'); + }); + }); + } else { + // @ts-ignore + this.socketPort.destroy((error?: Error): void => { + this.socketPort = null; + error == null ? + resolve() : + reject(new Error(`Error while closing serialPort '${error}'`)); + this.emit('close'); + }); + } + } else { + resolve(); + this.emit('close'); + } + }); + } + + public waitFor(matcher: WaitressMatcher, timeout: number = timeouts.default): + { start: () => { promise: Promise; ID: number }; ID: number } { + return this.waitress.waitFor(matcher, timeout); + } + + private async openSerialPort(): Promise { + this.serialPort = new SerialPort(this.path, { + baudRate: this.baudRate, + dataBits: 8, + parity: 'none', /* one of ['none', 'even', 'mark', 'odd', 'space'] */ + stopBits: 1, /* one of [1,2] */ + lock: false, + autoOpen: false + }); + this.parser = this.serialPort.pipe( + new SerialPort.parsers.Delimiter( + {delimiter: [ZiGateFrame.STOP_BYTE], includeDelimiter: true} + ), + ); + this.parser.on('data', this.onSerialData.bind(this)); + + this.portWrite = this.serialPort; + return new Promise((resolve, reject): void => { + this.serialPort.open(async (err: unknown): Promise => { + if (err) { + this.serialPort = null; + this.parser = null; + this.path = null; + this.initialized = false; + const error = `Error while opening serialPort '${err}'`; + debug.error(error); + reject(new Error(error)); + } else { + debug.log('Successfully connected ZiGate port \'' + this.path + '\''); + this.serialPort.on('error', (error) => { + debug.error(`serialPort error: ${error}`); + }); + this.serialPort.on('close', this.onPortClose.bind(this)); + this.initialized = true; + resolve(); + } + }); + }); + } + + private async openSocketPort(): Promise { + const info = SocketPortUtils.parseTcpPath(this.path); + debug.log(`Opening TCP socket with ${info.host}:${info.port}`); + + this.socketPort = new net.Socket(); + this.socketPort.setNoDelay(true); + this.socketPort.setKeepAlive(true, 15000); + + + this.parser = this.socketPort.pipe( + new SerialPort.parsers.Delimiter({delimiter: [ZiGateFrame.STOP_BYTE], includeDelimiter: true}), + ); + this.parser.on('data', this.onSerialData.bind(this)); + + this.portWrite = this.socketPort; + return new Promise((resolve, reject): void => { + this.socketPort.on('connect', function () { + debug.log('Socket connected'); + }); + + // eslint-disable-next-line + const self = this; + + this.socketPort.on('ready', async function () { + debug.log('Socket ready'); + self.initialized = true; + resolve(); + }); + + this.socketPort.once('close', this.onPortClose); + + this.socketPort.on('error', (error) => { + debug.log('Socket error', error); + // reject(new Error(`Error while opening socket`)); + reject(); + self.initialized = false; + }); + + this.socketPort.connect(info.port, info.host); + }); + } + + private onSerialError(err: string): void { + debug.error('serial error: ', err); + } + + private onPortClose(): void { + debug.log('serial closed'); + this.initialized = false; + this.emit('close'); + } + + private onSerialData(buffer: Buffer): void { + try { + debug.log(`--- parseNext `, buffer); + + const frame = new ZiGateFrame(buffer); + if (!(frame instanceof ZiGateFrame)) return; // @Todo fix + + const code = frame.readMsgCode(); + const msgName = (ZiGateMessageCode[code] ? ZiGateMessageCode[code] : '') + ' 0x' + zeroPad(code); + + debug.log(`--> parsed frame \x1b[1;34m>>>> ${msgName} <<<<`); + + try { + const ziGateObject = ZiGateObject.fromZiGateFrame(frame); + + this.waitress.resolve(ziGateObject); + + switch (code) { + case ZiGateMessageCode.DataIndication: + + switch (ziGateObject.payload.clusterID) { + case 0x8002: + break; + default: + if (ziGateObject.payload.profileID != 0x00) { + try { + const zclFrame = ZclFrame.fromBuffer( + ziGateObject.payload.clusterID, + ziGateObject.payload.payload + ); + this.emit('received', {ziGateObject, zclFrame}); + } catch (error) { + debug.error("could not parse zclFrame: " + error); + this.emit('receivedRaw', {ziGateObject}); + } + } + } + break; + + case ZiGateMessageCode.LeaveIndication: + this.emit('LeaveIndication', {ziGateObject}); + break; + case ZiGateMessageCode.DeviceAnnounce: + this.emit('DeviceAnnounce', {ziGateObject}); + break; + } + + } catch (error) { + debug.error('Parsing error: %o', error) + } + + } catch (error) { + debug.error(`Error while parsing Frame '${error.stack}'`); + } + } + + private waitressTimeoutFormatter(matcher: WaitressMatcher, timeout: number): string { + return `${matcher} after ${timeout}ms`; + } + + private waitressValidator(ziGateObject: ZiGateObject, matcher: WaitressMatcher): boolean { + + const validator = (rule: ZiGateResponseMatcherRule): boolean => { + try { + let expectedValue: string | number; + if (typeof rule.value === "undefined" && typeof rule.expectedProperty !== "undefined") { + expectedValue = resolve(rule.expectedProperty, matcher.ziGateObject); + } else if (typeof rule.value === "undefined" && typeof rule.expectedExtraParameter !== "undefined") { + expectedValue = resolve(rule.expectedExtraParameter, matcher.extraParameters); + } else { + expectedValue = rule.value; + } + const receivedValue = resolve(rule.receivedProperty, ziGateObject); + return rule.matcher(expectedValue, receivedValue); + } catch (e) { + return false; + } + }; + return matcher.rules.every(validator); + } +} diff --git a/test/controller.test.ts b/test/controller.test.ts index 6b62d5d331..4ad195e41a 100755 --- a/test/controller.test.ts +++ b/test/controller.test.ts @@ -2,6 +2,7 @@ import "regenerator-runtime/runtime"; import {Controller} from '../src/controller'; import {ZStackAdapter} from '../src/adapter/z-stack/adapter'; 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 } from "../src/zcl"; @@ -330,6 +331,13 @@ jest.mock('../src/adapter/deconz/adapter/deconzAdapter', () => { }); }); +jest.mock('../src/adapter/zigate/adapter/zigateAdapter', () => { + return jest.fn().mockImplementation(() => { + return { + }; + }); +}); + const getTempFile = (filename) => { const tempPath = path.resolve('temp'); if (!fs.existsSync(tempPath)){ @@ -350,9 +358,16 @@ const mockDeconzAdapterAutoDetectPath = jest.fn().mockReturnValue("/dev/autodete DeconzAdapter.isValidPath = mockDeconzAdapterIsValidPath; DeconzAdapter.autoDetectPath = mockDeconzAdapterAutoDetectPath; +const mockZiGateAdapterIsValidPath = jest.fn().mockReturnValue(true); +const mockZiGateAdapterAutoDetectPath = jest.fn().mockReturnValue("/dev/autodetected"); +ZiGateAdapter.isValidPath = mockZiGateAdapterIsValidPath; +ZiGateAdapter.autoDetectPath = mockZiGateAdapterAutoDetectPath; + const mocksRestore = [ mockAdapterStart, mockAdapterPermitJoin, mockAdapterStop, mockAdapterRemoveDevice, mocksendZclFrameToAll, - mockZStackAdapterIsValidPath, mockZStackAdapterAutoDetectPath, mockDeconzAdapterIsValidPath, mockDeconzAdapterAutoDetectPath, + mockZStackAdapterIsValidPath, mockZStackAdapterAutoDetectPath, + mockDeconzAdapterIsValidPath, mockDeconzAdapterAutoDetectPath, + mockZiGateAdapterIsValidPath, mockZiGateAdapterAutoDetectPath, ]; const events = { @@ -2636,8 +2651,12 @@ describe('Controller', () => { mockZStackAdapterAutoDetectPath.mockReturnValueOnce('/dev/test'); mockDeconzAdapterIsValidPath.mockReturnValueOnce(false); mockDeconzAdapterAutoDetectPath.mockReturnValueOnce('/dev/test'); + mockZiGateAdapterIsValidPath.mockReturnValueOnce(false); + mockZiGateAdapterAutoDetectPath.mockReturnValueOnce('/dev/test'); await Adapter.create(null, {path: null, baudRate: 100, rtscts: false, adapter: 'deconz'}, null, null); expect(DeconzAdapter).toHaveBeenCalledWith(null, {"baudRate": 100, "path": "/dev/test", "rtscts": false, adapter: 'deconz'}, null, null); + await Adapter.create(null, {path: null, baudRate: 100, rtscts: false, adapter: 'zigate'}, null, null); + expect(ZiGateAdapter).toHaveBeenCalledWith(null, {"baudRate": 100, "path": "/dev/test", "rtscts": false, adapter: 'zigate'}, null, null); }); it('Adapter create should throw on uknown adapter', async () => { @@ -2646,8 +2665,8 @@ describe('Controller', () => { mockDeconzAdapterIsValidPath.mockReturnValueOnce(false); mockDeconzAdapterAutoDetectPath.mockReturnValueOnce('/dev/test'); let error; - try {await Adapter.create(null, {path: null, baudRate: 100, rtscts: false, adapter: 'zigate'}, null, null)} catch (e) {error = e;} - expect(error).toStrictEqual(new Error(`Adapter 'zigate' does not exists, possible options: zstack, deconz`)); + try {await Adapter.create(null, {path: null, baudRate: 100, rtscts: false, adapter: 'efr'}, null, null)} catch (e) {error = e;} + expect(error).toStrictEqual(new Error(`Adapter 'efr' does not exists, possible options: zstack, deconz, zigate`)); }); it('Emit read from device', async () => { @@ -3094,4 +3113,4 @@ describe('Controller', () => { expect(endpoint.getInputClusters().map(c => c.name)).toStrictEqual(['genBasic', 'genIdentify', 'genGroups', 'genScenes', 'genOnOff', 'genLevelCtrl', 'lightingColorCtrl']); expect(endpoint.getOutputClusters().map(c => c.name)).toStrictEqual(['genDeviceTempCfg']); }); -}); \ No newline at end of file +}); diff --git a/tsconfig.json b/tsconfig.json index 8fff2d1af0..f1dae6fcb1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,4 +19,4 @@ "exclude": [ "src/deprecated" ] -} \ No newline at end of file +}