Skip to content

Commit

Permalink
Merge dev (#655)
Browse files Browse the repository at this point in the history
* Refactor code via ChatGPT o1-preview

I don't know how to code so ChatGPT has to do this for me πŸ˜…πŸ˜…πŸ˜…

* Moar code refactor

* Improve warn logging for mqtt decode fails

* Fix some bad code

* Slightly improve logging for offline devices

* Improvements to online/offline logging

* Update vacuum.js

* Bugfix

* Update deviceInfo only when it's changed

* Don't send message if mqtt of tcp disconnected

* S6 MaxV supports isAvoidCarpetSupported

* Don't mark a device as remote if it's online

* Improve handling of messages which are not encoded

* Refresh home data every 60s

This is needed to detect the online/offline state much better and correctly handle detection of remote devices

* Improve protocol 500 handling

* home data needs to update with main update intervall

* Add node 22.x, github actions to node 20.x

* Update README.md

* Use canvas 3.0.0-rc2

* Update package-lock.json

* Update test-and-release.yml
  • Loading branch information
copystring authored Sep 22, 2024
1 parent 1981c77 commit 7364eff
Show file tree
Hide file tree
Showing 10 changed files with 455 additions and 411 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test-and-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
37 changes: 21 additions & 16 deletions lib/deviceFeatures.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {};
Expand Down Expand Up @@ -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),
Expand All @@ -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: [
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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: [
Expand All @@ -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
Expand Down Expand Up @@ -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];
Expand All @@ -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") {
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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;
Expand Down
9 changes: 6 additions & 3 deletions lib/localConnector.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
18 changes: 15 additions & 3 deletions lib/messageQueueHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
43 changes: 39 additions & 4 deletions lib/roborock_mqtt_connector.js
Original file line number Diff line number Diff line change
Expand Up @@ -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")`);
Expand Down Expand Up @@ -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")}`);
Expand Down
3 changes: 2 additions & 1 deletion lib/vacuum.js
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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);
}
Expand Down
Loading

0 comments on commit 7364eff

Please sign in to comment.