From f94f3c1101bbdf764f5b51a26a6c0abe6d0c873a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Beye?= Date: Sun, 7 Sep 2025 14:21:35 +0200 Subject: [PATCH] feat(vendor.midea): Everything carpet --- .../CarpetSensorModeControlCapability.js | 1 + backend/lib/msmart/BEightParser.js | 25 +++ backend/lib/msmart/MSmartConst.js | 4 + .../dtos/MSmartCarpetBehaviorSettingsDTO.js | 34 +++ backend/lib/msmart/dtos/MSmartStatusDTO.js | 2 + backend/lib/msmart/dtos/index.js | 1 + backend/lib/robots/midea/MideaQuirkFactory.js | 197 +++++++++++++++++- .../lib/robots/midea/MideaValetudoRobot.js | 6 + .../MideaCarpetModeControlCapability.js | 84 ++++++++ .../MideaCarpetSensorModeControlCapability.js | 95 +++++++++ .../lib/robots/midea/capabilities/index.js | 2 + ...orModeControlCapabilityRouter.openapi.json | 9 +- frontend/src/api/types.ts | 2 +- frontend/src/robot/RobotOptions.tsx | 10 +- 14 files changed, 464 insertions(+), 8 deletions(-) create mode 100644 backend/lib/msmart/dtos/MSmartCarpetBehaviorSettingsDTO.js create mode 100644 backend/lib/robots/midea/capabilities/MideaCarpetModeControlCapability.js create mode 100644 backend/lib/robots/midea/capabilities/MideaCarpetSensorModeControlCapability.js diff --git a/backend/lib/core/capabilities/CarpetSensorModeControlCapability.js b/backend/lib/core/capabilities/CarpetSensorModeControlCapability.js index 10ce3f4f..7bda579b 100644 --- a/backend/lib/core/capabilities/CarpetSensorModeControlCapability.js +++ b/backend/lib/core/capabilities/CarpetSensorModeControlCapability.js @@ -58,6 +58,7 @@ CarpetSensorModeControlCapability.MODE = Object.freeze({ AVOID: "avoid", LIFT: "lift", DETACH: "detach", + CROSS: "cross", }); diff --git a/backend/lib/msmart/BEightParser.js b/backend/lib/msmart/BEightParser.js index 4d87ae5f..7040a051 100644 --- a/backend/lib/msmart/BEightParser.js +++ b/backend/lib/msmart/BEightParser.js @@ -79,6 +79,11 @@ class BEightParser { return new dtos.MSmartCleaningSettings1DTO(data); } + case MSmartConst.ACTION.GET_CARPET_BEHAVIOR_SETTINGS: { + const data = BEightParser._parse_carpet_behavior_settings_payload(payload); + + return new dtos.MSmartCarpetBehaviorSettingsDTO(data); + } default: { Logger.warn( `Unhandled ACTION packet with typeId '${payload[2]}'`, @@ -316,6 +321,7 @@ class BEightParser { data.adb_switch = !!(generalSwitchBits8 & 0b00000001); data.station_v2_switch = !!(generalSwitchBits8 & 0b00000010); data.static_stain_recognition_switch = !!(generalSwitchBits8 & 0b00000100); + data.stairless_mode_switch = !!(generalSwitchBits8 & 0b00001000); } @@ -395,6 +401,25 @@ class BEightParser { return data; } + + /** + * @private + * @param {Buffer} payload + * @returns {object} + */ + static _parse_carpet_behavior_settings_payload(payload) { + const data = {}; + + data.carpet_behavior = payload[3]; + data.parameter_bitfield = payload[4]; + + data.clean_carpet_first = !!(data.parameter_bitfield & dtos.MSmartCarpetBehaviorSettingsDTO.PARAMETER_BIT.CLEAN_CARPET_FIRST); + data.deep_carpet_cleaning = !!(data.parameter_bitfield & dtos.MSmartCarpetBehaviorSettingsDTO.PARAMETER_BIT.DEEP_CARPET_CLEANING); + data.carpet_suction_boost = !!(data.parameter_bitfield & dtos.MSmartCarpetBehaviorSettingsDTO.PARAMETER_BIT.CARPET_SUCTION_BOOST); + data.enhanced_carpet_avoidance = !!(data.parameter_bitfield & dtos.MSmartCarpetBehaviorSettingsDTO.PARAMETER_BIT.ENHANCED_CARPET_AVOIDANCE); + + return data; + } } module.exports = BEightParser; diff --git a/backend/lib/msmart/MSmartConst.js b/backend/lib/msmart/MSmartConst.js index 4e775e80..207a2f37 100644 --- a/backend/lib/msmart/MSmartConst.js +++ b/backend/lib/msmart/MSmartConst.js @@ -15,6 +15,8 @@ const SETTING = Object.freeze({ SET_DOCK_INTERVALS: 0x56, SET_OPERATION_MODE: 0x58, TRIGGER_STATION_ACTION: 0x5A, + SET_CARPET_BEHAVIOR_SETTINGS: 0x5E, + SET_STAIRLESS_MODE: 0x63, SET_DND: 0x92, SET_VOLUME: 0x93, SET_VARIOUS_TOGGLES: 0x9C, @@ -29,8 +31,10 @@ const ACTION = Object.freeze({ GET_DOCK_POSITION: 0x24, GET_ACTIVE_ZONES: 0x27, LOCATE: 0x57, + // 0x59 seems to maybe provide a bunch of feature bits? Reporting capabilities of the robot GET_DND: 0x90, GET_CLEANING_SETTINGS_1: 0xAA, // FIXME: naming + GET_CARPET_BEHAVIOR_SETTINGS: 0xAB }); const EVENT = Object.freeze({ diff --git a/backend/lib/msmart/dtos/MSmartCarpetBehaviorSettingsDTO.js b/backend/lib/msmart/dtos/MSmartCarpetBehaviorSettingsDTO.js new file mode 100644 index 00000000..82402341 --- /dev/null +++ b/backend/lib/msmart/dtos/MSmartCarpetBehaviorSettingsDTO.js @@ -0,0 +1,34 @@ +const MSmartDTO = require("./MSmartDTO"); + +class MSmartCarpetBehaviorSettingsDTO extends MSmartDTO { + /** + * @param {object} data + * @param {number} data.carpet_behavior 0 = avoid, 1 = ignore, 2 = adapt, 3 = cross + * @param {number} data.parameter_bitfield The raw bitfield byte for carpet sub-settings + * @param {boolean} data.clean_carpet_first + * @param {boolean} data.deep_carpet_cleaning + * @param {boolean} data.carpet_suction_boost + * @param {boolean} data.enhanced_carpet_avoidance + */ + constructor(data) { + super(); + + this.carpet_behavior = data.carpet_behavior; + this.parameter_bitfield = data.parameter_bitfield; + this.clean_carpet_first = data.clean_carpet_first; + this.deep_carpet_cleaning = data.deep_carpet_cleaning; + this.carpet_suction_boost = data.carpet_suction_boost; + this.enhanced_carpet_avoidance = data.enhanced_carpet_avoidance; + + Object.freeze(this); + } +} + +MSmartCarpetBehaviorSettingsDTO.PARAMETER_BIT = Object.freeze({ + CLEAN_CARPET_FIRST: 0b0001, + DEEP_CARPET_CLEANING: 0b0010, + CARPET_SUCTION_BOOST: 0b0100, + ENHANCED_CARPET_AVOIDANCE: 0b1000 +}); + +module.exports = MSmartCarpetBehaviorSettingsDTO; diff --git a/backend/lib/msmart/dtos/MSmartStatusDTO.js b/backend/lib/msmart/dtos/MSmartStatusDTO.js index 10482040..2ddb4bc5 100644 --- a/backend/lib/msmart/dtos/MSmartStatusDTO.js +++ b/backend/lib/msmart/dtos/MSmartStatusDTO.js @@ -97,6 +97,7 @@ class MSmartStatusDTO extends MSmartDTO { * @param {boolean} [data.adb_switch] * @param {boolean} [data.station_v2_switch] * @param {boolean} [data.static_stain_recognition_switch] + * @param {boolean} [data.stairless_mode_switch] */ constructor(data) { super(); @@ -195,6 +196,7 @@ class MSmartStatusDTO extends MSmartDTO { this.adb_switch = data.adb_switch; this.station_v2_switch = data.station_v2_switch; this.static_stain_recognition_switch = data.static_stain_recognition_switch; + this.stairless_mode_switch = data.stairless_mode_switch; Object.freeze(this); } diff --git a/backend/lib/msmart/dtos/index.js b/backend/lib/msmart/dtos/index.js index a5c34a70..8ad488b7 100644 --- a/backend/lib/msmart/dtos/index.js +++ b/backend/lib/msmart/dtos/index.js @@ -1,5 +1,6 @@ module.exports = { MSmartActiveZonesDTO: require("./MSmartActiveZonesDTO"), + MSmartCarpetBehaviorSettingsDTO: require("./MSmartCarpetBehaviorSettingsDTO"), MSmartCleaningSettings1DTO: require("./MSmartCleaningSettings1DTO"), MSmartDTO: require("./MSmartDTO"), MSmartDndConfigurationDTO: require("./MSmartDNDConfigurationDTO"), diff --git a/backend/lib/robots/midea/MideaQuirkFactory.js b/backend/lib/robots/midea/MideaQuirkFactory.js index c6fe7dd7..b076d1ba 100644 --- a/backend/lib/robots/midea/MideaQuirkFactory.js +++ b/backend/lib/robots/midea/MideaQuirkFactory.js @@ -1,7 +1,9 @@ const BEightParser = require("../../msmart/BEightParser"); +const MSmartCarpetBehaviorSettingsDTO = require("../../msmart/dtos/MSmartCarpetBehaviorSettingsDTO"); const MSmartCleaningSettings1DTO = require("../../msmart/dtos/MSmartCleaningSettings1DTO"); const MSmartConst = require("../../msmart/MSmartConst"); const MSmartPacket = require("../../msmart/MSmartPacket"); +const MSmartStatusDTO = require("../../msmart/dtos/MSmartStatusDTO"); const Quirk = require("../../core/Quirk"); class MideaQuirkFactory { @@ -182,7 +184,7 @@ class MideaQuirkFactory { case MideaQuirkFactory.KNOWN_QUIRKS.QUIET_AUTO_EMPTY: return new Quirk({ id: id, - title: "Quiet Auto Empty", + title: "Quiet Auto-Empty", description: "Reduces the noise level during the auto-empty process. Will also make it less effective.", options: ["normal", "quiet"], getter: async () => { @@ -233,6 +235,195 @@ class MideaQuirkFactory { await this.robot.sendCommand(packet.toHexString()); } }); + case MideaQuirkFactory.KNOWN_QUIRKS.CLIFF_SENSORS: + return new Quirk({ + id: id, + title: "Cliff Sensors", + description: "! DANGEROUS ! - This allows you to disable the cliff sensors. The robot WILL fall down stairs and possibly destroy itself if you do so.", + options: ["on", "off"], + getter: async () => { + const response = await this.robot.sendCommand(new MSmartPacket({ + messageType: MSmartPacket.MESSAGE_TYPE.ACTION, + payload: MSmartPacket.buildPayload(MSmartConst.ACTION.GET_STATUS) + }).toHexString()); + const parsedResponse = BEightParser.PARSE(response); + + if (parsedResponse instanceof MSmartStatusDTO) { + return parsedResponse.stairless_mode_switch ? "off" : "on"; + } else { + throw new Error("Invalid response from robot"); + } + }, + setter: async (value) => { + let val; + + switch (value) { + case "off": + val = 1; + break; + case "on": + val = 0; + break; + default: + throw new Error("Invalid value"); + } + + await this.robot.sendCommand(new MSmartPacket({ + messageType: MSmartPacket.MESSAGE_TYPE.SETTING, + payload: MSmartPacket.buildPayload( + MSmartConst.SETTING.SET_STAIRLESS_MODE, + Buffer.from([val]) + ) + }).toHexString()); + } + }); + case MideaQuirkFactory.KNOWN_QUIRKS.CARPET_FIRST: + return new Quirk({ + id: id, + title: "Carpet First", + description: "When enabled, the robot will first clean all carpet areas before then continuing with the rest of the cleanup.", + options: ["off", "on"], + getter: async () => { + const response = await this.robot.sendCommand(new MSmartPacket({ + messageType: MSmartPacket.MESSAGE_TYPE.ACTION, + payload: MSmartPacket.buildPayload(MSmartConst.ACTION.GET_CARPET_BEHAVIOR_SETTINGS) + }).toHexString()); + const parsedResponse = BEightParser.PARSE(response); + + if (parsedResponse instanceof MSmartCarpetBehaviorSettingsDTO) { + return parsedResponse.clean_carpet_first ? "on" : "off"; + } else { + throw new Error("Invalid response from robot"); + } + }, + setter: async (value) => { + const response = await this.robot.sendCommand(new MSmartPacket({ + messageType: MSmartPacket.MESSAGE_TYPE.ACTION, + payload: MSmartPacket.buildPayload(MSmartConst.ACTION.GET_CARPET_BEHAVIOR_SETTINGS) + }).toHexString()); + const parsedResponse = BEightParser.PARSE(response); + + if (!(parsedResponse instanceof MSmartCarpetBehaviorSettingsDTO)) { + throw new Error("Invalid response from robot"); + } + + let newParameterBitfield = parsedResponse.parameter_bitfield; + if (value === "on") { + newParameterBitfield = newParameterBitfield | MSmartCarpetBehaviorSettingsDTO.PARAMETER_BIT.CLEAN_CARPET_FIRST; + } else { + newParameterBitfield = newParameterBitfield & ~MSmartCarpetBehaviorSettingsDTO.PARAMETER_BIT.CLEAN_CARPET_FIRST; + } + + await this.robot.sendCommand(new MSmartPacket({ + messageType: MSmartPacket.MESSAGE_TYPE.SETTING, + payload: MSmartPacket.buildPayload( + MSmartConst.SETTING.SET_CARPET_BEHAVIOR_SETTINGS, + Buffer.from([ + parsedResponse.carpet_behavior, + newParameterBitfield + ]) + ) + }).toHexString()); + } + }); + case MideaQuirkFactory.KNOWN_QUIRKS.DEEP_CARPET_CLEANING: + return new Quirk({ + id: id, + title: "Deep Carpet Cleaning", + description: "When enabled, the robot will automatically slowly clean detected carpets with twice the cleanup passes in alternating directions.", + options: ["off", "on"], + getter: async () => { + const response = await this.robot.sendCommand(new MSmartPacket({ + messageType: MSmartPacket.MESSAGE_TYPE.ACTION, + payload: MSmartPacket.buildPayload(MSmartConst.ACTION.GET_CARPET_BEHAVIOR_SETTINGS) + }).toHexString()); + const parsedResponse = BEightParser.PARSE(response); + + if (parsedResponse instanceof MSmartCarpetBehaviorSettingsDTO) { + return parsedResponse.deep_carpet_cleaning ? "on" : "off"; + } else { + throw new Error("Invalid response from robot"); + } + }, + setter: async (value) => { + const response = await this.robot.sendCommand(new MSmartPacket({ + messageType: MSmartPacket.MESSAGE_TYPE.ACTION, + payload: MSmartPacket.buildPayload(MSmartConst.ACTION.GET_CARPET_BEHAVIOR_SETTINGS) + }).toHexString()); + const parsedResponse = BEightParser.PARSE(response); + + if (!(parsedResponse instanceof MSmartCarpetBehaviorSettingsDTO)) { + throw new Error("Invalid response from robot"); + } + + let newParameterBitfield = parsedResponse.parameter_bitfield; + if (value === "on") { + newParameterBitfield = newParameterBitfield | MSmartCarpetBehaviorSettingsDTO.PARAMETER_BIT.DEEP_CARPET_CLEANING; + } else { + newParameterBitfield = newParameterBitfield & ~MSmartCarpetBehaviorSettingsDTO.PARAMETER_BIT.DEEP_CARPET_CLEANING; + } + + await this.robot.sendCommand(new MSmartPacket({ + messageType: MSmartPacket.MESSAGE_TYPE.SETTING, + payload: MSmartPacket.buildPayload( + MSmartConst.SETTING.SET_CARPET_BEHAVIOR_SETTINGS, + Buffer.from([ + parsedResponse.carpet_behavior, + newParameterBitfield + ]) + ) + }).toHexString()); + } + }); + case MideaQuirkFactory.KNOWN_QUIRKS.INCREASED_CARPET_AVOIDANCE: + return new Quirk({ + id: id, + title: "Increased Carpet Avoidance", + description: "When enabled, when avoiding carpets, the robot will stay further away from them.", + options: ["off", "on"], + getter: async () => { + const response = await this.robot.sendCommand(new MSmartPacket({ + messageType: MSmartPacket.MESSAGE_TYPE.ACTION, + payload: MSmartPacket.buildPayload(MSmartConst.ACTION.GET_CARPET_BEHAVIOR_SETTINGS) + }).toHexString()); + const parsedResponse = BEightParser.PARSE(response); + + if (parsedResponse instanceof MSmartCarpetBehaviorSettingsDTO) { + return parsedResponse.enhanced_carpet_avoidance ? "on" : "off"; + } else { + throw new Error("Invalid response from robot"); + } + }, + setter: async (value) => { + const response = await this.robot.sendCommand(new MSmartPacket({ + messageType: MSmartPacket.MESSAGE_TYPE.ACTION, + payload: MSmartPacket.buildPayload(MSmartConst.ACTION.GET_CARPET_BEHAVIOR_SETTINGS) + }).toHexString()); + const parsedResponse = BEightParser.PARSE(response); + + if (!(parsedResponse instanceof MSmartCarpetBehaviorSettingsDTO)) { + throw new Error("Invalid response from robot"); + } + + let newParameterBitfield = parsedResponse.parameter_bitfield; + if (value === "on") { + newParameterBitfield = newParameterBitfield | MSmartCarpetBehaviorSettingsDTO.PARAMETER_BIT.ENHANCED_CARPET_AVOIDANCE; + } else { + newParameterBitfield = newParameterBitfield & ~MSmartCarpetBehaviorSettingsDTO.PARAMETER_BIT.ENHANCED_CARPET_AVOIDANCE; + } + + await this.robot.sendCommand(new MSmartPacket({ + messageType: MSmartPacket.MESSAGE_TYPE.SETTING, + payload: MSmartPacket.buildPayload( + MSmartConst.SETTING.SET_CARPET_BEHAVIOR_SETTINGS, + Buffer.from([ + parsedResponse.carpet_behavior, + newParameterBitfield + ]) + ) + }).toHexString()); + } + }); default: throw new Error(`There's no quirk with id ${id}`); } @@ -244,6 +435,10 @@ MideaQuirkFactory.KNOWN_QUIRKS = { HAIR_CUTTING_ONE_TIME_TURBO: "224b6a0a-1a51-48d7-9d4d-61645399d368", AI_OBSTACLE_CLASSIFICATION: "75af01c4-5c24-4cb3-9619-41b46b6ce333", QUIET_AUTO_EMPTY: "96a98473-d60c-4ed9-8ef4-b71f023085a0", + CLIFF_SENSORS: "ef7a7a7f-370b-485d-80fb-704206a9b354", + CARPET_FIRST: "4b100fec-08d5-4227-9edf-eb0198d6ea20", + DEEP_CARPET_CLEANING: "d2ad3f99-c1b0-4195-9a98-4f13bdb0f1e8", + INCREASED_CARPET_AVOIDANCE: "f3ff1c65-9fe7-4312-b196-83ce91107fe8", }; module.exports = MideaQuirkFactory; diff --git a/backend/lib/robots/midea/MideaValetudoRobot.js b/backend/lib/robots/midea/MideaValetudoRobot.js index 3ff821e6..934a5344 100644 --- a/backend/lib/robots/midea/MideaValetudoRobot.js +++ b/backend/lib/robots/midea/MideaValetudoRobot.js @@ -146,6 +146,8 @@ class MideaValetudoRobot extends ValetudoRobot { capabilities.MideaObstacleAvoidanceControlCapability, capabilities.MideaAutoEmptyDockAutoEmptyIntervalControlCapability, capabilities.MideaMopDockMopWashTemperatureControlCapability, + capabilities.MideaCarpetModeControlCapability, + capabilities.MideaCarpetSensorModeControlCapability, ].forEach(capability => { this.registerCapability(new capability({robot: this})); }); @@ -166,6 +168,10 @@ class MideaValetudoRobot extends ValetudoRobot { quirkFactory.getQuirk(MideaQuirkFactory.KNOWN_QUIRKS.HAIR_CUTTING_ONE_TIME_TURBO), quirkFactory.getQuirk(MideaQuirkFactory.KNOWN_QUIRKS.AI_OBSTACLE_CLASSIFICATION), quirkFactory.getQuirk(MideaQuirkFactory.KNOWN_QUIRKS.QUIET_AUTO_EMPTY), + quirkFactory.getQuirk(MideaQuirkFactory.KNOWN_QUIRKS.CLIFF_SENSORS), + quirkFactory.getQuirk(MideaQuirkFactory.KNOWN_QUIRKS.CARPET_FIRST), + quirkFactory.getQuirk(MideaQuirkFactory.KNOWN_QUIRKS.DEEP_CARPET_CLEANING), + quirkFactory.getQuirk(MideaQuirkFactory.KNOWN_QUIRKS.INCREASED_CARPET_AVOIDANCE), ] })); diff --git a/backend/lib/robots/midea/capabilities/MideaCarpetModeControlCapability.js b/backend/lib/robots/midea/capabilities/MideaCarpetModeControlCapability.js new file mode 100644 index 00000000..c4140d29 --- /dev/null +++ b/backend/lib/robots/midea/capabilities/MideaCarpetModeControlCapability.js @@ -0,0 +1,84 @@ +const BEightParser = require("../../../msmart/BEightParser"); +const CarpetModeControlCapability = require("../../../core/capabilities/CarpetModeControlCapability"); +const MSmartCarpetBehaviorSettingsDTO = require("../../../msmart/dtos/MSmartCarpetBehaviorSettingsDTO"); +const MSmartConst = require("../../../msmart/MSmartConst"); +const MSmartPacket = require("../../../msmart/MSmartPacket"); + + +/** + * @extends CarpetModeControlCapability + */ +class MideaCarpetModeControlCapability extends CarpetModeControlCapability { + /** + * @private + * @returns {Promise} + */ + async _getSettings() { + const packet = new MSmartPacket({ + messageType: MSmartPacket.MESSAGE_TYPE.ACTION, + payload: MSmartPacket.buildPayload(MSmartConst.ACTION.GET_CARPET_BEHAVIOR_SETTINGS) + }); + + const response = await this.robot.sendCommand(packet.toHexString()); + const parsedResponse = BEightParser.PARSE(response); + + if (parsedResponse instanceof MSmartCarpetBehaviorSettingsDTO) { + return parsedResponse; + } else { + throw new Error("Invalid response from robot"); + } + } + + /** + * @returns {Promise} + */ + async isEnabled() { + const settings = await this._getSettings(); + + return settings.carpet_suction_boost; + } + + /** + * @returns {Promise} + */ + async enable() { + const currentSettings = await this._getSettings(); + const newParameterBitfield = currentSettings.parameter_bitfield | MSmartCarpetBehaviorSettingsDTO.PARAMETER_BIT.CARPET_SUCTION_BOOST; + + const packet = new MSmartPacket({ + messageType: MSmartPacket.MESSAGE_TYPE.SETTING, + payload: MSmartPacket.buildPayload( + MSmartConst.SETTING.SET_CARPET_BEHAVIOR_SETTINGS, + Buffer.from([ + currentSettings.carpet_behavior, + newParameterBitfield + ]) + ) + }); + + await this.robot.sendCommand(packet.toHexString()); + } + + /** + * @returns {Promise} + */ + async disable() { + const currentSettings = await this._getSettings(); + const newParameterBitfield = currentSettings.parameter_bitfield & ~MSmartCarpetBehaviorSettingsDTO.PARAMETER_BIT.CARPET_SUCTION_BOOST; + + const packet = new MSmartPacket({ + messageType: MSmartPacket.MESSAGE_TYPE.SETTING, + payload: MSmartPacket.buildPayload( + MSmartConst.SETTING.SET_CARPET_BEHAVIOR_SETTINGS, + Buffer.from([ + currentSettings.carpet_behavior, + newParameterBitfield + ]) + ) + }); + + await this.robot.sendCommand(packet.toHexString()); + } +} + +module.exports = MideaCarpetModeControlCapability; diff --git a/backend/lib/robots/midea/capabilities/MideaCarpetSensorModeControlCapability.js b/backend/lib/robots/midea/capabilities/MideaCarpetSensorModeControlCapability.js new file mode 100644 index 00000000..f819e304 --- /dev/null +++ b/backend/lib/robots/midea/capabilities/MideaCarpetSensorModeControlCapability.js @@ -0,0 +1,95 @@ +const BEightParser = require("../../../msmart/BEightParser"); +const CarpetSensorModeControlCapability = require("../../../core/capabilities/CarpetSensorModeControlCapability"); +const MSmartCarpetBehaviorSettingsDTO = require("../../../msmart/dtos/MSmartCarpetBehaviorSettingsDTO"); +const MSmartConst = require("../../../msmart/MSmartConst"); +const MSmartPacket = require("../../../msmart/MSmartPacket"); + +/** + * @extends CarpetSensorModeControlCapability + */ +class MideaCarpetSensorModeControlCapability extends CarpetSensorModeControlCapability { + /** + * @private + * @returns {Promise} + */ + async _getSettings() { + const packet = new MSmartPacket({ + messageType: MSmartPacket.MESSAGE_TYPE.ACTION, + payload: MSmartPacket.buildPayload(MSmartConst.ACTION.GET_CARPET_BEHAVIOR_SETTINGS) + }); + + const response = await this.robot.sendCommand(packet.toHexString()); + const parsedResponse = BEightParser.PARSE(response); + + if (parsedResponse instanceof MSmartCarpetBehaviorSettingsDTO) { + return parsedResponse; + } else { + throw new Error("Invalid response from robot"); + } + } + + async getMode() { + const settings = await this._getSettings(); + + switch (settings.carpet_behavior) { + case 0: + return CarpetSensorModeControlCapability.MODE.AVOID; + case 1: + return CarpetSensorModeControlCapability.MODE.OFF; + case 2: + return CarpetSensorModeControlCapability.MODE.LIFT; + case 3: + return CarpetSensorModeControlCapability.MODE.CROSS; + default: + throw new Error(`Received invalid mode ${settings.carpet_behavior}`); + } + } + + async setMode(newMode) { + const currentSettings = await this._getSettings(); + let val; + + switch (newMode) { + case CarpetSensorModeControlCapability.MODE.AVOID: + val = 0; + break; + case CarpetSensorModeControlCapability.MODE.OFF: + val = 1; + break; + case CarpetSensorModeControlCapability.MODE.LIFT: + val = 2; + break; + case CarpetSensorModeControlCapability.MODE.CROSS: + val = 3; + break; + default: + throw new Error(`Received invalid mode ${newMode}`); + } + + const packet = new MSmartPacket({ + messageType: MSmartPacket.MESSAGE_TYPE.SETTING, + payload: MSmartPacket.buildPayload( + MSmartConst.SETTING.SET_CARPET_BEHAVIOR_SETTINGS, + Buffer.from([ + val, + currentSettings.parameter_bitfield + ]) + ) + }); + + await this.robot.sendCommand(packet.toHexString()); + } + + getProperties() { + return { + supportedModes: [ + CarpetSensorModeControlCapability.MODE.AVOID, + CarpetSensorModeControlCapability.MODE.OFF, + CarpetSensorModeControlCapability.MODE.LIFT, + CarpetSensorModeControlCapability.MODE.CROSS + ] + }; + } +} + +module.exports = MideaCarpetSensorModeControlCapability; diff --git a/backend/lib/robots/midea/capabilities/index.js b/backend/lib/robots/midea/capabilities/index.js index e262e33b..56ecc175 100644 --- a/backend/lib/robots/midea/capabilities/index.js +++ b/backend/lib/robots/midea/capabilities/index.js @@ -3,6 +3,8 @@ module.exports = { MideaAutoEmptyDockManualTriggerCapability: require("./MideaAutoEmptyDockManualTriggerCapability"), MideaBasicControlCapability: require("./MideaBasicControlCapability"), MideaCameraLightControlCapability: require("./MideaCameraLightControlCapability"), + MideaCarpetModeControlCapability: require("./MideaCarpetModeControlCapability"), + MideaCarpetSensorModeControlCapability: require("./MideaCarpetSensorModeControlCapability"), MideaCollisionAvoidantNavigationControlCapability: require("./MideaCollisionAvoidantNavigationControlCapability"), MideaCombinedVirtualRestrictionsCapability: require("./MideaCombinedVirtualRestrictionsCapability"), MideaCurrentStatisticsCapability: require("./MideaCurrentStatisticsCapability"), diff --git a/backend/lib/webserver/capabilityRouters/doc/CarpetSensorModeControlCapabilityRouter.openapi.json b/backend/lib/webserver/capabilityRouters/doc/CarpetSensorModeControlCapabilityRouter.openapi.json index e5132d1f..50d1e0d8 100644 --- a/backend/lib/webserver/capabilityRouters/doc/CarpetSensorModeControlCapabilityRouter.openapi.json +++ b/backend/lib/webserver/capabilityRouters/doc/CarpetSensorModeControlCapabilityRouter.openapi.json @@ -19,7 +19,8 @@ "off", "avoid", "lift", - "detach" + "detach", + "cross" ] } } @@ -46,7 +47,8 @@ "off", "avoid", "lift", - "detach" + "detach", + "cross" ] } } @@ -86,7 +88,8 @@ "off", "avoid", "lift", - "detach" + "detach", + "cross" ] } } diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts index e18ad5d9..6535bca9 100644 --- a/frontend/src/api/types.ts +++ b/frontend/src/api/types.ts @@ -570,7 +570,7 @@ export interface ValetudoCustomizations { friendlyName: string; } -export type CarpetSensorMode = "off" | "avoid" | "lift" | "detach"; +export type CarpetSensorMode = "off" | "avoid" | "lift" | "detach" | "cross"; export interface CarpetSensorModePayload { mode: CarpetSensorMode diff --git a/frontend/src/robot/RobotOptions.tsx b/frontend/src/robot/RobotOptions.tsx index fe3dcb40..b2bf913e 100644 --- a/frontend/src/robot/RobotOptions.tsx +++ b/frontend/src/robot/RobotOptions.tsx @@ -132,8 +132,9 @@ const CarpetModeControlCapabilitySwitchListMenuItem = () => { const CarpetSensorModeControlCapabilitySelectListMenuItem = () => { const SORT_ORDER = { - "off": 4, - "detach": 3, + "off": 5, + "detach": 4, + "cross" : 3, "avoid": 2, "lift": 1 }; @@ -173,6 +174,9 @@ const CarpetSensorModeControlCapabilitySelectListMenuItem = () => { case "detach": label = "Detach Mop"; break; + case "cross": + label = "Cross Carpet"; + break; } return { @@ -294,7 +298,7 @@ const AutoEmptyDockAutoEmptyIntervalControlCapabilitySelectListMenuItem = () => loadingOptions={autoEmptyDockAutoEmptyIntervalPropertiesPending || isPending} loadError={autoEmptyDockAutoEmptyIntervalPropertiesError} primaryLabel="Dock Auto-Empty" - secondaryLabel="Select if and how often the dock should auto-empty the robot." + secondaryLabel="Select if and/or how often the dock should auto-empty the robot." icon={} /> );