Files
node-intellicenter/cjs/unit.js
Parnic 458048b58a
All checks were successful
Node.js CI / build (18.x) (push) Successful in 16m17s
Node.js CI / build (20.x) (push) Successful in 11s
Node.js CI / build (22.x) (push) Successful in 11s
Reinstate ws WebSocket library
In some environments, such as MagicMirror modules, the WebSocket class is not defined. This is apparently a client-only class and generally only exists in browsers, but I'm confused how the unit tests were passing when run against just node. Either way, this means we have to disable the test again since ws is not compatible with jest-websocket-mock, and I haven't found an alternative mock server that is.
2025-01-25 12:06:01 -06:00

184 lines
7.0 KiB
JavaScript

"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (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 () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Unit = void 0;
const events_1 = require("events");
const debug_1 = __importDefault(require("debug"));
const ws = __importStar(require("ws"));
const debugUnit = (0, debug_1.default)("ic:unit");
/**
* Contains methods to connect to and communicate with an IntelliCenter controller.
*
* Call `connect` to connect to the unit.
* Use `send` to send a message.
* Subscribe to events to process socket conditions, notify updates, and message responses (if not `await`ing the response)
*
* Available events:
*
* * `"response-{messageID}"` - fired once per message sent with `send()` where {messageID} is the ID specified in the {@linkcode ICRequest} given to `send()`
* * `"notify"` - fired when an update is available to a property previously subscribed to via a {@linkcode SubscribeToUpdates} request
* * `"close"` - fired any time the client is closed by any means (timeout, by request, error, etc.)
* * `"open"` - fired when the socket connects to the unit successfully
* * `"error"` - fired when the socket encounters an unrecoverable error and will close
* * `"timeout"` - fired when the socket has not received a ping response within the allowed threshold and will close
* * `"connected"` - fired when a connection has completed successfully
*/
class Unit extends events_1.EventEmitter {
endpoint;
port;
client;
pingTimeout;
pingTimer;
pingInterval = 60000;
constructor(endpoint, port = 6680) {
super();
this.endpoint = endpoint;
this.port = port;
this.endpoint = endpoint;
this.port = port;
}
/**
* Connects to the specified unit and maintains a connection to it until `close()` is called.
*/
async connect() {
if (this.client) {
throw new Error("can't open a client that is already open");
}
debugUnit(`connecting to ws://${this.endpoint}:${this.port.toString()}`);
this.client = new ws.WebSocket(`ws://${this.endpoint}:${this.port.toString()}`);
const { onOpen, onError, onClientMessage, socketCleanup } = this;
this.client.on("error", onError);
this.client.on("open", onOpen);
this.client.on("close", socketCleanup);
this.client.on("message", onClientMessage);
this.pingTimer = setInterval(() => {
debugUnit("sending ping");
// this isn't an actual command that is recognized by the system, we just want to make sure they're still there.
this.client?.send(JSON.stringify({ command: "ping" }));
}, this.pingInterval);
await new Promise((resolve, reject) => {
this.client?.once("error", reject);
this.client?.once("open", resolve);
});
debugUnit("connected");
this.emit("connected");
}
onOpen = () => {
this.emit("open");
this.heartbeat();
};
onError = (evt) => {
// todo: emit event so we can reconnect? auto reconnect?
debugUnit("error in websocket: $o", evt);
this.emit("error", evt);
this.socketCleanup();
};
/**
* Closes the connection to the unit.
*/
close() {
if (!this.client) {
return;
}
debugUnit("closing connection by request");
this.client.close();
}
socketCleanup = () => {
debugUnit("socket cleanup");
this.emit("close");
this.client?.removeAllListeners();
this.client = undefined;
if (this.pingTimeout) {
clearTimeout(this.pingTimeout);
this.pingTimeout = undefined;
}
if (this.pingTimer) {
clearInterval(this.pingTimer);
this.pingTimer = undefined;
}
};
heartbeat = () => {
debugUnit("received heartbeat");
clearTimeout(this.pingTimeout);
this.pingTimeout = setTimeout(() => {
debugUnit("terminating connection due to heartbeat timeout");
this.emit("timeout");
try {
this.client?.close();
}
catch (ex) {
debugUnit("exception trying to close client from ping timeout: %o", ex);
}
this.socketCleanup();
}, this.pingInterval + 5000);
};
onClientMessage = (msg) => {
debugUnit("message received, length %d", msg.length);
this.heartbeat();
const respObj = JSON.parse(msg.toString());
if (respObj.command.toLowerCase() === "notifylist") {
debugUnit(" it's a subscription confirmation or update");
this.emit(`notify`, respObj);
}
this.emit(`response-${respObj.messageID}`, respObj);
};
/**
* Sends a request to the unit.
*
* @param request an message from {@linkcode messages} to send to the unit.
* @returns a promise that resolves into the {@linkcode ICResponse} with information about the request.
*/
async send(request) {
if (!this.client) {
return await new Promise(() => {
throw new Error("client not connected");
});
}
const payload = JSON.stringify(request);
debugUnit("sending message of length %d with id %s", payload.length, request.messageID);
this.client.send(payload);
return await new Promise((resolve) => {
this.once(`response-${request.messageID}`, (resp) => {
debugUnit(" returning response to message %s", request.messageID);
resolve(resp);
});
});
}
}
exports.Unit = Unit;
//# sourceMappingURL=unit.js.map