UNPKG

iobroker.roborock

Version:
251 lines (217 loc) 8.55 kB
export class MockAdapter { public objects: Record<string, any> = {}; public states: Record<string, any> = {}; public log: any; public config: any = {}; public requestsHandler: any; public mqtt_api: any; public local_api: any; public http_api: any; // mock support methods public instance: number = 0; public namespace: string = "roborock.0"; public pendingRequests: Map<number, any> = new Map(); public nonce: Buffer = Buffer.alloc(16); public translations: Record<string, string> = {}; public logLevel: "debug" | "info" | "warn" | "error" = "warn"; private logLevels = { debug: 0, info: 1, warn: 2, error: 3 }; public catchError(error: any, attribute: string): void { this.log.error(`[CatchError] ${attribute}: ${error}`); } /** Match real adapter: safe string from thrown value. */ public errorMessage(e: unknown): string { return e instanceof Error ? e.message : String(e); } /** Match real adapter: stack if Error, else message, else String(e). */ public errorStack(e: unknown): string { if (e instanceof Error) return e.stack ?? e.message; return String(e); } public setInterval(callback: (...args: any[]) => void, ms: number, ...args: any[]): any { return setInterval(callback, ms, ...args); } public setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): any { return setTimeout(callback, ms, ...args); } public clearInterval(intervalId: any): void { clearInterval(intervalId); } public clearTimeout(timeoutId: any): void { clearTimeout(timeoutId); } public getDeviceProtocolVersion = async (): Promise<string> => { return "1.0"; }; public getB01Variant = async (): Promise<"Q7" | "Q10" | null> => { return null; }; public rLog(connection: "MQTT" | "TCP" | "UDP" | "HTTP" | "Cloud" | "Local" | "System" | "MapManager" | "Requests" | "Unknown", duid: string | null | undefined, direction: "<-" | "->" | "Info" | "Error" | "Warn" | "Debug", version: string | undefined, protocol: string | number | undefined, message: string, level: "debug" | "info" | "warn" | "error" = "debug"): void { if (this.logLevels[level] < this.logLevels[this.logLevel]) return; const duidStr = duid ? `[${duid}] ` : ""; const versionStr = version ? ` (PV: ${version})` : ""; const protoStr = protocol ? ` (P: ${protocol})` : ""; const logMsg = `[${connection}] ${duidStr}${direction}${versionStr}${protoStr} ${message}`; if (this.log[level]) { this.log[level](logMsg); } else { console.log(`[FALLBACK-${level}] ${logMsg}`); } } private logMessage(level: "debug" | "info" | "warn" | "error", msg: string, logFn: (m: string) => void): void { if (this.logLevels[level] >= this.logLevels[this.logLevel]) { logFn(`[${level.toUpperCase()}] ${msg}`); } } constructor() { this.log = { info: (msg: string) => this.logMessage("info", msg, console.log), warn: (msg: string) => this.logMessage("warn", msg, console.warn), error: (msg: string) => this.logMessage("error", msg, console.error), debug: (msg: string) => this.logMessage("debug", msg, console.log), silly: () => {} }; this.setState = this.setState.bind(this); this.setStateAsync = this.setStateAsync.bind(this); this.setStateChanged = this.setStateChanged.bind(this); this.http_api = { getMatchedRoomIDs: () => [], getRobotModel: () => "", getFwFeaturesResult: () => ({}), storeFwFeaturesResult: () => {} }; (this as any).translationManager = { get: (key: string, def?: string) => def || key }; } public async setObject(id: string, obj: any): Promise<void> { this.objects[id] = obj; } public async setObjectNotExistsAsync(id: string, obj: any): Promise<void> { if (!this.objects[id]) { this.objects[id] = obj; } } public extendObject = async (id: string, obj: any): Promise<void> => { this.objects[id] = { ...this.objects[id], ...obj }; }; public async getObjectAsync(id: string): Promise<any> { return this.objects[id]; } public async delObjectAsync(id: string, options?: { recursive?: boolean }): Promise<void> { const recursive = options?.recursive === true; if (recursive) { const prefix = `${id}.`; for (const objectId of Object.keys(this.objects)) { if (objectId === id || objectId.startsWith(prefix)) { delete this.objects[objectId]; } } for (const stateId of Object.keys(this.states)) { if (stateId === id || stateId.startsWith(prefix)) { delete this.states[stateId]; } } return; } delete this.objects[id]; delete this.states[id]; } public setState = (id: string, state: any, ack?: boolean | ((err?: any) => void), callback?: (err?: any) => void): Promise<void> => { if (typeof ack === "function") { callback = ack; ack = undefined; } // Fire and forget / callback style return this.setStateAsync(id, state).then(() => { if (callback) callback(); }).catch((e) => { if (callback) callback(e); throw e; }); }; public setStateAsync = async (id: string, state: any): Promise<void> => { // Handle { val: ... } object or direct value let val = state; if (typeof state === "object" && state !== null && "val" in state) { val = state.val; } this.rLog("System", id, "Debug", undefined, undefined, `Setting state to ${val}`, "debug"); this.states[id] = val; // Type Verification const obj = this.objects[id]; if (obj && obj.common && obj.common.type) { const expectedType = obj.common.type; const actualType = typeof val; if (expectedType === "array" || expectedType === "object") { if (actualType !== "object" && actualType !== "string") { // Strings are sometimes allowed for JSON throw new Error(`Type mismatch for ${id}. Expected ${expectedType}, got ${actualType} (${val})`); } } else if (expectedType === "mixed") { // Any type allowable } else if (actualType !== expectedType) { // Allow number/string auto-conversion if simple if (expectedType === "string" && actualType === "number") return; // Optional: Allow null if not strictly forbidden? Usually ioBroker allows null. if (val === null || val === undefined) return; // Strict check for others throw new Error(`Type mismatch for ${id}. Expected ${expectedType}, got ${actualType} (${val})`); } } }; public setStateChanged = async (id: string, state: any, ack?: boolean | ((err?: any) => void), callback?: (err?: any) => void): Promise<void> => { if (typeof ack === "function") { callback = ack; ack = undefined; } let val = state; if (typeof state === "object" && state !== null && "val" in state) { val = state.val; } if (this.states[id] !== val) { await this.setStateAsync(id, state); } try { if (callback) callback(); } catch (e: any) { if (callback) callback(e); throw e; } }; public async getStatesAsync(pattern: string): Promise<Record<string, ioBroker.State> | null> { const result: Record<string, ioBroker.State> = {}; const regexPattern = new RegExp("^" + pattern.replace(/\./g, "\\.").replace(/\*/g, ".*") + "$"); for (const id in this.states) { if (regexPattern.test(id)) { result[id] = { val: this.states[id], ack: true, ts: Date.now(), lc: Date.now(), from: "mock" }; } } return Object.keys(result).length > 0 ? result : null; } public async getForeignStatesAsync(pattern: string | string[]): Promise<Record<string, ioBroker.State> | null> { const patterns = Array.isArray(pattern) ? pattern : [pattern]; const result: Record<string, ioBroker.State> = {}; for (const p of patterns) { // Strip namespace for mock lookup if present const lookupPattern = p.startsWith(this.namespace + ".") ? p.substring(this.namespace.length + 1) : p; const matches = await this.getStatesAsync(lookupPattern); if (matches) { // We must return the original IDs (with namespace) if they were requested that way for (const [id, state] of Object.entries(matches)) { const finalId = p.includes("*") ? id : p; // simplistic for mock result[finalId] = state; } } } return Object.keys(result).length > 0 ? result : null; } public async getStateAsync(id: string): Promise<any> { return { val: this.states[id], ack: true }; } public async expectState(id: string, expected: Partial<ioBroker.State>): Promise<void> { const state = this.states[id]; if (state === undefined) { throw new Error(`State ${id} not found`); } if (expected.val !== undefined && state !== expected.val) { throw new Error(`State ${id} expected ${expected.val} but got ${state}`); } } }