Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge dev #655

Merged
merged 21 commits into from
Sep 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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