ember-zli
Version:
Interact with EmberZNet-based adapters using zigbee-herdsman 'ember' driver
159 lines (158 loc) • 5.95 kB
JavaScript
import EventEmitter from "node:events";
import { Socket } from "node:net";
import { Readable } from "node:stream";
import { SerialPort } from "zigbee-herdsman/dist/adapter/serialPort.js";
import { logger } from "../index.js";
import { CONFIG_HIGHWATER_MARK, TCP_REGEX } from "./consts.js";
const NS = { namespace: "transport" };
class TransportWriter extends Readable {
writeBuffer(buffer) {
this.emit("data", buffer);
}
_read() { }
}
export var TransportEvent;
(function (TransportEvent) {
TransportEvent["CLOSED"] = "closed";
TransportEvent["DATA"] = "data";
TransportEvent["FAILED"] = "failed";
})(TransportEvent || (TransportEvent = {}));
/**
* Serial or Socket based transport based on passed conf.
*/
export class Transport extends EventEmitter {
connected;
portConf;
portWriter;
#portSerial;
#portSocket;
constructor(portConf) {
super();
this.connected = false;
this.portConf = portConf;
}
get isSerial() {
return Boolean(this.#portSerial);
}
async close(emitClosed, emitFailed = true) {
if (this.#portSerial?.isOpen) {
logger.info("Closing serial connection...", NS);
try {
await this.#portSerial.asyncFlushAndClose();
}
catch (error) {
logger.error(`Failed to close port: ${error}.`, NS);
this.#portSerial.removeAllListeners();
if (emitFailed) {
this.emit(TransportEvent.FAILED);
}
return;
}
this.#portSerial.removeAllListeners();
}
else if (this.#portSocket !== undefined && !this.#portSocket.closed) {
logger.info("Closing socket connection...", NS);
this.#portSocket.destroy();
this.#portSocket.removeAllListeners();
}
if (emitClosed) {
this.emit(TransportEvent.CLOSED);
}
}
async initPort(customPortWriter, baudRate = this.portConf.baudRate) {
// will do nothing if nothing's open
await this.close(false);
if (TCP_REGEX.test(this.portConf.path)) {
const info = new URL(this.portConf.path);
logger.debug(`Opening TCP socket with ${info.hostname}:${info.port}`, NS);
this.#portSocket = new Socket();
this.#portSocket.setNoDelay(true);
this.#portSocket.setKeepAlive(true, 15000);
this.portWriter = customPortWriter ?? new TransportWriter({ highWaterMark: CONFIG_HIGHWATER_MARK });
this.portWriter.pipe(this.#portSocket);
this.#portSocket.on("data", this.emitData.bind(this));
return await new Promise((resolve, reject) => {
const openError = (err) => {
reject(err);
};
if (this.#portSocket === undefined) {
reject(new Error("Invalid socket"));
return;
}
this.#portSocket.on("connect", () => {
logger.debug("Socket connected", NS);
});
this.#portSocket.on("ready", () => {
logger.info("Socket ready", NS);
this.#portSocket.removeListener("error", openError);
this.#portSocket.once("close", this.onPortClose.bind(this));
this.#portSocket.on("error", this.onPortError.bind(this));
this.connected = true;
resolve();
});
this.#portSocket.once("error", openError);
this.#portSocket.connect(Number.parseInt(info.port, 10), info.hostname);
});
}
const serialOpts = {
autoOpen: false,
baudRate,
dataBits: 8,
parity: "none",
path: this.portConf.path,
rtscts: this.portConf.rtscts,
stopBits: 1,
xoff: this.portConf.xoff,
xon: this.portConf.xon,
};
logger.debug(`Opening serial port with ${JSON.stringify(serialOpts)}`, NS);
this.#portSerial = new SerialPort(serialOpts);
this.portWriter = customPortWriter ?? new TransportWriter({ highWaterMark: CONFIG_HIGHWATER_MARK });
this.portWriter.pipe(this.#portSerial);
this.#portSerial.on("data", this.emitData.bind(this));
await this.#portSerial.asyncOpen();
logger.info("Serial port opened", NS);
this.#portSerial.once("close", this.onPortClose.bind(this));
this.#portSerial.on("error", this.onPortError.bind(this));
this.connected = true;
}
async serialSet(options, afterDelayMS) {
await new Promise((resolve, reject) => {
const fn = () => this.#portSerial?.set(options, (error) => (error ? reject(error) : resolve()));
if (afterDelayMS) {
setTimeout(fn, afterDelayMS);
}
else {
fn();
}
});
}
write(buffer) {
if (this.portWriter === undefined) {
logger.error("No port available to write.", NS);
this.emit(TransportEvent.FAILED);
}
else {
logger.debug(`Sending transport data: ${buffer.toString("hex")}.`, NS);
this.portWriter.writeBuffer(buffer);
}
}
emitData(data) {
this.emit(TransportEvent.DATA, data);
}
onPortClose(error) {
logger.info("Transport closed.", NS);
if (error && this.connected) {
logger.info(`Transport close ${error}`, NS);
this.emit(TransportEvent.FAILED);
}
else {
this.emit(TransportEvent.CLOSED);
}
}
onPortError(error) {
this.connected = false;
logger.info(`Transport ${error}`, NS);
this.emit(TransportEvent.FAILED);
}
}