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/nut-client.ts"],
|
|
4
|
+
"sourcesContent": ["import * as net from \"node:net\";\nimport * as tls from \"node:tls\";\nimport { computeReconnectDelay } from \"./coerce\";\nimport type { NutClientOptions, NutCommand, NutLogger, NutRange, NutVariable, UpsInfo } from \"./types\";\nimport { NUT_DEFAULT_COMMAND_TIMEOUT } from \"./types\";\n\n/** Reconnect backoff bounds (exponential: 1s, 2s, 4s \u2026 capped at 60s). */\nconst RECONNECT_BASE_MS = 1000;\nconst RECONNECT_MAX_MS = 60000;\n\n/** NUT protocol error with error code (see types.ts:NUT_ERRORS for the documented set). */\nexport class NutError extends Error {\n /**\n * @param code NUT error code (a server may send codes outside the documented set, so this is `string`)\n * @param message Optional custom message\n */\n constructor(\n public readonly code: string,\n message?: string,\n ) {\n super(message ?? `NUT error: ${code}`);\n this.name = \"NutError\";\n }\n}\n\n/** NUT command timeout error. */\nexport class NutTimeoutError extends Error {\n /**\n * @param command The command that timed out\n */\n constructor(public readonly command: string) {\n super(`NUT command timed out: ${command}`);\n this.name = \"NutTimeoutError\";\n }\n}\n\n// Errors from connect()/STARTTLS that signal a TLS *configuration* problem (server offers\n// no TLS, or its certificate was rejected) rather than a transient network failure.\nconst TLS_FATAL_ERROR_CODES = new Set<string>([\n // NUT-level: the server cannot/does not start TLS\n \"FEATURE-NOT-CONFIGURED\",\n \"FEATURE-NOT-SUPPORTED\",\n \"ALREADY-SSL-MODE\",\n // Node certificate-verification failures (only reachable with tlsRejectUnauthorized=true)\n \"DEPTH_ZERO_SELF_SIGNED_CERT\",\n \"SELF_SIGNED_CERT_IN_CHAIN\",\n \"UNABLE_TO_VERIFY_LEAF_SIGNATURE\",\n \"CERT_HAS_EXPIRED\",\n \"ERR_TLS_CERT_ALTNAME_INVALID\",\n]);\n\n/**\n * Whether a failed TLS connect is a configuration problem (won't fix itself \u2192 go yellow,\n * no retry) as opposed to a transient network error (ECONNREFUSED/ETIMEDOUT \u2192 retry).\n *\n * @param err Caught value from a failed connect()/STARTTLS\n */\nexport function isTlsConfigError(err: unknown): boolean {\n if (err instanceof NutError) {\n return TLS_FATAL_ERROR_CODES.has(err.code);\n }\n const code = (err as { code?: unknown } | null)?.code;\n if (typeof code !== \"string\") {\n return false;\n }\n // Explicit set above, plus the whole OpenSSL/TLS-layer code family.\n return TLS_FATAL_ERROR_CODES.has(code) || code.startsWith(\"ERR_TLS_\") || code.startsWith(\"ERR_SSL_\");\n}\n\ninterface QueueEntry {\n command: string;\n resolve: (lines: string[]) => void;\n reject: (err: Error) => void;\n timer: unknown;\n multiLine: boolean;\n}\n\n/** Persistent TCP client for the NUT protocol (port 3493), with optional STARTTLS. */\nexport class NutClient {\n private socket: net.Socket | tls.TLSSocket | null = null;\n private buffer = \"\";\n private queue: QueueEntry[] = [];\n private active: QueueEntry | null = null;\n private multiLineBuffer: string[] = [];\n private multiLineExpectedEnd = \"\";\n private connected = false;\n private destroyed = false;\n private tlsActive = false;\n\n private readonly host: string;\n private readonly port: number;\n private readonly localAddress?: string;\n private readonly commandTimeout: number;\n private readonly useTls: boolean;\n private readonly tlsRejectUnauthorized: boolean;\n private readonly log?: NutLogger;\n // Injected managed timers (adapter.setTimeout/clearTimeout in production \u2192 auto-cleared on\n // unload; global timers as fallback for standalone use/tests).\n private readonly setTimer: (cb: () => void, ms: number) => unknown;\n private readonly clearTimer: (handle: unknown) => void;\n\n private reconnectAttempt = 0;\n private reconnectTimer: unknown = null;\n // Persistent mode (set by start()) owns the unified retry loop: it retries the initial\n // connect, reconnects on drops, and stops yellow on a fatal TLS-config error. A plain\n // connect() (e.g. the connection test) leaves this false \u2014 one-shot, never retries.\n private persistent = false;\n private onConnectHandler: (() => void) | null = null;\n private onFatalHandler: ((err: unknown) => void) | null = null;\n\n /**\n * @param host NUT server hostname or IP\n * @param port NUT server port\n * @param options Connection options\n */\n constructor(host: string, port: number, options?: NutClientOptions) {\n this.host = host;\n this.port = port;\n this.localAddress = options?.localAddress;\n this.commandTimeout = options?.commandTimeout ?? NUT_DEFAULT_COMMAND_TIMEOUT;\n this.useTls = options?.useTls ?? false;\n this.tlsRejectUnauthorized = options?.tlsRejectUnauthorized ?? false;\n this.log = options?.logger;\n this.setTimer = options?.setTimer ?? ((cb, ms) => globalThis.setTimeout(cb, ms));\n this.clearTimer = options?.clearTimer ?? (h => globalThis.clearTimeout(h as ReturnType<typeof setTimeout>));\n }\n\n /**\n * Register a callback invoked after every successful (re)connection in persistent mode.\n * Runs the post-connect setup (discover/auth/poll); must be idempotent.\n *\n * @param handler Connect callback\n */\n setOnConnect(handler: () => void): void {\n this.onConnectHandler = handler;\n }\n\n /**\n * Register a callback invoked when the persistent connection fails fatally (TLS\n * misconfiguration) \u2014 the retry loop stops and the caller should go yellow.\n *\n * @param handler Fatal-error callback\n */\n setOnFatal(handler: (err: unknown) => void): void {\n this.onFatalHandler = handler;\n }\n\n /**\n * Start the persistent runtime connection: connect now and keep retrying with exponential\n * backoff, reconnecting automatically on later drops. A fatal TLS-config error stops the\n * loop (onFatal). Use connect() directly for a one-shot (e.g. the connection test).\n */\n start(): void {\n this.persistent = true;\n this.reconnectAttempt = 0;\n this.attemptConnect();\n }\n\n /** One iteration of the persistent loop: connect, then fire onConnect or handle the failure. */\n private attemptConnect(): void {\n if (this.destroyed) {\n return;\n }\n this.connect()\n .then(() => {\n this.reconnectAttempt = 0;\n this.onConnectHandler?.();\n })\n .catch((err: unknown) => this.handleConnectFailure(err));\n }\n\n /**\n * Decide a failed persistent connect: a TLS-config error stops the loop (onFatal, yellow);\n * any other error schedules a backed-off retry.\n *\n * @param err The connect/STARTTLS failure\n */\n private handleConnectFailure(err: unknown): void {\n if (this.destroyed) {\n return;\n }\n // Tear down the failed socket so the next attempt starts clean. Clearing `connected`\n // first means the close handler sees wasConnected=false and won't double-schedule.\n this.connected = false;\n const sock = this.socket;\n this.socket = null;\n sock?.destroy();\n\n const msg = err instanceof Error ? err.message : String(err);\n if (this.useTls && isTlsConfigError(err)) {\n this.log?.warn(`TLS connection to NUT server ${this.host}:${this.port} failed \u2014 not retrying: ${msg}`);\n this.onFatalHandler?.(err);\n return;\n }\n this.log?.debug(`Connect attempt failed: ${msg}`);\n this.scheduleReconnect();\n }\n\n /** Establish TCP connection (and STARTTLS upgrade if configured) to the NUT server. */\n connect(): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n if (this.destroyed) {\n reject(new Error(\"Client has been destroyed\"));\n return;\n }\n\n // Bound the connect phase (TCP connect + optional STARTTLS handshake). net/tls have no\n // built-in deadline here, and the per-command timeout only applies once connected, so a\n // blackholed SYN or a stalled TLS handshake would otherwise hang for the OS timeout\n // (~1-2 min) \u2014 freezing reconnect attempts and the admin connection test.\n let settled = false;\n const deadline = this.setTimer(() => {\n if (settled) {\n return;\n }\n settled = true;\n const sock = this.socket;\n this.socket = null;\n this.connected = false;\n sock?.destroy();\n reject(new Error(`Connect to NUT server ${this.host}:${this.port} timed out`));\n }, this.commandTimeout);\n const settle = (err?: Error): void => {\n if (settled) {\n return;\n }\n settled = true;\n this.clearTimer(deadline);\n if (err) {\n reject(err);\n } else {\n resolve();\n }\n };\n\n const opts: net.NetConnectOpts = { host: this.host, port: this.port };\n if (this.localAddress) {\n opts.localAddress = this.localAddress;\n }\n\n const socket = net.createConnection(opts, () => {\n this.connected = true;\n this.tlsActive = false;\n this.buffer = \"\";\n this.log?.debug(`Connected to NUT server ${this.host}:${this.port}`);\n if (this.useTls) {\n this.startTls()\n .then(() => settle())\n .catch(settle);\n } else {\n settle();\n }\n });\n this.socket = socket;\n // Detect a dead peer (NAT/firewall idle-drop leaves a half-open socket otherwise).\n socket.setKeepAlive(true, 30000);\n this.wireSocket(socket, settle);\n });\n }\n\n /**\n * Attach data/error/close handlers to the current socket.\n *\n * @param socket The socket (plaintext or TLS) to wire up\n * @param rejectConnect Optional connect() rejector, called if the socket errors before connecting\n */\n private wireSocket(socket: net.Socket | tls.TLSSocket, rejectConnect?: (err: Error) => void): void {\n socket.setEncoding(\"utf8\");\n socket.on(\"data\", (data: string) => this.onData(data));\n socket.on(\"error\", (err: Error) => {\n this.log?.debug(`Socket error: ${err.message}`);\n if (!this.connected && rejectConnect) {\n rejectConnect(err);\n }\n });\n socket.on(\"close\", () => {\n const wasConnected = this.connected;\n this.connected = false;\n // Drain the WHOLE queue, not just the active command: a queued entry left behind would keep\n // a live command timer that fires later and tears down a subsequently-reconnected socket.\n this.rejectAll(new Error(\"Connection closed\"));\n // Only the persistent runtime connection auto-reconnects on a drop; a one-shot\n // connect() (e.g. the connection test) must not.\n if (wasConnected && !this.destroyed && this.persistent) {\n this.log?.warn(`Connection to NUT server ${this.host}:${this.port} lost`);\n this.scheduleReconnect();\n }\n });\n }\n\n /** Upgrade the plaintext socket to TLS via STARTTLS. */\n private async startTls(): Promise<void> {\n const plain = this.socket as net.Socket;\n await this.sendCommand(\"STARTTLS\", false); // throws NutError on FEATURE-NOT-CONFIGURED/-SUPPORTED\n // After \"OK STARTTLS\" the server begins TLS immediately \u2014 nothing plaintext may follow.\n plain.removeAllListeners(\"data\");\n plain.removeAllListeners(\"error\");\n plain.removeAllListeners(\"close\");\n this.buffer = \"\";\n\n // SNI must be a hostname \u2014 RFC 6066 forbids an IP literal (Node warns + ignores it).\n const servername = net.isIP(this.host) === 0 ? this.host : undefined;\n await new Promise<void>((resolve, reject) => {\n const tlsSocket = tls.connect(\n { socket: plain, rejectUnauthorized: this.tlsRejectUnauthorized, servername },\n () => {\n this.tlsActive = true;\n this.log?.debug(`STARTTLS established with ${this.host}:${this.port}`);\n resolve();\n },\n );\n tlsSocket.once(\"error\", (err: Error) => reject(err));\n this.socket = tlsSocket;\n this.wireSocket(tlsSocket);\n });\n }\n\n /** Synchronous teardown \u2014 destroys socket, no LOGOUT sent. */\n destroy(): void {\n this.destroyed = true;\n if (this.reconnectTimer) {\n this.clearTimer(this.reconnectTimer);\n this.reconnectTimer = null;\n }\n this.cancelAll();\n if (this.socket) {\n this.socket.destroy();\n this.socket = null;\n }\n this.connected = false;\n }\n\n /**\n * Synchronous graceful teardown for onUnload \u2014 sends a best-effort LOGOUT and half-closes\n * so the write flushes (vs. destroy()'s hard reset). Any server reply is ignored.\n */\n shutdown(): void {\n this.destroyed = true;\n if (this.reconnectTimer) {\n this.clearTimer(this.reconnectTimer);\n this.reconnectTimer = null;\n }\n this.cancelAll();\n const sock = this.socket;\n this.socket = null;\n this.connected = false;\n if (sock) {\n try {\n sock.end(\"LOGOUT\\n\");\n } catch {\n sock.destroy();\n }\n }\n }\n\n /** Reject all pending and queued commands. */\n cancelAll(): void {\n this.rejectAll(new Error(\"Client cancelled\"));\n }\n\n /**\n * Reject the active command AND every queued command (clearing their timers), then reset the\n * multi-line parse state. Used by cancelAll (destroy/resync) and by the socket-close handler \u2014\n * a queued entry left behind with a live timer would fire later and tear down a\n * subsequently-reconnected socket.\n *\n * @param err Rejection reason handed to every pending command\n */\n private rejectAll(err: Error): void {\n if (this.active) {\n this.clearTimer(this.active.timer);\n this.active.reject(err);\n this.active = null;\n }\n for (const entry of this.queue) {\n this.clearTimer(entry.timer);\n entry.reject(err);\n }\n this.queue = [];\n this.multiLineBuffer = [];\n this.multiLineExpectedEnd = \"\";\n }\n\n /** Whether the TCP connection is currently established. */\n get isConnected(): boolean {\n return this.connected;\n }\n\n /** Whether the connection is TLS-encrypted. */\n get isTls(): boolean {\n return this.tlsActive;\n }\n\n /** Discover all UPS devices on the NUT server. */\n listUps(): Promise<UpsInfo[]> {\n return this.parseList(\"LIST UPS\", /^UPS\\s+(\\S+)\\s+\"((?:[^\"\\\\]|\\\\.)*)\"/, m => ({\n name: m[1],\n description: unescapeNut(m[2]),\n }));\n }\n\n /**\n * List all variables for a UPS.\n *\n * @param ups UPS name\n */\n listVar(ups: string): Promise<NutVariable[]> {\n return this.parseList(`LIST VAR ${ups}`, /^VAR\\s+\\S+\\s+(\\S+)\\s+\"((?:[^\"\\\\]|\\\\.)*)\"/, m => ({\n name: m[1],\n value: unescapeNut(m[2]),\n }));\n }\n\n /**\n * List writable variables for a UPS.\n *\n * @param ups UPS name\n */\n listRw(ups: string): Promise<NutVariable[]> {\n return this.parseList(`LIST RW ${ups}`, /^RW\\s+\\S+\\s+(\\S+)\\s+\"((?:[^\"\\\\]|\\\\.)*)\"/, m => ({\n name: m[1],\n value: unescapeNut(m[2]),\n }));\n }\n\n /**\n * List available instant commands for a UPS.\n *\n * @param ups UPS name\n */\n listCmd(ups: string): Promise<NutCommand[]> {\n return this.parseList(`LIST CMD ${ups}`, /^CMD\\s+\\S+\\s+(\\S+)/, m => ({ name: m[1] }));\n }\n\n /**\n * List enum values for a variable.\n *\n * @param ups UPS name\n * @param varName Variable name\n */\n listEnum(ups: string, varName: string): Promise<string[]> {\n return this.parseList(`LIST ENUM ${ups} ${varName}`, /^ENUM\\s+\\S+\\s+\\S+\\s+\"((?:[^\"\\\\]|\\\\.)*)\"/, m =>\n unescapeNut(m[1]),\n );\n }\n\n /**\n * List range constraints for a variable.\n *\n * @param ups UPS name\n * @param varName Variable name\n */\n listRange(ups: string, varName: string): Promise<NutRange[]> {\n return this.parseList(\n `LIST RANGE ${ups} ${varName}`,\n /^RANGE\\s+\\S+\\s+\\S+\\s+\"((?:[^\"\\\\]|\\\\.)*)\"\\s+\"((?:[^\"\\\\]|\\\\.)*)\"/,\n m => ({ min: unescapeNut(m[1]), max: unescapeNut(m[2]) }),\n );\n }\n\n /**\n * Generic LIST/multi-line parser \u2014 runs a regex per response line and maps matches.\n *\n * @param command The LIST command to send\n * @param lineRegex Regex applied to each response line\n * @param map Maps a matched line to a result item\n */\n private async parseList<T>(command: string, lineRegex: RegExp, map: (m: RegExpExecArray) => T): Promise<T[]> {\n const lines = await this.sendCommand(command, true);\n const result: T[] = [];\n for (const line of lines) {\n const match = lineRegex.exec(line);\n if (match) {\n result.push(map(match));\n }\n }\n return result;\n }\n\n /**\n * Get a single variable value.\n *\n * @param ups UPS name\n * @param varName Variable name\n */\n async getVar(ups: string, varName: string): Promise<string> {\n const lines = await this.sendCommand(`GET VAR ${ups} ${varName}`, false);\n const match = /^VAR\\s+\\S+\\s+\\S+\\s+\"((?:[^\"\\\\]|\\\\.)*)\"/.exec(lines[0]);\n if (!match) {\n throw new Error(`Unexpected GET VAR response: ${lines[0]}`);\n }\n return unescapeNut(match[1]);\n }\n\n /**\n * Set a writable variable.\n *\n * @param ups UPS name\n * @param varName Variable name\n * @param value New value\n */\n async setVar(ups: string, varName: string, value: string): Promise<void> {\n await this.sendCommand(`SET VAR ${ups} ${varName} \"${escapeNut(value)}\"`, false);\n }\n\n /**\n * Execute an instant command.\n *\n * @param ups UPS name\n * @param cmd Command name\n */\n async instCmd(ups: string, cmd: string): Promise<void> {\n await this.sendCommand(`INSTCMD ${ups} ${cmd}`, false);\n }\n\n /**\n * Authenticate with the NUT server.\n *\n * @param username NUT username\n * @param password NUT password\n */\n async authenticate(username: string, password: string): Promise<void> {\n await this.sendCommand(`USERNAME ${username}`, false);\n await this.sendCommand(`PASSWORD ${password}`, false);\n }\n\n /**\n * Register as monitoring client for a UPS.\n *\n * @param ups UPS name\n */\n async login(ups: string): Promise<void> {\n await this.sendCommand(`LOGIN ${ups}`, false);\n }\n\n /** Best-effort LOGOUT (graceful lifecycle; ignores errors). */\n async logout(): Promise<void> {\n try {\n await this.sendCommand(\"LOGOUT\", false);\n } catch {\n // Ignore \u2014 we are shutting down anyway.\n }\n }\n\n private sendCommand(command: string, multiLine: boolean): Promise<string[]> {\n return new Promise<string[]>((resolve, reject) => {\n if (!this.connected || !this.socket) {\n reject(new Error(\"Not connected\"));\n return;\n }\n\n // The command timeout is armed when the command becomes ACTIVE (processQueue), not here:\n // otherwise the time a command waits in the queue behind a slower one eats into its budget,\n // and a queued command can spuriously time out \u2014 dropping an otherwise-healthy connection.\n const entry: QueueEntry = { command, resolve, reject, timer: null, multiLine };\n this.queue.push(entry);\n\n if (!this.active) {\n this.processQueue();\n }\n });\n }\n\n /**\n * Drop the desynced connection and reconnect on a clean stream (resync).\n *\n * @param command The command that timed out\n */\n private resyncAfterTimeout(command: string): void {\n // Only the active command carries a live timer (armed in processQueue), so a fired timeout\n // always refers to the active command \u2014 already rejected in its timer callback. Null it so\n // the cancelAll() below doesn't reject it a second time.\n if (this.active?.command === command) {\n this.active = null;\n }\n // Drop the socket \u2014 reflect it synchronously so callers see the desync immediately and the\n // async 'close' handler sees wasConnected=false (no double-schedule). cancelAll() drains the\n // rest of the queue; scheduleReconnect() brings the connection back on a clean stream.\n this.connected = false;\n this.cancelAll();\n this.socket?.destroy();\n this.scheduleReconnect();\n }\n\n private processQueue(): void {\n if (this.active || this.queue.length === 0) {\n return;\n }\n\n const entry = this.queue.shift()!;\n this.active = entry;\n\n // Arm the command timeout now that the command is active (see sendCommand): the full budget\n // applies to active time only, never to queue-wait. A fired timer therefore always refers to\n // the active command.\n entry.timer = this.setTimer(() => {\n entry.reject(new NutTimeoutError(entry.command));\n // A timed-out command desyncs the stream (NUT has no request IDs \u2014 a late reply would be\n // mis-attributed to the next command). Drop the connection and reconnect to resync.\n this.resyncAfterTimeout(entry.command);\n }, this.commandTimeout);\n\n this.log?.debug(`>> ${entry.command}`);\n\n if (entry.multiLine) {\n this.multiLineBuffer = [];\n const query = entry.command.replace(/^LIST\\s+/, \"\");\n this.multiLineExpectedEnd = `END LIST ${query}`;\n }\n\n this.socket?.write(`${entry.command}\\n`);\n }\n\n private onData(data: string): void {\n this.buffer += data;\n const lines = this.buffer.split(\"\\n\");\n this.buffer = lines.pop()!;\n\n for (const line of lines) {\n // NUT uses LF; tolerate CRLF from non-conformant servers.\n this.processLine(line.endsWith(\"\\r\") ? line.slice(0, -1) : line);\n }\n }\n\n private processLine(line: string): void {\n if (!this.active) {\n this.log?.debug(`<< (no active command) ${line}`);\n return;\n }\n\n if (line.startsWith(\"ERR \")) {\n const code = line.slice(4).trim();\n this.clearTimer(this.active.timer);\n const entry = this.active;\n this.active = null;\n this.multiLineBuffer = [];\n this.multiLineExpectedEnd = \"\";\n entry.reject(new NutError(code));\n this.processQueue();\n return;\n }\n\n if (this.active.multiLine) {\n if (line.startsWith(\"BEGIN LIST \")) {\n return;\n }\n if (line === this.multiLineExpectedEnd) {\n this.clearTimer(this.active.timer);\n const entry = this.active;\n const result = [...this.multiLineBuffer];\n this.active = null;\n this.multiLineBuffer = [];\n this.multiLineExpectedEnd = \"\";\n entry.resolve(result);\n this.processQueue();\n return;\n }\n this.multiLineBuffer.push(line);\n return;\n }\n\n // Single-line response\n this.clearTimer(this.active.timer);\n const entry = this.active;\n this.active = null;\n entry.resolve([line]);\n this.processQueue();\n }\n\n private scheduleReconnect(): void {\n // Only the persistent runtime loop reconnects; guard against double-scheduling.\n if (this.destroyed || this.reconnectTimer || !this.persistent) {\n return;\n }\n\n this.reconnectAttempt += 1;\n const delay = computeReconnectDelay(this.reconnectAttempt, RECONNECT_BASE_MS, RECONNECT_MAX_MS);\n this.log?.debug(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempt})`);\n\n this.reconnectTimer = this.setTimer(() => {\n this.reconnectTimer = null;\n this.log?.debug(`Attempting reconnect to ${this.host}:${this.port}`);\n this.attemptConnect();\n }, delay);\n }\n}\n\nfunction unescapeNut(s: string): string {\n return s.replace(/\\\\\"/g, '\"').replace(/\\\\\\\\/g, \"\\\\\");\n}\n\nfunction escapeNut(s: string): string {\n return s.replace(/\\\\/g, \"\\\\\\\\\").replace(/\"/g, '\\\\\"');\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["entry", "_a"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var state_manager_exports = {};
|
|
20
|
+
__export(state_manager_exports, {
|
|
21
|
+
StateManager: () => StateManager,
|
|
22
|
+
nutVarToReadableName: () => nutVarToReadableName,
|
|
23
|
+
nutVarToStateId: () => nutVarToStateId
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(state_manager_exports);
|
|
26
|
+
var import_i18n = require("./i18n");
|
|
27
|
+
var import_status_parser = require("./status-parser");
|
|
28
|
+
var import_type_detector = require("./type-detector");
|
|
29
|
+
const CHANNEL_I18N = {
|
|
30
|
+
battery: "channelBattery",
|
|
31
|
+
device: "channelDevice",
|
|
32
|
+
driver: "channelDriver",
|
|
33
|
+
input: "channelInput",
|
|
34
|
+
output: "channelOutput",
|
|
35
|
+
ups: "channelUps",
|
|
36
|
+
outlet: "channelOutlet",
|
|
37
|
+
ambient: "channelAmbient",
|
|
38
|
+
status: "channelStatus",
|
|
39
|
+
commands: "channelCommands",
|
|
40
|
+
info: "channelUpsInfo"
|
|
41
|
+
};
|
|
42
|
+
const COMMAND_I18N = {
|
|
43
|
+
"beeper.disable": "cmdBeeperDisable",
|
|
44
|
+
"beeper.enable": "cmdBeeperEnable",
|
|
45
|
+
"beeper.mute": "cmdBeeperMute",
|
|
46
|
+
"beeper.toggle": "cmdBeeperToggle",
|
|
47
|
+
"load.off": "cmdLoadOff",
|
|
48
|
+
"load.on": "cmdLoadOn",
|
|
49
|
+
"load.off.delay": "cmdLoadOffDelay",
|
|
50
|
+
"load.on.delay": "cmdLoadOnDelay",
|
|
51
|
+
"shutdown.default": "cmdShutdownDefault",
|
|
52
|
+
"shutdown.return": "cmdShutdownReturn",
|
|
53
|
+
"shutdown.stayoff": "cmdShutdownStayoff",
|
|
54
|
+
"shutdown.stop": "cmdShutdownStop",
|
|
55
|
+
"shutdown.reboot": "cmdShutdownReboot",
|
|
56
|
+
"shutdown.reboot.graceful": "cmdShutdownRebootGraceful",
|
|
57
|
+
"test.battery.start": "cmdTestBatteryStart",
|
|
58
|
+
"test.battery.start.quick": "cmdTestBatteryStartQuick",
|
|
59
|
+
"test.battery.start.low": "cmdTestBatteryStartLow",
|
|
60
|
+
"test.battery.start.deep": "cmdTestBatteryStartDeep",
|
|
61
|
+
"test.battery.stop": "cmdTestBatteryStop",
|
|
62
|
+
"test.panel.start": "cmdTestPanelStart",
|
|
63
|
+
"test.panel.stop": "cmdTestPanelStop",
|
|
64
|
+
"test.failure.start": "cmdTestFailureStart",
|
|
65
|
+
"test.failure.stop": "cmdTestFailureStop",
|
|
66
|
+
"test.system.start": "cmdTestSystemStart",
|
|
67
|
+
"calibrate.start": "cmdCalibrateStart",
|
|
68
|
+
"calibrate.stop": "cmdCalibrateStop",
|
|
69
|
+
"bypass.start": "cmdBypassStart",
|
|
70
|
+
"bypass.stop": "cmdBypassStop",
|
|
71
|
+
"reset.input.minmax": "cmdResetInputMinmax",
|
|
72
|
+
"reset.watchdog": "cmdResetWatchdog"
|
|
73
|
+
};
|
|
74
|
+
const TRANSLATED_VARIABLES = /* @__PURE__ */ new Set([
|
|
75
|
+
"battery.charge",
|
|
76
|
+
"battery.charge.low",
|
|
77
|
+
"battery.runtime",
|
|
78
|
+
"battery.type",
|
|
79
|
+
"battery.voltage",
|
|
80
|
+
"battery.temperature",
|
|
81
|
+
"battery.capacity",
|
|
82
|
+
"battery.charger.status",
|
|
83
|
+
"device.mfr",
|
|
84
|
+
"device.model",
|
|
85
|
+
"device.serial",
|
|
86
|
+
"device.type",
|
|
87
|
+
"driver.name",
|
|
88
|
+
"driver.version",
|
|
89
|
+
"driver.version.data",
|
|
90
|
+
"driver.version.internal",
|
|
91
|
+
"driver.parameter.pollfreq",
|
|
92
|
+
"driver.parameter.pollinterval",
|
|
93
|
+
"driver.parameter.port",
|
|
94
|
+
"driver.parameter.synchronous",
|
|
95
|
+
"driver.flag.ignorelb",
|
|
96
|
+
"driver.version.usb",
|
|
97
|
+
"input.voltage",
|
|
98
|
+
"input.frequency",
|
|
99
|
+
"input.transfer.high",
|
|
100
|
+
"input.transfer.low",
|
|
101
|
+
"input.voltage.extended",
|
|
102
|
+
"output.voltage",
|
|
103
|
+
"output.frequency",
|
|
104
|
+
"output.voltage.nominal",
|
|
105
|
+
"output.frequency.nominal",
|
|
106
|
+
"output.current",
|
|
107
|
+
"ups.status",
|
|
108
|
+
"ups.load",
|
|
109
|
+
"ups.power",
|
|
110
|
+
"ups.realpower",
|
|
111
|
+
"ups.power.nominal",
|
|
112
|
+
"ups.temperature",
|
|
113
|
+
"ups.delay.shutdown",
|
|
114
|
+
"ups.delay.start",
|
|
115
|
+
"ups.timer.shutdown",
|
|
116
|
+
"ups.timer.start",
|
|
117
|
+
"ups.firmware",
|
|
118
|
+
"ups.beeper.status",
|
|
119
|
+
"ups.mfr",
|
|
120
|
+
"ups.model",
|
|
121
|
+
"ups.serial",
|
|
122
|
+
"ups.vendorid",
|
|
123
|
+
"ups.productid",
|
|
124
|
+
"outlet.desc",
|
|
125
|
+
"outlet.id",
|
|
126
|
+
"outlet.switchable",
|
|
127
|
+
"outlet.status",
|
|
128
|
+
"ambient.temperature",
|
|
129
|
+
"ambient.humidity"
|
|
130
|
+
]);
|
|
131
|
+
function varTranslation(nutVarName) {
|
|
132
|
+
if (TRANSLATED_VARIABLES.has(nutVarName)) {
|
|
133
|
+
return (0, import_i18n.tName)(nutVarName);
|
|
134
|
+
}
|
|
135
|
+
const generic = nutVarName.replace(/\.(\d+|L\d(-(L\d|N))?|N)\./, ".");
|
|
136
|
+
if (generic !== nutVarName && TRANSLATED_VARIABLES.has(generic)) {
|
|
137
|
+
return (0, import_i18n.tName)(generic);
|
|
138
|
+
}
|
|
139
|
+
return void 0;
|
|
140
|
+
}
|
|
141
|
+
function nutVarToStateId(upsName, nutVarName) {
|
|
142
|
+
const firstDot = nutVarName.indexOf(".");
|
|
143
|
+
if (firstDot < 0) {
|
|
144
|
+
return `${upsName}.${nutVarName}`;
|
|
145
|
+
}
|
|
146
|
+
const channel = nutVarName.slice(0, firstDot);
|
|
147
|
+
const leaf = nutVarName.slice(firstDot + 1).replace(/\./g, "-");
|
|
148
|
+
return `${upsName}.${channel}.${leaf}`;
|
|
149
|
+
}
|
|
150
|
+
function nutVarToReadableName(nutVarName) {
|
|
151
|
+
const firstDot = nutVarName.indexOf(".");
|
|
152
|
+
const leaf = firstDot >= 0 ? nutVarName.slice(firstDot + 1) : nutVarName;
|
|
153
|
+
return leaf.replace(/\./g, " ").replace(/^./, (c) => c.toUpperCase());
|
|
154
|
+
}
|
|
155
|
+
class StateManager {
|
|
156
|
+
adapter;
|
|
157
|
+
createdIds = /* @__PURE__ */ new Set();
|
|
158
|
+
/** NUT variables already warned about as garbage in a numeric field (warn once). */
|
|
159
|
+
warnedGarbageVars = /* @__PURE__ */ new Set();
|
|
160
|
+
/** Last device name derived from mfr+model per UPS — lets a transient/wrong fallback self-correct. */
|
|
161
|
+
fallbackNames = /* @__PURE__ */ new Map();
|
|
162
|
+
/**
|
|
163
|
+
* stateId → original NUT variable/command name. The dot→dash id mapping is lossy for names
|
|
164
|
+
* containing a literal dash (three-phase input.L1-L2.*), so onStateChange reads the real name
|
|
165
|
+
* back from here instead of reversing the id.
|
|
166
|
+
*/
|
|
167
|
+
nutNames = /* @__PURE__ */ new Map();
|
|
168
|
+
/**
|
|
169
|
+
* @param adapter The ioBroker adapter instance
|
|
170
|
+
*/
|
|
171
|
+
constructor(adapter) {
|
|
172
|
+
this.adapter = adapter;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Create device + standard channels for a discovered UPS.
|
|
176
|
+
*
|
|
177
|
+
* @param upsName NUT UPS identifier (e.g. "ups0")
|
|
178
|
+
* @param description UPS description from LIST UPS
|
|
179
|
+
*/
|
|
180
|
+
async ensureUpsDevice(upsName, description) {
|
|
181
|
+
this.adapter.log.debug(`ensureUpsDevice: ${upsName} desc='${description}'`);
|
|
182
|
+
await this.adapter.extendObject(
|
|
183
|
+
upsName,
|
|
184
|
+
{
|
|
185
|
+
type: "device",
|
|
186
|
+
common: {
|
|
187
|
+
name: description,
|
|
188
|
+
statusStates: {
|
|
189
|
+
onlineId: `${this.adapter.namespace}.${upsName}.info.reachable`
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
native: {}
|
|
193
|
+
},
|
|
194
|
+
{ preserve: { common: ["name"] } }
|
|
195
|
+
);
|
|
196
|
+
this.createdIds.add(upsName);
|
|
197
|
+
await this.ensureChannel(upsName, "info");
|
|
198
|
+
await this.ensureState(`${upsName}.info.reachable`, {
|
|
199
|
+
type: "boolean",
|
|
200
|
+
role: "indicator.reachable",
|
|
201
|
+
read: true,
|
|
202
|
+
write: false,
|
|
203
|
+
name: (0, import_i18n.tName)("upsReachable"),
|
|
204
|
+
def: false
|
|
205
|
+
});
|
|
206
|
+
await this.cleanupDeprecatedInfoStates(upsName);
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Update device common.name from LIST VAR data when LIST UPS description is unusable.
|
|
210
|
+
*
|
|
211
|
+
* The fallback must NOT use `preserve: common.name` — the device object always has a
|
|
212
|
+
* name (set by ensureUpsDevice from the unusable description), so a preserved write
|
|
213
|
+
* would never apply (js-controller 7.x `removePreservedProperties` drops the new name
|
|
214
|
+
* whenever the old object has one; this silently killed the feature in v0.2.5-v0.4.1).
|
|
215
|
+
* User-modified names stay safe through the guard instead: the fallback only fires
|
|
216
|
+
* while the CURRENT name is still the auto-set placeholder or a value WE derived
|
|
217
|
+
* earlier — so a transient/wrong mfr+model that arrived first self-corrects, while a
|
|
218
|
+
* user-chosen name is left untouched. No broker round-trip while the derived name is stable.
|
|
219
|
+
*
|
|
220
|
+
* @param upsName UPS identifier
|
|
221
|
+
* @param description UPS description from LIST UPS
|
|
222
|
+
* @param variables Variables from LIST VAR
|
|
223
|
+
*/
|
|
224
|
+
async updateDeviceName(upsName, description, variables) {
|
|
225
|
+
var _a, _b, _c, _d, _e;
|
|
226
|
+
if (description && description !== "Description unavailable") {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const mfr = (_b = (_a = variables.find((v) => v.name === "device.mfr")) == null ? void 0 : _a.value) == null ? void 0 : _b.trim();
|
|
230
|
+
const model = (_d = (_c = variables.find((v) => v.name === "device.model")) == null ? void 0 : _c.value) == null ? void 0 : _d.trim();
|
|
231
|
+
if (!mfr && !model) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const name = [mfr, model].filter(Boolean).join(" ");
|
|
235
|
+
if (this.fallbackNames.get(upsName) === name) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const obj = await this.adapter.getObjectAsync(upsName);
|
|
239
|
+
const currentName = (_e = obj == null ? void 0 : obj.common) == null ? void 0 : _e.name;
|
|
240
|
+
const replaceable = currentName === description || currentName === "Description unavailable" || currentName === "" || currentName === this.fallbackNames.get(upsName);
|
|
241
|
+
if (!replaceable) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
this.adapter.log.debug(`updateDeviceName ${upsName}: using fallback '${name}' (mfr+model)`);
|
|
245
|
+
await this.adapter.extendObject(upsName, { common: { name } });
|
|
246
|
+
this.fallbackNames.set(upsName, name);
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Ensure a channel exists for a NUT domain (e.g. "battery", "ups").
|
|
250
|
+
*
|
|
251
|
+
* @param upsName UPS identifier
|
|
252
|
+
* @param channelName Channel name (NUT domain)
|
|
253
|
+
*/
|
|
254
|
+
async ensureChannel(upsName, channelName) {
|
|
255
|
+
const id = `${upsName}.${channelName}`;
|
|
256
|
+
const i18nKey = CHANNEL_I18N[channelName];
|
|
257
|
+
const name = i18nKey ? (0, import_i18n.tName)(i18nKey) : channelName;
|
|
258
|
+
await this.ensureObject(id, {
|
|
259
|
+
type: "channel",
|
|
260
|
+
common: { name },
|
|
261
|
+
native: {}
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Update variables from LIST VAR, creating states as needed.
|
|
266
|
+
* Variables are processed sorted by dot-depth (shallow first) to ensure
|
|
267
|
+
* parent states exist before children.
|
|
268
|
+
*
|
|
269
|
+
* @param upsName UPS identifier
|
|
270
|
+
* @param variables Variables from LIST VAR
|
|
271
|
+
* @param rwNames Set of writable variable names from LIST RW
|
|
272
|
+
*/
|
|
273
|
+
async updateVariables(upsName, variables, rwNames) {
|
|
274
|
+
var _a;
|
|
275
|
+
this.adapter.log.debug(`updateVariables ${upsName}: ${variables.length} vars, ${rwNames.size} writable`);
|
|
276
|
+
const sorted = [...variables].sort((a, b) => {
|
|
277
|
+
const depthA = a.name.split(".").length;
|
|
278
|
+
const depthB = b.name.split(".").length;
|
|
279
|
+
return depthA - depthB;
|
|
280
|
+
});
|
|
281
|
+
for (const v of sorted) {
|
|
282
|
+
const firstDot = v.name.indexOf(".");
|
|
283
|
+
if (firstDot >= 0) {
|
|
284
|
+
await this.ensureChannel(upsName, v.name.slice(0, firstDot));
|
|
285
|
+
}
|
|
286
|
+
const isWritable = rwNames.has(v.name);
|
|
287
|
+
const detected = (0, import_type_detector.detectType)(v.name, v.value, isWritable);
|
|
288
|
+
if (detected.expectedNumeric) {
|
|
289
|
+
if (!this.warnedGarbageVars.has(v.name)) {
|
|
290
|
+
this.warnedGarbageVars.add(v.name);
|
|
291
|
+
this.adapter.log.warn(
|
|
292
|
+
`Discarding non-numeric value ${JSON.stringify(v.value)} for numeric variable '${v.name}' on ${upsName}`
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
const stateId = nutVarToStateId(upsName, v.name);
|
|
298
|
+
this.nutNames.set(stateId, v.name);
|
|
299
|
+
const states = (0, import_type_detector.detectStates)(v.name);
|
|
300
|
+
await this.ensureState(stateId, {
|
|
301
|
+
type: detected.type,
|
|
302
|
+
role: detected.role,
|
|
303
|
+
unit: detected.unit,
|
|
304
|
+
read: detected.read,
|
|
305
|
+
write: detected.write,
|
|
306
|
+
name: (_a = varTranslation(v.name)) != null ? _a : nutVarToReadableName(v.name),
|
|
307
|
+
states
|
|
308
|
+
});
|
|
309
|
+
await this.adapter.setStateChangedAsync(stateId, { val: detected.parsedValue, ack: true });
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Parse ups.status and update status channel with individual boolean flags + severity.
|
|
314
|
+
*
|
|
315
|
+
* @param upsName UPS identifier
|
|
316
|
+
* @param rawStatus Raw ups.status value
|
|
317
|
+
* @param chargerStatus Optional battery.charger.status (modern source for charging/discharging)
|
|
318
|
+
*/
|
|
319
|
+
async updateStatusFlags(upsName, rawStatus, chargerStatus) {
|
|
320
|
+
var _a;
|
|
321
|
+
await this.ensureChannel(upsName, "status");
|
|
322
|
+
const result = (0, import_status_parser.parseStatus)(rawStatus, chargerStatus);
|
|
323
|
+
const activeFlags = import_status_parser.ALL_FLAG_KEYS.filter((k) => result.flags[k]).join(", ") || "none";
|
|
324
|
+
this.adapter.log.debug(
|
|
325
|
+
`updateStatusFlags ${upsName}: raw='${rawStatus}' severity=${result.severity} active=[${activeFlags}]`
|
|
326
|
+
);
|
|
327
|
+
await this.ensureAndSet(
|
|
328
|
+
`${upsName}.status.raw`,
|
|
329
|
+
{ type: "string", role: "text", read: true, write: false, name: (0, import_i18n.tName)("statusRaw") },
|
|
330
|
+
result.raw
|
|
331
|
+
);
|
|
332
|
+
await this.ensureAndSet(
|
|
333
|
+
`${upsName}.status.severity`,
|
|
334
|
+
{
|
|
335
|
+
type: "number",
|
|
336
|
+
role: "value",
|
|
337
|
+
read: true,
|
|
338
|
+
write: false,
|
|
339
|
+
name: (0, import_i18n.tName)("statusSeverity"),
|
|
340
|
+
states: { 0: "OK", 1: "Info", 2: "Warning", 3: "Critical", 4: "Emergency" }
|
|
341
|
+
},
|
|
342
|
+
result.severity
|
|
343
|
+
);
|
|
344
|
+
await this.ensureAndSet(
|
|
345
|
+
`${upsName}.status.display`,
|
|
346
|
+
{ type: "string", role: "text", read: true, write: false, name: (0, import_i18n.tName)("statusDisplay") },
|
|
347
|
+
(0, import_status_parser.getDisplayString)(rawStatus)
|
|
348
|
+
);
|
|
349
|
+
for (const flagKey of import_status_parser.ALL_FLAG_KEYS) {
|
|
350
|
+
const meta = import_status_parser.FLAG_META[flagKey];
|
|
351
|
+
await this.ensureAndSet(
|
|
352
|
+
`${upsName}.status.${flagKey}`,
|
|
353
|
+
{
|
|
354
|
+
type: "boolean",
|
|
355
|
+
role: (_a = meta == null ? void 0 : meta.role) != null ? _a : "indicator",
|
|
356
|
+
read: true,
|
|
357
|
+
write: false,
|
|
358
|
+
name: meta ? (0, import_i18n.tName)(meta.i18nKey) : flagKey
|
|
359
|
+
},
|
|
360
|
+
result.flags[flagKey]
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Create button states for instant commands.
|
|
366
|
+
*
|
|
367
|
+
* @param upsName UPS identifier
|
|
368
|
+
* @param commands Commands from LIST CMD
|
|
369
|
+
*/
|
|
370
|
+
async createCommandButtons(upsName, commands) {
|
|
371
|
+
await this.ensureChannel(upsName, "commands");
|
|
372
|
+
for (const cmd of commands) {
|
|
373
|
+
const stateId = `${upsName}.commands.${cmd.name.replace(/\./g, "-")}`;
|
|
374
|
+
this.nutNames.set(stateId, cmd.name);
|
|
375
|
+
const cmdI18nKey = COMMAND_I18N[cmd.name];
|
|
376
|
+
await this.ensureState(stateId, {
|
|
377
|
+
type: "boolean",
|
|
378
|
+
role: "button",
|
|
379
|
+
read: false,
|
|
380
|
+
write: true,
|
|
381
|
+
name: cmdI18nKey ? (0, import_i18n.tName)(cmdI18nKey) : cmd.name.replace(/\./g, " ").replace(/^./, (c) => c.toUpperCase()),
|
|
382
|
+
def: false
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* The original NUT variable/command name for a created state id. onStateChange uses it instead
|
|
388
|
+
* of reversing the dot→dash id mapping, which is lossy for names carrying a literal dash
|
|
389
|
+
* (e.g. three-phase input.L1-L2.voltage). Undefined for states not backed by a NUT var/command.
|
|
390
|
+
*
|
|
391
|
+
* @param stateId Local state id (e.g. "ups0.input.L1-L2-voltage")
|
|
392
|
+
*/
|
|
393
|
+
nutNameForState(stateId) {
|
|
394
|
+
return this.nutNames.get(stateId);
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Remove device objects for UPS devices no longer reported by the NUT server.
|
|
398
|
+
*
|
|
399
|
+
* @param currentUpsNames Set of currently discovered UPS names
|
|
400
|
+
*/
|
|
401
|
+
async cleanupRemovedUps(currentUpsNames) {
|
|
402
|
+
const adapterObjects = await this.adapter.getAdapterObjectsAsync();
|
|
403
|
+
const deviceIds = /* @__PURE__ */ new Set();
|
|
404
|
+
for (const [id, obj] of Object.entries(adapterObjects)) {
|
|
405
|
+
if (obj.type === "device") {
|
|
406
|
+
const localId = id.replace(`${this.adapter.namespace}.`, "");
|
|
407
|
+
if (!currentUpsNames.has(localId)) {
|
|
408
|
+
deviceIds.add(localId);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
for (const deviceId of deviceIds) {
|
|
413
|
+
this.adapter.log.info(`Removing stale UPS device: ${deviceId}`);
|
|
414
|
+
await this.adapter.delObjectAsync(deviceId, { recursive: true });
|
|
415
|
+
this.dropCacheUnder(deviceId);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Remove orphaned objects from previous adapter versions and v0.1.0 dot-style objects.
|
|
420
|
+
*
|
|
421
|
+
* @param knownUpsNames Set of currently discovered UPS names
|
|
422
|
+
*/
|
|
423
|
+
async cleanupLegacyObjects(knownUpsNames) {
|
|
424
|
+
const adapterObjects = await this.adapter.getAdapterObjectsAsync();
|
|
425
|
+
const orphanRoots = /* @__PURE__ */ new Set();
|
|
426
|
+
const dotStyleIds = [];
|
|
427
|
+
for (const fullId of Object.keys(adapterObjects)) {
|
|
428
|
+
const localId = fullId.replace(`${this.adapter.namespace}.`, "");
|
|
429
|
+
const parts = localId.split(".");
|
|
430
|
+
const topLevel = parts[0];
|
|
431
|
+
if (topLevel === "info") {
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
if (!knownUpsNames.has(topLevel)) {
|
|
435
|
+
orphanRoots.add(topLevel);
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
if (parts.length > 3) {
|
|
439
|
+
dotStyleIds.push(localId);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
for (const root of orphanRoots) {
|
|
443
|
+
this.adapter.log.info(`Removing orphaned root object from previous adapter version: ${root}`);
|
|
444
|
+
await this.adapter.delObjectAsync(root, { recursive: true });
|
|
445
|
+
this.dropCacheUnder(root);
|
|
446
|
+
}
|
|
447
|
+
const sorted = dotStyleIds.sort((a, b) => b.split(".").length - a.split(".").length);
|
|
448
|
+
for (const id of sorted) {
|
|
449
|
+
this.adapter.log.debug(`Removing v0.1.0 dot-style object: ${id}`);
|
|
450
|
+
await this.adapter.delObjectAsync(id);
|
|
451
|
+
this.createdIds.delete(id);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
async cleanupDeprecatedInfoStates(upsName) {
|
|
455
|
+
const cacheKey = `${upsName}.__deprecatedCleanup`;
|
|
456
|
+
if (this.createdIds.has(cacheKey)) {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
this.createdIds.add(cacheKey);
|
|
460
|
+
const deprecated = [
|
|
461
|
+
`${upsName}.info.name`,
|
|
462
|
+
`${upsName}.info.description`,
|
|
463
|
+
// Renamed to info.reachable in 0.4.0 (the old `online` leaf collided with the
|
|
464
|
+
// status.online / OL flag). Without this delete the old state lingers frozen at its
|
|
465
|
+
// last value — ioBroker does not auto-remove states an adapter stops writing.
|
|
466
|
+
`${upsName}.info.online`,
|
|
467
|
+
// Dropped non-standard status flags — not real NUT status_set tokens (COMM/NOCOMM),
|
|
468
|
+
// or renamed (highEfficiency → ecoMode). NB: `testing` is NOT here — TEST is a real
|
|
469
|
+
// token (apc_modbus, powercom, …) and is a current flag again.
|
|
470
|
+
`${upsName}.status.commEstablished`,
|
|
471
|
+
`${upsName}.status.commLost`,
|
|
472
|
+
`${upsName}.status.highEfficiency`
|
|
473
|
+
];
|
|
474
|
+
for (const id of deprecated) {
|
|
475
|
+
try {
|
|
476
|
+
await this.adapter.delObjectAsync(id);
|
|
477
|
+
this.adapter.log.debug(`Removed deprecated state: ${id}`);
|
|
478
|
+
} catch {
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Remove all cached IDs under a prefix.
|
|
484
|
+
*
|
|
485
|
+
* @param prefix ID prefix to clear
|
|
486
|
+
*/
|
|
487
|
+
dropCacheUnder(prefix) {
|
|
488
|
+
for (const id of [...this.createdIds]) {
|
|
489
|
+
if (id === prefix || id.startsWith(`${prefix}.`)) {
|
|
490
|
+
this.createdIds.delete(id);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
for (const id of [...this.nutNames.keys()]) {
|
|
494
|
+
if (id === prefix || id.startsWith(`${prefix}.`)) {
|
|
495
|
+
this.nutNames.delete(id);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
async ensureObject(id, obj) {
|
|
500
|
+
if (this.createdIds.has(id)) {
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
await this.adapter.setObjectNotExistsAsync(id, {
|
|
504
|
+
type: obj.type,
|
|
505
|
+
common: obj.common,
|
|
506
|
+
native: obj.native
|
|
507
|
+
});
|
|
508
|
+
this.createdIds.add(id);
|
|
509
|
+
}
|
|
510
|
+
async ensureState(id, common) {
|
|
511
|
+
if (this.createdIds.has(id)) {
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
await this.adapter.extendObject(
|
|
515
|
+
id,
|
|
516
|
+
{
|
|
517
|
+
type: "state",
|
|
518
|
+
common,
|
|
519
|
+
native: {}
|
|
520
|
+
},
|
|
521
|
+
{ preserve: { common: ["name"] } }
|
|
522
|
+
);
|
|
523
|
+
this.createdIds.add(id);
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Ensure a state exists (once) and write its current value — the create-then-set pair used for
|
|
527
|
+
* every status/flag datapoint.
|
|
528
|
+
*
|
|
529
|
+
* @param id State id
|
|
530
|
+
* @param common Object definition forwarded to ensureState
|
|
531
|
+
* @param val Value to write (acknowledged)
|
|
532
|
+
*/
|
|
533
|
+
async ensureAndSet(id, common, val) {
|
|
534
|
+
await this.ensureState(id, common);
|
|
535
|
+
await this.adapter.setStateChangedAsync(id, { val, ack: true });
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Enrich an existing state with ENUM/RANGE metadata from the NUT server.
|
|
539
|
+
* Uses extendObject to deep-merge — overwrites only the provided keys.
|
|
540
|
+
*
|
|
541
|
+
* @param id State object ID
|
|
542
|
+
* @param patch Metadata to apply (states for ENUM, min/max for RANGE)
|
|
543
|
+
* @param patch.states ENUM value map
|
|
544
|
+
* @param patch.min RANGE minimum
|
|
545
|
+
* @param patch.max RANGE maximum
|
|
546
|
+
*/
|
|
547
|
+
async enrichStateMetadata(id, patch) {
|
|
548
|
+
this.adapter.log.debug(`enrichStateMetadata ${id}: ${JSON.stringify(patch)}`);
|
|
549
|
+
const common = {};
|
|
550
|
+
if (patch.states) {
|
|
551
|
+
common.states = patch.states;
|
|
552
|
+
}
|
|
553
|
+
if (patch.min !== void 0) {
|
|
554
|
+
common.min = patch.min;
|
|
555
|
+
}
|
|
556
|
+
if (patch.max !== void 0) {
|
|
557
|
+
common.max = patch.max;
|
|
558
|
+
}
|
|
559
|
+
if (Object.keys(common).length > 0) {
|
|
560
|
+
await this.adapter.extendObject(id, { common }, { preserve: { common: ["name"] } });
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
565
|
+
0 && (module.exports = {
|
|
566
|
+
StateManager,
|
|
567
|
+
nutVarToReadableName,
|
|
568
|
+
nutVarToStateId
|
|
569
|
+
});
|
|
570
|
+
//# sourceMappingURL=state-manager.js.map
|