UNPKG

matterbridge-roborock-vacuum-plugin

Version:
238 lines 9.64 kB
import crypto from 'node:crypto'; import https from 'node:https'; import { default as axios } from 'axios'; import * as AxiosLogger from 'axios-logger'; import axiosRetry from 'axios-retry'; import { debugStringify } from 'matterbridge/logger'; export class RoborockIoTApi { logger; api; vacuumNeedAPIV3 = ['roborock.vacuum.ss07']; // Q10 S5 Plus constructor(userdata, logger, axiosFactory = axios) { this.logger = logger; this.api = axiosFactory.create({ baseURL: userdata.rriot.r.a, timeout: 10000, maxRedirects: 5, httpsAgent: new https.Agent({ keepAlive: true }), }); // Retry transient network errors (including ECONNRESET) with exponential backoff try { axiosRetry(this.api, { retries: 3, retryDelay: axiosRetry.exponentialDelay, retryCondition: (error) => { const errTyped = error; const isNetwork = axiosRetry.isNetworkOrIdempotentRequestError(errTyped); const code = errTyped?.code; const isEconnreset = code === 'ECONNRESET'; const isTimedOut = code === 'ETIMEDOUT' || code === 'ECONNABORTED' || /timeout/i.test(errTyped?.message ?? ''); return isNetwork || isEconnreset || isTimedOut; }, onRetry: (retryCount, error, _requestConfig) => { this.logger.warn(`Retrying request, attempt #${retryCount} due to error: ${error ? debugStringify(error) : 'unknown'}`); }, }); } catch (err) { this.logger.error(`Failed to configure axios-retry: ${err ? debugStringify(err) : 'unknown'}`); } this.api.interceptors.request.use((config) => { try { const url = this.api ? new URL(this.api.getUri(config)).pathname : ''; config.headers.Authorization = this.getHawkAuthentication(userdata.rriot, url); } catch (error) { this.logger.error(`Failed to initialize RESTAPI ${error ? debugStringify(error) : 'undefined'}`); } return config; }); this.api.interceptors.request.use((request) => { return AxiosLogger.requestLogger(request, { prefixText: 'Roborock IoT API', dateFormat: 'HH:MM:ss', headers: true, data: true, method: true, url: true, params: true, logger: this.logger.debug.bind(this.logger), }); }, AxiosLogger.errorLogger); this.api.interceptors.response.use((response) => { AxiosLogger.responseLogger(response, { prefixText: 'Roborock IoT API', dateFormat: 'HH:MM:ss', headers: true, data: true, status: true, statusText: true, params: true, logger: this.logger.debug.bind(this.logger), }); return response; }, AxiosLogger.errorLogger); } async getHomeWithProducts(homeId) { let homeData = await this.getHome(homeId); let homeDataV2; let homeDataV3; if (!homeData) { this.logger.error('[getHomeWithProducts Step 1] Failed to retrieve the home data'); homeDataV2 = await this.getHomev2(homeId); homeData = homeDataV2; } if (!homeData) { this.logger.error('[getHomeWithProducts Step 2] Failed to retrieve the home data'); homeDataV3 = await this.getHomev3(homeId); homeData = homeDataV3; } if (!homeData) { this.logger.error('[getHomeWithProducts Step 3] Failed to retrieve the home data'); return undefined; } if (homeData.products.some((p) => this.vacuumNeedAPIV3.includes(p.model))) { this.logger.debug('Using v3 API for home data retrieval'); homeDataV3 = homeDataV3 ?? (await this.getHomev3(homeId)); if (!homeDataV3) { throw new Error('Failed to retrieve the home data from v3 API'); } homeData.devices = [ ...homeData.devices, ...homeDataV3.devices.filter((d) => !homeData.devices.some((x) => x.duid === d.duid)), ]; homeData.receivedDevices = [ ...homeData.receivedDevices, ...homeDataV3.receivedDevices.filter((d) => !homeData.receivedDevices.some((x) => x.duid === d.duid)), ]; } if (homeData.rooms.length === 0) { homeDataV2 = homeDataV2 ?? (await this.getHomev2(homeId)); if (homeDataV2?.rooms && homeDataV2.rooms.length > 0) { homeData.rooms = homeDataV2.rooms; } else { homeDataV3 = homeDataV3 ?? (await this.getHomev3(homeId)); if (homeDataV3?.rooms && homeDataV3.rooms.length > 0) { homeData.rooms = homeDataV3.rooms; } } } return homeData; } async getHome(homeId) { try { const result = await this.api.get(`user/homes/${homeId}`); const apiResponse = result.data; if (apiResponse.result) { return apiResponse.result; } this.logger.error('[getHome] Failed to retrieve the home data'); return undefined; } catch (error) { this.logger.error(`[getHome] Failed: ${error ? debugStringify(error) : 'unknown'}`); return undefined; } } async getHomev2(homeId) { try { const result = await this.api.get(`v2/user/homes/${homeId}`); const apiResponse = result.data; if (apiResponse.result) { return apiResponse.result; } this.logger.error('[getHomev2] Failed to retrieve the home data'); return undefined; } catch (error) { this.logger.error(`[getHomev2] Failed: ${error ? debugStringify(error) : 'unknown'}`); return undefined; } } async getHomev3(homeId) { try { const result = await this.api.get(`v3/user/homes/${homeId}`); // can be v3 also const apiResponse = result.data; if (apiResponse.result) { return apiResponse.result; } this.logger.error('[getHomev3] Failed to retrieve the home data'); return undefined; } catch (error) { this.logger.error(`[getHomev3] Failed: ${error ? debugStringify(error) : 'unknown'}`); return undefined; } } async getScenes(homeId) { try { const result = await this.api.get(`user/scene/home/${homeId}`); const apiResponse = result.data; if (apiResponse.result) { return apiResponse.result; } this.logger.error('[getScenes] Failed to retrieve scenes'); return undefined; } catch (error) { this.logger.error(`[getScenes] Failed: ${error ? debugStringify(error) : 'unknown'}`); return undefined; } } async startScene(sceneId) { try { const result = await this.api.post(`user/scene/${sceneId}/execute`); const apiResponse = result.data; if (apiResponse.success) { return apiResponse.success; } this.logger.error('[startScene] Failed to execute scene'); return undefined; } catch (error) { this.logger.error(`[startScene] Failed: ${error ? debugStringify(error) : 'unknown'}`); return undefined; } } async getCustom(url) { try { const result = await this.api.get(url); const apiResponse = result.data; if (apiResponse.result) { return apiResponse.result; } this.logger.error('[getCustom] Failed to execute custom request'); return undefined; } catch (error) { this.logger.error(`[getCustom] Failed: ${error ? debugStringify(error) : 'unknown'}`); return undefined; } } processExtraHawkValues(values) { if (values === undefined) return ''; const sortedKeys = Object.keys(values).sort(); const parts = sortedKeys.map((key) => `${key}=${values[key]}`); return crypto.createHash('md5').update(parts.join('&')).digest('hex'); } getHawkAuthentication(rriot, url, params, formdata) { const timestamp = Math.floor(Date.now() / 1000); const nonce = crypto.randomBytes(6).toString('base64url'); const paramsStr = this.processExtraHawkValues(params); const formdataStr = this.processExtraHawkValues(formdata); const prestr = [ rriot.u, rriot.s, nonce, String(timestamp), crypto.createHash('md5').update(url).digest('hex'), paramsStr, formdataStr, ].join(':'); const mac = crypto.createHmac('sha256', rriot.h).update(prestr).digest('base64'); return `Hawk id="${rriot.u}",s="${rriot.s}",ts="${timestamp}",nonce="${nonce}",mac="${mac}"`; } } //# sourceMappingURL=iotClient.js.map