UNPKG

matterbridge-roborock-vacuum-plugin

Version:
178 lines 7.41 kB
import { debugStringify } from 'matterbridge/logger'; import { MESSAGE_TIMEOUT_MS } from '../../../constants/index.js'; import { Q10RequestCode } from '../../enums/Q10RequestCode.js'; import { HeaderMessage, ResponseBody, ResponseMessage } from '../../models/index.js'; import { Protocol } from '../../models/protocol.js'; const RESPONSE_PROTOCOL_MAP = new Map([ [Protocol.hello_request, new Set([Protocol.hello_response])], [Protocol.ping_request, new Set([Protocol.ping_response])], [Protocol.general_request, new Set([Protocol.general_response])], [Protocol.rpc_request, new Set([Protocol.rpc_response])], ]); // Reverse map: numeric string → enum name (e.g. '121' → 'state') const CODE_NAME_MAP = new Map(Object.entries(Q10RequestCode) .filter(([, v]) => typeof v === 'number') .map(([name, value]) => [String(value), name])); const DEFAULT_COLLECTION_WINDOW_MS = 500; const DEFAULT_TIMESTAMP_TOLERANCE = 1; export class B01PendingResponseTracker { logger; collectionWindowMs; timestampTolerance; pending = []; constructor(logger, collectionWindowMs = DEFAULT_COLLECTION_WINDOW_MS, timestampTolerance = DEFAULT_TIMESTAMP_TOLERANCE) { this.logger = logger; this.collectionWindowMs = collectionWindowMs; this.timestampTolerance = timestampTolerance; } waitFor(request, duid) { return new Promise((resolve, reject) => { const expectedTimestamp = request.timestamp + 1; const expectedProtocols = RESPONSE_PROTOCOL_MAP.get(request.protocol) ?? new Set([request.protocol + 1]); this.logger.debug(`[B01PendingResponseTracker] Waiting for responses with timestamp in [${expectedTimestamp}, ${expectedTimestamp + this.timestampTolerance - 1}], protocols: ${[...expectedProtocols].join(', ')}`); const overallTimer = setTimeout(() => { this.removeEntry(entry); reject(new Error(`[B01PendingResponseTracker] Timeout for request: ${debugStringify(request)}`)); }, MESSAGE_TIMEOUT_MS); const collectionTimer = setTimeout(() => { /* no-op until first response arrives */ }, MESSAGE_TIMEOUT_MS); const entry = { duid, expectedTimestamp, expectedProtocols, collectedData: {}, lastHeader: new HeaderMessage('', 0, 0, 0, 0), resolve, reject, collectionTimer, overallTimer, }; this.pending.push(entry); }); } tryResolve(response) { if (!response.body) { return; } for (const entry of this.pending) { if (!this.matches(response, entry)) { continue; } this.mergeData(entry, response); this.resetCollectionWindow(entry); return; } } cancelAll() { for (const entry of this.pending) { clearTimeout(entry.collectionTimer); clearTimeout(entry.overallTimer); entry.reject(new Error('[B01PendingResponseTracker] Cancelled')); } this.pending.length = 0; } matches(response, entry) { const ts = response.header.timestamp; const proto = response.header.protocol; return (ts >= entry.expectedTimestamp && ts < entry.expectedTimestamp + this.timestampTolerance && entry.expectedProtocols.has(proto) && entry.duid === response.duid); } mergeData(entry, response) { if (!response.body) { return; } const data = response.body.data; // Separate object keys (e.g. '101') from primitive keys (e.g. '121', '122') const objectKeys = []; const primitiveKeys = []; for (const key of Object.keys(data)) { const value = data[key]; if (typeof value === 'object' && value !== null && !Buffer.isBuffer(value)) { objectKeys.push(key); } else { primitiveKeys.push(key); } } // Merge object keys normally for (const key of objectKeys) { const existing = entry.collectedData[key]; const incoming = data[key]; if (existing !== undefined && typeof existing === 'object' && !Buffer.isBuffer(existing)) { entry.collectedData[key] = { ...existing, ...incoming }; } else { entry.collectedData[key] = incoming; } } // Fold primitive keys into the primary object key (e.g. '101') if one exists // Check current response first, then fall back to accumulated data const parentKey = objectKeys[0] ?? this.findParentObjectKey(entry); if (parentKey && primitiveKeys.length > 0) { const parent = entry.collectedData[parentKey]; for (const key of primitiveKeys) { parent[key] = data[key]; } } else { for (const key of primitiveKeys) { entry.collectedData[key] = data[key]; } } entry.lastHeader = response.header; this.logger.debug(`[B01PendingResponseTracker] Collected data chunk, keys: ${Object.keys(entry.collectedData).join(', ')}`); } findParentObjectKey(entry) { for (const key of Object.keys(entry.collectedData)) { const value = entry.collectedData[key]; if (typeof value === 'object' && value !== null && !Buffer.isBuffer(value)) { return key; } } return undefined; } resetCollectionWindow(entry) { clearTimeout(entry.collectionTimer); entry.collectionTimer = setTimeout(() => { this.logger.debug(`[B01PendingResponseTracker] Collection window closed, resolving merged response`); clearTimeout(entry.overallTimer); this.removeEntry(entry); const mappedData = this.mapDataKeys(entry.collectedData); const mergedBody = new ResponseBody(mappedData); const mergedResponse = new ResponseMessage(entry.duid, entry.lastHeader, mergedBody); entry.resolve(mergedResponse); }, this.collectionWindowMs); } mapDataKeys(data) { const result = {}; for (const key of Object.keys(data)) { const mappedKey = CODE_NAME_MAP.get(key) ?? key; const value = data[key]; if (typeof value === 'object' && value !== null && !Buffer.isBuffer(value)) { const inner = {}; for (const innerKey of Object.keys(value)) { const mappedInnerKey = CODE_NAME_MAP.get(innerKey) ?? innerKey; inner[mappedInnerKey] = value[innerKey]; } result[mappedKey] = inner; } else { result[mappedKey] = value; } } return result; } removeEntry(entry) { const idx = this.pending.indexOf(entry); if (idx >= 0) { clearTimeout(entry.collectionTimer); clearTimeout(entry.overallTimer); this.pending.splice(idx, 1); } } } //# sourceMappingURL=b01PendingResponseTracker.js.map