Skip to content

Commit

Permalink
Rewrite of mqtt connection logic
Browse files Browse the repository at this point in the history
I don't know how to code so ChatGPT did this most of the work 😅

Add missing features to Qrevo Slim
Start websocket & web server onReady
Update LICENSE
Update README.md
  • Loading branch information
copystring committed Feb 8, 2025
1 parent f2ff206 commit 7fe3121
Show file tree
Hide file tree
Showing 16 changed files with 2,026 additions and 1,921 deletions.
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2024 copystring <copystring@gmail.com>
Copyright (c) 2025 copystring <copystring@gmail.com>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,7 @@ This feature only works when map creation is enabled in the adapter options!
## License
MIT License

Copyright (c) 2024 copystring <copystring@gmail.com>
Copyright (c) 2025 copystring <copystring@gmail.com>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
128 changes: 69 additions & 59 deletions lib/deviceFeatures.js → lib/device_features.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ const stateCodes = {
16: "Go To",
17: "Zone Clean",
18: "Room Clean",
22: "Empying dust container",
22: "Emptying dust container",
23: "Washing the mop",
26: "Going to wash the mop",
28: "In call",
Expand Down Expand Up @@ -305,12 +305,9 @@ const actions = {
}
};

class deviceFeatures {
constructor(adapter, features, featuresStr, duid) {
class device_features {
constructor(adapter) {
this.adapter = adapter;
this.features = features;
this.featuresStr = featuresStr;
this.duid = duid;
this.cleaningInfo = {};
this.cleaningRecords = {};
this.consumables = {};
Expand Down Expand Up @@ -431,19 +428,19 @@ class deviceFeatures {
deviceStates.camera_status = "number";
}

isVideoLiveCallSupported() {
isVideoLiveCallSupported(duid) {
deviceStates.home_sec_enable_password = "number";
deviceStates.home_sec_status = "number";
const ip = this.adapter.config.hostname_ip;
const streamTypes = {
stream_html: `http://${ip}:1984/stream.html?src=${this.duid}`,
webrtc_html: `http://${ip}:1984/webrtc.html?src=${this.duid}&media=video`,
stream_mp4: `http://${ip}:1984/api/stream.mp4?src=${this.duid}`,
rtsp: `rtsp://${ip}:8554/${this.duid}?video`,
stream_html: `http://${ip}:1984/stream.html?src=${duid}`,
webrtc_html: `http://${ip}:1984/webrtc.html?src=${duid}&media=video`,
stream_mp4: `http://${ip}:1984/api/stream.mp4?src=${duid}`,
rtsp: `rtsp://${ip}:8554/${duid}?video`,
};

for (const [name, stream_uri] of Object.entries(streamTypes)) {
this.adapter.setObjectAsync(`Devices.${this.duid}.camera.${name}`, {
this.adapter.setObjectAsync(`Devices.${duid}.camera.${name}`, {
type: "state",
common: {
name: name,
Expand Down Expand Up @@ -483,9 +480,9 @@ class deviceFeatures {
deviceStates.mop_mode = { type: "number", states: { 300: "Standard", 301: "Deep", 303: "Deep+" } };
}

isCustomWaterBoxDistanceSupported() {
isCustomWaterBoxDistanceSupported(duid) {
// in this special case, create the command directly instead of the usual way
this.adapter.setObjectAsync(`Devices.${this.duid}.commands.set_water_box_distance_off`, {
this.adapter.setObjectAsync(`Devices.${duid}.commands.set_water_box_distance_off`, {
type: "state",
common: {
name: this.adapter.translations.set_water_box_distance_off,
Expand Down Expand Up @@ -540,11 +537,11 @@ class deviceFeatures {
isSupportLedStatusSwitch() {
// nothing for now
}
isMultiFloorSupported() {
isMultiFloorSupported(duid) {
const features = ["max_multi_map", "max_bak_map", "multi_map_count"];

for (const [, feature] of Object.entries(features)) {
this.adapter.setObjectAsync(`Devices.${this.duid}.floors.${feature}`, {
this.adapter.setObjectAsync(`Devices.${duid}.floors.${feature}`, {
type: "state",
common: {
name: feature,
Expand All @@ -567,23 +564,25 @@ class deviceFeatures {
// nothing for now
}

getFeatureList() {
const robotModel = this.adapter.getProductAttribute(this.duid, "model");
getFeatureList(duid) {
const robotModel = this.adapter.http_api.getRobotModel(duid);
const featureSet = this.adapter.http_api.getFeatureSet(duid);
const newFeatureSet = this.adapter.http_api.getNewFeatureSet(duid);

return {
isWashThenChargeCmdSupported: ((this.features / Math.pow(2, 32)) >> 5) & 1,
isDustCollectionSettingSupported: !!(33554432 & this.features),
isSupportedDrying: ((this.features / Math.pow(2, 32)) >> 15) & 1,
isShakeMopSetSupported: !!(262144 & this.features),
isVideoMonitorSupported: !!(8 & this.features), // I tested this for S7 MaxV, S8 MaxV
isVideoSettingSupported: !!(64 & this.features), // I tested this for S7 MaxV, S8 MaxV
isCarpetSupported: !!(512 & this.features),
isPhotoUploadSupported: !!(65536 & this.features),
isAvoidCollisionSupported: !!(134217728 & this.features),
isCornerCleanModeSupported: !!(2147483648 & this.features),
isWashThenChargeCmdSupported: ((featureSet / Math.pow(2, 32)) >> 5) & 1,
isDustCollectionSettingSupported: !!(33554432 & featureSet),
isSupportedDrying: ((featureSet / Math.pow(2, 32)) >> 15) & 1,
isShakeMopSetSupported: !!(262144 & featureSet),
isVideoMonitorSupported: !!(8 & featureSet), // I tested this for S7 MaxV, S8 MaxV
isVideoSettingSupported: !!(64 & featureSet), // I tested this for S7 MaxV, S8 MaxV
isCarpetSupported: !!(512 & featureSet),
isPhotoUploadSupported: !!(65536 & featureSet),
isAvoidCollisionSupported: !!(134217728 & featureSet),
isCornerCleanModeSupported: !!(2147483648 & featureSet),
// 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(robotModel),
isSupportSetSwitchMapMode: !!(268435456 & this.features),
isSupportSetSwitchMapMode: !!(268435456 & featureSet),
// 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: [
"roborock.vacuum.s6", // S6
Expand All @@ -605,7 +604,8 @@ class deviceFeatures {
"roborock.vacuum.a97", // S8 MaxV (Ultra)
"roborock.vacuum.a104", // Roborock Qrevo S
"roborock.vacuum.a135", // Qrevo Curv
"roborock.vacuum.a117" // Qrevo Master
"roborock.vacuum.a117", // Qrevo Master
"roborock.vacuum.a21", // Qrevo Slim
].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: [
Expand Down Expand Up @@ -648,10 +648,11 @@ class deviceFeatures {
"roborock.vacuum.a97", // S8 MaxV (Ultra)
"roborock.vacuum.a104", // Roborock Qrevo S
"roborock.vacuum.a135", // Qrevo Curv
"roborock.vacuum.a117" // Qrevo Master
"roborock.vacuum.a117", // Qrevo Master
"roborock.vacuum.a21", // Qrevo Slim
].includes(robotModel),
isCustomWaterBoxDistanceSupported: !!(2147483648 & this.features),
isBackChargeAutoWashSupported: this.featuresStr && !!(4096 & parseInt("0x" + this.featuresStr.slice(-8))),
isCustomWaterBoxDistanceSupported: !!(2147483648 & featureSet),
isBackChargeAutoWashSupported: newFeatureSet && !!(4096 & parseInt("0x" + newFeatureSet.slice(-8))),
isAvoidCarpetSupported: [
"roborock.vacuum.a10", // S6 MaxV
"roborock.vacuum.a40", // Q7
Expand All @@ -673,26 +674,27 @@ class deviceFeatures {
"roborock.vacuum.a117" // Qrevo Master
].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: !!(parseInt(`0x${newFeatureSet || "0"}`.slice(-10, -9)) & 2),
isVoiceControlSupported: [
"roborock.vacuum.a27", // S7 MaxV (Ultra)
],
isElectronicWaterBoxSupported: false, // nothing for now. If this is needed, add the models here
isCleanRouteFastModeSupported: this.featuresStr && !!(256 & parseInt("0x" + this.featuresStr.slice(-8))),
isCleanRouteFastModeSupported: newFeatureSet && !!(256 & parseInt("0x" + newFeatureSet.slice(-8))),
isVideoLiveCallSupported: [
"roborock.vacuum.a10", // S6 MaxV
"roborock.vacuum.a27", // S7 MaxV (Ultra)
"roborock.vacuum.a97", // S8 MaxV (Ultra)
"roborock.vacuum.a87", // Qrevo MaxV
"roborock.vacuum.a135", // Qrevo Curv
"roborock.vacuum.a117" // Qrevo Master
"roborock.vacuum.a117", // Qrevo Master
"roborock.vacuum.a21", // Qrevo Slim
].includes(robotModel),
};
}

async processSupportedFeatures() {
const robotModel = this.adapter.getProductAttribute(this.duid, "model");
const productCategory = this.adapter.getProductAttribute(this.duid, "category");
async processSupportedFeatures(duid) {
const robotModel = this.adapter.http_api.getRobotModel(duid);
const productCategory = this.adapter.http_api.getProductCategory(duid);

if (productCategory == "robot.vacuum.cleaner") {
// process states etc. depending on model
Expand Down Expand Up @@ -935,6 +937,16 @@ class deviceFeatures {
"set_replenish_mode",
"set_clean_times",
"set_task_id",
"set_monitor_status",
"set_in_warmup",
"set_charge_status",
"set_clean_percent",
"set_dss",
"set_rss",
"set_common_status",
"set_kct",
"set_switch_status",
"set_last_clean_t",
]
};

Expand All @@ -948,63 +960,63 @@ class deviceFeatures {
}
}
} else {
this.adapter.catchError(`This robot is not fully supported just yet. Contact the dev to get this robot fully supported!`, null, null, robotModel);
this.adapter.catchError(`This robot is not fully supported just yet. Contact the dev to get this robot fully supported!`);
}

this.adapter.createBaseRobotObjects(this.duid);
this.adapter.createBaseRobotObjects(duid);

const featureList = this.getFeatureList();
this.adapter.log.debug(`Supported features of robot ${this.duid} - ${robotModel}: ${JSON.stringify(featureList)}`);
const featureList = this.getFeatureList(duid);
this.adapter.log.debug(`Supported features of robot ${duid} - ${robotModel}: ${JSON.stringify(featureList)}`);
Object.keys(featureList).forEach((feature) => {
if (featureList[feature]) {
if (typeof this[feature] === "function") {
this[feature]();
this[feature](duid);
}
}
});

// process commands
for (const [command, type] of Object.entries(commands)) {
if (typeof type == "string") {
await this.adapter.createCommand(this.duid, command, type);
await this.adapter.createCommand(duid, command, type);
} else {
await this.adapter.createCommand(this.duid, command, type.type, type.defaultState, type.states);
await this.adapter.createCommand(duid, command, type.type, type.defaultState, type.states);
}
}

// process device states
for (const [state, type] of Object.entries(deviceStates)) {
if (typeof type == "string") {
await this.adapter.createDeviceStatus(this.duid, state, type);
await this.adapter.createDeviceStatus(duid, state, type);
} else {
await this.adapter.createDeviceStatus(this.duid, state, type.type, type.states, type.unit);
await this.adapter.createDeviceStatus(duid, state, type.type, type.states, type.unit);
}
}

// process consumables
for (const [consumable, object] of Object.entries(this.consumables)) {
await this.adapter.createConsumable(this.duid, consumable, object.type, object.states, object.unit);
await this.adapter.createConsumable(duid, consumable, object.type, object.states, object.unit);
}
// process reset of consumables
for (const [, resetConsumable] of Object.entries(resetConsumables)) {
await this.adapter.createResetConsumables(this.duid, resetConsumable);
await this.adapter.createResetConsumables(duid, resetConsumable);
}

// process cleaning info
for (const [cleaningInfoKey, cleaningInfoEntry] of Object.entries(this.cleaningInfo)) {
await this.adapter.createCleaningInfo(this.duid, cleaningInfoKey, cleaningInfoEntry);
await this.adapter.createCleaningInfo(duid, cleaningInfoKey, cleaningInfoEntry);
}

// process cleaning records
for (const [cleaningRecord, object] of Object.entries(this.cleaningRecords)) {
await this.adapter.createCleaningRecord(this.duid, cleaningRecord, object.type, object.states, object.unit);
await this.adapter.createCleaningRecord(duid, cleaningRecord, object.type, object.states, object.unit);
}
} 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);
this.adapter.createBasicVacuumObjects(duid);
} else if (productCategory == "roborock.wm") {
// washing machine
this.adapter.createBasicWashingMachineObjects(this.duid);
this.adapter.createBasicWashingMachineObjects(duid);
}
}

Expand Down Expand Up @@ -1053,8 +1065,8 @@ class deviceFeatures {
}
}

getConsumablesDivider(consumable) {
const robotModel = this.adapter.getProductAttribute(this.duid, "model");
getConsumablesDivider(duid, consumable) {
const robotModel = this.adapter.http_api.getRobotModel(duid);
const consumables = robotModel == "roborock.vacuum.s4" ? consumablesInt : consumablesString;

if (consumables[consumable]) {
Expand Down Expand Up @@ -1085,6 +1097,4 @@ class deviceFeatures {
}
}

module.exports = {
deviceFeatures,
};
module.exports = device_features;
Loading

0 comments on commit 7fe3121

Please sign in to comment.