UNPKG

iobroker.roborock

Version:
198 lines (171 loc) 9.24 kB
import type { Roborock } from "../../../main"; import { MapDecryptor as B01MapDecryptor } from "../../map/b01/MapDecryptor"; type Q10FeatureHandler = { applyQ10CleanRecordBlob?: (payload: Buffer) => Promise<boolean>; applyQ10ConsumablesFromDpResult?: (data: Record<string, unknown>) => Promise<void>; applyQ10MapInfoFromDpResult?: (result: Record<string, unknown>) => Promise<void>; applyQ10NetworkFromDp81?: (net: Record<string, unknown>) => Promise<void>; applyQ10ShadowDpPayload?: (dps: Record<string, unknown>) => Promise<void>; applyQ10StatusFromDpResult?: (status: Record<string, unknown>) => Promise<void>; applyQ10TimersFromDpResult?: (timers: unknown[]) => Promise<void>; hasPendingQ10CleanRecordBlobRequest?: () => boolean; }; export class Q10DpDispatcher { private readonly lastProtocol102SummaryByDuid = new Map<string, string>(); constructor(private readonly adapter: Roborock) {} private async getQ10Handler(duid: string): Promise<Q10FeatureHandler | undefined> { const b01Variant = await this.adapter.getB01Variant(duid); if (b01Variant !== "Q10") return undefined; return this.adapter.deviceFeatureHandlers.get(duid) as unknown as Q10FeatureHandler | undefined; } private logSummaryIfChanged(duid: string, summary: string | null): void { if (!summary) { this.lastProtocol102SummaryByDuid.delete(duid); return; } const previousSummary = this.lastProtocol102SummaryByDuid.get(duid); if (previousSummary === summary) return; this.lastProtocol102SummaryByDuid.set(duid, summary); this.adapter.rLog("MQTT", duid, "Debug", "102", undefined, `[Q10DP] ${summary}`, "debug"); } private summarizeFlatShadow(dpsRoot: Record<string, unknown>): { summary: string | null; hasUnknownKeys: boolean } { const commonDps = dpsRoot["101"] && typeof dpsRoot["101"] === "object" && !Array.isArray(dpsRoot["101"]) ? dpsRoot["101"] as Record<string, unknown> : undefined; const summaryParts: string[] = []; const recordPayload = commonDps?.["52"]; if (recordPayload && typeof recordPayload === "object" && !Array.isArray(recordPayload)) { const dp52 = recordPayload as Record<string, unknown>; const op = String(dp52.op ?? ""); if (op === "select" && Number(dp52.result ?? 0) === 1) { return { summary: null, hasUnknownKeys: false }; } if (op === "list" && Array.isArray(dp52.data)) { return { summary: null, hasUnknownKeys: false }; } } const mapListPayload = commonDps?.["61"]; if (mapListPayload && typeof mapListPayload === "object" && !Array.isArray(mapListPayload)) { const dp61 = mapListPayload as Record<string, unknown>; if (Array.isArray(dp61.data)) { summaryParts.push(`maps:${dp61.data.length}`); } } const mapMetaPayload = commonDps?.["64"]; if (mapMetaPayload && typeof mapMetaPayload === "object" && !Array.isArray(mapMetaPayload)) { const dp64 = mapMetaPayload as Record<string, unknown>; if (Array.isArray(dp64.data)) { summaryParts.push(`map_meta:${dp64.data.length}`); } } const carpetPayload = commonDps?.["65"]; if (carpetPayload && typeof carpetPayload === "object" && !Array.isArray(carpetPayload)) { const dp65 = carpetPayload as Record<string, unknown>; if (Array.isArray(dp65.data)) { summaryParts.push(`carpets:${dp65.data.length}`); } } const hasPrimaryShadowState = dpsRoot["121"] !== undefined || dpsRoot["122"] !== undefined || dpsRoot["123"] !== undefined || dpsRoot["124"] !== undefined; if (dpsRoot["121"] !== undefined) summaryParts.push(`status=${dpsRoot["121"]}`); if (dpsRoot["122"] !== undefined) summaryParts.push(`battery=${dpsRoot["122"]}`); if (dpsRoot["123"] !== undefined) summaryParts.push(`fan=${dpsRoot["123"]}`); if (dpsRoot["124"] !== undefined) summaryParts.push(`water=${dpsRoot["124"]}`); if (hasPrimaryShadowState) { if (commonDps?.["36"] !== undefined) summaryParts.push(`voice_lang=${commonDps["36"]}`); if (commonDps?.["108"] !== undefined) summaryParts.push(`voice_ver=${commonDps["108"]}`); if (commonDps?.["109"] !== undefined) summaryParts.push(`country=${commonDps["109"]}`); if (commonDps?.["81"] && typeof commonDps["81"] === "object" && !Array.isArray(commonDps["81"])) { const signal = (commonDps["81"] as Record<string, unknown>).signal; if (signal !== undefined) summaryParts.push(`rssi=${signal}`); } } const knownTopLevelKeys = new Set(["101", "121", "122", "123", "124", "125", "126", "127", "136", "137", "138", "139", "141", "142"]); const knownCommonKeys = new Set([ "6", "7", "25", "26", "29", "30", "31", "32", "33", "36", "37", "40", "45", "47", "50", "51", "52", "53", "60", "61", "64", "65", "67", "76", "78", "79", "81", "83", "86", "87", "88", "90", "92", "93", "96", "104", "105", "106", "108", "109", "207" ]); const unknownTopLevelKeys = Object.keys(dpsRoot).filter((key) => !knownTopLevelKeys.has(key)); const unknownCommonKeys = commonDps ? Object.keys(commonDps).filter((key) => !knownCommonKeys.has(key)) : []; const unknownKeys = [...unknownTopLevelKeys, ...unknownCommonKeys.map((key) => `101.${key}`)]; return { summary: unknownKeys.length === 0 && summaryParts.length > 0 ? summaryParts.join(" | ") : null, hasUnknownKeys: unknownKeys.length > 0 }; } public async dispatchProtocol102(duid: string, parsed: Record<string, unknown>, dps102: Record<string, unknown>): Promise<boolean> { const handler = await this.getQ10Handler(duid); if (!handler) return false; const dpsRoot = parsed.dps && typeof parsed.dps === "object" && !Array.isArray(parsed.dps) ? parsed.dps as Record<string, unknown> : undefined; const dps101 = dpsRoot?.["101"]; const resultList = Array.isArray(dps102.result) ? dps102.result : []; const result0 = resultList[0]; const isPropPost = dps102.method === "prop.post"; const hasStatusResult = !!(result0 && typeof result0 === "object" && "state" in (result0 as Record<string, unknown>)); const hasMapInfoResult = !!(result0 && typeof result0 === "object" && "map_info" in (result0 as Record<string, unknown>)); const hasConsumableResult = !!( result0 && typeof result0 === "object" && ( "main_brush_work_time" in (result0 as Record<string, unknown>) || "filter_element_work_time" in (result0 as Record<string, unknown>) || "dust_collection_work_times" in (result0 as Record<string, unknown>) ) ); const hasTimerResult = resultList.length > 0 && resultList.every((entry: unknown) => Array.isArray(entry) && entry.length >= 3); const isFlatQ10Shadow = !!(dpsRoot && (dpsRoot["101"] || dpsRoot["121"] || dpsRoot["122"] || dpsRoot["123"] || dpsRoot["124"])); const result0Keys = result0 && typeof result0 === "object" && !Array.isArray(result0) ? Object.keys(result0 as Record<string, unknown>).slice(0, 12).join(",") : ""; const summaryParts: string[] = []; if (hasStatusResult) summaryParts.push("status"); if (hasMapInfoResult) summaryParts.push("map_info"); if (hasConsumableResult) summaryParts.push("consumables"); if (hasTimerResult) summaryParts.push(`timers:${resultList.length}`); if (result0Keys) summaryParts.push(`result0:${result0Keys}`); const flatShadowInfo = this.summarizeFlatShadow(dpsRoot ?? {}); const summary = summaryParts.join(" | ") || flatShadowInfo.summary; this.logSummaryIfChanged(duid, summary || null); if (hasStatusResult && !isPropPost && typeof handler.applyQ10StatusFromDpResult === "function") { await handler.applyQ10StatusFromDpResult(result0 as Record<string, unknown>); } if (hasMapInfoResult && typeof handler.applyQ10MapInfoFromDpResult === "function") { await handler.applyQ10MapInfoFromDpResult(result0 as Record<string, unknown>); } if (hasConsumableResult && typeof handler.applyQ10ConsumablesFromDpResult === "function") { await handler.applyQ10ConsumablesFromDpResult(result0 as Record<string, unknown>); } if (hasTimerResult && typeof handler.applyQ10TimersFromDpResult === "function") { await handler.applyQ10TimersFromDpResult(resultList); } if (isFlatQ10Shadow && typeof handler.applyQ10ShadowDpPayload === "function") { await handler.applyQ10ShadowDpPayload(dpsRoot!); } const net81 = dps101 && typeof dps101 === "object" && !Array.isArray(dps101) ? (dps101 as Record<string, unknown>)["81"] : undefined; if (net81 && typeof net81 === "object" && !Array.isArray(net81) && typeof handler.applyQ10NetworkFromDp81 === "function") { await handler.applyQ10NetworkFromDp81(net81 as Record<string, unknown>); } return (hasStatusResult || hasMapInfoResult || hasConsumableResult || hasTimerResult || isFlatQ10Shadow || !!net81) && !flatShadowInfo.hasUnknownKeys; } public async tryHandleCleanRecordBlob(duid: string, payloadBuf: Buffer): Promise<boolean> { const q10BlobType = B01MapDecryptor.getQ10BlobType(payloadBuf); if (q10BlobType !== 3) return false; const handler = await this.getQ10Handler(duid); if (!handler || typeof handler.applyQ10CleanRecordBlob !== "function") return false; if (typeof handler.hasPendingQ10CleanRecordBlobRequest === "function" && !handler.hasPendingQ10CleanRecordBlobRequest()) { return false; } return handler.applyQ10CleanRecordBlob(payloadBuf); } }