diff --git a/.github/workflows/test-and-release.yml b/.github/workflows/test-and-release.yml index f46d4c48c9..a71996ebff 100644 --- a/.github/workflows/test-and-release.yml +++ b/.github/workflows/test-and-release.yml @@ -40,7 +40,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - node-version: [18.x, 20.x] + node-version: [18.x, 20.x, 22.x] os: [ubuntu-latest, windows-latest] steps: diff --git a/README.md b/README.md index 8c8aba2660..d2b31fe384 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,11 @@ This feature only works when map creation is enabled in the adapter options! Placeholder for the next version (at the beginning of the line): ### **WORK IN PROGRESS** --> +### **WORK IN PROGRESS** + * (copystring) Refactor some code + * (copystring) improve handling of online/offline detection and related logging + * (copystring) S6 MaxV supports avoid carpet + ### 0.6.14 (2024-09-13) * (copystring) Fix bug in app_goto_target parameter validation diff --git a/lib/deviceFeatures.js b/lib/deviceFeatures.js index 17419549e7..6ff8db2ab2 100644 --- a/lib/deviceFeatures.js +++ b/lib/deviceFeatures.js @@ -293,13 +293,11 @@ const actions = { }; class deviceFeatures { - constructor(adapter, features, featuresStr, duid, model, productCategory) { + constructor(adapter, features, featuresStr, duid) { this.adapter = adapter; this.features = features; this.featuresStr = featuresStr; this.duid = duid; - this.model = model; - this.productCategory = productCategory; this.cleaningInfo = {}; this.cleaningRecords = {}; this.consumables = {}; @@ -557,6 +555,8 @@ class deviceFeatures { } getFeatureList() { + const robotModel = this.adapter.getProductAttribute(this.duid, "model"); + return { isWashThenChargeCmdSupported: ((this.features / Math.pow(2, 32)) >> 5) & 1, isDustCollectionSettingSupported: !!(33554432 & this.features), @@ -569,7 +569,7 @@ class deviceFeatures { isAvoidCollisionSupported: !!(134217728 & this.features), isCornerCleanModeSupported: !!(2147483648 & this.features), // isCameraSupported: [p.Products.TanosV_CN, p.Products.TanosV_CE, p.Products.TopazSV_CN, p.Products.TopazSV_CE, p.Products.TanosSV].hasElement(p.DMM.currentProduct), - isCameraSupported: !!["roborock.vacuum.a10", "roborock.vacuum.a27", "roborock.vacuum.a51", "roborock.vacuum.a87"].includes(this.model), + isCameraSupported: !!["roborock.vacuum.a10", "roborock.vacuum.a27", "roborock.vacuum.a51", "roborock.vacuum.a87"].includes(robotModel), isSupportSetSwitchMapMode: !!(268435456 & this.features), // isMopForbiddenSupported: !!(p.DMM.isTanosV || p.DMM.isTanos || p.DMM.isTopazSV || p.DMM.isPearlPlus) || !![p.Products.TanosE, p.Products.TanosSL, p.Products.TanosS, p.Products.TanosSPlus, p.Products.TanosSMax, p.Products.Ultron, p.Products.UltronLite, p.Products.Pearl, p.Products.RubysLite].hasElement(p.DMM.currentProduct), isMopForbiddenSupported: [ @@ -590,7 +590,7 @@ class deviceFeatures { "roborock.vacuum.a87", // Qrevo MaxV "roborock.vacuum.a101", // Q Revo Pro "roborock.vacuum.a97", // S8 MaxV (Ultra) - ].includes(this.model), + ].includes(robotModel), // isShakeMopStrengthSupported: p.DMM.currentProduct == p.Products.TanosS || p.DMM.currentProduct == p.Products.TanosSPlus || p.DMM.isGarnet || p.DMM.isTopazSV || p.DMM.isPearlPlus || p.DMM.isCoral || p.DMM.isTopazS || p.DMM.isTopazSPlus || p.DMM.isTopazSC || p.DMM.isTopazSV || p.DMM.isPearlPlus || p.DMM.isTanosSMax || p.DMM.isUltron || p.DMM.isUltronSPlus || p.DMM.isUltronSMop || p.DMM.isUltronSV || p.DMM.isPearl isShakeMopStrengthSupported: [ "roborock.vacuum.a08", // S6 Pure @@ -611,7 +611,7 @@ class deviceFeatures { "roborock.vacuum.s5e", // S5 Max "roborock.vacuum.a87", // Qrevo MaxV "roborock.vacuum.a101", // Q Revo Pro - ].includes(this.model), + ].includes(robotModel), // isWaterBoxSupported: [p.Products.Tanos_CE, p.Products.Tanos_CN].hasElement(p.DMM.currentProduct) isWaterBoxSupported: [ "roborock.vacuum.s5e", // S5 Max @@ -631,10 +631,11 @@ class deviceFeatures { "roborock.vacuum.a87", // Qrevo MaxV "roborock.vacuum.a101", // Q Revo Pro "roborock.vacuum.a97", // S8 MaxV (Ultra) - ].includes(this.model), + ].includes(robotModel), isCustomWaterBoxDistanceSupported: !!(2147483648 & this.features), isBackChargeAutoWashSupported: this.featuresStr && !!(4096 & parseInt("0x" + this.featuresStr.slice(-8))), isAvoidCarpetSupported: [ + "roborock.vacuum.a10", // S6 MaxV "roborock.vacuum.a40", // Q7 "roborock.vacuum.s6", // S6 "roborock.vacuum.a72", // Q5 Pro @@ -650,7 +651,7 @@ class deviceFeatures { "roborock.vacuum.a87", // Qrevo MaxV "roborock.vacuum.a101", // Q Revo Pro "roborock.vacuum.a97", // S8 MaxV (Ultra) - ].includes(this.model), + ].includes(robotModel), // this isn't the correct way to use this. This code must be from a different robot // isVoiceControlSupported: !!(parseInt(`0x${this.featuresStr || "0"}`.slice(-10, -9)) & 2), isVoiceControlSupported: [ @@ -663,12 +664,15 @@ class deviceFeatures { "roborock.vacuum.a27", // S7 MaxV (Ultra) "roborock.vacuum.a97", // S8 MaxV (Ultra) "roborock.vacuum.a87", // Qrevo MaxV - ].includes(this.model), + ].includes(robotModel), }; } async processSupportedFeatures() { - if (this.productCategory == "robot.vacuum.cleaner") { + const robotModel = this.adapter.getProductAttribute(this.duid, "model"); + const productCategory = this.adapter.getProductAttribute(this.duid, "category"); + + if (productCategory == "robot.vacuum.cleaner") { // process states etc. depending on model const modelConfig = { // S6 Pure @@ -840,7 +844,7 @@ class deviceFeatures { }; // process modelConfig - const configActions = modelConfig[this.model]; + const configActions = modelConfig[robotModel]; if (configActions) { for (const actionName of configActions) { const action = actions[actionName]; @@ -849,13 +853,13 @@ class deviceFeatures { } } } else { - this.adapter.catchError(`This robot ${this.model} is not fully supported just yet. Contact the dev to get this robot fully supported!`); + this.adapter.catchError(`This robot ${robotModel} is not fully supported just yet. Contact the dev to get this robot fully supported!`); } this.adapter.createBaseRobotObjects(this.duid); const featureList = this.getFeatureList(); - this.adapter.log.debug(`Supported features of robot ${this.duid} - ${this.model}: ${JSON.stringify(featureList)}`); + this.adapter.log.debug(`Supported features of robot ${this.duid} - ${robotModel}: ${JSON.stringify(featureList)}`); Object.keys(featureList).forEach((feature) => { if (featureList[feature]) { if (typeof this[feature] === "function") { @@ -900,10 +904,10 @@ class deviceFeatures { for (const [cleaningRecord, object] of Object.entries(this.cleaningRecords)) { await this.adapter.createCleaningRecord(this.duid, cleaningRecord, object.type, object.states, object.unit); } - } else if (this.productCategory == "roborock.vacuum") { + } else if (productCategory == "roborock.vacuum") { // vacuum (not sure if it's actually roborock.vacuum. Might be something else. Haven't testet) this.adapter.createBasicVacuumObjects(this.duid); - } else if (this.productCategory == "roborock.wm") { + } else if (productCategory == "roborock.wm") { // washing machine this.adapter.createBasicWashingMachineObjects(this.duid); } @@ -955,7 +959,8 @@ class deviceFeatures { } getConsumablesDivider(consumable) { - const consumables = this.model == "roborock.vacuum.s4" ? consumablesInt : consumablesString; + const robotModel = this.adapter.getProductAttribute(this.duid, "model"); + const consumables = robotModel == "roborock.vacuum.s4" ? consumablesInt : consumablesString; if (consumables[consumable]) { return consumables[consumable].divider; diff --git a/lib/localConnector.js b/lib/localConnector.js index 369ae6c320..7cb2f30647 100644 --- a/lib/localConnector.js +++ b/lib/localConnector.js @@ -70,9 +70,12 @@ class localConnector { reject(error); }); }).catch((error) => { - this.adapter.log.info(`error on tcp client for ${duid}. Marking this device as remote device. Connecting via MQTT instead ${error.message}`); - this.adapter.remoteDevices.add(duid); - // this.adapter.catchError(`Failed to create tcp client: ${error.stack}`, `function createClient`, duid); + const online = this.adapter.onlineChecker(duid); + if (online) { // if the device is online, we can assume that the device is a remote device + this.adapter.log.info(`error on tcp client for ${duid}. Marking this device as remote device. Connecting via MQTT instead ${error.message}`); + this.adapter.remoteDevices.add(duid); + // this.adapter.catchError(`Failed to create tcp client: ${error.stack}`, `function createClient`, duid); + } }); client.on("data", async (message) => { diff --git a/lib/messageQueueHandler.js b/lib/messageQueueHandler.js index d858da5ef5..249605ec3f 100644 --- a/lib/messageQueueHandler.js +++ b/lib/messageQueueHandler.js @@ -24,21 +24,33 @@ class messageQueueHandler { const roborockMessage = await this.adapter.message.buildRoborockMessage(duid, protocol, timestamp, payload); const deviceOnline = await this.adapter.onlineChecker(duid); + const mqttConnectionState = this.adapter.rr_mqtt_connector.isConnected(); + const localConnectionState = this.adapter.localConnector.isConnected(duid); + if (roborockMessage) { return new Promise((resolve, reject) => { if (!deviceOnline) { this.adapter.pendingRequests.delete(messageID); - reject(new Error(`Device ${duid} offline. Not sending request!`)); + this.adapter.log.debug(`Device ${duid} offline. Not sending for method ${method} request!`); + reject(); + } + else if (!mqttConnectionState && remoteConnection) { + this.adapter.pendingRequests.delete(messageID); + this.adapter.log.debug(`Cloud connection not available. Not sending for method ${method} request!`); + reject(); + } + else if (!localConnectionState && !remoteConnection) { + this.adapter.pendingRequests.delete(messageID); + this.adapter.log.debug(`Adapter not connect locally to robot ${duid}. Not sending for method ${method} request!`); + reject(); } else { // setup Timeout const timeout = this.adapter.setTimeout(() => { this.adapter.pendingRequests.delete(messageID); this.adapter.localConnector.clearChunkBuffer(duid); if (remoteConnection) { - const mqttConnectionState = this.adapter.rr_mqtt_connector.isConnected(); reject(new Error(`Cloud request with id ${messageID} with method ${method} timed out after 10 seconds. MQTT connection state: ${mqttConnectionState}`)); } else { - const localConnectionState = this.adapter.localConnector.isConnected(duid); reject(new Error(`Local request with id ${messageID} with method ${method} timed out after 10 seconds Local connect state: ${localConnectionState}`)); } }, requestTimeout); diff --git a/lib/roborock_mqtt_connector.js b/lib/roborock_mqtt_connector.js index f85eec06c1..5dad5c7c35 100644 --- a/lib/roborock_mqtt_connector.js +++ b/lib/roborock_mqtt_connector.js @@ -117,8 +117,7 @@ class roborock_mqtt_connector { await client.on("reconnect", (error) => { if (error) { this.adapter.catchError(`Failed to reconnect to MQTT server.`, `mqtt client reconnect`); - } - else { + } else { client.subscribe(`rr/m/o/${rriot.u}/${mqttUser}/#`, (err, granted) => { if (err) { this.adapter.catchError(`Failed to subscribe to Roborock MQTT Server! Error: ${err}, granted: ${JSON.stringify(granted)}`, `client.on("reconnect")`); @@ -231,8 +230,44 @@ class roborock_mqtt_connector { } } } - } else { - this.adapter.log.warn(`Unable to decode message for ${duid}. The the device is most likely offline. data: ${JSON.stringify(data)}`); + } else if (data.protocol == 500) { // 500 is for general information + const dataString = data.payload.toString("utf8"); + let parsedData; + + try { + parsedData = JSON.parse(dataString); + } catch (error) { + // If parsing fails, the data might be corrupted or in an unexpected format + this.adapter.log.warn(`Unable to parse message for ${duid}. Error: ${error.message}. Data: ${dataString}`); + return; + } + + // Check if the device is online + if (parsedData.online == false) { + this.adapter.log.info(`Couldn't process message. The device ${duid} is offline.`); + } else if (parsedData.online == true) { + // this.adapter.log.info(`Device ${duid} is online.`); + } else if ( + // Check for firmware update information + parsedData.mqttOtaData + ) { + const otaStatus = parsedData.mqttOtaData.mqttOtaStatus?.status; + const otaProgress = parsedData.mqttOtaData.mqttOtaProgress?.progress; + + if (otaStatus) { + this.adapter.log.info(`Device ${duid} firmware update status: ${otaStatus}`); + } + + if (otaProgress !== undefined) { + this.adapter.log.info(`Device ${duid} firmware update progress: ${otaProgress}%`); + } + } else { + // Received an unrecognized message + this.adapter.log.warn(`Received an unrecognized message for ${duid}. Data: ${dataString}`); + } + } + else { + this.adapter.log.debug(`Received message with unknown protocol ${data.protocol} data: ${JSON.stringify(data)}.`); } } catch (error) { this.adapter.log.error(`client.on message: ${error.stack} with topic ${topic} and message ${message.toString("hex")}`); diff --git a/lib/vacuum.js b/lib/vacuum.js index 43b760f1e7..fae5c2ec04 100644 --- a/lib/vacuum.js +++ b/lib/vacuum.js @@ -284,6 +284,7 @@ class vacuum { for (const state in dockingStationStatus) { this.adapter.setStateAsync(`Devices.${duid}.dockingStationStatus.${state}`, { val: parseInt(dockingStationStatus[state]), ack: true }); } + break; case "map_status": { deviceStatus[0][attribute] = deviceStatus[0][attribute] >> 2 ?? -1; // to get the currently selected map perform bitwise right shift @@ -324,7 +325,7 @@ class vacuum { break; } - this.adapter.setStateAsync(`Devices.${duid}.deviceStatus.${attribute}`, { val: deviceStatus[0][attribute], ack: true }); + this.adapter.setStateChangedAsync(`Devices.${duid}.deviceStatus.${attribute}`, { val: deviceStatus[0][attribute], ack: true }); } this.adapter.manageDeviceIntervals(duid); } diff --git a/main.js b/main.js index fb678561d6..d50e07a5f5 100644 --- a/main.js +++ b/main.js @@ -9,7 +9,6 @@ const express = require("express"); const childProcess = require("child_process"); const go2rtcPath = require("go2rtc-static"); // Pfad zur Binärdatei - const rrLocalConnector = require("./lib/localConnector").localConnector; const roborock_mqtt_connector = require("./lib/roborock_mqtt_connector").roborock_mqtt_connector; const rrMessage = require("./lib/message").message; @@ -73,14 +72,17 @@ class Roborock extends utils.Adapter { // create new clientID if it doesn't exist yet let clientID = ""; - this.getStateAsync("clientID").then(async (storedClientID) => { + try { + const storedClientID = await this.getStateAsync("clientID"); if (storedClientID) { clientID = storedClientID.val?.toString() ?? ""; } else { clientID = crypto.randomUUID(); await this.setStateAsync("clientID", { val: clientID, ack: true }); } - }); + } catch (error) { + this.log.error(`Error while retrieving or setting clientID: ${error.message}`); + } if (!this.config.username || !this.config.password) { this.log.error("Username or password missing!"); @@ -160,9 +162,9 @@ class Roborock extends utils.Adapter { }); // create devices and set states - const products = homedataResult.products; - const devices = homedataResult.devices.concat(homedataResult.receivedDevices); - this.localKeys = new Map(devices.map((device) => [device.duid, device.localKey])); + this.products = homedataResult.products; + this.devices = homedataResult.devices.concat(homedataResult.receivedDevices); + this.localKeys = new Map(this.devices.map((device) => [device.duid, device.localKey])); // this.adapter.log.debug(`initUser test: ${JSON.stringify(Array.from(this.adapter.localKeys.entries()))}`); await this.rr_mqtt_connector.initUser(userdata); @@ -177,24 +179,24 @@ class Roborock extends utils.Adapter { this.roomIDs[roomID] = roomName; } - this.log.debug("RoomIDs debug: " + JSON.stringify(this.roomIDs)); + this.log.debug(`RoomIDs debug: ${JSON.stringify(this.roomIDs)}`); // reconnect every 3 hours (10800 seconds) this.reconnectIntervall = this.setInterval(async () => { - this.log.debug("Reconnecting after 3 hours!"); + this.log.debug(`Reconnecting after 3 hours!`); await this.rr_mqtt_connector.reconnectClient(); }, 3600 * 1000); this.processScene(scene); - this.homedataInterval = this.setInterval(this.updateHomeData.bind(this), 180 * 1000, homeId); + this.homedataInterval = this.setInterval(this.updateHomeData.bind(this), this.config.updateInterval * 1000, homeId); await this.updateHomeData(homeId); const discoveredDevices = await this.localConnector.getLocalDevices(); - await this.createDevices(products, devices); - await this.getNetworkInfo(devices); + await this.createDevices(); + await this.getNetworkInfo(); // merge udp discovered devices with local devices found via mqtt Object.entries(discoveredDevices).forEach(([duid, ip]) => { @@ -210,7 +212,7 @@ class Roborock extends utils.Adapter { await this.localConnector.createClient(duid, ip); } - this.initializeDeviceUpdates(products, devices); + this.initializeDeviceUpdates(); // These need to start only after all states have been set if (this.config.enable_map_creation == true) { @@ -236,42 +238,37 @@ class Roborock extends utils.Adapter { } async getUserData(loginApi) { - // try log in. - const userdata = await loginApi - .post( + try { + const response = await loginApi.post( "api/v1/login", new URLSearchParams({ username: this.config.username, password: this.config.password, needtwostepauth: "false", }).toString() - ) - .then((res) => res.data.data) - .catch((error) => { - this.catchError(error.stack, "getUserData"); - return; - }); + ); + const userdata = response.data.data; - // Alternative without password: - // await loginApi.post("api/v1/sendEmailCode", new url.URLSearchParams({username: username, type: "auth"}).toString()).then(res => res.data); - // // ... get code from user ... - // userdata = await loginApi.post("api/v1/loginWithCode", new url.URLSearchParams({username: username, verifycode: code, verifycodetype: "AUTH_EMAIL_CODE"}).toString()).then(res => res.data.data); + if (!userdata) { + throw new Error("Login returned empty userdata."); + } - if (userdata == null) { - this.deleteStateAsync("HomeData"); - this.deleteStateAsync("UserData"); - this.log.error("Error! Failed to login. Maybe wrong username or password?"); - return; - } - await this.setStateAsync("UserData", { - val: JSON.stringify(userdata), - ack: true, - }); + await this.setStateAsync("UserData", { + val: JSON.stringify(userdata), + ack: true, + }); - return userdata; + return userdata; + } catch (error) { + this.log.error(`Error in getUserData: ${error.message}`); + await this.deleteStateAsync("HomeData"); + await this.deleteStateAsync("UserData"); + throw error; + } } - async getNetworkInfo(devices) { + async getNetworkInfo() { + const devices = this.devices; for (const device in devices) { const duid = devices[device].duid; const vacuum = this.vacuums[duid]; @@ -279,18 +276,18 @@ class Roborock extends utils.Adapter { } } - async createDevices(products, devices) { - for (const device in devices) { - const productId = devices[device]["productId"]; - // const robotModel = products[device]["model"]; - const robotModel = this.getRobotModel(products, productId); - const productCategory = this.getProductCategory(products, productId); - const duid = devices[device].duid; - const name = devices[device].name; + async createDevices() { + const devices = this.devices; + + for (const device of devices) { + const duid = device.duid; + const name = device.name; + + const robotModel = this.getProductAttribute(duid, "model"); this.vacuums[duid] = new vacuum_class(this, robotModel); this.vacuums[duid].name = name; - this.vacuums[duid].features = new deviceFeatures(this, devices[device].featureSet, devices[device].newFeatureSet, duid, robotModel, productCategory); + this.vacuums[duid].features = new deviceFeatures(this, device.featureSet, device.newFeatureSet, duid); await this.vacuums[duid].features.processSupportedFeatures(); @@ -300,33 +297,36 @@ class Roborock extends utils.Adapter { this.subscribeStates("Devices." + duid + ".commands.*"); this.subscribeStates("Devices." + duid + ".reset_consumables.*"); this.subscribeStates("Devices." + duid + ".programs.startProgram"); + this.subscribeStates("Devices." + duid + ".deviceInfo.online"); } } - async initializeDeviceUpdates(products, devices) { + async initializeDeviceUpdates() { this.log.debug(`initializeDeviceUpdates`); - for (const deviceId in devices) { - const device = devices[deviceId]; + + const devices = this.devices; + + for (const device of devices) { const duid = device.duid; - const productId = device["productId"]; - const robotModel = this.getRobotModel(products, productId); + const robotModel = this.getProductAttribute(duid); this.vacuums[duid].mainUpdateInterval = () => this.setInterval(this.updateDataMinimumData.bind(this), this.config.updateInterval * 1000, duid, this.vacuums[duid], robotModel); + if (device.online) { - this.log.debug(duid + " online. Starting mainUpdateInterval."); + this.log.debug(`${duid} online. Starting mainUpdateInterval.`); this.vacuums[duid].mainUpdateInterval(); // actually start mainUpdateInterval() } this.vacuums[duid].getStatusIntervall = () => this.setInterval(this.getStatus.bind(this), 1000, duid, this.vacuums[duid], robotModel); + if (device.online) { - this.log.debug(duid + " online. Starting getStatusIntervall."); + this.log.debug(`${duid} online. Starting getStatusIntervall.`); this.vacuums[duid].getStatusIntervall(); // actually start getStatusIntervall() - // Map updater gets started automatically via getParameter with get_status } - this.updateDataExtraData(duid, this.vacuums[duid]); - this.updateDataMinimumData(duid, this.vacuums[duid], robotModel); + await this.updateDataExtraData(duid, this.vacuums[duid]); + await this.updateDataMinimumData(duid, this.vacuums[duid], robotModel); await this.vacuums[duid].getCleanSummary(duid); @@ -410,7 +410,7 @@ class Roborock extends utils.Adapter { async startMapUpdater(duid) { if (!this.vacuums[duid].mapUpdater) { - this.log.debug("Started map updater on robot: " + duid); + this.log.debug(`Started map updater on robot: ${duid}`); this.vacuums[duid].mapUpdater = this.setInterval(() => { this.vacuums[duid].getMap(duid); }, this.config.map_creation_interval * 1000); @@ -437,7 +437,7 @@ class Roborock extends utils.Adapter { webserver.on("error", (error) => { // This code will run if there was an error starting the server - this.log.error("Error occurred: " + error); + this.log.error(`Error occurred: ${error}`); }); } async stopWebserver() { @@ -450,7 +450,7 @@ class Roborock extends utils.Adapter { socketServer.on("connection", async (socket) => { this.socket = socket; - this.log.debug("Websocket client connected"); + this.log.debug(`Websocket client connected`); socket.on("pong", () => { this.socket = socket; @@ -458,7 +458,7 @@ class Roborock extends utils.Adapter { this.webSocketInterval = this.setInterval(() => { if (!this.socket) { - this.log.debug("Client disconnected. Stopping interval."); + this.log.debug(`Client disconnected. Stopping interval.`); this.clearInterval(this.webSocketInterval); socket.terminate(); return; @@ -508,7 +508,7 @@ class Roborock extends utils.Adapter { if (homedata) { const homedataVal = homedata.val; if (typeof homedataVal == "string") { - // this.log.debug("Sniffing message received!"); + // this.log.debug(`Sniffing message received!`); const homedataParsed = JSON.parse(homedataVal); this.decodeSniffedMessage(data, homedataParsed.devices); @@ -531,7 +531,7 @@ class Roborock extends utils.Adapter { }); socket.on("close", () => { - this.log.debug("Client disconnected"); + this.log.debug(`Client disconnected`); this.clearInterval(this.webSocketInterval); this.socket = null; }); @@ -545,58 +545,47 @@ class Roborock extends utils.Adapter { socketServer.close(); } - getRobotModel(products, productID) { - for (const product in products) { - if (products[product].id == productID) { - return products[product].model; - } - } - } + getProductAttribute(duid, attribute) { + const products = this.products; + const productID = this.devices.find((device) => device.duid == duid).productId; + const product = products.find((product) => product.id == productID); - getProductCategory(products, productID) { - for (const product in products) { - if (products[product].id == productID) { - return products[product].category; - } - } + return product ? product[attribute] : null; } startMainUpdateInterval(duid, online) { - const robotModel = this.getRobotModel(duid); + const robotModel = this.getProductAttribute(duid, "model"); this.vacuums[duid].mainUpdateInterval = () => this.setInterval(this.updateDataMinimumData.bind(this), this.config.updateInterval * 1000, duid, this.vacuums[duid], robotModel); if (online) { - this.log.debug(duid + " online. Starting mainUpdateInterval."); + this.log.debug(`${duid} online. Starting mainUpdateInterval.`); this.vacuums[duid].mainUpdateInterval(); // actually start mainUpdateInterval() // Map updater gets startet automatically via getParameter with get_status } } decodeSniffedMessage(data, devices) { - devices.forEach((device) => { - const data_string = JSON.stringify(data); - - const parts = data_string.split("/"); - const duid_sniffed = parts[parts.length - 1].slice(0, -3); - - if (duid_sniffed == device.duid) { - const localKey = JSON.stringify(device.localKey).slice(1, -1); - // this.log.debug("duid_sniffed: " + duid_sniffed); - // this.log.debug("Device duid: " + JSON.stringify(device.duid).slice(1, -1)); - // this.log.debug("Device localKey: " + localKey); - - const startIndex = data_string.indexOf("'") + 1; - const endIndex = data_string.lastIndexOf("'"); - const hex_payload = data_string.substring(startIndex, endIndex); - const msg = Buffer.from(hex_payload, "hex"); - // this.log.debug("Sniffing msg: " + msg); - - const decodedMessage = this.message._decodeMsg(msg, localKey); - // this.log.debug("decodedMessage: " + JSON.stringify(decodedMessage)); - this.log.debug("Decoded sniffing message: " + JSON.stringify(JSON.parse(decodedMessage.payload))); + const dataString = JSON.stringify(data); + + const duidMatch = dataString.match(/\/(\w+)\.\w{3}'/); + if (duidMatch) { + const duidSniffed = duidMatch[1]; + + const device = devices.find((device) => device.duid === duidSniffed); + if (device) { + const localKey = device.localKey; + + const payloadMatch = dataString.match(/'([a-fA-F0-9]+)'/); + if (payloadMatch) { + const hexPayload = payloadMatch[1]; + const msg = Buffer.from(hexPayload, "hex"); + + const decodedMessage = this.message._decodeMsg(msg, localKey); + this.log.debug(`Decoded sniffing message: ${JSON.stringify(JSON.parse(decodedMessage.payload))}`); + } } - }); + } } async onlineChecker(duid) { @@ -683,7 +672,7 @@ class Roborock extends utils.Adapter { } async updateDataMinimumData(duid, vacuum, robotModel) { - this.log.debug("Latest data requested"); + this.log.debug(`Latest data requested`); if (robotModel == "roborock.wm.a102") { // nothing for now @@ -770,7 +759,7 @@ class Roborock extends utils.Adapter { const request = this.messageQueue.get(requestId); if (!request?.timeout102 && !request?.timeout301) { this.messageQueue.delete(requestId); - // this.log.debug("Cleared messageQueue"); + // this.log.debug(`Cleared messageQueue`); } else { this.log.debug(`Not clearing messageQueue. ${request.timeout102} - ${request.timeout301}`); } @@ -789,7 +778,7 @@ class Roborock extends utils.Adapter { val: JSON.stringify(homedata), ack: true, }); - this.log.debug("homedata successfully updated"); + this.log.debug(`homedata successfully updated`); await this.updateConsumablesPercent(homedata.devices); await this.updateConsumablesPercent(homedata.receivedDevices); @@ -799,27 +788,27 @@ class Roborock extends utils.Adapter { this.log.warn("homedata failed to download"); } } catch (error) { - this.log.error("Failed to update updateHomeData with error: " + error); + this.log.error(`Failed to update updateHomeData with error: ${error}`); } } } + async updateConsumablesPercent(devices) { - for (const device in devices) { - const duid = devices[device].duid; + for (const device of devices) { + const duid = device.duid; + const deviceStatus = device.deviceStatus; - for (const deviceAttribute in devices[device].deviceStatus) { - const targetConsumable = await this.getObjectAsync(`Devices.${duid}.consumables.${deviceAttribute}`); + for (const [attribute, value] of Object.entries(deviceStatus)) { + const targetConsumable = await this.getObjectAsync(`Devices.${duid}.consumables.${attribute}`); if (targetConsumable) { - const val = - devices[device].deviceStatus[deviceAttribute] >= 0 && devices[device].deviceStatus[deviceAttribute] <= 100 - ? parseInt(devices[device].deviceStatus[deviceAttribute]) - : 0; - this.setStateAsync("Devices." + duid + ".consumables." + deviceAttribute, { val: val, ack: true }); + const val = value >= 0 && value <= 100 ? parseInt(value) : 0; + await this.setStateAsync(`Devices.${duid}.consumables.${attribute}`, { val: val, ack: true }); } } } } + async updateDeviceInfo(devices) { for (const device in devices) { const duid = devices[device].duid; @@ -843,7 +832,7 @@ class Roborock extends utils.Adapter { }, native: {}, }); - this.setStateAsync("Devices." + duid + ".deviceInfo." + deviceAttribute, { val: devices[device][deviceAttribute], ack: true }); + this.setStateChangedAsync("Devices." + duid + ".deviceInfo." + deviceAttribute, { val: devices[device][deviceAttribute], ack: true }); } } } @@ -1031,7 +1020,7 @@ class Roborock extends utils.Adapter { async createCleaningRecord(duid, state, type, states, unit) { let start = 0; let end = 19; - const robotModel = this.getRobotModel(duid); + const robotModel = this.getProductAttribute(duid, "model"); if (robotModel == "roborock.vacuum.a97") { start = 1; end = 20; @@ -1206,14 +1195,11 @@ class Roborock extends utils.Adapter { let cameraCount = 0; const go2rtcConfig = { streams: {} }; - for (const robot in robots) { - const duid = robot; + for (const duid of Object.keys(robots)) { if (this.localKeys) { const localKey = this.localKeys.get(duid); - const u = userdata.rriot.u; - const s = userdata.rriot.s; - const k = userdata.rriot.k; + const { u, s, k } = userdata.rriot; if (this.vacuums[duid].features.getFeatureList().isCameraSupported) { cameraCount++; @@ -1223,43 +1209,43 @@ class Roborock extends utils.Adapter { } if (cameraCount > 0) { - const go2rtc_process = childProcess.spawn(go2rtcPath.toString(), ["-config", JSON.stringify(go2rtcConfig)], { shell: false, detached: false, windowsHide: true }); + try { + const go2rtcProcess = childProcess.spawn(go2rtcPath.toString(), ["-config", JSON.stringify(go2rtcConfig)], { shell: false, detached: false, windowsHide: true }); - go2rtc_process.on("error", (error) => { - this.log.error(`Error starting go2rtc: ${error}`); - }); + go2rtcProcess.on("error", (error) => { + this.log.error(`Error starting go2rtc: ${error.message}`); + }); - go2rtc_process.stdout.on("data", (data) => { - this.log.debug(`go2rtc output: ${data}`); - }); + go2rtcProcess.stdout.on("data", (data) => { + this.log.debug(`go2rtc output: ${data}`); + }); - go2rtc_process.stderr.on("data", (data) => { - this.log.error(`go2rtc error output: ${data}`); - }); + go2rtcProcess.stderr.on("data", (data) => { + this.log.error(`go2rtc error output: ${data}`); + }); - process.on("exit", () => { - go2rtc_process.kill(); - }); + process.on("exit", () => { + go2rtcProcess.kill(); + }); + } catch (error) { + this.log.error(`Failed to start go2rtc: ${error.message}`); + } } } async catchError(error, attribute, duid, model) { - const onlineState = await this.onlineChecker(duid); - - if (onlineState) { + if (error) { if (error.toString().includes("retry") || error.toString().includes("locating") || error.toString().includes("timed out after 10 seconds")) { - this.log.warn(`Failed to execute ${attribute} on robot ${duid} (${model || "unknown model"}) ${error}`); + this.log.warn(`Failed to execute ${attribute} on robot ${duid} (${model || "unknown model"}): ${error}`); } else { - this.log.error(`Failed to execute ${attribute} on robot ${duid} (${model || "unknown model"}) ${error.stack || error}`); + this.log.error(`Failed to execute ${attribute} on robot ${duid} (${model || "unknown model"}): ${error.stack || error}`); if (this.supportsFeature && this.supportsFeature("PLUGINS")) { if (this.sentryInstance) { - this.sentryInstance.getSentryObject().captureException(`error: ${error}`); + this.sentryInstance.getSentryObject().captureException(`Error: ${error}`); } } } - } else { - this.log.warn(`Robot ${duid} is offline. ${attribute} failed.`); } } @@ -1302,10 +1288,15 @@ class Roborock extends utils.Adapter { */ async onStateChange(id, state) { if (state) { - if (state.ack == false) { - const idParts = id.split("."); - const duid = idParts[3]; - const folder = idParts[4]; + const idParts = id.split("."); + const duid = idParts[3]; + const folder = idParts[4]; + + if (state.ack) { + if (id.endsWith("online")) { + this.log.info(`Device ${duid} is now ${state.val ? "online" : "offline"}`); + } + } else { const command = idParts[5]; this.log.debug(`onStateChange: ${command} with value: ${state.val}`); @@ -1347,6 +1338,8 @@ class Roborock extends utils.Adapter { let expectedFormat = "[x1, y1, x2, y2]"; if (command === "app_zoned_clean") { expectedFormat += " or [x1, y1, x2, y2, repeat] (where repeat is between 1 and 3)"; + } else if (command === "app_goto_target") { + expectedFormat = "[x, y]"; } this.log.error(`Invalid command parameters for ${command}: ${state.val}. Expected format: ${expectedFormat}`); } diff --git a/package-lock.json b/package-lock.json index 69c5b88dea..702c3f977b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@tsconfig/node18": "^18.2.4", "axios": "^1.7.7", "binary-parser": "^2.2.1", - "canvas": "^2.11.2", + "canvas": "^3.0.0-rc2", "crc-32": "^1.2.2", "esbuild": "^0.23.0", "eventemitter2": "^6.4.9", @@ -1708,105 +1708,6 @@ "node": ">=v12.0.0" } }, - "node_modules/@mapbox/node-pre-gyp": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", - "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", - "dependencies": { - "detect-libc": "^2.0.0", - "https-proxy-agent": "^5.0.0", - "make-dir": "^3.1.0", - "node-fetch": "^2.6.7", - "nopt": "^5.0.0", - "npmlog": "^5.0.1", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.11" - }, - "bin": { - "node-pre-gyp": "bin/node-pre-gyp" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/are-we-there-yet": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", - "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", - "deprecated": "This package is no longer supported.", - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/gauge": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", - "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", - "deprecated": "This package is no longer supported.", - "dependencies": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.2", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.1", - "object-assign": "^4.1.1", - "signal-exit": "^3.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/npmlog": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", - "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", - "deprecated": "This package is no longer supported.", - "dependencies": { - "are-we-there-yet": "^2.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^3.0.0", - "set-blocking": "^2.0.0" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2817,11 +2718,6 @@ "@types/node": "*" } }, - "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" - }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -2885,6 +2781,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, "dependencies": { "debug": "4" }, @@ -3321,17 +3218,18 @@ } }, "node_modules/canvas": { - "version": "2.11.2", - "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.11.2.tgz", - "integrity": "sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==", + "version": "3.0.0-rc2", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-3.0.0-rc2.tgz", + "integrity": "sha512-esx4bYDznnqgRX4G8kaEaf0W3q8xIc51WpmrIitDzmcoEgwnv9wSKdzT6UxWZ4wkVu5+ileofppX0TpyviJRdQ==", "hasInstallScript": true, + "license": "MIT", "dependencies": { - "@mapbox/node-pre-gyp": "^1.0.0", - "nan": "^2.17.0", + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.1", "simple-get": "^3.0.3" }, "engines": { - "node": ">=6" + "node": "^18.12.0 || >= 20.9.0" } }, "node_modules/catharsis": { @@ -3436,12 +3334,10 @@ } }, "node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "engines": { - "node": ">=10" - } + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" }, "node_modules/cjs-module-lexer": { "version": "1.3.1", @@ -3801,6 +3697,15 @@ "node": ">=6" } }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -3839,11 +3744,6 @@ "node": ">=0.4.0" } }, - "node_modules/delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -3870,6 +3770,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "license": "Apache-2.0", "engines": { "node": ">=8" } @@ -4599,6 +4500,15 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, "node_modules/express": { "version": "4.21.0", "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", @@ -5025,32 +4935,11 @@ "node": ">=12" } }, - "node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/fs-minipass/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true }, "node_modules/fsevents": { "version": "2.3.3", @@ -5180,6 +5069,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, "node_modules/glob": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", @@ -5728,6 +5623,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, "dependencies": { "agent-base": "6", "debug": "4" @@ -5823,6 +5719,7 @@ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -5833,6 +5730,12 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, "node_modules/into-stream": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-3.1.0.tgz", @@ -6305,28 +6208,6 @@ "node": ">=10" } }, - "node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/make-dir/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/markdown-it": { "version": "14.1.0", "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", @@ -6486,41 +6367,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, "bin": { "mkdirp": "bin/cmd.js" }, @@ -6528,6 +6379,12 @@ "node": ">=10" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, "node_modules/mocha": { "version": "10.6.0", "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.6.0.tgz", @@ -6712,10 +6569,11 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, - "node_modules/nan": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.20.0.tgz", - "integrity": "sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==" + "node_modules/napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", + "license": "MIT" }, "node_modules/natural-compare": { "version": "1.4.0", @@ -6750,6 +6608,24 @@ "integrity": "sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==", "dev": true }, + "node_modules/node-abi": { + "version": "3.68.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.68.0.tgz", + "integrity": "sha512-7vbj10trelExNjFSBm5kTvZXXa7pZyKWx9RCKIyqe6I9Ev3IzGpQoqBP3a+cOdxY+pWj6VkP28n/2wWysBHD/A==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -6778,20 +6654,6 @@ "node": ">= 6.13.0" } }, - "node_modules/nopt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", - "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", - "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -7064,14 +6926,6 @@ "node": ">=8" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -7243,6 +7097,57 @@ "node": ">=0.10.0" } }, + "node_modules/prebuild-install": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", + "integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prebuild-install/node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -7515,6 +7420,30 @@ "node": ">= 0.8" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -7925,7 +7854,8 @@ "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true }, "node_modules/simple-concat": { "version": "1.0.1", @@ -8220,20 +8150,67 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" + "node_modules/tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/tar-fs/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/tar-fs/node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" }, "engines": { - "node": ">=10" + "node": ">=6" } }, "node_modules/tar-stream": { @@ -8397,6 +8374,18 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -8779,7 +8768,8 @@ "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true }, "node_modules/yargs": { "version": "17.7.2", diff --git a/package.json b/package.json index ef03997360..56824e2cf6 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "@tsconfig/node18": "^18.2.4", "axios": "^1.7.7", "binary-parser": "^2.2.1", - "canvas": "^2.11.2", + "canvas": "^3.0.0-rc2", "crc-32": "^1.2.2", "esbuild": "^0.23.0", "eventemitter2": "^6.4.9",