zigbee-herdsman-zigate
Version:
An open source ZigBee gateway solution with node.js.
601 lines (526 loc) • 23.4 kB
text/typescript
/* istanbul ignore file */
/* eslint-disable */
import Debug from 'debug';
import events from 'events';
import SerialPort from 'serialport';
import Writer from './writer';
import Parser from './parser';
import Frame from './frame';
import * as Events from '../../events';
import SerialPortUtils from '../../serialPortUtils';
import PARAM from './constants';
import { Command, Request, parameterT, ApsDataRequest, ReceivedDataResponse, DataStateResponse } from './constants';
// @ts-ignore
import slip from 'slip';
const debug = Debug('zigbee-herdsman:deconz:driver');
const autoDetectDefinitions = [
{manufacturer: 'dresden elektronik ingenieurtechnik GmbH', vendorId: '1cf1', productId: '0030'}, // Conbee II
];
var queue: Array<object> = [];
var busyQueue: Array<object> = [];
var apsQueue: Array<object> = [];
var apsBusyQueue: Array<object> = [];
var timeoutCounter = 0;
export { busyQueue, apsBusyQueue };
var frameParser = require('./frameParser');
const littleEndian = true;
class Driver extends events.EventEmitter {
private path: string;
private serialPort: SerialPort;
private initialized: boolean;
private writer: Writer;
private parser: Parser;
private frameParserEvent = frameParser.frameParserEvents;
private seqNumber: number;
private timeoutResetTimeout: any;
private apsRequestFreeSlots: number;
public constructor(path: string) {
super();
this.path = path;
this.initialized = false;
this.seqNumber = 0;
this.timeoutResetTimeout = null;
this.apsRequestFreeSlots = 1;
const that = this;
setInterval(() => { that.processQueue(); }, 50); //300
setInterval(() => { that.processBusyQueue(); }, 10); //200
setInterval(() => { that.processApsQueue(); }, 50); //300
setInterval(() => { that.processApsBusyQueue(); }, 10); //200
setInterval(() => { that.deviceStateRequest()
.then(result => {})
.catch(error => {}); }, 10000);
setInterval(() => {
that.writeParameterRequest(0x26, 600) // reset watchdog // 10 minutes
.then(result => {})
.catch(error => {
//try again
debug("try again to reset watchdog");
that.writeParameterRequest(0x26, 600)
.then(result => {})
.catch(error => {debug("warning watchdog was not reset");});
});
}, (1000 * 60 * 8)); // 8 minutes
this.onParsed = this.onParsed.bind(this);
this.frameParserEvent.on('receivedDataNotification', (data: number) => {this.checkDeviceStatus(data)});
}
public static async isValidPath(path: string): Promise<boolean> {
return SerialPortUtils.is(path, autoDetectDefinitions);
}
public static async autoDetectPath(): Promise<string> {
const paths = await SerialPortUtils.find(autoDetectDefinitions);
return paths.length > 0 ? paths[0] : null;
}
public open(): Promise<void> {
debug(`Opening with ${this.path}`);
this.serialPort = new SerialPort(this.path, {baudRate: 38400, autoOpen: false});
this.writer = new Writer();
// @ts-ignore
this.writer.pipe(this.serialPort);
this.parser = new Parser();
this.serialPort.pipe(this.parser);
this.parser.on('parsed', this.onParsed);
//this.unpiParser.on('error', this.onUnpiParsedError);
return new Promise((resolve, reject): void => {
this.serialPort.open(async (error: object): Promise<void> => {
if (error) {
reject(new Error(`Error while opening serialport '${error}'`));
this.initialized = false;
if (this.serialPort.isOpen) {
this.serialPort.close();
}
} else {
debug('Serialport opened');
this.initialized = true;
resolve();
}
});
});
}
public close(): void {
this.serialPort.close();
}
public readParameterRequest(parameterId: number) : Promise<Command> {
const seqNumber = this.nextSeqNumber();
return new Promise((resolve, reject): void => {
//debug(`push read parameter request to queue. seqNr: ${seqNumber} paramId: ${parameterId}`);
const ts = 0;
const commandId = PARAM.PARAM.FrameType.ReadParameter;
const req: Request = {commandId, parameterId, seqNumber, resolve, reject, ts};
queue.push(req);
});
}
public writeParameterRequest(parameterId: number, parameter: parameterT) : Promise<void> {
const seqNumber = this.nextSeqNumber();
return new Promise((resolve, reject): void => {
//debug(`push write parameter request to queue. seqNr: ${seqNumber} paramId: ${parameterId} parameter: ${parameter}`);
const ts = 0;
const commandId = PARAM.PARAM.FrameType.WriteParameter;
const req: Request = {commandId, parameterId, parameter, seqNumber, resolve, reject, ts};
queue.push(req);
});
}
public readFirmwareVersionRequest() : Promise<number[]> {
const seqNumber = this.nextSeqNumber();
return new Promise((resolve, reject): void => {
//debug(`push read firmware version request to queue. seqNr: ${seqNumber}`);
const ts = 0;
const commandId = PARAM.PARAM.FrameType.ReadFirmwareVersion;
const req: Request = {commandId, seqNumber, resolve, reject, ts};
queue.push(req);
});
}
private sendReadParameterRequest(parameterId: number, seqNumber: number) {
/* command id, sequence number, 0, framelength(U16), payloadlength(U16), parameter id */
const requestFrame = [PARAM.PARAM.FrameType.ReadParameter, seqNumber, 0x00, 0x08, 0x00, 0x01, 0x00, parameterId];
if (parameterId === PARAM.PARAM.Network.NETWORK_KEY) {
const requestFrame2= [PARAM.PARAM.FrameType.ReadParameter, seqNumber, 0x00, 0x09, 0x00, 0x02, 0x00, parameterId, 0x00];
this.sendRequest(requestFrame2);
} else {
this.sendRequest(requestFrame);
}
}
private sendWriteParameterRequest(parameterId: number, value: parameterT, seqNumber: number) {
/* command id, sequence number, 0, framelength(U16), payloadlength(U16), parameter id, pameter */
const parameterLength = this.getLengthOfParameter(parameterId);
//debug("SEND WRITE_PARAMETER Request - parameter id: " + parameterId + " value: " + value.toString(16) + " length: " + parameterLength);
const payloadLength = 1 + parameterLength;
const frameLength = 7 + payloadLength;
const fLength1 = frameLength & 0xff;
const fLength2 = frameLength >> 8;
const pLength1 = payloadLength & 0xff;
const pLength2 = payloadLength >> 8;
if (parameterId === PARAM.PARAM.Network.NETWORK_KEY) {
const requestFrame2= [PARAM.PARAM.FrameType.WriteParameter, seqNumber, 0x00, 0x19, 0x00, 0x12, 0x00, parameterId, 0x00].concat(value);
this.sendRequest(requestFrame2);
} else {
const requestframe = [PARAM.PARAM.FrameType.WriteParameter, seqNumber, 0x00, fLength1, fLength2, pLength1, pLength2, parameterId].concat(this.parameterBuffer(value, parameterLength));
this.sendRequest(requestframe);
}
}
private getLengthOfParameter(parameterId: number) : number {
switch (parameterId) {
case 9: case 16: case 21: case 28: case 33: case 36:
return 1;
case 5: case 7: case 34:
return 2;
case 10: case 38:
return 4;
case 1: case 8: case 11: case 14:
return 8;
case 24: case 25:
return 16;
default:
return 0;
}
}
private parameterBuffer(parameter: parameterT, parameterLength: number) : Array<number> {
const paramArray = new Array();
if (typeof parameter === 'number') {
// for parameter <= 4 Byte
if (parameterLength > 4)
throw new Error("parameter to big for type number");
for (let i = 0; i < parameterLength; i++) {
paramArray[i] = (parameter >> (8 * i)) & 0xff;
}
} else {
return parameter.reverse();
}
return paramArray;
}
private sendReadFirmwareVersionRequest(seqNumber: number) {
/* command id, sequence number, 0, framelength(U16) */
const requestFrame = [PARAM.PARAM.FrameType.ReadFirmwareVersion, seqNumber, 0x00, 0x05, 0x00];
this.sendRequest(requestFrame);
}
private sendReadDeviceStateRequest(seqNumber: number) {
/* command id, sequence number, 0, framelength(U16) */
const requestFrame = [PARAM.PARAM.FrameType.ReadDeviceState, seqNumber, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00];
this.sendRequest(requestFrame);
}
private sendRequest(buffer: number[]) {
const crc = this.calcCrc(Buffer.from(buffer));
const frame = Buffer.from(buffer.concat([crc[0], crc[1]]));
const slipframe = slip.encode(frame);
this.serialPort.write(slipframe, function(err) {
if (err) {
console.log("Error writing serial Port: " + err.message);
}
});
}
private processQueue() {
if (queue.length === 0) {
return;
}
if (busyQueue.length > 0) {
// debug("don't process queue. Request pending");
return;
}
const req: Request = queue.shift();
req.ts = Date.now();
switch (req.commandId) {
case PARAM.PARAM.FrameType.ReadParameter:
debug(`send read parameter request from queue. seqNr: ${req.seqNumber} paramId: ${req.parameterId}`);
this.sendReadParameterRequest(req.parameterId, req.seqNumber);
break;
case PARAM.PARAM.FrameType.WriteParameter:
debug(`send write parameter request from queue. seqNr: ${req.seqNumber} paramId: ${req.parameterId} param: ${req.parameter}`);
this.sendWriteParameterRequest(req.parameterId, req.parameter, req.seqNumber);
break;
case PARAM.PARAM.FrameType.ReadFirmwareVersion:
debug(`send read firmware version request from queue. seqNr: ${req.seqNumber}`);
this.sendReadFirmwareVersionRequest(req.seqNumber);
break;
case PARAM.PARAM.FrameType.ReadDeviceState:
debug(`send read device state from queue. seqNr: ${req.seqNumber}`);
this.sendReadDeviceStateRequest(req.seqNumber);
break;
case PARAM.PARAM.NetworkState.CHANGE_NETWORK_STATE:
debug(`send change network state request from queue. seqNr: ${req.seqNumber}`);
this.sendChangeNetworkStateRequest(req.seqNumber, req.networkState);
break;
default:
throw new Error("process queue - unknown command id");
break;
}
busyQueue.push(req);
}
private async processBusyQueue() {
let i = busyQueue.length;
while (i--) {
const req: Request = busyQueue[i];
const now = Date.now();
if ((now - req.ts) > 10000) {
debug(`Timeout for request - CMD: 0x${req.commandId.toString(16)} seqNr: ${req.seqNumber}`);
//remove from busyQueue
busyQueue.splice(i, 1);
timeoutCounter++;
// after a timeout the timeoutcounter will be reset after 1 min. If another timeout happen then the timeoutcounter
// will not be reset
clearTimeout(this.timeoutResetTimeout);
this.timeoutResetTimeout = null;
this.resetTimeoutCounterAfter1min();
req.reject("TIMEOUT");
if (timeoutCounter >= 3) {
timeoutCounter = 0;
debug("to many timeouts - restart serial connecion");
if (this.serialPort.isOpen) {
this.serialPort.close();
}
await this.open();
}
}
}
}
public changeNetworkStateRequest(networkState: number) : Promise<void> {
const seqNumber = this.nextSeqNumber();
return new Promise((resolve, reject): void => {
//debug(`push change network state request to apsQueue. seqNr: ${seqNumber}`);
const ts = 0;
const commandId = PARAM.PARAM.NetworkState.CHANGE_NETWORK_STATE;
const req: Request = {commandId, networkState, seqNumber, resolve, reject, ts};
queue.push(req);
});
}
private sendChangeNetworkStateRequest(seqNumber: number, networkState: number) {
const requestFrame = [PARAM.PARAM.NetworkState.CHANGE_NETWORK_STATE, seqNumber, 0x00, 0x06, 0x00, networkState];
this.sendRequest(requestFrame);
}
private deviceStateRequest() {
const seqNumber = this.nextSeqNumber();
return new Promise((resolve, reject): void => {
//debug(`DEVICE_STATE Request - seqNr: ${seqNumber}`);
const ts = 0;
const commandId = PARAM.PARAM.FrameType.ReadDeviceState;
const req: Request = {commandId, seqNumber, resolve, reject, ts};
queue.push(req);
});
}
private async checkDeviceStatus(currentDeviceStatus: number) {
const networkState = currentDeviceStatus & 0x03;
const apsDataConfirm = (currentDeviceStatus >> 2) & 0x01;
const apsDataIndication = (currentDeviceStatus >> 3) & 0x01;
const configChanged = (currentDeviceStatus >> 4) & 0x01;
const apsRequestFreeSlots = (currentDeviceStatus >> 5) & 0x01;
this.apsRequestFreeSlots = apsRequestFreeSlots;
debug("networkstate: " + networkState + " apsDataConfirm: " + apsDataConfirm + " apsDataIndication: " + apsDataIndication +
" configChanged: " + configChanged + " apsRequestFreeSlots: " + apsRequestFreeSlots);
if (apsDataConfirm === 1) {
try {
const x = await this.querySendDataStateRequest();
} catch {
//debug("APS Error - data confirm");
}
} else if (apsDataIndication === 1) {
try {
const x = await this.readReceivedDataRequest();
} catch {
//debug("APS Error - data indication");
}
} else if (configChanged === 1) {
// when network settings changed
}
}
private readReceivedDataRequest() : Promise<void> {
const seqNumber = this.nextSeqNumber();
return new Promise((resolve, reject): void => {
//debug(`push read received data request to apsQueue. seqNr: ${seqNumber}`);
const ts = 0;
const commandId = PARAM.PARAM.APS.DATA_INDICATION;
const req: Request = {commandId, seqNumber, resolve, reject, ts};
apsQueue.push(req);
});
}
public enqueueSendDataRequest(request: ApsDataRequest) : Promise<void | ReceivedDataResponse> {
const seqNumber = this.nextSeqNumber();
return new Promise((resolve, reject): void => {
//debug(`push enqueue send data request to apsQueue. seqNr: ${seqNumber}`);
const ts = 0;
const requestId = request.requestId;
const commandId = PARAM.PARAM.APS.DATA_REQUEST;
const req: Request = {commandId, seqNumber, request, resolve, reject, ts};
apsQueue.push(req);
});
}
private querySendDataStateRequest() : Promise<void> {
const seqNumber = this.nextSeqNumber();
return new Promise((resolve, reject): void => {
//debug(`push query send data state request to apsQueue. seqNr: ${seqNumber}`);
const ts = 0;
const commandId = PARAM.PARAM.APS.DATA_CONFIRM;
const req: Request = {commandId, seqNumber, resolve, reject, ts};
apsQueue.push(req);
});
}
private processApsQueue() {
if (apsQueue.length === 0) {
return;
}
if (this.apsRequestFreeSlots !== 1) {
debug("no free slots. Delay sending of APS Request");
this.sleep(1000);
return;
}
const req: Request = apsQueue.shift();
req.ts = Date.now();
apsBusyQueue.push(req);
switch (req.commandId) {
case PARAM.PARAM.APS.DATA_INDICATION:
//debug(`read received data request. seqNr: ${req.seqNumber}`);
this.sendReadReceivedDataRequest(req.seqNumber);
break;
case PARAM.PARAM.APS.DATA_CONFIRM:
//debug(`query send data state request. seqNr: ${req.seqNumber}`);
this.sendQueryDataStateRequest(req.seqNumber);
break;
case PARAM.PARAM.APS.DATA_REQUEST:
//debug(`send data request. seqNr: ${req.seqNumber}`);
this.sendEnqueueSendDataRequest(req.request, req.seqNumber);
break;
default:
throw new Error("process APS queue - unknown command id");
break;
}
}
private sendQueryDataStateRequest(seqNumber: number) {
debug(`DATA_CONFIRM - sending data state request - SeqNr. ${seqNumber}`);
const requestFrame = [PARAM.PARAM.APS.DATA_CONFIRM, seqNumber, 0x00, 0x07, 0x00, 0x00, 0x00];
this.sendRequest(requestFrame);
}
private sendReadReceivedDataRequest(seqNumber: number) {
debug(`DATA_INDICATION - sending read data request - SeqNr. ${seqNumber}`);
// payloadlength = 0, flag = none
const requestFrame = [PARAM.PARAM.APS.DATA_INDICATION, seqNumber, 0x00, 0x08, 0x00, 0x01, 0x00, 0x01];
this.sendRequest(requestFrame);
}
private sendEnqueueSendDataRequest(request: ApsDataRequest, seqNumber: number) {
const payloadLength = 12 + ((request.destAddrMode === 0x01) ? 2 : (request.destAddrMode === 0x02) ? 3 : 9) + request.asduLength;
const frameLength = 7 + payloadLength;
const cid1 = request.clusterId & 0xff;
const cid2 = (request.clusterId >> 8) & 0xff;
const asdul1 = request.asduLength & 0xff;
const asdul2 = (request.asduLength >> 8) & 0xff;
let destArray: Array<number> = [];
let dest = "";
if (request.destAddr16 != null) {
destArray[0] = request.destAddr16 & 0xff;
destArray[1] = (request.destAddr16 >> 8) & 0xff;
dest = request.destAddr16.toString(16);
}
if (request.destAddr64 != null) {
dest = request.destAddr64;
destArray = this.macAddrStringToArray(request.destAddr64);
}
if (request.destEndpoint != null) {
destArray.push(request.destEndpoint);
dest += " EP:";
dest += request.destEndpoint;
}
debug(`DATA_REQUEST - destAddr: 0x${dest} SeqNr. ${seqNumber} request id: ${request.requestId}`);
const requestFrame = [PARAM.PARAM.APS.DATA_REQUEST, seqNumber, 0x00, frameLength & 0xff, (frameLength >> 8) & 0xff,
payloadLength & 0xff, (payloadLength >> 8) & 0xff,
request.requestId, 0x00, request.destAddrMode].concat(
destArray).concat([request.profileId & 0xff, (request.profileId >> 8) & 0xff,
cid1, cid2, request.srcEndpoint, asdul1, asdul2]).concat(
request.asduPayload).concat([request.txOptions, request.radius]);
this.sendRequest(requestFrame);
}
private processApsBusyQueue() {
let i = apsBusyQueue.length;
while (i--) {
const req: Request = apsBusyQueue[i];
const now = Date.now();
let timeout = 60000;
if (req.request != null && req.request.timeout != null) {
timeout = req.request.timeout * 1000; // seconds * 1000 = milliseconds
}
if ((now - req.ts) > timeout) {
debug(`Timeout for aps request CMD: 0x${req.commandId.toString(16)} seq: ${req.seqNumber}`);
//remove from busyQueue
apsBusyQueue.splice(i, 1);
req.reject("APS TIMEOUT");
}
}
}
private calcCrc(buffer: Uint8Array) : Array<number>{
let crc = 0;
for (let i=0; i < buffer.length; i++) {
crc += buffer[i];
}
const crc0 = (~crc + 1) & 0xff;
const crc1 = ((~crc + 1) >> 8) & 0xff;
return [crc0, crc1];
}
public macAddrStringToArray(addr: string) : Array<number>{
if (addr.indexOf("0x") === 0) {
addr = addr.slice(2, addr.length);
}
if (addr.length < 16) {
for (let l = 0; l < (16 - addr.length); l++) {
addr = "0" + addr;
}
}
let result: number[] = new Array<number>();
let y = 0;
for (let i = 0; i < 8; i++) {
result[i] = parseInt(addr.substr(y,2), 16);
y += 2;
}
const reverse = result.reverse();
return reverse;
}
public macAddrArrayToString(addr: Array<number>) : string{
if (addr.length != 8) {
throw new Error("invalid array length for MAC address: " + addr.length);
}
let result: string = "0x";
let char = '';
let i = 8;
while (i--) {
char = addr[i].toString(16);
if (char.length < 2) {
char = "0" + char;
}
result += char;
}
return result;
}
/**
* generalArrayToString result is not reversed!
*/
public generalArrayToString(key: Array<number>, length: number) : string{
let result: string = "0x";
let char = '';
let i = 0;
while (i < length) {
char = key[i].toString(16);
if (char.length < 2) {
char = "0" + char;
}
result += char;
i++;
}
return result;
}
private nextSeqNumber(): number {
this.seqNumber++;
if (this.seqNumber > 254) {
this.seqNumber = 1;
}
return this.seqNumber;
}
private onParsed(frame: Uint8Array): void {
this.emit('rxFrame', frame);
}
private sleep(ms: number) : Promise<void>{
return new Promise(resolve => setTimeout(resolve, ms));
}
private resetTimeoutCounterAfter1min() {
if (this.timeoutResetTimeout === null) {
this.timeoutResetTimeout = setTimeout(function(){
timeoutCounter = 0;
this.timeoutResetTimeout = null;
}, 60000);
}
}
}
export default Driver;