UNPKG

zigbee-herdsman-zigate

Version:

An open source ZigBee gateway solution with node.js.

337 lines 15.5 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const unpi_1 = require("../unpi"); const utils_1 = require("../../../utils"); const serialPortUtils_1 = __importDefault(require("../../serialPortUtils")); const socketPortUtils_1 = __importDefault(require("../../socketPortUtils")); const Constants = __importStar(require("../constants")); const zpiObject_1 = __importDefault(require("./zpiObject")); const constants_1 = require("../unpi/constants"); const serialport_1 = __importDefault(require("serialport")); const net_1 = __importDefault(require("net")); const events_1 = __importDefault(require("events")); const es6_1 = __importDefault(require("fast-deep-equal/es6")); const debug_1 = __importDefault(require("debug")); const { COMMON: { ZnpCommandStatus }, Utils: { statusDescription } } = Constants; const timeouts = { SREQ: 6000, reset: 30000, default: 10000, }; const debug = { error: debug_1.default('zigbee-herdsman:adapter:zStack:znp:error'), timeout: debug_1.default('zigbee-herdsman:adapter:zStack:znp:timeout'), log: debug_1.default('zigbee-herdsman:adapter:zStack:znp:log'), SREQ: debug_1.default('zigbee-herdsman:adapter:zStack:znp:SREQ'), AREQ: debug_1.default('zigbee-herdsman:adapter:zStack:znp:AREQ'), SRSP: debug_1.default('zigbee-herdsman:adapter:zStack:znp:SRSP'), }; const autoDetectDefinitions = [ { manufacturer: 'Texas Instruments', vendorId: '0451', productId: '16c8' }, { manufacturer: 'Texas Instruments', vendorId: '0451', productId: '16a8' }, { manufacturer: 'Texas Instruments', vendorId: '0451', productId: 'bef3' }, { manufacturer: 'Electrolama', vendorId: '0403', productId: '6015' }, ]; class Znp extends events_1.default.EventEmitter { constructor(path, baudRate, rtscts) { super(); this.path = path; this.baudRate = typeof baudRate === 'number' ? baudRate : 115200; this.rtscts = typeof rtscts === 'boolean' ? rtscts : false; this.portType = socketPortUtils_1.default.isTcpPath(path) ? 'socket' : 'serial'; this.initialized = false; this.queue = new utils_1.Queue(); this.waitress = new utils_1.Waitress(this.waitressValidator, this.waitressTimeoutFormatter); this.onUnpiParsed = this.onUnpiParsed.bind(this); this.onPortClose = this.onPortClose.bind(this); } log(type, message) { if (type === constants_1.Type.SRSP) { debug.SRSP(message); } else if (type === constants_1.Type.AREQ) { debug.AREQ(message); } else { /* istanbul ignore else */ if (type === constants_1.Type.SREQ) { debug.SREQ(message); } else { throw new Error(`Unknown type '${type}'`); } } } onUnpiParsed(frame) { try { const object = zpiObject_1.default.fromUnpiFrame(frame); const message = `<-- ${constants_1.Subsystem[object.subsystem]} - ${object.command} - ${JSON.stringify(object.payload)}`; this.log(object.type, message); this.waitress.resolve(object); this.emit('received', object); } catch (error) { debug.error(`Error while parsing to ZpiObject '${error.stack}'`); } } isInitialized() { return this.initialized; } onPortClose() { debug.log('Port closed'); this.initialized = false; this.emit('close'); } open() { return __awaiter(this, void 0, void 0, function* () { return this.portType === 'serial' ? this.openSerialPort() : this.openSocketPort(); }); } openSerialPort() { return __awaiter(this, void 0, void 0, function* () { const options = { baudRate: this.baudRate, rtscts: this.rtscts, autoOpen: false }; debug.log(`Opening SerialPort with ${this.path} and ${JSON.stringify(options)}`); this.serialPort = new serialport_1.default(this.path, options); this.unpiWriter = new unpi_1.Writer(); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore this.unpiWriter.pipe(this.serialPort); this.unpiParser = new unpi_1.Parser(); this.serialPort.pipe(this.unpiParser); this.unpiParser.on('parsed', this.onUnpiParsed); return new Promise((resolve, reject) => { this.serialPort.open((error) => __awaiter(this, void 0, void 0, function* () { if (error) { reject(new Error(`Error while opening serialport '${error}'`)); this.initialized = false; if (this.serialPort.isOpen) { this.serialPort.close(); } } else { debug.log('Serialport opened'); yield this.skipBootloader(); this.serialPort.once('close', this.onPortClose); this.serialPort.once('error', (error) => { debug.error(`Serialport error: ${error}`); }); this.initialized = true; resolve(); } })); }); }); } openSocketPort() { return __awaiter(this, void 0, void 0, function* () { const info = socketPortUtils_1.default.parseTcpPath(this.path); debug.log(`Opening TCP socket with ${info.host}:${info.port}`); this.socketPort = new net_1.default.Socket(); this.socketPort.setNoDelay(true); this.socketPort.setKeepAlive(true, 15000); this.unpiWriter = new unpi_1.Writer(); this.unpiWriter.pipe(this.socketPort); this.unpiParser = new unpi_1.Parser(); this.socketPort.pipe(this.unpiParser); this.unpiParser.on('parsed', this.onUnpiParsed); return new Promise((resolve, reject) => { this.socketPort.on('connect', function () { debug.log('Socket connected'); }); // eslint-disable-next-line const self = this; this.socketPort.on('ready', function () { return __awaiter(this, void 0, void 0, function* () { debug.log('Socket ready'); yield self.skipBootloader(); self.initialized = true; resolve(); }); }); this.socketPort.once('close', this.onPortClose); this.socketPort.on('error', function () { debug.log('Socket error'); reject(new Error(`Error while opening socket`)); self.initialized = false; }); this.socketPort.connect(info.port, info.host); }); }); } skipBootloader() { return __awaiter(this, void 0, void 0, function* () { // Skip bootloader on some CC2652 devices (e.g. zzh-p) if (this.serialPort) { yield this.setSerialPortOptions({ dtr: false, rts: false }); yield utils_1.Wait(150); yield this.setSerialPortOptions({ dtr: false, rts: true }); yield utils_1.Wait(150); yield this.setSerialPortOptions({ dtr: false, rts: false }); yield utils_1.Wait(150); } // Skip bootloader on CC2530/CC2531 // Send magic byte: https://github.com/Koenkk/zigbee2mqtt/issues/1343 to bootloader // and give ZNP 1 second to start. const buffer = Buffer.from([0xef]); debug.log('Writing skip bootloader payload'); this.unpiWriter.writeBuffer(buffer); yield utils_1.Wait(1000); }); } setSerialPortOptions(options) { return __awaiter(this, void 0, void 0, function* () { return new Promise((resolve) => { this.serialPort.set(options, () => { resolve(); }); }); }); } static isValidPath(path) { return __awaiter(this, void 0, void 0, function* () { // For TCP paths we cannot get device information, therefore we cannot validate it. if (socketPortUtils_1.default.isTcpPath(path)) { return false; } try { return serialPortUtils_1.default.is(utils_1.RealpathSync(path), autoDetectDefinitions); } catch (error) { debug.error(`Failed to determine if path is valid: '${error}'`); return false; } }); } static autoDetectPath() { return __awaiter(this, void 0, void 0, function* () { const paths = yield serialPortUtils_1.default.find(autoDetectDefinitions); // CC1352P_2 and CC26X2R1 lists as 2 USB devices with same manufacturer, productId and vendorId // one is the actual chip interface, other is the XDS110. // The chip is always exposed on the first one after alphabetical sorting. paths.sort((a, b) => (a < b) ? -1 : 1); return paths.length > 0 ? paths[0] : null; }); } close() { return new Promise((resolve, reject) => { if (this.initialized) { if (this.portType === 'serial') { this.serialPort.flush(() => { this.serialPort.close((error) => { this.initialized = false; error == null ? resolve() : reject(new Error(`Error while closing serialport '${error}'`)); this.emit('close'); }); }); } else { this.socketPort.destroy(); resolve(); } } else { resolve(); this.emit('close'); } }); } request(subsystem, command, payload, waiterID = null, expectedStatuses = [ZnpCommandStatus.SUCCESS]) { if (!this.initialized) { throw new Error('Cannot request when znp has not been initialized yet'); } const object = zpiObject_1.default.createRequest(subsystem, command, payload); const message = `--> ${constants_1.Subsystem[object.subsystem]} - ${object.command} - ${JSON.stringify(payload)}`; return this.queue.execute(() => __awaiter(this, void 0, void 0, function* () { this.log(object.type, message); const frame = object.toUnpiFrame(); if (object.type === constants_1.Type.SREQ) { const timeout = object.command === 'bdbStartCommissioning' || object.command === 'startupFromApp' ? 20000 : timeouts.SREQ; const waiter = this.waitress.waitFor({ type: constants_1.Type.SRSP, subsystem: object.subsystem, command: object.command }, timeout); this.unpiWriter.writeFrame(frame); const result = yield waiter.start().promise; if (result && result.payload.hasOwnProperty('status') && !expectedStatuses.includes(result.payload.status)) { if (typeof waiterID === 'number') { this.waitress.remove(waiterID); } throw new Error(`SREQ '${message}' failed with status '${statusDescription(result.payload.status)}' (expected '${expectedStatuses.map(statusDescription)}')`); } else { return result; } } else if (object.type === constants_1.Type.AREQ && object.isResetCommand()) { const waiter = this.waitress.waitFor({ type: constants_1.Type.AREQ, subsystem: constants_1.Subsystem.SYS, command: 'resetInd' }, timeouts.reset); this.queue.clear(); this.unpiWriter.writeFrame(frame); return waiter.start().promise; } else { /* istanbul ignore else */ if (object.type === constants_1.Type.AREQ) { this.unpiWriter.writeFrame(frame); return undefined; } else { throw new Error(`Unknown type '${object.type}'`); } } })); } waitressTimeoutFormatter(matcher, timeout) { return `${constants_1.Type[matcher.type]} - ${constants_1.Subsystem[matcher.subsystem]} - ${matcher.command} after ${timeout}ms`; } waitFor(type, subsystem, command, payload = {}, timeout = timeouts.default) { return this.waitress.waitFor({ type, subsystem, command, payload }, timeout); } waitressValidator(zpiObject, matcher) { const requiredMatch = matcher.type === zpiObject.type && matcher.subsystem == zpiObject.subsystem && matcher.command === zpiObject.command; let payloadMatch = true; if (matcher.payload) { for (const [key, value] of Object.entries(matcher.payload)) { if (!es6_1.default(zpiObject.payload[key], value)) { payloadMatch = false; break; } } } return requiredMatch && payloadMatch; } } exports.default = Znp; //# sourceMappingURL=znp.js.map