UNPKG

iobroker.roborock

Version:
610 lines (527 loc) 18.4 kB
// // conn.ts // "use strict"; // Declare global variables from <script> tags for TypeScript declare let io: any; // Besser: npm install @types/socket.io-client declare let $: any; // Besser: npm install @types/jquery declare let storage: { get: (key: string) => any; set: (key: string, val: any) => void; empty: () => void }; declare let socketNamespace: string | undefined; declare let socketUrl: string | undefined; declare let socketSession: string | undefined; declare let socketForceWebSockets: boolean | undefined; declare let app: { onConnChange: (isConnected: boolean) => void; readLocalFile: (filename: string, callback: (err: any, data: string | null, mimeType?: string) => void) => void; }; // --- TypeScript-Interfaces --- interface ConnOptions { name?: string; connLink?: string; socketSession?: string; socketForceWebSockets?: boolean; mayReconnect?: () => boolean; } interface ConnCallbacks { onConnChange?: (isConnected: boolean) => void; onUpdate?: (id: string, state: any) => void; // ioBroker.State onRefresh?: ((...args: any[]) => any) | null; onAuth?: ((...args: any[]) => any) | null; onCommand?: (instance: string, command: string, data: any) => any; onError?: (err: any) => void; onObjectChange?: (id: string, obj: any) => void; } /** * Modern, class-based and Promise-based version of servConn. */ export class Connection { // Private properties private _socket: any = null; // Typ SocketIOClient.Socket private _isConnected: boolean = false; private _disconnectedSince: number | null = null; private _connCallbacks: ConnCallbacks = {}; private _isAuthDone: boolean = false; private _type: string = "socket.io"; private _reconnectInterval: number = 10000; private _reloadInterval: number = 30; private _isSecure: boolean = false; private _useStorage: boolean = false; private _objects: Record<string, any> | null = null; // Auth-Promise: Methods that require auth will await this promise. private _authPromise: Promise<boolean> | null = null; private _resolveAuth: (value: boolean) => void = () => {}; private _rejectAuth: (reason?: any) => void = () => {}; // Public properties public namespace: string = "vis.0"; public user: string = ""; // Internal timers private _connectInterval: any = null; private _countInterval: any = null; private _timer: any = null; private _lastTimer: number = 0; constructor(options?: { useStorage?: boolean }) { if (options?.useStorage) { this._useStorage = true; } } /** * Initializes the connection and starts the authentication process. */ public init(connOptions: ConnOptions, connCallbacks: ConnCallbacks, objectsRequired: boolean) { this._connCallbacks = connCallbacks; // Create the promise that all API calls will await this._authPromise = new Promise((resolve, reject) => { this._resolveAuth = resolve; this._rejectAuth = reject; }); if (typeof socketNamespace !== "undefined") { this.namespace = socketNamespace; } connOptions = connOptions || {}; if (!connOptions.name) { connOptions.name = this.namespace; } // --- Logic for determining the connection type (local vs. socket.io) --- if (document.URL.split("/local/")[1] || (typeof socketUrl === "undefined" && !connOptions.connLink) || (typeof socketUrl !== "undefined" && socketUrl === "local")) { this._type = "local"; } // --- Establish Connection --- if (this._type === "local") { this._isConnected = true; this._isAuthDone = true; if (this._connCallbacks.onConnChange) this._connCallbacks.onConnChange(true); if (typeof app !== "undefined") app.onConnChange(true); this._resolveAuth(true); // Resolve auth promise return; } if (typeof io === "undefined") { console.error("socket.io not loaded!"); return; } let connLink = connOptions.connLink || window.localStorage.getItem("connLink"); if (!connLink && typeof socketUrl !== "undefined") connLink = socketUrl; connOptions.socketSession = connOptions.socketSession || (typeof socketSession !== "undefined" ? socketSession : "nokey"); if (connOptions.socketForceWebSockets === undefined && typeof socketForceWebSockets !== "undefined") { connOptions.socketForceWebSockets = socketForceWebSockets; } let url: string; if (connLink) { url = connLink; if (connLink[0] === ":") { url = `${location.protocol}//${location.hostname}${connLink}`; } } else { url = `${location.protocol}//${location.host}`; } this._socket = io.connect(url, { query: "key=" + connOptions.socketSession, "reconnection limit": 10000, "max reconnection attempts": Infinity, reconnection: false, upgrade: !connOptions.socketForceWebSockets, rememberUpgrade: connOptions.socketForceWebSockets, transports: connOptions.socketForceWebSockets ? ["websocket"] : undefined, }); // --- Register socket listeners --- this._socket.on("connect", () => this._onSocketConnect(connOptions, objectsRequired)); this._socket.on("reauthenticate", () => this._onSocketReauthenticate()); this._socket.on("connect_error", () => this._onSocketConnectError(connOptions)); this._socket.on("disconnect", () => this._onSocketDisconnect(connOptions)); this._socket.on("reconnect", () => this._onSocketReconnect()); this._socket.on("objectChange", (id: string, obj: any) => this._onSocketObjectChange(id, obj)); this._socket.on("stateChange", (id: string, state: any) => this._onSocketStateChange(id, state)); this._socket.on("permissionError", (err: any) => this._onSocketPermissionError(err)); } // --- Private Socket Handlers --- private _onSocketConnect(connOptions: ConnOptions, objectsRequired: boolean) { if (this._disconnectedSince) { const offlineTime = new Date().getTime() - this._disconnectedSince; console.log(`was offline for ${offlineTime / 1000}s`); if (this._reloadInterval && offlineTime > this._reloadInterval * 1000) { window.location.reload(); } this._disconnectedSince = null; } if (this._connectInterval) { clearInterval(this._connectInterval); this._connectInterval = null; } if (this._countInterval) { clearInterval(this._countInterval); this._countInterval = null; } const elem = document.getElementById("server-disconnect"); if (elem) elem.style.display = "none"; this._socket.emit("name", connOptions.name); console.log(new Date().toISOString() + " Connected => authenticate"); const wait = setTimeout(() => { console.error("No answer from server"); window.location.reload(); }, 3000); this._socket.emit("authenticate", (isOk: boolean, isSecure: boolean) => { clearTimeout(wait); console.log(new Date().toISOString() + " Authenticated: " + isOk); if (isOk) { this._onAuth(objectsRequired, isSecure); } else { console.log("permissionError"); this._rejectAuth(new Error("Permission Error")); } }); } private _onAuth(objectsRequired: boolean, isSecure: boolean) { // Set auth status before processing commands. this._isAuthDone = true; this._isSecure = isSecure; if (this._isSecure) { this._lastTimer = new Date().getTime(); this._monitor(); } if (objectsRequired) this._socket.emit("subscribeObjects", "*"); if (this._isConnected === true) { // Prevent double-firing on reconnect this._resolveAuth(true); // Resolve auth promise regardless return; } this._isConnected = true; if (this._connCallbacks.onConnChange) { setTimeout(() => { this._socket.emit("authEnabled", (_auth: boolean, user: string) => { this.user = user; this._connCallbacks.onConnChange!(this._isConnected); if (typeof app !== "undefined") app.onConnChange(this._isConnected); }); }, 0); } // Release all pending API calls (awaiting _checkReady) this._resolveAuth(true); } private _onSocketReauthenticate() { if (this._connCallbacks.onConnChange) { this._connCallbacks.onConnChange(false); if (typeof app !== "undefined") app.onConnChange(false); } console.warn("reauthenticate"); window.location.reload(); } private _onSocketConnectError(connOptions: ConnOptions) { if (typeof $ !== "undefined") { $(".splash-screen-text").css("color", "#002951"); } this.reconnect(connOptions); } private _onSocketDisconnect(connOptions: ConnOptions) { this._disconnectedSince = new Date().getTime(); this._isConnected = false; // Create a new, unresolved promise for the next connection this._authPromise = new Promise((resolve, reject) => { this._resolveAuth = resolve; this._rejectAuth = reject; }); if (this._connCallbacks.onConnChange) { setTimeout(() => { const elem = document.getElementById("server-disconnect"); if (elem) elem.style.display = ""; this._connCallbacks.onConnChange!(this._isConnected); if (typeof app !== "undefined") app.onConnChange(this._isConnected); }, 5000); } else { const elem = document.getElementById("server-disconnect"); if (elem) elem.style.display = ""; } this.reconnect(connOptions); } private _onSocketReconnect() { const offlineTime = new Date().getTime() - (this._disconnectedSince || 0); console.log(`was offline for ${offlineTime / 1000}s`); if (this._reloadInterval && offlineTime > this._reloadInterval * 1000) { window.location.reload(); } } private _onSocketObjectChange(id: string, obj: any) { if (this._useStorage && typeof storage !== "undefined") { // ... (storage logic) ... } if (this._connCallbacks.onObjectChange) { this._connCallbacks.onObjectChange(id, obj); } } private _onSocketStateChange(id: string, state: any) { if (!id || state === null || typeof state !== "object") return; // ... (original stateChange logic for commands) ... if (this._connCallbacks.onUpdate) { this._connCallbacks.onUpdate(id, state); } } private _onSocketPermissionError(err: any) { if (this._connCallbacks.onError) { this._connCallbacks.onError(err); } else { console.log("permissionError", err); } } /** * Checks if the connection is initialized and authenticated. * Awaits the auth promise. */ private async _checkReady(commandName: string): Promise<boolean> { if (this._type === "local") { return true; // Always ready in local mode } if (!this._authPromise) { console.error(`Connection not initialized. Call .init() first for ${commandName}`); throw new Error("Connection not initialized"); } // Waits until _onAuth() has called _resolveAuth(true). await this._authPromise; // Additional checks (replaces _checkConnection) if (!this._isConnected) { console.log(`No connection for ${commandName}`); throw new Error("No connection"); } if (this._socket === null) { console.log(`socket.io not initialized for ${commandName}`); throw new Error("Socket.io not initialized"); } // We check _isAuthDone, which is set in _onAuth. if (!this._isAuthDone) { console.warn(`Auth not done for ${commandName}. Race condition?`); throw new Error("Authentication not complete"); } return true; } // --- Public API Methods (now with async/await) --- public getType = () => this._type; public getIsConnected = () => this._isConnected; public getIsLoginRequired = () => this._isSecure; public getUser = () => this.user; public setReloadTimeout = (timeout: number) => { this._reloadInterval = parseInt(timeout as any, 10); }; public setReconnectInterval = (interval: number) => { this._reconnectInterval = parseInt(interval as any, 10); }; public reconnect(connOptions: ConnOptions) { if ((!connOptions.mayReconnect || connOptions.mayReconnect()) && !this._connectInterval) { this._connectInterval = setInterval(() => { console.log("Trying connect..."); this._socket.connect(); // ... (remaining reconnect logic with jQuery) ... }, this._reconnectInterval); // ... } } public async getVersion(): Promise<string> { await this._checkReady("getVersion"); return new Promise((resolve) => { this._socket.emit("getVersion", (_err: any, version: string) => { resolve(version); }); }); } /** * Sends a command to an adapter instance. * (Your added function) */ public async sendTo(adapterInstance: string, command: string, message: any): Promise<any> { await this._checkReady("sendTo"); return new Promise((resolve, reject) => { this._socket.emit("sendTo", adapterInstance, command, message, (result: any) => { if (result && result.error) { reject(new Error(result.error)); } else { resolve(result); } }); }); } /** * Queries objects based on a view. * (Your added function) */ public async getObjectView(design: string, search: string, params: any): Promise<{ rows: any[] }> { await this._checkReady("getObjectView"); return new Promise((resolve, reject) => { this._socket.emit("getObjectView", design, search, params, (err: any, res: any) => { if (err) { reject(err); } else { resolve(res); } }); }); } /** * Reads a single object from the ioBroker database. */ public async getObject(id: string): Promise<any> { await this._checkReady("getObject"); // Wait for auth to complete return new Promise((resolve, reject) => { this._socket.emit("getObject", id, (err: any, obj: any) => { if (err) { reject(err); } else { resolve(obj); } }); }); } public async getStates(IDs: string[] | null): Promise<Record<string, any>> { if (this._type === "local") return {}; await this._checkReady("getStates"); return new Promise((resolve, reject) => { this._socket.emit("getStates", IDs, (err: any, data: any) => { if (err || !data) { reject(err || "Authentication required"); } else { resolve(data); } }); }); } /** * Subscribes to a specific state (or pattern) on the server. */ public async subscribeState(id: string): Promise<void> { await this._checkReady("subscribeState"); // Wait for auth to complete return new Promise((resolve) => { this._socket.emit("subscribe", id, () => { resolve(); }); }); } /** * Unsubscribes from a specific state (or pattern) on the server. */ public async unsubscribeState(id: string): Promise<void> { await this._checkReady("unsubscribeState"); // Wait for auth to complete return new Promise((resolve) => { this._socket.emit("unsubscribe", id, () => { resolve(); }); }); } /** * Reads all objects, enums, adapters, channels, and devices. * Flattens the callback hell with async/await. */ public async getObjects(useCache: boolean = false): Promise<Record<string, any>> { if (this._useStorage && useCache) { if (typeof storage !== "undefined") { const objects = this._objects || storage.get("objects"); if (objects) return objects; } else if (this._objects) { return this._objects; } } await this._checkReady("getObjects"); try { // 1. Get main objects const [err, data] = await new Promise<[any, Record<string, any>]>((resolve) => { this._socket.emit("getObjects", (err: any, data: any) => resolve([err, data])); }); if (err) throw err; // 2. Get enums const enumsRes = await this.getObjectView("system", "enum", { startkey: "enum.", endkey: "enum.\u9999" }); const enums: Record<string, any> = {}; for (const row of enumsRes.rows) { data[row.id] = row.value; enums[row.id] = row.value; } // 3. Get adapter instances const adaptersRes = await this.getObjectView("system", "instance", { startkey: "system.adapter.", endkey: "system.adapter.\u9999" }); for (const row of adaptersRes.rows) { data[row.id] = row.value; } // 4. Get channels const channelsRes = await this.getObjectView("system", "channel", { startkey: "", endkey: "\u9999" }); for (const row of channelsRes.rows) { data[row.id] = row.value; } // 5. Get devices const devicesRes = await this.getObjectView("system", "device", { startkey: "", endkey: "\u9999" }); for (const row of devicesRes.rows) { data[row.id] = row.value; } // Save if caching is used if (this._useStorage) { this._fillChildren(data); this._objects = data; if (typeof storage !== "undefined") { storage.set("objects", data); storage.set("enums", enums); storage.set("timeSync", new Date().getTime()); } } return data; } catch (error) { console.error("Error in getObjects:", error); throw error; } } // ... (Here the remaining methods like readFile, writeFile, getChildren etc. // convert to async/await using the same pattern) ... // Example for readFile: public async readFile(filename: string, isRemote: boolean = false): Promise<{ data: any; mimeType?: string }> { if (this._type === "local") { try { const data = storage.get(filename); return { data: data ? JSON.parse(data) : null }; } catch (err) { throw err; } } await this._checkReady("readFile"); if (!isRemote && typeof app !== "undefined") { return new Promise((resolve, reject) => { app.readLocalFile(filename.replace(/^\/vis\.0\//, ""), (err, data, mimeType) => { if (err) reject(err); else resolve({ data, mimeType }); }); }); } let adapter = this.namespace; if (filename[0] === "/") { const p = filename.split("/"); adapter = p[1]; p.splice(0, 2); filename = p.join("/"); } return new Promise((resolve, reject) => { this._socket.emit("readFile", adapter, filename, (err: any, data: any, mimeType: string) => { if (err) reject(err); else resolve({ data, mimeType }); }); }); } // --- Private Helper Functions --- private _monitor() { if (this._timer) return; const ts = new Date().getTime(); if (this._reloadInterval && ts - this._lastTimer > this._reloadInterval * 1000) { window.location.reload(); } else { this._lastTimer = ts; } this._timer = setTimeout(() => { this._timer = null; this._monitor(); }, 10000); } private _fillChildren(objects: Record<string, any>) { const items = Object.keys(objects).sort(); for (let i = 0; i < items.length; i++) { const id = items[i]; if (objects[id].common) { let j = i + 1; const children: string[] = []; const len = id.length + 1; const name = id + "."; while (j < items.length && items[j].substring(0, len) === name) { children.push(items[j++]); } objects[id].children = children; } } } }