iobroker.roborock
Version:
251 lines (217 loc) • 8.55 kB
text/typescript
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}`);
}
}
}