matterbridge-roborock-vacuum-plugin
Version:
Matterbridge Roborock Vacuum Plugin
178 lines • 7.41 kB
JavaScript
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