iobroker.nut2 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +250 -0
- package/admin/i18n/de.json +150 -0
- package/admin/i18n/en.json +150 -0
- package/admin/i18n/es.json +150 -0
- package/admin/i18n/fr.json +150 -0
- package/admin/i18n/it.json +150 -0
- package/admin/i18n/nl.json +150 -0
- package/admin/i18n/pl.json +150 -0
- package/admin/i18n/pt.json +150 -0
- package/admin/i18n/ru.json +150 -0
- package/admin/i18n/uk.json +150 -0
- package/admin/i18n/zh-cn.json +150 -0
- package/admin/jsonConfig.json +204 -0
- package/admin/nut2.png +0 -0
- package/admin/nut2.svg +19 -0
- package/build/lib/coerce.js +114 -0
- package/build/lib/coerce.js.map +7 -0
- package/build/lib/i18n.js +32 -0
- package/build/lib/i18n.js.map +7 -0
- package/build/lib/message-router.js +99 -0
- package/build/lib/message-router.js.map +7 -0
- package/build/lib/nut-client.js +633 -0
- package/build/lib/nut-client.js.map +7 -0
- package/build/lib/state-manager.js +570 -0
- package/build/lib/state-manager.js.map +7 -0
- package/build/lib/status-parser.js +131 -0
- package/build/lib/status-parser.js.map +7 -0
- package/build/lib/type-detector.js +255 -0
- package/build/lib/type-detector.js.map +7 -0
- package/build/lib/types.js +59 -0
- package/build/lib/types.js.map +7 -0
- package/build/main.js +521 -0
- package/build/main.js.map +7 -0
- package/io-package.json +238 -0
- package/package.json +80 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/lib/message-router.ts"],
|
|
4
|
+
"sourcesContent": ["import type { NutClient } from \"./nut-client\";\nimport { coerceCommandTimeoutMs, coerceHost, coercePort, errText, localAddressOf } from \"./coerce\";\nimport type { AdapterConfig, NutClientOptions, NutLogger } from \"./types\";\n\n/**\n * Dependencies the message router needs. Extracted as a pure interface\n * so `dispatchMessage` is testable without an adapter instance.\n */\nexport interface MessageRouterDeps {\n /** Adapter logger. */\n log: {\n debug(msg: string): void;\n warn(msg: string): void;\n };\n /** ioBroker sendTo bound against the adapter instance. */\n sendTo: (\n from: string,\n command: string,\n response: unknown,\n callback: ioBroker.MessageCallbackInfo | undefined,\n ) => void;\n /** Factory for throwaway NutClient used by checkConnection (options carry localAddress/timeout/TLS). */\n createTestClient: (host: string, port: number, options?: NutClientOptions) => NutClient;\n /** Called right after createTestClient returns. */\n onTestClientCreated?: (client: NutClient) => void;\n /** Called after checkConnection settles (success or fail). */\n onTestClientDone?: (client: NutClient) => void;\n}\n\n/** NutClient constructor shape for dependency injection. */\ntype NutClientConstructor = new (host: string, port: number, options?: NutClientOptions) => NutClient;\n\n/**\n * Build the standard test-client factory used in production.\n *\n * @param NutClientClass NutClient constructor\n * @param logger Logger to forward into the NutClient\n */\nexport function makeTestClientFactory(\n NutClientClass: NutClientConstructor,\n logger: NutLogger,\n): MessageRouterDeps[\"createTestClient\"] {\n return (host, port, options) => new NutClientClass(host, port, { ...options, logger });\n}\n\n/**\n * Dispatch a single ioBroker message. Handles `checkConnection` from the\n * admin UI and provides the default-branch contract so unknown commands\n * get `{ error: \"Unknown command\" }` instead of leaving the callback hanging.\n *\n * @param obj The incoming message payload\n * @param deps Test-injectable dependencies\n */\nexport async function dispatchMessage(obj: ioBroker.Message, deps: MessageRouterDeps): Promise<void> {\n deps.log.debug(`onMessage: command='${obj?.command}' from='${obj?.from}' has-callback=${!!obj?.callback}`);\n if (!obj.callback) {\n return;\n }\n try {\n switch (obj.command) {\n case \"checkConnection\": {\n const raw = obj.message;\n const config =\n typeof raw === \"object\" && raw !== null && !Array.isArray(raw)\n ? (raw as Partial<AdapterConfig>)\n : ({} as Partial<AdapterConfig>);\n const host = coerceHost(config.host);\n\n if (!host) {\n deps.log.debug(\"checkConnection: missing host in message\");\n deps.sendTo(obj.from, obj.command, { error: \"Host is required\" }, obj.callback);\n return;\n }\n\n const port = coercePort(config.port);\n const username = typeof config.username === \"string\" ? config.username : \"\";\n const password = typeof config.password === \"string\" ? config.password : \"\";\n\n // Mirror the production client so the test exercises the real path (multi-homed bind, TLS).\n const localAddress = localAddressOf(config.networkInterface);\n const options: NutClientOptions = {\n localAddress,\n commandTimeout: coerceCommandTimeoutMs(config.commandTimeout),\n useTls: !!config.useTls,\n tlsRejectUnauthorized: !!config.tlsRejectUnauthorized,\n };\n\n const testClient = deps.createTestClient(host, port, options);\n deps.onTestClientCreated?.(testClient);\n try {\n await testClient.connect();\n const upsList = await testClient.listUps();\n const names = upsList.map(u => u.name).join(\", \");\n deps.log.debug(`checkConnection: found ${upsList.length} UPS(es): ${names}`);\n\n if (username && password) {\n // No per-UPS LOGIN (see onConnected in main.ts): NUT allows one LOGIN per connection\n // and it is upsmon-only; USERNAME/PASSWORD is the real credential check.\n await testClient.authenticate(username, password);\n deps.sendTo(\n obj.from,\n obj.command,\n { result: `Connected and authenticated \u2014 ${upsList.length} UPS(es): ${names}` },\n obj.callback,\n );\n } else {\n deps.sendTo(\n obj.from,\n obj.command,\n { result: `Connected \u2014 ${upsList.length} UPS(es): ${names}` },\n obj.callback,\n );\n }\n } finally {\n testClient.destroy();\n deps.onTestClientDone?.(testClient);\n }\n break;\n }\n default:\n deps.log.debug(`onMessage: unknown command '${obj.command}'`);\n deps.sendTo(obj.from, obj.command, { error: \"Unknown command\" }, obj.callback);\n }\n } catch (err) {\n deps.log.debug(`onMessage: '${obj.command}' failed: ${errText(err)}`);\n deps.sendTo(obj.from, obj.command, { error: errText(err) }, obj.callback);\n }\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,oBAAwF;AAqCjF,SAAS,sBACd,gBACA,QACuC;AACvC,SAAO,CAAC,MAAM,MAAM,YAAY,IAAI,eAAe,MAAM,MAAM,EAAE,GAAG,SAAS,OAAO,CAAC;AACvF;AAUA,eAAsB,gBAAgB,KAAuB,MAAwC;AArDrG;AAsDE,OAAK,IAAI,MAAM,uBAAuB,2BAAK,OAAO,WAAW,2BAAK,IAAI,kBAAkB,CAAC,EAAC,2BAAK,SAAQ,EAAE;AACzG,MAAI,CAAC,IAAI,UAAU;AACjB;AAAA,EACF;AACA,MAAI;AACF,YAAQ,IAAI,SAAS;AAAA,MACnB,KAAK,mBAAmB;AACtB,cAAM,MAAM,IAAI;AAChB,cAAM,SACJ,OAAO,QAAQ,YAAY,QAAQ,QAAQ,CAAC,MAAM,QAAQ,GAAG,IACxD,MACA,CAAC;AACR,cAAM,WAAO,0BAAW,OAAO,IAAI;AAEnC,YAAI,CAAC,MAAM;AACT,eAAK,IAAI,MAAM,0CAA0C;AACzD,eAAK,OAAO,IAAI,MAAM,IAAI,SAAS,EAAE,OAAO,mBAAmB,GAAG,IAAI,QAAQ;AAC9E;AAAA,QACF;AAEA,cAAM,WAAO,0BAAW,OAAO,IAAI;AACnC,cAAM,WAAW,OAAO,OAAO,aAAa,WAAW,OAAO,WAAW;AACzE,cAAM,WAAW,OAAO,OAAO,aAAa,WAAW,OAAO,WAAW;AAGzE,cAAM,mBAAe,8BAAe,OAAO,gBAAgB;AAC3D,cAAM,UAA4B;AAAA,UAChC;AAAA,UACA,oBAAgB,sCAAuB,OAAO,cAAc;AAAA,UAC5D,QAAQ,CAAC,CAAC,OAAO;AAAA,UACjB,uBAAuB,CAAC,CAAC,OAAO;AAAA,QAClC;AAEA,cAAM,aAAa,KAAK,iBAAiB,MAAM,MAAM,OAAO;AAC5D,mBAAK,wBAAL,8BAA2B;AAC3B,YAAI;AACF,gBAAM,WAAW,QAAQ;AACzB,gBAAM,UAAU,MAAM,WAAW,QAAQ;AACzC,gBAAM,QAAQ,QAAQ,IAAI,OAAK,EAAE,IAAI,EAAE,KAAK,IAAI;AAChD,eAAK,IAAI,MAAM,0BAA0B,QAAQ,MAAM,aAAa,KAAK,EAAE;AAE3E,cAAI,YAAY,UAAU;AAGxB,kBAAM,WAAW,aAAa,UAAU,QAAQ;AAChD,iBAAK;AAAA,cACH,IAAI;AAAA,cACJ,IAAI;AAAA,cACJ,EAAE,QAAQ,sCAAiC,QAAQ,MAAM,aAAa,KAAK,GAAG;AAAA,cAC9E,IAAI;AAAA,YACN;AAAA,UACF,OAAO;AACL,iBAAK;AAAA,cACH,IAAI;AAAA,cACJ,IAAI;AAAA,cACJ,EAAE,QAAQ,oBAAe,QAAQ,MAAM,aAAa,KAAK,GAAG;AAAA,cAC5D,IAAI;AAAA,YACN;AAAA,UACF;AAAA,QACF,UAAE;AACA,qBAAW,QAAQ;AACnB,qBAAK,qBAAL,8BAAwB;AAAA,QAC1B;AACA;AAAA,MACF;AAAA,MACA;AACE,aAAK,IAAI,MAAM,+BAA+B,IAAI,OAAO,GAAG;AAC5D,aAAK,OAAO,IAAI,MAAM,IAAI,SAAS,EAAE,OAAO,kBAAkB,GAAG,IAAI,QAAQ;AAAA,IACjF;AAAA,EACF,SAAS,KAAK;AACZ,SAAK,IAAI,MAAM,eAAe,IAAI,OAAO,iBAAa,uBAAQ,GAAG,CAAC,EAAE;AACpE,SAAK,OAAO,IAAI,MAAM,IAAI,SAAS,EAAE,WAAO,uBAAQ,GAAG,EAAE,GAAG,IAAI,QAAQ;AAAA,EAC1E;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,633 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
var nut_client_exports = {};
|
|
30
|
+
__export(nut_client_exports, {
|
|
31
|
+
NutClient: () => NutClient,
|
|
32
|
+
NutError: () => NutError,
|
|
33
|
+
NutTimeoutError: () => NutTimeoutError,
|
|
34
|
+
isTlsConfigError: () => isTlsConfigError
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(nut_client_exports);
|
|
37
|
+
var net = __toESM(require("node:net"));
|
|
38
|
+
var tls = __toESM(require("node:tls"));
|
|
39
|
+
var import_coerce = require("./coerce");
|
|
40
|
+
var import_types = require("./types");
|
|
41
|
+
const RECONNECT_BASE_MS = 1e3;
|
|
42
|
+
const RECONNECT_MAX_MS = 6e4;
|
|
43
|
+
class NutError extends Error {
|
|
44
|
+
/**
|
|
45
|
+
* @param code NUT error code (a server may send codes outside the documented set, so this is `string`)
|
|
46
|
+
* @param message Optional custom message
|
|
47
|
+
*/
|
|
48
|
+
constructor(code, message) {
|
|
49
|
+
super(message != null ? message : `NUT error: ${code}`);
|
|
50
|
+
this.code = code;
|
|
51
|
+
this.name = "NutError";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
class NutTimeoutError extends Error {
|
|
55
|
+
/**
|
|
56
|
+
* @param command The command that timed out
|
|
57
|
+
*/
|
|
58
|
+
constructor(command) {
|
|
59
|
+
super(`NUT command timed out: ${command}`);
|
|
60
|
+
this.command = command;
|
|
61
|
+
this.name = "NutTimeoutError";
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const TLS_FATAL_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
65
|
+
// NUT-level: the server cannot/does not start TLS
|
|
66
|
+
"FEATURE-NOT-CONFIGURED",
|
|
67
|
+
"FEATURE-NOT-SUPPORTED",
|
|
68
|
+
"ALREADY-SSL-MODE",
|
|
69
|
+
// Node certificate-verification failures (only reachable with tlsRejectUnauthorized=true)
|
|
70
|
+
"DEPTH_ZERO_SELF_SIGNED_CERT",
|
|
71
|
+
"SELF_SIGNED_CERT_IN_CHAIN",
|
|
72
|
+
"UNABLE_TO_VERIFY_LEAF_SIGNATURE",
|
|
73
|
+
"CERT_HAS_EXPIRED",
|
|
74
|
+
"ERR_TLS_CERT_ALTNAME_INVALID"
|
|
75
|
+
]);
|
|
76
|
+
function isTlsConfigError(err) {
|
|
77
|
+
if (err instanceof NutError) {
|
|
78
|
+
return TLS_FATAL_ERROR_CODES.has(err.code);
|
|
79
|
+
}
|
|
80
|
+
const code = err == null ? void 0 : err.code;
|
|
81
|
+
if (typeof code !== "string") {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
return TLS_FATAL_ERROR_CODES.has(code) || code.startsWith("ERR_TLS_") || code.startsWith("ERR_SSL_");
|
|
85
|
+
}
|
|
86
|
+
class NutClient {
|
|
87
|
+
socket = null;
|
|
88
|
+
buffer = "";
|
|
89
|
+
queue = [];
|
|
90
|
+
active = null;
|
|
91
|
+
multiLineBuffer = [];
|
|
92
|
+
multiLineExpectedEnd = "";
|
|
93
|
+
connected = false;
|
|
94
|
+
destroyed = false;
|
|
95
|
+
tlsActive = false;
|
|
96
|
+
host;
|
|
97
|
+
port;
|
|
98
|
+
localAddress;
|
|
99
|
+
commandTimeout;
|
|
100
|
+
useTls;
|
|
101
|
+
tlsRejectUnauthorized;
|
|
102
|
+
log;
|
|
103
|
+
// Injected managed timers (adapter.setTimeout/clearTimeout in production → auto-cleared on
|
|
104
|
+
// unload; global timers as fallback for standalone use/tests).
|
|
105
|
+
setTimer;
|
|
106
|
+
clearTimer;
|
|
107
|
+
reconnectAttempt = 0;
|
|
108
|
+
reconnectTimer = null;
|
|
109
|
+
// Persistent mode (set by start()) owns the unified retry loop: it retries the initial
|
|
110
|
+
// connect, reconnects on drops, and stops yellow on a fatal TLS-config error. A plain
|
|
111
|
+
// connect() (e.g. the connection test) leaves this false — one-shot, never retries.
|
|
112
|
+
persistent = false;
|
|
113
|
+
onConnectHandler = null;
|
|
114
|
+
onFatalHandler = null;
|
|
115
|
+
/**
|
|
116
|
+
* @param host NUT server hostname or IP
|
|
117
|
+
* @param port NUT server port
|
|
118
|
+
* @param options Connection options
|
|
119
|
+
*/
|
|
120
|
+
constructor(host, port, options) {
|
|
121
|
+
var _a, _b, _c, _d, _e;
|
|
122
|
+
this.host = host;
|
|
123
|
+
this.port = port;
|
|
124
|
+
this.localAddress = options == null ? void 0 : options.localAddress;
|
|
125
|
+
this.commandTimeout = (_a = options == null ? void 0 : options.commandTimeout) != null ? _a : import_types.NUT_DEFAULT_COMMAND_TIMEOUT;
|
|
126
|
+
this.useTls = (_b = options == null ? void 0 : options.useTls) != null ? _b : false;
|
|
127
|
+
this.tlsRejectUnauthorized = (_c = options == null ? void 0 : options.tlsRejectUnauthorized) != null ? _c : false;
|
|
128
|
+
this.log = options == null ? void 0 : options.logger;
|
|
129
|
+
this.setTimer = (_d = options == null ? void 0 : options.setTimer) != null ? _d : ((cb, ms) => globalThis.setTimeout(cb, ms));
|
|
130
|
+
this.clearTimer = (_e = options == null ? void 0 : options.clearTimer) != null ? _e : ((h) => globalThis.clearTimeout(h));
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Register a callback invoked after every successful (re)connection in persistent mode.
|
|
134
|
+
* Runs the post-connect setup (discover/auth/poll); must be idempotent.
|
|
135
|
+
*
|
|
136
|
+
* @param handler Connect callback
|
|
137
|
+
*/
|
|
138
|
+
setOnConnect(handler) {
|
|
139
|
+
this.onConnectHandler = handler;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Register a callback invoked when the persistent connection fails fatally (TLS
|
|
143
|
+
* misconfiguration) — the retry loop stops and the caller should go yellow.
|
|
144
|
+
*
|
|
145
|
+
* @param handler Fatal-error callback
|
|
146
|
+
*/
|
|
147
|
+
setOnFatal(handler) {
|
|
148
|
+
this.onFatalHandler = handler;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Start the persistent runtime connection: connect now and keep retrying with exponential
|
|
152
|
+
* backoff, reconnecting automatically on later drops. A fatal TLS-config error stops the
|
|
153
|
+
* loop (onFatal). Use connect() directly for a one-shot (e.g. the connection test).
|
|
154
|
+
*/
|
|
155
|
+
start() {
|
|
156
|
+
this.persistent = true;
|
|
157
|
+
this.reconnectAttempt = 0;
|
|
158
|
+
this.attemptConnect();
|
|
159
|
+
}
|
|
160
|
+
/** One iteration of the persistent loop: connect, then fire onConnect or handle the failure. */
|
|
161
|
+
attemptConnect() {
|
|
162
|
+
if (this.destroyed) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
this.connect().then(() => {
|
|
166
|
+
var _a;
|
|
167
|
+
this.reconnectAttempt = 0;
|
|
168
|
+
(_a = this.onConnectHandler) == null ? void 0 : _a.call(this);
|
|
169
|
+
}).catch((err) => this.handleConnectFailure(err));
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Decide a failed persistent connect: a TLS-config error stops the loop (onFatal, yellow);
|
|
173
|
+
* any other error schedules a backed-off retry.
|
|
174
|
+
*
|
|
175
|
+
* @param err The connect/STARTTLS failure
|
|
176
|
+
*/
|
|
177
|
+
handleConnectFailure(err) {
|
|
178
|
+
var _a, _b, _c;
|
|
179
|
+
if (this.destroyed) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
this.connected = false;
|
|
183
|
+
const sock = this.socket;
|
|
184
|
+
this.socket = null;
|
|
185
|
+
sock == null ? void 0 : sock.destroy();
|
|
186
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
187
|
+
if (this.useTls && isTlsConfigError(err)) {
|
|
188
|
+
(_a = this.log) == null ? void 0 : _a.warn(`TLS connection to NUT server ${this.host}:${this.port} failed \u2014 not retrying: ${msg}`);
|
|
189
|
+
(_b = this.onFatalHandler) == null ? void 0 : _b.call(this, err);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
(_c = this.log) == null ? void 0 : _c.debug(`Connect attempt failed: ${msg}`);
|
|
193
|
+
this.scheduleReconnect();
|
|
194
|
+
}
|
|
195
|
+
/** Establish TCP connection (and STARTTLS upgrade if configured) to the NUT server. */
|
|
196
|
+
connect() {
|
|
197
|
+
return new Promise((resolve, reject) => {
|
|
198
|
+
if (this.destroyed) {
|
|
199
|
+
reject(new Error("Client has been destroyed"));
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
let settled = false;
|
|
203
|
+
const deadline = this.setTimer(() => {
|
|
204
|
+
if (settled) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
settled = true;
|
|
208
|
+
const sock = this.socket;
|
|
209
|
+
this.socket = null;
|
|
210
|
+
this.connected = false;
|
|
211
|
+
sock == null ? void 0 : sock.destroy();
|
|
212
|
+
reject(new Error(`Connect to NUT server ${this.host}:${this.port} timed out`));
|
|
213
|
+
}, this.commandTimeout);
|
|
214
|
+
const settle = (err) => {
|
|
215
|
+
if (settled) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
settled = true;
|
|
219
|
+
this.clearTimer(deadline);
|
|
220
|
+
if (err) {
|
|
221
|
+
reject(err);
|
|
222
|
+
} else {
|
|
223
|
+
resolve();
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
const opts = { host: this.host, port: this.port };
|
|
227
|
+
if (this.localAddress) {
|
|
228
|
+
opts.localAddress = this.localAddress;
|
|
229
|
+
}
|
|
230
|
+
const socket = net.createConnection(opts, () => {
|
|
231
|
+
var _a;
|
|
232
|
+
this.connected = true;
|
|
233
|
+
this.tlsActive = false;
|
|
234
|
+
this.buffer = "";
|
|
235
|
+
(_a = this.log) == null ? void 0 : _a.debug(`Connected to NUT server ${this.host}:${this.port}`);
|
|
236
|
+
if (this.useTls) {
|
|
237
|
+
this.startTls().then(() => settle()).catch(settle);
|
|
238
|
+
} else {
|
|
239
|
+
settle();
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
this.socket = socket;
|
|
243
|
+
socket.setKeepAlive(true, 3e4);
|
|
244
|
+
this.wireSocket(socket, settle);
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Attach data/error/close handlers to the current socket.
|
|
249
|
+
*
|
|
250
|
+
* @param socket The socket (plaintext or TLS) to wire up
|
|
251
|
+
* @param rejectConnect Optional connect() rejector, called if the socket errors before connecting
|
|
252
|
+
*/
|
|
253
|
+
wireSocket(socket, rejectConnect) {
|
|
254
|
+
socket.setEncoding("utf8");
|
|
255
|
+
socket.on("data", (data) => this.onData(data));
|
|
256
|
+
socket.on("error", (err) => {
|
|
257
|
+
var _a;
|
|
258
|
+
(_a = this.log) == null ? void 0 : _a.debug(`Socket error: ${err.message}`);
|
|
259
|
+
if (!this.connected && rejectConnect) {
|
|
260
|
+
rejectConnect(err);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
socket.on("close", () => {
|
|
264
|
+
var _a;
|
|
265
|
+
const wasConnected = this.connected;
|
|
266
|
+
this.connected = false;
|
|
267
|
+
this.rejectAll(new Error("Connection closed"));
|
|
268
|
+
if (wasConnected && !this.destroyed && this.persistent) {
|
|
269
|
+
(_a = this.log) == null ? void 0 : _a.warn(`Connection to NUT server ${this.host}:${this.port} lost`);
|
|
270
|
+
this.scheduleReconnect();
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
/** Upgrade the plaintext socket to TLS via STARTTLS. */
|
|
275
|
+
async startTls() {
|
|
276
|
+
const plain = this.socket;
|
|
277
|
+
await this.sendCommand("STARTTLS", false);
|
|
278
|
+
plain.removeAllListeners("data");
|
|
279
|
+
plain.removeAllListeners("error");
|
|
280
|
+
plain.removeAllListeners("close");
|
|
281
|
+
this.buffer = "";
|
|
282
|
+
const servername = net.isIP(this.host) === 0 ? this.host : void 0;
|
|
283
|
+
await new Promise((resolve, reject) => {
|
|
284
|
+
const tlsSocket = tls.connect(
|
|
285
|
+
{ socket: plain, rejectUnauthorized: this.tlsRejectUnauthorized, servername },
|
|
286
|
+
() => {
|
|
287
|
+
var _a;
|
|
288
|
+
this.tlsActive = true;
|
|
289
|
+
(_a = this.log) == null ? void 0 : _a.debug(`STARTTLS established with ${this.host}:${this.port}`);
|
|
290
|
+
resolve();
|
|
291
|
+
}
|
|
292
|
+
);
|
|
293
|
+
tlsSocket.once("error", (err) => reject(err));
|
|
294
|
+
this.socket = tlsSocket;
|
|
295
|
+
this.wireSocket(tlsSocket);
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
/** Synchronous teardown — destroys socket, no LOGOUT sent. */
|
|
299
|
+
destroy() {
|
|
300
|
+
this.destroyed = true;
|
|
301
|
+
if (this.reconnectTimer) {
|
|
302
|
+
this.clearTimer(this.reconnectTimer);
|
|
303
|
+
this.reconnectTimer = null;
|
|
304
|
+
}
|
|
305
|
+
this.cancelAll();
|
|
306
|
+
if (this.socket) {
|
|
307
|
+
this.socket.destroy();
|
|
308
|
+
this.socket = null;
|
|
309
|
+
}
|
|
310
|
+
this.connected = false;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Synchronous graceful teardown for onUnload — sends a best-effort LOGOUT and half-closes
|
|
314
|
+
* so the write flushes (vs. destroy()'s hard reset). Any server reply is ignored.
|
|
315
|
+
*/
|
|
316
|
+
shutdown() {
|
|
317
|
+
this.destroyed = true;
|
|
318
|
+
if (this.reconnectTimer) {
|
|
319
|
+
this.clearTimer(this.reconnectTimer);
|
|
320
|
+
this.reconnectTimer = null;
|
|
321
|
+
}
|
|
322
|
+
this.cancelAll();
|
|
323
|
+
const sock = this.socket;
|
|
324
|
+
this.socket = null;
|
|
325
|
+
this.connected = false;
|
|
326
|
+
if (sock) {
|
|
327
|
+
try {
|
|
328
|
+
sock.end("LOGOUT\n");
|
|
329
|
+
} catch {
|
|
330
|
+
sock.destroy();
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
/** Reject all pending and queued commands. */
|
|
335
|
+
cancelAll() {
|
|
336
|
+
this.rejectAll(new Error("Client cancelled"));
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Reject the active command AND every queued command (clearing their timers), then reset the
|
|
340
|
+
* multi-line parse state. Used by cancelAll (destroy/resync) and by the socket-close handler —
|
|
341
|
+
* a queued entry left behind with a live timer would fire later and tear down a
|
|
342
|
+
* subsequently-reconnected socket.
|
|
343
|
+
*
|
|
344
|
+
* @param err Rejection reason handed to every pending command
|
|
345
|
+
*/
|
|
346
|
+
rejectAll(err) {
|
|
347
|
+
if (this.active) {
|
|
348
|
+
this.clearTimer(this.active.timer);
|
|
349
|
+
this.active.reject(err);
|
|
350
|
+
this.active = null;
|
|
351
|
+
}
|
|
352
|
+
for (const entry of this.queue) {
|
|
353
|
+
this.clearTimer(entry.timer);
|
|
354
|
+
entry.reject(err);
|
|
355
|
+
}
|
|
356
|
+
this.queue = [];
|
|
357
|
+
this.multiLineBuffer = [];
|
|
358
|
+
this.multiLineExpectedEnd = "";
|
|
359
|
+
}
|
|
360
|
+
/** Whether the TCP connection is currently established. */
|
|
361
|
+
get isConnected() {
|
|
362
|
+
return this.connected;
|
|
363
|
+
}
|
|
364
|
+
/** Whether the connection is TLS-encrypted. */
|
|
365
|
+
get isTls() {
|
|
366
|
+
return this.tlsActive;
|
|
367
|
+
}
|
|
368
|
+
/** Discover all UPS devices on the NUT server. */
|
|
369
|
+
listUps() {
|
|
370
|
+
return this.parseList("LIST UPS", /^UPS\s+(\S+)\s+"((?:[^"\\]|\\.)*)"/, (m) => ({
|
|
371
|
+
name: m[1],
|
|
372
|
+
description: unescapeNut(m[2])
|
|
373
|
+
}));
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* List all variables for a UPS.
|
|
377
|
+
*
|
|
378
|
+
* @param ups UPS name
|
|
379
|
+
*/
|
|
380
|
+
listVar(ups) {
|
|
381
|
+
return this.parseList(`LIST VAR ${ups}`, /^VAR\s+\S+\s+(\S+)\s+"((?:[^"\\]|\\.)*)"/, (m) => ({
|
|
382
|
+
name: m[1],
|
|
383
|
+
value: unescapeNut(m[2])
|
|
384
|
+
}));
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* List writable variables for a UPS.
|
|
388
|
+
*
|
|
389
|
+
* @param ups UPS name
|
|
390
|
+
*/
|
|
391
|
+
listRw(ups) {
|
|
392
|
+
return this.parseList(`LIST RW ${ups}`, /^RW\s+\S+\s+(\S+)\s+"((?:[^"\\]|\\.)*)"/, (m) => ({
|
|
393
|
+
name: m[1],
|
|
394
|
+
value: unescapeNut(m[2])
|
|
395
|
+
}));
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* List available instant commands for a UPS.
|
|
399
|
+
*
|
|
400
|
+
* @param ups UPS name
|
|
401
|
+
*/
|
|
402
|
+
listCmd(ups) {
|
|
403
|
+
return this.parseList(`LIST CMD ${ups}`, /^CMD\s+\S+\s+(\S+)/, (m) => ({ name: m[1] }));
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* List enum values for a variable.
|
|
407
|
+
*
|
|
408
|
+
* @param ups UPS name
|
|
409
|
+
* @param varName Variable name
|
|
410
|
+
*/
|
|
411
|
+
listEnum(ups, varName) {
|
|
412
|
+
return this.parseList(
|
|
413
|
+
`LIST ENUM ${ups} ${varName}`,
|
|
414
|
+
/^ENUM\s+\S+\s+\S+\s+"((?:[^"\\]|\\.)*)"/,
|
|
415
|
+
(m) => unescapeNut(m[1])
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* List range constraints for a variable.
|
|
420
|
+
*
|
|
421
|
+
* @param ups UPS name
|
|
422
|
+
* @param varName Variable name
|
|
423
|
+
*/
|
|
424
|
+
listRange(ups, varName) {
|
|
425
|
+
return this.parseList(
|
|
426
|
+
`LIST RANGE ${ups} ${varName}`,
|
|
427
|
+
/^RANGE\s+\S+\s+\S+\s+"((?:[^"\\]|\\.)*)"\s+"((?:[^"\\]|\\.)*)"/,
|
|
428
|
+
(m) => ({ min: unescapeNut(m[1]), max: unescapeNut(m[2]) })
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Generic LIST/multi-line parser — runs a regex per response line and maps matches.
|
|
433
|
+
*
|
|
434
|
+
* @param command The LIST command to send
|
|
435
|
+
* @param lineRegex Regex applied to each response line
|
|
436
|
+
* @param map Maps a matched line to a result item
|
|
437
|
+
*/
|
|
438
|
+
async parseList(command, lineRegex, map) {
|
|
439
|
+
const lines = await this.sendCommand(command, true);
|
|
440
|
+
const result = [];
|
|
441
|
+
for (const line of lines) {
|
|
442
|
+
const match = lineRegex.exec(line);
|
|
443
|
+
if (match) {
|
|
444
|
+
result.push(map(match));
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return result;
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Get a single variable value.
|
|
451
|
+
*
|
|
452
|
+
* @param ups UPS name
|
|
453
|
+
* @param varName Variable name
|
|
454
|
+
*/
|
|
455
|
+
async getVar(ups, varName) {
|
|
456
|
+
const lines = await this.sendCommand(`GET VAR ${ups} ${varName}`, false);
|
|
457
|
+
const match = /^VAR\s+\S+\s+\S+\s+"((?:[^"\\]|\\.)*)"/.exec(lines[0]);
|
|
458
|
+
if (!match) {
|
|
459
|
+
throw new Error(`Unexpected GET VAR response: ${lines[0]}`);
|
|
460
|
+
}
|
|
461
|
+
return unescapeNut(match[1]);
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Set a writable variable.
|
|
465
|
+
*
|
|
466
|
+
* @param ups UPS name
|
|
467
|
+
* @param varName Variable name
|
|
468
|
+
* @param value New value
|
|
469
|
+
*/
|
|
470
|
+
async setVar(ups, varName, value) {
|
|
471
|
+
await this.sendCommand(`SET VAR ${ups} ${varName} "${escapeNut(value)}"`, false);
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Execute an instant command.
|
|
475
|
+
*
|
|
476
|
+
* @param ups UPS name
|
|
477
|
+
* @param cmd Command name
|
|
478
|
+
*/
|
|
479
|
+
async instCmd(ups, cmd) {
|
|
480
|
+
await this.sendCommand(`INSTCMD ${ups} ${cmd}`, false);
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Authenticate with the NUT server.
|
|
484
|
+
*
|
|
485
|
+
* @param username NUT username
|
|
486
|
+
* @param password NUT password
|
|
487
|
+
*/
|
|
488
|
+
async authenticate(username, password) {
|
|
489
|
+
await this.sendCommand(`USERNAME ${username}`, false);
|
|
490
|
+
await this.sendCommand(`PASSWORD ${password}`, false);
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Register as monitoring client for a UPS.
|
|
494
|
+
*
|
|
495
|
+
* @param ups UPS name
|
|
496
|
+
*/
|
|
497
|
+
async login(ups) {
|
|
498
|
+
await this.sendCommand(`LOGIN ${ups}`, false);
|
|
499
|
+
}
|
|
500
|
+
/** Best-effort LOGOUT (graceful lifecycle; ignores errors). */
|
|
501
|
+
async logout() {
|
|
502
|
+
try {
|
|
503
|
+
await this.sendCommand("LOGOUT", false);
|
|
504
|
+
} catch {
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
sendCommand(command, multiLine) {
|
|
508
|
+
return new Promise((resolve, reject) => {
|
|
509
|
+
if (!this.connected || !this.socket) {
|
|
510
|
+
reject(new Error("Not connected"));
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
const entry = { command, resolve, reject, timer: null, multiLine };
|
|
514
|
+
this.queue.push(entry);
|
|
515
|
+
if (!this.active) {
|
|
516
|
+
this.processQueue();
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Drop the desynced connection and reconnect on a clean stream (resync).
|
|
522
|
+
*
|
|
523
|
+
* @param command The command that timed out
|
|
524
|
+
*/
|
|
525
|
+
resyncAfterTimeout(command) {
|
|
526
|
+
var _a, _b;
|
|
527
|
+
if (((_a = this.active) == null ? void 0 : _a.command) === command) {
|
|
528
|
+
this.active = null;
|
|
529
|
+
}
|
|
530
|
+
this.connected = false;
|
|
531
|
+
this.cancelAll();
|
|
532
|
+
(_b = this.socket) == null ? void 0 : _b.destroy();
|
|
533
|
+
this.scheduleReconnect();
|
|
534
|
+
}
|
|
535
|
+
processQueue() {
|
|
536
|
+
var _a, _b;
|
|
537
|
+
if (this.active || this.queue.length === 0) {
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
const entry = this.queue.shift();
|
|
541
|
+
this.active = entry;
|
|
542
|
+
entry.timer = this.setTimer(() => {
|
|
543
|
+
entry.reject(new NutTimeoutError(entry.command));
|
|
544
|
+
this.resyncAfterTimeout(entry.command);
|
|
545
|
+
}, this.commandTimeout);
|
|
546
|
+
(_a = this.log) == null ? void 0 : _a.debug(`>> ${entry.command}`);
|
|
547
|
+
if (entry.multiLine) {
|
|
548
|
+
this.multiLineBuffer = [];
|
|
549
|
+
const query = entry.command.replace(/^LIST\s+/, "");
|
|
550
|
+
this.multiLineExpectedEnd = `END LIST ${query}`;
|
|
551
|
+
}
|
|
552
|
+
(_b = this.socket) == null ? void 0 : _b.write(`${entry.command}
|
|
553
|
+
`);
|
|
554
|
+
}
|
|
555
|
+
onData(data) {
|
|
556
|
+
this.buffer += data;
|
|
557
|
+
const lines = this.buffer.split("\n");
|
|
558
|
+
this.buffer = lines.pop();
|
|
559
|
+
for (const line of lines) {
|
|
560
|
+
this.processLine(line.endsWith("\r") ? line.slice(0, -1) : line);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
processLine(line) {
|
|
564
|
+
var _a;
|
|
565
|
+
if (!this.active) {
|
|
566
|
+
(_a = this.log) == null ? void 0 : _a.debug(`<< (no active command) ${line}`);
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
if (line.startsWith("ERR ")) {
|
|
570
|
+
const code = line.slice(4).trim();
|
|
571
|
+
this.clearTimer(this.active.timer);
|
|
572
|
+
const entry2 = this.active;
|
|
573
|
+
this.active = null;
|
|
574
|
+
this.multiLineBuffer = [];
|
|
575
|
+
this.multiLineExpectedEnd = "";
|
|
576
|
+
entry2.reject(new NutError(code));
|
|
577
|
+
this.processQueue();
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
if (this.active.multiLine) {
|
|
581
|
+
if (line.startsWith("BEGIN LIST ")) {
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
if (line === this.multiLineExpectedEnd) {
|
|
585
|
+
this.clearTimer(this.active.timer);
|
|
586
|
+
const entry2 = this.active;
|
|
587
|
+
const result = [...this.multiLineBuffer];
|
|
588
|
+
this.active = null;
|
|
589
|
+
this.multiLineBuffer = [];
|
|
590
|
+
this.multiLineExpectedEnd = "";
|
|
591
|
+
entry2.resolve(result);
|
|
592
|
+
this.processQueue();
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
this.multiLineBuffer.push(line);
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
this.clearTimer(this.active.timer);
|
|
599
|
+
const entry = this.active;
|
|
600
|
+
this.active = null;
|
|
601
|
+
entry.resolve([line]);
|
|
602
|
+
this.processQueue();
|
|
603
|
+
}
|
|
604
|
+
scheduleReconnect() {
|
|
605
|
+
var _a;
|
|
606
|
+
if (this.destroyed || this.reconnectTimer || !this.persistent) {
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
this.reconnectAttempt += 1;
|
|
610
|
+
const delay = (0, import_coerce.computeReconnectDelay)(this.reconnectAttempt, RECONNECT_BASE_MS, RECONNECT_MAX_MS);
|
|
611
|
+
(_a = this.log) == null ? void 0 : _a.debug(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempt})`);
|
|
612
|
+
this.reconnectTimer = this.setTimer(() => {
|
|
613
|
+
var _a2;
|
|
614
|
+
this.reconnectTimer = null;
|
|
615
|
+
(_a2 = this.log) == null ? void 0 : _a2.debug(`Attempting reconnect to ${this.host}:${this.port}`);
|
|
616
|
+
this.attemptConnect();
|
|
617
|
+
}, delay);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
function unescapeNut(s) {
|
|
621
|
+
return s.replace(/\\"/g, '"').replace(/\\\\/g, "\\");
|
|
622
|
+
}
|
|
623
|
+
function escapeNut(s) {
|
|
624
|
+
return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
625
|
+
}
|
|
626
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
627
|
+
0 && (module.exports = {
|
|
628
|
+
NutClient,
|
|
629
|
+
NutError,
|
|
630
|
+
NutTimeoutError,
|
|
631
|
+
isTlsConfigError
|
|
632
|
+
});
|
|
633
|
+
//# sourceMappingURL=nut-client.js.map
|