From 8fc5a0ab297ee27fbf925816751044d8bfb0c42a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Beye?= Date: Sun, 12 Oct 2025 17:43:23 +0200 Subject: [PATCH] feat(vendor.midea): J12 Ultra --- backend/lib/NTPClient.js | 20 +-- backend/lib/core/ValetudoRobot.js | 4 +- .../CarpetSensorModeControlCapability.js | 3 +- backend/lib/msmart/BEightParser.js | 43 +++-- backend/lib/msmart/MSmartConst.js | 4 +- backend/lib/msmart/MSmartDummycloud.js | 164 ++++++++++++------ backend/lib/msmart/dtos/MSmartStatusDTO.js | 2 + .../midea/MideaJ12UltraValetudoRobot.js | 63 +++++++ .../midea/MideaJ15ProUltraValetudoRobot.js | 18 +- backend/lib/robots/midea/MideaMapParser.js | 56 +++--- backend/lib/robots/midea/MideaQuirkFactory.js | 55 ++++++ .../lib/robots/midea/MideaValetudoRobot.js | 44 +++-- ...ockAutoEmptyIntervalControlCapabilityV1.js | 76 ++++++++ ...ckAutoEmptyIntervalControlCapabilityV2.js} | 4 +- .../MideaCarpetModeControlCapabilityV1.js | 66 +++++++ ... => MideaCarpetModeControlCapabilityV2.js} | 4 +- ...ideaCarpetSensorModeControlCapabilityV1.js | 75 ++++++++ ...deaCarpetSensorModeControlCapabilityV2.js} | 17 +- .../capabilities/MideaMapResetCapability.js | 31 ++++ .../MideaMopTwistControlCapabilityV1.js | 62 +++++++ ...js => MideaMopTwistControlCapabilityV2.js} | 4 +- .../lib/robots/midea/capabilities/index.js | 12 +- backend/lib/robots/midea/index.js | 3 +- backend/lib/utils/LinuxTools.js | 25 ++- ...orModeControlCapabilityRouter.openapi.json | 9 +- frontend/src/api/types.ts | 2 +- frontend/src/robot/RobotOptions.tsx | 8 +- 27 files changed, 710 insertions(+), 164 deletions(-) create mode 100644 backend/lib/robots/midea/MideaJ12UltraValetudoRobot.js create mode 100644 backend/lib/robots/midea/capabilities/MideaAutoEmptyDockAutoEmptyIntervalControlCapabilityV1.js rename backend/lib/robots/midea/capabilities/{MideaAutoEmptyDockAutoEmptyIntervalControlCapability.js => MideaAutoEmptyDockAutoEmptyIntervalControlCapabilityV2.js} (96%) create mode 100644 backend/lib/robots/midea/capabilities/MideaCarpetModeControlCapabilityV1.js rename backend/lib/robots/midea/capabilities/{MideaCarpetModeControlCapability.js => MideaCarpetModeControlCapabilityV2.js} (95%) create mode 100644 backend/lib/robots/midea/capabilities/MideaCarpetSensorModeControlCapabilityV1.js rename backend/lib/robots/midea/capabilities/{MideaCarpetSensorModeControlCapability.js => MideaCarpetSensorModeControlCapabilityV2.js} (87%) create mode 100644 backend/lib/robots/midea/capabilities/MideaMopTwistControlCapabilityV1.js rename backend/lib/robots/midea/capabilities/{MideaMopTwistControlCapability.js => MideaMopTwistControlCapabilityV2.js} (93%) diff --git a/backend/lib/NTPClient.js b/backend/lib/NTPClient.js index 4315d50b..7009c447 100644 --- a/backend/lib/NTPClient.js +++ b/backend/lib/NTPClient.js @@ -1,7 +1,6 @@ const ntp = require("@destinationstransfers/ntp"); -const execSync = require("child_process").execSync; - +const LinuxTools = require("./utils/LinuxTools"); const Logger = require("./Logger"); const States = require("./entities/core/ntpClient"); const Tools = require("./utils/Tools"); @@ -142,22 +141,7 @@ class NTPClient { setTime(date) { if (this.config.get("embedded") === true) { - let dateString = ""; - - dateString += date.getFullYear().toString(); - dateString += "-"; - dateString += (date.getMonth() + 1).toString().padStart(2, 0); - dateString += "-"; - dateString += date.getDate().toString().padStart(2, 0); - dateString += " "; - dateString += date.getHours().toString().padStart(2,0); - dateString += ":"; - dateString += date.getMinutes().toString().padStart(2,0); - dateString += ":"; - dateString += date.getSeconds().toString().padStart(2,0); - - - execSync("date -s \""+dateString+"\""); + LinuxTools.SET_TIME(date); Logger.info("Successfully set the robot time via NTP to", date); } else { diff --git a/backend/lib/core/ValetudoRobot.js b/backend/lib/core/ValetudoRobot.js index e3a0b506..177df314 100644 --- a/backend/lib/core/ValetudoRobot.js +++ b/backend/lib/core/ValetudoRobot.js @@ -224,7 +224,9 @@ class ValetudoRobot { this.executeMapPoll().then((response) => { repollSeconds = this.determineNextMapPollInterval(response); - }).catch(() => { + }).catch((err) => { + Logger.debug("Error while executing map poll", err); + repollSeconds = ValetudoRobot.MAP_POLLING_INTERVALS.ERROR; }).finally(() => { this.mapPollTimeout = setTimeout(() => { diff --git a/backend/lib/core/capabilities/CarpetSensorModeControlCapability.js b/backend/lib/core/capabilities/CarpetSensorModeControlCapability.js index 7bda579b..11113c03 100644 --- a/backend/lib/core/capabilities/CarpetSensorModeControlCapability.js +++ b/backend/lib/core/capabilities/CarpetSensorModeControlCapability.js @@ -57,8 +57,7 @@ CarpetSensorModeControlCapability.MODE = Object.freeze({ OFF: "off", AVOID: "avoid", LIFT: "lift", - DETACH: "detach", - CROSS: "cross", + DETACH: "detach" }); diff --git a/backend/lib/msmart/BEightParser.js b/backend/lib/msmart/BEightParser.js index 0dbef258..ea5ab7e3 100644 --- a/backend/lib/msmart/BEightParser.js +++ b/backend/lib/msmart/BEightParser.js @@ -129,6 +129,15 @@ class BEightParser { // payload[12]; // unclear // payload.readUInt32LE(13); // possibly expected duration of the timer in seconds + return "SKIP"; + case 0x52: + // No clue where this is coming from. Seen on the J12 about once every minute. Might be a state update? + return "SKIP"; + case 0x20: + // Seems to be relating to map state? + return "SKIP"; + case 0x21: + // No clue return "SKIP"; default: { Logger.warn( @@ -206,7 +215,7 @@ class BEightParser { data.has_mop = !!(mopStatusByte & 0b00000001); // Mops attached bool data.has_vibrate_mop = !!(mopStatusByte & 0b00000010); - data.carpet_switch = payload[19]; // bool, TODO: validate offset + data.carpet_switch = payload[19]; // bool, apparently superseded and just relevant for the j12? // 20 is unknown @@ -246,7 +255,7 @@ class BEightParser { // 46-49 seem to be a 4 byte number? async_number? not sure - const generalSwitchBits1 = payload[50]; + const generalSwitchBits1 = payload[50]; // Also known as general_switch data.personal_clean_prefer_switch = !!(generalSwitchBits1 & 0b00000001); data.station_inject_fluid_switch = !!(generalSwitchBits1 & 0b00000010); data.station_inject_soft_fluid_switch = !!(generalSwitchBits1 & 0b00000100); @@ -277,6 +286,7 @@ class BEightParser { data.telnet_switch = !!(generalSwitchBits3 & 0b00001000); data.mop_auto_dry_switch = !!(generalSwitchBits3 & 0b00010000); data.ai_grade_avoidance_mode = !!(generalSwitchBits3 & 0b00100000); + data.tail_sweep_clean_switch = !!(generalSwitchBits3 & 0b01000000); data.pound_sign_switch = !!(generalSwitchBits3 & 0b10000000); // TODO: naming - this is the criss cross pattern with multiple iterations data.stationCleanFrequency = payload[57]; @@ -290,21 +300,24 @@ class BEightParser { data.narrow_zone_recharge_switch = !!(generalSwitchBits4 & 0b00010000); data.verification_map_switch = !!(generalSwitchBits4 & 0b00100000); - const generalSwitchBits5 = payload[65]; - data.wake_up_switch = !!(generalSwitchBits5 & 0b00000001); - data.ai_carpet_avoid_switch = !!(generalSwitchBits5 & 0b00000010); - data.carpet_evade_adaptive_switch = !!(generalSwitchBits5 & 0b00000100); - data.stuck_mark_switch = !!(generalSwitchBits5 & 0b00001000); - data.mop_extend_switch = !!(generalSwitchBits5 & 0b00100000); - data.zigzag_to_end_switch = !!(generalSwitchBits5 & 0b01000000); + if (payload.length >= 67) { + const generalSwitchBits5 = payload[65]; + data.wake_up_switch = !!(generalSwitchBits5 & 0b00000001); + data.ai_carpet_avoid_switch = !!(generalSwitchBits5 & 0b00000010); + data.carpet_evade_adaptive_switch = !!(generalSwitchBits5 & 0b00000100); + data.stuck_mark_switch = !!(generalSwitchBits5 & 0b00001000); + data.mop_extend_switch = !!(generalSwitchBits5 & 0b00100000); + data.zigzag_to_end_switch = !!(generalSwitchBits5 & 0b01000000); - data.remaining_area = payload.readUInt16LE(66); + data.remaining_area = payload.readUInt16LE(66); - const generalSwitchBits6 = payload[68]; - data.ai_avoidance_switch = !!(generalSwitchBits6 & 0b00001000); - data.gap_deep_cleaning_switch = !!(generalSwitchBits6 & 0b00010000); - data.furniture_legs_cleaning_switch = !!(generalSwitchBits6 & 0b00100000); - data.edge_deep_vacuum_switch = !!(generalSwitchBits6 & 0b10000000); + const generalSwitchBits6 = payload[68]; + data.ai_avoidance_switch = !!(generalSwitchBits6 & 0b00001000); + data.gap_deep_cleaning_switch = !!(generalSwitchBits6 & 0b00010000); + data.furniture_legs_cleaning_switch = !!(generalSwitchBits6 & 0b00100000); + data.edge_deep_vacuum_switch = !!(generalSwitchBits6 & 0b10000000); + + } if (payload.length >= 71) { const generalSwitchBits7 = payload[70]; diff --git a/backend/lib/msmart/MSmartConst.js b/backend/lib/msmart/MSmartConst.js index 207a2f37..217f0361 100644 --- a/backend/lib/msmart/MSmartConst.js +++ b/backend/lib/msmart/MSmartConst.js @@ -9,9 +9,10 @@ const SETTING = Object.freeze({ MAP_MANAGEMENT: 0x24, JOIN_SEGMENTS: 0x26, SPLIT_SEGMENT: 0x27, - SET_VALID_MAP_IDS: 0x2D, // Used by the cloud to sync cloud state with device state with cloud being higher prio + SET_VALID_MAP_IDS: 0x2D, // Used by the cloud to sync cloud state with device state. The cloud being higher prio SET_FAN_SPEED: 0x50, SET_WATER_GRADE: 0x51, + SET_CARPET_MODE: 0x52, // J12. Not sure about newer robots SET_DOCK_INTERVALS: 0x56, SET_OPERATION_MODE: 0x58, TRIGGER_STATION_ACTION: 0x5A, @@ -21,6 +22,7 @@ const SETTING = Object.freeze({ SET_VOLUME: 0x93, SET_VARIOUS_TOGGLES: 0x9C, SET_HOT_WASH: 0xC5, + SET_AUTO_EMPTY_DURATION: 0xC7, SET_CLEANING_SETTINGS_1: 0xC9, // FIXME: naming }); diff --git a/backend/lib/msmart/MSmartDummycloud.js b/backend/lib/msmart/MSmartDummycloud.js index 60a16141..4cd49396 100644 --- a/backend/lib/msmart/MSmartDummycloud.js +++ b/backend/lib/msmart/MSmartDummycloud.js @@ -57,6 +57,7 @@ class MSmartDummycloud { this.commandTopic = "device/unknown/down"; this.aiCommandTopic = "ai/unknown/down"; + this.mapCommandTopic = "map/unknown/down"; /** * @type {Object.} */ sendCommand(command, options) { @@ -575,11 +609,12 @@ class MSmartDummycloud { /** * @private - * - * @param {string} command + * + * @param {string|object} command * @param {object} [options] * @param {number} [options.timeout] - milliseconds - * @param {"device"|"ai"} [options.target] - defaults to "device" + * @param {"device"|"ai"|"map"} [options.target] - defaults to "device" + * @param {boolean} [options.fireAndForget] * @returns {Promise} */ actualSendCommand(command, options = {}) { @@ -594,26 +629,40 @@ class MSmartDummycloud { }); const target = options?.target ?? "device"; - const targetTopic = target === "ai" ? this.aiCommandTopic : this.commandTopic; + let targetTopic; + switch (target) { + case "ai": + targetTopic = this.aiCommandTopic; + break; + case "device": + targetTopic = this.commandTopic; + break; + case "map": + targetTopic = this.mapCommandTopic; + break; + } - this.pendingRequests[nonce] = { - resolve: resolve, - reject: reject, - command: command, - onTimeoutCallback: () => { - Logger.debug(`Request with nonce ${nonce} timed out`); - delete this.pendingRequests[nonce]; + const fireAndForget = !!options?.fireAndForget; + if (!fireAndForget) { + this.pendingRequests[nonce] = { + resolve: resolve, + reject: reject, + command: command, + onTimeoutCallback: () => { + Logger.debug(`Request with nonce ${nonce} timed out`); + delete this.pendingRequests[nonce]; - reject(new MSmartTimeoutError({nonce: nonce, command: command})); - } - }; + reject(new MSmartTimeoutError({nonce: nonce, command: command})); + } + }; - this.pendingRequests[nonce].timeout_id = setTimeout( - () => { - this.pendingRequests[nonce].onTimeoutCallback(); - }, - options?.timeout ?? this.timeout - ); + this.pendingRequests[nonce].timeout_id = setTimeout( + () => { + this.pendingRequests[nonce].onTimeoutCallback(); + }, + options?.timeout ?? this.timeout + ); + } Logger.trace(`Sending command to ${targetTopic}`, payload); @@ -636,6 +685,9 @@ class MSmartDummycloud { } reject(error); + } else if (options.fireAndForget) { + // This is a bit janky, but it allows us to have the return type always be an MSmartPacket + resolve(new MSmartPacket({messageType: 0, payload: Buffer.alloc(0)})); } } ); diff --git a/backend/lib/msmart/dtos/MSmartStatusDTO.js b/backend/lib/msmart/dtos/MSmartStatusDTO.js index 1da87827..cfe5fded 100644 --- a/backend/lib/msmart/dtos/MSmartStatusDTO.js +++ b/backend/lib/msmart/dtos/MSmartStatusDTO.js @@ -69,6 +69,7 @@ class MSmartStatusDTO extends MSmartDTO { * @param {boolean} [data.telnet_switch] * @param {boolean} [data.mop_auto_dry_switch] * @param {boolean} [data.ai_grade_avoidance_mode] + * @param {boolean} [data.tail_sweep_clean_switch] * @param {boolean} [data.pound_sign_switch] * @param {number} [data.stationCleanFrequency] * @param {number} [data.beautify_map_grade] @@ -169,6 +170,7 @@ class MSmartStatusDTO extends MSmartDTO { this.telnet_switch = data.telnet_switch; this.mop_auto_dry_switch = data.mop_auto_dry_switch; this.ai_grade_avoidance_mode = data.ai_grade_avoidance_mode; + this.tail_sweep_clean_switch = data.tail_sweep_clean_switch; this.pound_sign_switch = data.pound_sign_switch; this.stationCleanFrequency = data.stationCleanFrequency; this.beautify_map_grade = data.beautify_map_grade; diff --git a/backend/lib/robots/midea/MideaJ12UltraValetudoRobot.js b/backend/lib/robots/midea/MideaJ12UltraValetudoRobot.js new file mode 100644 index 00000000..3010c88c --- /dev/null +++ b/backend/lib/robots/midea/MideaJ12UltraValetudoRobot.js @@ -0,0 +1,63 @@ +const capabilities = require("./capabilities"); +const fs = require("node:fs"); +const Logger = require("../../Logger"); +const MideaQuirkFactory = require("./MideaQuirkFactory"); +const MideaValetudoRobot = require("./MideaValetudoRobot"); +const QuirksCapability = require("../../core/capabilities/QuirksCapability"); + +class MideaJ12UltraValetudoRobot extends MideaValetudoRobot { + constructor(options) { + super( + Object.assign( + {}, + options, + { + oldMapPollStyle: true + } + ) + ); + + const quirkFactory = new MideaQuirkFactory({ + robot: this + }); + + [ + capabilities.MideaAutoEmptyDockAutoEmptyIntervalControlCapabilityV1, + capabilities.MideaCarpetModeControlCapabilityV1, + capabilities.MideaCarpetSensorModeControlCapabilityV1, + capabilities.MideaMopTwistControlCapabilityV1, + ].forEach(capability => { + this.registerCapability(new capability({robot: this})); + }); + + this.registerCapability(new QuirksCapability({ + robot: this, + quirks: [ + quirkFactory.getQuirk(MideaQuirkFactory.KNOWN_QUIRKS.AUTO_EMPTY_DURATION), + ] + })); + } + + getManufacturer() { + return "Eureka"; + } + + getModelName() { + return "J12 Ultra"; + } + + static IMPLEMENTATION_AUTO_DETECTION_HANDLER() { + let sn8; + + try { + sn8 = fs.readFileSync("/oem/midea/device.sn8").toString().trim(); + } catch (e) { + //This is intentionally failing if we're the wrong implementation + Logger.trace("cannot read", "/oem/midea/device.sn8", e); + } + + return ["750Y000D", "750Y000J"].includes(sn8); + } +} + +module.exports = MideaJ12UltraValetudoRobot; diff --git a/backend/lib/robots/midea/MideaJ15ProUltraValetudoRobot.js b/backend/lib/robots/midea/MideaJ15ProUltraValetudoRobot.js index fbfb5121..aa47089b 100644 --- a/backend/lib/robots/midea/MideaJ15ProUltraValetudoRobot.js +++ b/backend/lib/robots/midea/MideaJ15ProUltraValetudoRobot.js @@ -9,21 +9,32 @@ const {IMAGE_FILE_FORMAT} = require("../../utils/const"); class MideaJ15ProUltraValetudoRobot extends MideaValetudoRobot { constructor(options) { - super(options); + super( + Object.assign( + {}, + options, + { + waterGrades: MideaJ15ProUltraValetudoRobot.HIGH_RESOLUTION_WATER_GRADES, + } + ) + ); const quirkFactory = new MideaQuirkFactory({ robot: this }); [ + capabilities.MideaAutoEmptyDockAutoEmptyIntervalControlCapabilityV2, capabilities.MideaMopExtensionControlCapability, capabilities.MideaCameraLightControlCapability, capabilities.MideaObstacleAvoidanceControlCapability, capabilities.MideaMopDockMopWashTemperatureControlCapability, - capabilities.MideaCarpetSensorModeControlCapability, + capabilities.MideaCarpetSensorModeControlCapabilityV2, capabilities.MideaPetObstacleAvoidanceControlCapability, - capabilities.MideaMopTwistControlCapability, + capabilities.MideaMopTwistControlCapabilityV2, capabilities.MideaMopExtensionFurnitureLegHandlingControlCapability, + capabilities.MideaCollisionAvoidantNavigationControlCapability, + capabilities.MideaCarpetModeControlCapabilityV2 ].forEach(capability => { this.registerCapability(new capability({robot: this})); }); @@ -49,6 +60,7 @@ class MideaJ15ProUltraValetudoRobot extends MideaValetudoRobot { quirkFactory.getQuirk(MideaQuirkFactory.KNOWN_QUIRKS.DEEP_CARPET_CLEANING), quirkFactory.getQuirk(MideaQuirkFactory.KNOWN_QUIRKS.INCREASED_CARPET_AVOIDANCE), quirkFactory.getQuirk(MideaQuirkFactory.KNOWN_QUIRKS.STAIN_CLEANING), + quirkFactory.getQuirk(MideaQuirkFactory.KNOWN_QUIRKS.AUTO_EMPTY_DURATION), ] })); diff --git a/backend/lib/robots/midea/MideaMapParser.js b/backend/lib/robots/midea/MideaMapParser.js index 3fd7a802..9330139e 100644 --- a/backend/lib/robots/midea/MideaMapParser.js +++ b/backend/lib/robots/midea/MideaMapParser.js @@ -25,6 +25,9 @@ class MideaMapParser { this.layers = []; this.entities = []; + + this.mapInfoValid = false; + this.dockPositionValid = false; } /** @@ -115,33 +118,35 @@ class MideaMapParser { getCurrentMap() { const entities = [...this.entities]; - const dockCoords = this.convertToValetudoCoordinates(this.dockPosition.x, this.dockPosition.y); - const dockAngle = (-this.dockPosition.angle + 360) % 360; + if (this.dockPositionValid) { + const dockCoords = this.convertToValetudoCoordinates(this.dockPosition.x, this.dockPosition.y); + const dockAngle = (-this.dockPosition.angle + 360) % 360; - entities.push(new mapEntities.PointMapEntity({ - points: [ - dockCoords.x, - dockCoords.y - ], - metaData: { - angle: dockAngle - }, - type: mapEntities.PointMapEntity.TYPE.CHARGER_LOCATION - })); - - // TODO: only when docked - const hasRobotPosition = entities.some(e => e.type === mapEntities.PointMapEntity.TYPE.ROBOT_POSITION); - if (!hasRobotPosition) { entities.push(new mapEntities.PointMapEntity({ - points: [ // Offset by 1 unit so that they don't overlap 100% - dockCoords.x + MideaMapParser.PIXEL_SIZE, - dockCoords.y + MideaMapParser.PIXEL_SIZE + points: [ + dockCoords.x, + dockCoords.y ], metaData: { angle: dockAngle }, - type: mapEntities.PointMapEntity.TYPE.ROBOT_POSITION + type: mapEntities.PointMapEntity.TYPE.CHARGER_LOCATION })); + + // TODO: only when docked + const hasRobotPosition = entities.some(e => e.type === mapEntities.PointMapEntity.TYPE.ROBOT_POSITION); + if (!hasRobotPosition) { + entities.push(new mapEntities.PointMapEntity({ + points: [ // Offset by 1 unit so that they don't overlap 100% + dockCoords.x + MideaMapParser.PIXEL_SIZE, + dockCoords.y + MideaMapParser.PIXEL_SIZE + ], + metaData: { + angle: dockAngle + }, + type: mapEntities.PointMapEntity.TYPE.ROBOT_POSITION + })); + } } return new mapEntities.ValetudoMap({ @@ -261,6 +266,8 @@ class MideaMapParser { this.mapInfo.left = left; this.mapInfo.bottom = bottom; this.layers = layers; + + this.mapInfoValid = true; } /** @@ -374,9 +381,11 @@ class MideaMapParser { return ![ MideaMapParser.PATH_TYPES.MAPPING, MideaMapParser.PATH_TYPES.MOVING, + MideaMapParser.PATH_TYPES.POSITIONING, MideaMapParser.PATH_TYPES.RETURNING, MideaMapParser.PATH_TYPES.TAXIING, - MideaMapParser.PATH_TYPES.TAXIING_ZONES + MideaMapParser.PATH_TYPES.TAXIING_ZONES, + MideaMapParser.PATH_TYPES.RELOCATING, ].includes(e.metaData.vendorPathType); })); } @@ -391,6 +400,8 @@ class MideaMapParser { */ async handleDockPositionUpdate(data) { this.dockPosition = data; + + this.dockPositionValid = true; } /** @@ -610,13 +621,14 @@ MideaMapParser.PATH_TYPES = Object.freeze({ "NONE": 0, // Probably not a real type? "RETURNING": 10, + "POSITIONING": 20, "OUTLINE": 30, "TAXIING_ZONES": 40, "TAXIING_SEGMENT_CLEANING": 50, "CLEANING_TURN": 80, "CLEANING": 100, - // TODO: 120 + "RELOCATING": 120, // Just a guess "MAPPING": 170, "TAXIING": 180, diff --git a/backend/lib/robots/midea/MideaQuirkFactory.js b/backend/lib/robots/midea/MideaQuirkFactory.js index 1ca6154b..999fefd2 100644 --- a/backend/lib/robots/midea/MideaQuirkFactory.js +++ b/backend/lib/robots/midea/MideaQuirkFactory.js @@ -469,6 +469,60 @@ class MideaQuirkFactory { }).toHexString()); } }); + case MideaQuirkFactory.KNOWN_QUIRKS.AUTO_EMPTY_DURATION: + return new Quirk({ + id: id, + title: "Auto Empty Duration", + description: "Select how long the dock should empty the dustbin on each auto empty cycle.", + options: ["short", "medium", "long"], + getter: async() => { + const packet = new MSmartPacket({ + messageType: MSmartPacket.MESSAGE_TYPE.ACTION, + payload: MSmartPacket.buildPayload(MSmartConst.ACTION.GET_STATUS) + }); + + const response = await this.robot.sendCommand(packet.toHexString()); + const parsedResponse = BEightParser.PARSE(response); + + if (parsedResponse instanceof MSmartStatusDTO) { + switch (parsedResponse.collect_dust_mode) { + case 3: + return "long"; + case 2: + return "medium"; + case 1: + return "short"; + } + } else { + throw new Error("Invalid response from robot"); + } + }, + setter: async(value) => { + let val; + + switch (value) { + case "long": + val = 3; + break; + case "medium": + val = 2; + break; + case "short": + val = 1; + break; + default: + throw new Error(`Received invalid value ${value}`); + } + + await this.robot.sendCommand(new MSmartPacket({ + messageType: MSmartPacket.MESSAGE_TYPE.SETTING, + payload: MSmartPacket.buildPayload( + MSmartConst.SETTING.SET_AUTO_EMPTY_DURATION, + Buffer.from([val]) + ) + }).toHexString()); + } + }); default: throw new Error(`There's no quirk with id ${id}`); } @@ -485,6 +539,7 @@ MideaQuirkFactory.KNOWN_QUIRKS = { DEEP_CARPET_CLEANING: "d2ad3f99-c1b0-4195-9a98-4f13bdb0f1e8", INCREASED_CARPET_AVOIDANCE: "f3ff1c65-9fe7-4312-b196-83ce91107fe8", STAIN_CLEANING: "d4688a29-a6e4-43c2-ab3a-08ddae40655c", + AUTO_EMPTY_DURATION: "ac39aac4-c798-43b4-88ee-e4847a799a84" }; module.exports = MideaQuirkFactory; diff --git a/backend/lib/robots/midea/MideaValetudoRobot.js b/backend/lib/robots/midea/MideaValetudoRobot.js index d874c438..fde11e01 100644 --- a/backend/lib/robots/midea/MideaValetudoRobot.js +++ b/backend/lib/robots/midea/MideaValetudoRobot.js @@ -13,6 +13,8 @@ const MSmartDummycloud = require("../../msmart/MSmartDummycloud"); const MSmartPacket = require("../../msmart/MSmartPacket"); const ValetudoRobot = require("../../core/ValetudoRobot"); const stateAttrs = entities.state.attributes; +const LinuxTools = require("../../utils/LinuxTools"); +const Tools = require("../../utils/Tools"); const ValetudoRobotError = require("../../entities/core/ValetudoRobotError"); const ValetudoSelectionPreset = require("../../entities/core/ValetudoSelectionPreset"); @@ -26,8 +28,18 @@ const BIND_IP = "127.0.13.37"; * @abstract */ class MideaValetudoRobot extends ValetudoRobot { + /** + * + * @param {object} options + * @param {import("../../Configuration")} options.config + * @param {import("../../ValetudoEventStore")} options.valetudoEventStore + * @param {object} [options.waterGrades] + * @param {boolean} [options.oldMapPollStyle] + */ constructor(options) { super(options); + this.waterGrades = options.waterGrades ?? MideaValetudoRobot.WATER_GRADES; + this.oldMapPollStyle = !!options.oldMapPollStyle; // FIXME: this breaks the build_docs script. Find a better solution if (!fs.existsSync(CA_KEY_PATH) || !fs.existsSync(CA_CERT_PATH)) { @@ -107,8 +119,8 @@ class MideaValetudoRobot extends ValetudoRobot { })); this.registerCapability(new capabilities.MideaWaterUsageControlCapability({ robot: this, - presets: Object.keys(MideaValetudoRobot.HIGH_RESOLUTION_WATER_GRADES).map(k => { - return new ValetudoSelectionPreset({name: k, value: MideaValetudoRobot.HIGH_RESOLUTION_WATER_GRADES[k]}); + presets: Object.keys(this.waterGrades).map(k => { + return new ValetudoSelectionPreset({name: k, value: this.waterGrades[k]}); }) })); @@ -126,12 +138,9 @@ class MideaValetudoRobot extends ValetudoRobot { capabilities.MideaZoneCleaningCapability, capabilities.MideaCombinedVirtualRestrictionsCapability, capabilities.MideaKeyLockCapability, - capabilities.MideaCollisionAvoidantNavigationControlCapability, capabilities.MideaAutoEmptyDockManualTriggerCapability, capabilities.MideaMopDockCleanManualTriggerCapability, capabilities.MideaMopDockDryManualTriggerCapability, - capabilities.MideaAutoEmptyDockAutoEmptyIntervalControlCapability, - capabilities.MideaCarpetModeControlCapability, capabilities.MideaMopDockMopAutoDryingControlCapability, ].forEach(capability => { this.registerCapability(new capability({robot: this})); @@ -164,6 +173,15 @@ class MideaValetudoRobot extends ValetudoRobot { super.startup(); if (this.config.get("embedded") === true) { + // The J12 starts up with a time in 1970, which is too old for our root CA + const buildTimestamp = Tools.GET_BUILD_TIMESTAMP(); + if (buildTimestamp > new Date()) { + // Assuming that time is linearly moving forward, this gives us a realistic lower bound + LinuxTools.SET_TIME(buildTimestamp); + + Logger.info("Successfully set the robot time via the valetudo build timestamp to", buildTimestamp); + } + Logger.info(`Firmware Version: ${this.getFirmwareVersion() ?? "unknown"}`); } } @@ -276,8 +294,8 @@ class MideaValetudoRobot extends ValetudoRobot { } if (data.water_level !== undefined) { - let matchingWaterGrade = Object.keys(MideaValetudoRobot.HIGH_RESOLUTION_WATER_GRADES).find(key => { - return MideaValetudoRobot.HIGH_RESOLUTION_WATER_GRADES[key] === data.water_level; + let matchingWaterGrade = Object.keys(this.waterGrades).find(key => { + return this.waterGrades[key] === data.water_level; }); if (matchingWaterGrade) { @@ -365,10 +383,11 @@ class MideaValetudoRobot extends ValetudoRobot { } /** - * @param {string} command + * @param {string|object} command * @param {object} [options] * @param {number} [options.timeout] - milliseconds - * @param {"device"|"ai"} [options.target] - defaults to "device" + * @param {"device"|"ai"|"map"} [options.target] - defaults to "device" + * @param {boolean} [options.fireAndForget] * @returns {Promise} */ async sendCommand(command, options) { @@ -417,6 +436,7 @@ class MideaValetudoRobot extends ValetudoRobot { } async executeMapPoll() { + // TODO: Should these all be new instances every single poll? const mapPollPacket = new MSmartPacket({ messageType: MSmartPacket.MESSAGE_TYPE.ACTION, payload: MSmartPacket.buildPayload(MSmartConst.ACTION.POLL_MAP) @@ -430,7 +450,11 @@ class MideaValetudoRobot extends ValetudoRobot { payload: MSmartPacket.buildPayload(MSmartConst.ACTION.GET_ACTIVE_ZONES) }); - await this.sendCommand(mapPollPacket.toHexString()); + if (this.oldMapPollStyle) { + await this.sendCommand({command: "start"}, {target: "map", fireAndForget: true}); + } else { + await this.sendCommand(mapPollPacket.toHexString()); + } const dockPositionResponse = await this.sendCommand(dockPositionPollPacket.toHexString()); diff --git a/backend/lib/robots/midea/capabilities/MideaAutoEmptyDockAutoEmptyIntervalControlCapabilityV1.js b/backend/lib/robots/midea/capabilities/MideaAutoEmptyDockAutoEmptyIntervalControlCapabilityV1.js new file mode 100644 index 00000000..ae1179f3 --- /dev/null +++ b/backend/lib/robots/midea/capabilities/MideaAutoEmptyDockAutoEmptyIntervalControlCapabilityV1.js @@ -0,0 +1,76 @@ +const AutoEmptyDockAutoEmptyIntervalControlCapability = require("../../../core/capabilities/AutoEmptyDockAutoEmptyIntervalControlCapability"); +const BEightParser = require("../../../msmart/BEightParser"); +const MSmartConst = require("../../../msmart/MSmartConst"); +const MSmartPacket = require("../../../msmart/MSmartPacket"); +const MSmartStatusDTO = require("../../../msmart/dtos/MSmartStatusDTO"); + +/** + * @extends AutoEmptyDockAutoEmptyIntervalControlCapability + */ +class MideaAutoEmptyDockAutoEmptyIntervalControlCapabilityV1 extends AutoEmptyDockAutoEmptyIntervalControlCapability { + async getInterval() { + 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) { + if (parsedResponse.dustTimes === 0) { + return AutoEmptyDockAutoEmptyIntervalControlCapability.INTERVAL.OFF; + } + + if (parsedResponse.dustTimes > 1) { + return AutoEmptyDockAutoEmptyIntervalControlCapability.INTERVAL.INFREQUENT; + } else { + return AutoEmptyDockAutoEmptyIntervalControlCapability.INTERVAL.NORMAL; + } + } else { + throw new Error("Invalid response from robot"); + } + } + + async setInterval(newInterval) { + let val; + switch (newInterval) { + case AutoEmptyDockAutoEmptyIntervalControlCapability.INTERVAL.OFF: + val = 0; + break; + case AutoEmptyDockAutoEmptyIntervalControlCapability.INTERVAL.INFREQUENT: + val = 3; + break; + case AutoEmptyDockAutoEmptyIntervalControlCapability.INTERVAL.NORMAL: + val = 1; + break; + default: + throw new Error("Invalid interval"); + } + + await this.robot.sendCommand( + new MSmartPacket({ + messageType: MSmartPacket.MESSAGE_TYPE.SETTING, + payload: MSmartPacket.buildPayload( + MSmartConst.SETTING.SET_DOCK_INTERVALS, + Buffer.from([ + 0x01, // Auto-empty interval + val + ]) + ) + }).toHexString() + ); + } + + getProperties() { + return { + supportedIntervals: [ + AutoEmptyDockAutoEmptyIntervalControlCapability.INTERVAL.OFF, + AutoEmptyDockAutoEmptyIntervalControlCapability.INTERVAL.NORMAL, + AutoEmptyDockAutoEmptyIntervalControlCapability.INTERVAL.INFREQUENT, + ] + }; + } +} + +module.exports = MideaAutoEmptyDockAutoEmptyIntervalControlCapabilityV1; diff --git a/backend/lib/robots/midea/capabilities/MideaAutoEmptyDockAutoEmptyIntervalControlCapability.js b/backend/lib/robots/midea/capabilities/MideaAutoEmptyDockAutoEmptyIntervalControlCapabilityV2.js similarity index 96% rename from backend/lib/robots/midea/capabilities/MideaAutoEmptyDockAutoEmptyIntervalControlCapability.js rename to backend/lib/robots/midea/capabilities/MideaAutoEmptyDockAutoEmptyIntervalControlCapabilityV2.js index d9bb252f..3c4e469f 100644 --- a/backend/lib/robots/midea/capabilities/MideaAutoEmptyDockAutoEmptyIntervalControlCapability.js +++ b/backend/lib/robots/midea/capabilities/MideaAutoEmptyDockAutoEmptyIntervalControlCapabilityV2.js @@ -7,7 +7,7 @@ const MSmartStatusDTO = require("../../../msmart/dtos/MSmartStatusDTO"); /** * @extends AutoEmptyDockAutoEmptyIntervalControlCapability */ -class MideaAutoEmptyDockAutoEmptyIntervalControlCapability extends AutoEmptyDockAutoEmptyIntervalControlCapability { +class MideaAutoEmptyDockAutoEmptyIntervalControlCapabilityV2 extends AutoEmptyDockAutoEmptyIntervalControlCapability { async getInterval() { const response = await this.robot.sendCommand( new MSmartPacket({ @@ -93,4 +93,4 @@ class MideaAutoEmptyDockAutoEmptyIntervalControlCapability extends AutoEmptyDock } } -module.exports = MideaAutoEmptyDockAutoEmptyIntervalControlCapability; +module.exports = MideaAutoEmptyDockAutoEmptyIntervalControlCapabilityV2; diff --git a/backend/lib/robots/midea/capabilities/MideaCarpetModeControlCapabilityV1.js b/backend/lib/robots/midea/capabilities/MideaCarpetModeControlCapabilityV1.js new file mode 100644 index 00000000..a46cd624 --- /dev/null +++ b/backend/lib/robots/midea/capabilities/MideaCarpetModeControlCapabilityV1.js @@ -0,0 +1,66 @@ +const BEightParser = require("../../../msmart/BEightParser"); +const CarpetModeControlCapability = require("../../../core/capabilities/CarpetModeControlCapability"); +const MSmartConst = require("../../../msmart/MSmartConst"); +const MSmartPacket = require("../../../msmart/MSmartPacket"); +const MSmartStatusDTO = require("../../../msmart/dtos/MSmartStatusDTO"); + + +/** + * @extends CarpetModeControlCapability + */ +class MideaCarpetModeControlCapabilityV1 extends CarpetModeControlCapability { + /** + * @returns {Promise} + */ + async isEnabled() { + const packet = new MSmartPacket({ + messageType: MSmartPacket.MESSAGE_TYPE.ACTION, + payload: MSmartPacket.buildPayload(MSmartConst.ACTION.GET_STATUS) + }); + + const response = await this.robot.sendCommand(packet.toHexString()); + const parsedResponse = BEightParser.PARSE(response); + + if (parsedResponse instanceof MSmartStatusDTO) { + return parsedResponse.carpet_switch === 1; + } else { + throw new Error("Invalid response from robot"); + } + } + + /** + * @returns {Promise} + */ + async enable() { + const packet = new MSmartPacket({ + messageType: MSmartPacket.MESSAGE_TYPE.SETTING, + payload: MSmartPacket.buildPayload( + MSmartConst.SETTING.SET_CARPET_MODE, + Buffer.from([ + 0x01 + ]) + ) + }); + + await this.robot.sendCommand(packet.toHexString()); + } + + /** + * @returns {Promise} + */ + async disable() { + const packet = new MSmartPacket({ + messageType: MSmartPacket.MESSAGE_TYPE.SETTING, + payload: MSmartPacket.buildPayload( + MSmartConst.SETTING.SET_CARPET_MODE, + Buffer.from([ + 0x00 + ]) + ) + }); + + await this.robot.sendCommand(packet.toHexString()); + } +} + +module.exports = MideaCarpetModeControlCapabilityV1; diff --git a/backend/lib/robots/midea/capabilities/MideaCarpetModeControlCapability.js b/backend/lib/robots/midea/capabilities/MideaCarpetModeControlCapabilityV2.js similarity index 95% rename from backend/lib/robots/midea/capabilities/MideaCarpetModeControlCapability.js rename to backend/lib/robots/midea/capabilities/MideaCarpetModeControlCapabilityV2.js index c4140d29..28d713ab 100644 --- a/backend/lib/robots/midea/capabilities/MideaCarpetModeControlCapability.js +++ b/backend/lib/robots/midea/capabilities/MideaCarpetModeControlCapabilityV2.js @@ -8,7 +8,7 @@ const MSmartPacket = require("../../../msmart/MSmartPacket"); /** * @extends CarpetModeControlCapability */ -class MideaCarpetModeControlCapability extends CarpetModeControlCapability { +class MideaCarpetModeControlCapabilityV2 extends CarpetModeControlCapability { /** * @private * @returns {Promise} @@ -81,4 +81,4 @@ class MideaCarpetModeControlCapability extends CarpetModeControlCapability { } } -module.exports = MideaCarpetModeControlCapability; +module.exports = MideaCarpetModeControlCapabilityV2; diff --git a/backend/lib/robots/midea/capabilities/MideaCarpetSensorModeControlCapabilityV1.js b/backend/lib/robots/midea/capabilities/MideaCarpetSensorModeControlCapabilityV1.js new file mode 100644 index 00000000..a7087374 --- /dev/null +++ b/backend/lib/robots/midea/capabilities/MideaCarpetSensorModeControlCapabilityV1.js @@ -0,0 +1,75 @@ +const BEightParser = require("../../../msmart/BEightParser"); +const CarpetSensorModeControlCapability = require("../../../core/capabilities/CarpetSensorModeControlCapability"); +const MSmartConst = require("../../../msmart/MSmartConst"); +const MSmartPacket = require("../../../msmart/MSmartPacket"); +const MSmartStatusDTO = require("../../../msmart/dtos/MSmartStatusDTO"); + +/** + * @extends CarpetSensorModeControlCapability + */ +class MideaCarpetSensorModeControlCapabilityV1 extends CarpetSensorModeControlCapability { + /** + * @returns {Promise} + */ + async getMode() { + const packet = new MSmartPacket({ + messageType: MSmartPacket.MESSAGE_TYPE.ACTION, + payload: MSmartPacket.buildPayload(MSmartConst.ACTION.GET_STATUS) + }); + + const response = await this.robot.sendCommand(packet.toHexString()); + const parsedResponse = BEightParser.PARSE(response); + + if (parsedResponse instanceof MSmartStatusDTO) { + if (parsedResponse.carpet_evade_switch) { + // You'd think that carpet_evade_adaptive_switch would be the smart variant that is only active when mopping, + // but apparently it is not. What is called adaptive in the app translations, is non-adaptive normally?? + // If it turns out that I confused myself, 0x23 will be the right setting instead + return CarpetSensorModeControlCapability.MODE.AVOID; + } else { + return CarpetSensorModeControlCapability.MODE.OFF; + } + } else { + throw new Error("Invalid response from robot"); + } + } + + async setMode(newMode) { + let val; + + switch (newMode) { + case CarpetSensorModeControlCapability.MODE.OFF: + val = 0; + break; + case CarpetSensorModeControlCapability.MODE.AVOID: + val = 1; + break; + default: + throw new Error(`Received invalid mode ${newMode}`); + } + + const packet = new MSmartPacket({ + messageType: MSmartPacket.MESSAGE_TYPE.SETTING, + payload: MSmartPacket.buildPayload( + MSmartConst.SETTING.SET_VARIOUS_TOGGLES, + Buffer.from([ + 0x07, // switch carpet evade + val + ]) + ) + }); + + await this.robot.sendCommand(packet.toHexString()); + } + + getProperties() { + return { + supportedModes: [ + CarpetSensorModeControlCapability.MODE.AVOID, + CarpetSensorModeControlCapability.MODE.OFF + ] + }; + } +} + +module.exports = MideaCarpetSensorModeControlCapabilityV1; diff --git a/backend/lib/robots/midea/capabilities/MideaCarpetSensorModeControlCapability.js b/backend/lib/robots/midea/capabilities/MideaCarpetSensorModeControlCapabilityV2.js similarity index 87% rename from backend/lib/robots/midea/capabilities/MideaCarpetSensorModeControlCapability.js rename to backend/lib/robots/midea/capabilities/MideaCarpetSensorModeControlCapabilityV2.js index f819e304..e6621d30 100644 --- a/backend/lib/robots/midea/capabilities/MideaCarpetSensorModeControlCapability.js +++ b/backend/lib/robots/midea/capabilities/MideaCarpetSensorModeControlCapabilityV2.js @@ -7,7 +7,7 @@ const MSmartPacket = require("../../../msmart/MSmartPacket"); /** * @extends CarpetSensorModeControlCapability */ -class MideaCarpetSensorModeControlCapability extends CarpetSensorModeControlCapability { +class MideaCarpetSensorModeControlCapabilityV2 extends CarpetSensorModeControlCapability { /** * @private * @returns {Promise} @@ -32,14 +32,13 @@ class MideaCarpetSensorModeControlCapability extends CarpetSensorModeControlCapa 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; + case 0: // 0 = Avoid, meaning that the robot will never drive over carpets even when vacuuming, which makes little sense + case 3: // 3 = Cross, which is a lot more similar to what other vendors would have as "avoid" + return CarpetSensorModeControlCapability.MODE.AVOID; default: throw new Error(`Received invalid mode ${settings.carpet_behavior}`); } @@ -50,16 +49,13 @@ class MideaCarpetSensorModeControlCapability extends CarpetSensorModeControlCapa 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: + case CarpetSensorModeControlCapability.MODE.AVOID: val = 3; break; default: @@ -86,10 +82,9 @@ class MideaCarpetSensorModeControlCapability extends CarpetSensorModeControlCapa CarpetSensorModeControlCapability.MODE.AVOID, CarpetSensorModeControlCapability.MODE.OFF, CarpetSensorModeControlCapability.MODE.LIFT, - CarpetSensorModeControlCapability.MODE.CROSS ] }; } } -module.exports = MideaCarpetSensorModeControlCapability; +module.exports = MideaCarpetSensorModeControlCapabilityV2; diff --git a/backend/lib/robots/midea/capabilities/MideaMapResetCapability.js b/backend/lib/robots/midea/capabilities/MideaMapResetCapability.js index 4b9f1838..fa756956 100644 --- a/backend/lib/robots/midea/capabilities/MideaMapResetCapability.js +++ b/backend/lib/robots/midea/capabilities/MideaMapResetCapability.js @@ -1,5 +1,7 @@ +const BEightParser = require("../../../msmart/BEightParser"); const MapResetCapability = require("../../../core/capabilities/MapResetCapability"); const MSmartConst = require("../../../msmart/MSmartConst"); +const MSmartMapListDTO = require("../../../msmart/dtos/MSmartMapListDTO"); const MSmartPacket = require("../../../msmart/MSmartPacket"); const {sleep} = require("../../../utils/misc"); @@ -17,6 +19,35 @@ class MideaMapResetCapability extends MapResetCapability { * @returns {Promise} */ async reset() { + const listMapsPacket = new MSmartPacket({ + messageType: MSmartPacket.MESSAGE_TYPE.ACTION, + payload: MSmartPacket.buildPayload(MSmartConst.ACTION.LIST_MAPS) + }); + + const listMapsResponse = await this.robot.sendCommand(listMapsPacket.toHexString()); + const parsedListMapsResponse = BEightParser.PARSE(listMapsResponse); + + if (!(parsedListMapsResponse instanceof MSmartMapListDTO)) { + throw new Error("Failed to list map ids."); + } + + const idsToDelete = new Set([...parsedListMapsResponse.savedMapIds, parsedListMapsResponse.currentMapId]); + for (const mapId of idsToDelete) { + const mapDeletePacket = new MSmartPacket({ + messageType: MSmartPacket.MESSAGE_TYPE.SETTING, + payload: MSmartPacket.buildPayload( + MSmartConst.SETTING.MAP_MANAGEMENT, + Buffer.from([ + 0x02, // delete + mapId + ]) + ) + }); + + await this.robot.sendCommand(mapDeletePacket.toHexString()); + } + + // This is enough for the J15PU, but for good measure (and the J12), we also delete all of them manually beforehand const setMapIndexPacket = new MSmartPacket({ messageType: MSmartPacket.MESSAGE_TYPE.SETTING, payload: MSmartPacket.buildPayload( diff --git a/backend/lib/robots/midea/capabilities/MideaMopTwistControlCapabilityV1.js b/backend/lib/robots/midea/capabilities/MideaMopTwistControlCapabilityV1.js new file mode 100644 index 00000000..294e029c --- /dev/null +++ b/backend/lib/robots/midea/capabilities/MideaMopTwistControlCapabilityV1.js @@ -0,0 +1,62 @@ +const BEightParser = require("../../../msmart/BEightParser"); +const MopTwistControlCapability = require("../../../core/capabilities/MopTwistControlCapability"); +const MSmartConst = require("../../../msmart/MSmartConst"); +const MSmartPacket = require("../../../msmart/MSmartPacket"); +const MSmartStatusDTO = require("../../../msmart/dtos/MSmartStatusDTO"); + +/** + * @extends MopTwistControlCapability + */ +class MideaMopTwistControlCapabilityV1 extends MopTwistControlCapability { + + /** + * @returns {Promise} + */ + async isEnabled() { + 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.tail_sweep_clean_switch; + } else { + throw new Error("Invalid response from robot"); + } + } + + /** + * @returns {Promise} + */ + async enable() { + await this.robot.sendCommand(new MSmartPacket({ + messageType: MSmartPacket.MESSAGE_TYPE.SETTING, + payload: MSmartPacket.buildPayload( + MSmartConst.SETTING.SET_VARIOUS_TOGGLES, + Buffer.from([ + 0x16, // SUPER_TAIL_FLICK_CLEANING + 0x01 // true + ]) + ) + }).toHexString()); + } + + /** + * @returns {Promise} + */ + async disable() { + await this.robot.sendCommand(new MSmartPacket({ + messageType: MSmartPacket.MESSAGE_TYPE.SETTING, + payload: MSmartPacket.buildPayload( + MSmartConst.SETTING.SET_VARIOUS_TOGGLES, + Buffer.from([ + 0x16, // SUPER_TAIL_FLICK_CLEANING + 0x00 // false + ]) + ) + }).toHexString()); + } +} + +module.exports = MideaMopTwistControlCapabilityV1; diff --git a/backend/lib/robots/midea/capabilities/MideaMopTwistControlCapability.js b/backend/lib/robots/midea/capabilities/MideaMopTwistControlCapabilityV2.js similarity index 93% rename from backend/lib/robots/midea/capabilities/MideaMopTwistControlCapability.js rename to backend/lib/robots/midea/capabilities/MideaMopTwistControlCapabilityV2.js index 482ca99e..b28d14bf 100644 --- a/backend/lib/robots/midea/capabilities/MideaMopTwistControlCapability.js +++ b/backend/lib/robots/midea/capabilities/MideaMopTwistControlCapabilityV2.js @@ -7,7 +7,7 @@ const MSmartStatusDTO = require("../../../msmart/dtos/MSmartStatusDTO"); /** * @extends MopTwistControlCapability */ -class MideaMopTwistControlCapability extends MopTwistControlCapability { +class MideaMopTwistControlCapabilityV2 extends MopTwistControlCapability { /** * @returns {Promise} @@ -59,4 +59,4 @@ class MideaMopTwistControlCapability extends MopTwistControlCapability { } } -module.exports = MideaMopTwistControlCapability; +module.exports = MideaMopTwistControlCapabilityV2; diff --git a/backend/lib/robots/midea/capabilities/index.js b/backend/lib/robots/midea/capabilities/index.js index 00a3e0cb..dff325e6 100644 --- a/backend/lib/robots/midea/capabilities/index.js +++ b/backend/lib/robots/midea/capabilities/index.js @@ -1,10 +1,13 @@ module.exports = { - MideaAutoEmptyDockAutoEmptyIntervalControlCapability: require("./MideaAutoEmptyDockAutoEmptyIntervalControlCapability"), + MideaAutoEmptyDockAutoEmptyIntervalControlCapabilityV1: require("./MideaAutoEmptyDockAutoEmptyIntervalControlCapabilityV1"), + MideaAutoEmptyDockAutoEmptyIntervalControlCapabilityV2: require("./MideaAutoEmptyDockAutoEmptyIntervalControlCapabilityV2"), MideaAutoEmptyDockManualTriggerCapability: require("./MideaAutoEmptyDockManualTriggerCapability"), MideaBasicControlCapability: require("./MideaBasicControlCapability"), MideaCameraLightControlCapability: require("./MideaCameraLightControlCapability"), - MideaCarpetModeControlCapability: require("./MideaCarpetModeControlCapability"), - MideaCarpetSensorModeControlCapability: require("./MideaCarpetSensorModeControlCapability"), + MideaCarpetModeControlCapabilityV1: require("./MideaCarpetModeControlCapabilityV1"), + MideaCarpetModeControlCapabilityV2: require("./MideaCarpetModeControlCapabilityV2"), + MideaCarpetSensorModeControlCapabilityV1: require("./MideaCarpetSensorModeControlCapabilityV1"), + MideaCarpetSensorModeControlCapabilityV2: require("./MideaCarpetSensorModeControlCapabilityV2"), MideaCollisionAvoidantNavigationControlCapability: require("./MideaCollisionAvoidantNavigationControlCapability"), MideaCombinedVirtualRestrictionsCapability: require("./MideaCombinedVirtualRestrictionsCapability"), MideaCurrentStatisticsCapability: require("./MideaCurrentStatisticsCapability"), @@ -23,7 +26,8 @@ module.exports = { MideaMopDockMopWashTemperatureControlCapability: require("./MideaMopDockMopWashTemperatureControlCapability"), MideaMopExtensionControlCapability: require("./MideaMopExtensionControlCapability"), MideaMopExtensionFurnitureLegHandlingControlCapability: require("./MideaMopExtensionFurnitureLegHandlingControlCapability"), - MideaMopTwistControlCapability: require("./MideaMopTwistControlCapability"), + MideaMopTwistControlCapabilityV1: require("./MideaMopTwistControlCapabilityV1"), + MideaMopTwistControlCapabilityV2: require("./MideaMopTwistControlCapabilityV2"), MideaObstacleAvoidanceControlCapability: require("./MideaObstacleAvoidanceControlCapability"), MideaObstacleImagesCapability: require("./MideaObstacleImagesCapability"), MideaOperationModeControlCapability: require("./MideaOperationModeControlCapability"), diff --git a/backend/lib/robots/midea/index.js b/backend/lib/robots/midea/index.js index 470d7f59..57f0ad2d 100644 --- a/backend/lib/robots/midea/index.js +++ b/backend/lib/robots/midea/index.js @@ -1,3 +1,4 @@ module.exports = { - "MideaJ15ValetudoRobot": require("./MideaJ15ProUltraValetudoRobot") + "MideaJ12UltraValetudoRobot": require("./MideaJ12UltraValetudoRobot"), + "MideaJ15ProUltraValetudoRobot": require("./MideaJ15ProUltraValetudoRobot") }; diff --git a/backend/lib/utils/LinuxTools.js b/backend/lib/utils/LinuxTools.js index 432e1bc4..a018ffa4 100644 --- a/backend/lib/utils/LinuxTools.js +++ b/backend/lib/utils/LinuxTools.js @@ -1,6 +1,6 @@ const fs = require("fs"); const LinuxToolsHelper = require("./LinuxToolsHelper"); -const {spawnSync} = require("child_process"); +const {spawnSync, execSync} = require("child_process"); class LinuxTools { /** @@ -106,6 +106,29 @@ class LinuxTools { return routingTableEntries.find(e => e["Destination"] === "0.0.0.0" && e["Gateway"] !== "0.0.0.0")?.["Gateway"]; } + + /** + * + * @param {Date} date + */ + static SET_TIME(date) { + let dateString = ""; + + dateString += date.getFullYear().toString(); + dateString += "-"; + dateString += (date.getMonth() + 1).toString().padStart(2, "0"); + dateString += "-"; + dateString += date.getDate().toString().padStart(2, "0"); + dateString += " "; + dateString += date.getHours().toString().padStart(2, "0"); + dateString += ":"; + dateString += date.getMinutes().toString().padStart(2, "0"); + dateString += ":"; + dateString += date.getSeconds().toString().padStart(2, "0"); + + + execSync("date -s \""+dateString+"\""); + } } module.exports = LinuxTools; diff --git a/backend/lib/webserver/capabilityRouters/doc/CarpetSensorModeControlCapabilityRouter.openapi.json b/backend/lib/webserver/capabilityRouters/doc/CarpetSensorModeControlCapabilityRouter.openapi.json index 50d1e0d8..e5132d1f 100644 --- a/backend/lib/webserver/capabilityRouters/doc/CarpetSensorModeControlCapabilityRouter.openapi.json +++ b/backend/lib/webserver/capabilityRouters/doc/CarpetSensorModeControlCapabilityRouter.openapi.json @@ -19,8 +19,7 @@ "off", "avoid", "lift", - "detach", - "cross" + "detach" ] } } @@ -47,8 +46,7 @@ "off", "avoid", "lift", - "detach", - "cross" + "detach" ] } } @@ -88,8 +86,7 @@ "off", "avoid", "lift", - "detach", - "cross" + "detach" ] } } diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts index ef8fdd27..13668031 100644 --- a/frontend/src/api/types.ts +++ b/frontend/src/api/types.ts @@ -573,7 +573,7 @@ export interface ValetudoCustomizations { friendlyName: string; } -export type CarpetSensorMode = "off" | "avoid" | "lift" | "detach" | "cross"; +export type CarpetSensorMode = "off" | "avoid" | "lift" | "detach"; export interface CarpetSensorModePayload { mode: CarpetSensorMode diff --git a/frontend/src/robot/RobotOptions.tsx b/frontend/src/robot/RobotOptions.tsx index 1c62ba5b..faf54378 100644 --- a/frontend/src/robot/RobotOptions.tsx +++ b/frontend/src/robot/RobotOptions.tsx @@ -144,9 +144,8 @@ const CarpetModeControlCapabilitySwitchListMenuItem = () => { const CarpetSensorModeControlCapabilitySelectListMenuItem = () => { const SORT_ORDER = { - "off": 5, - "detach": 4, - "cross" : 3, + "off": 4, + "detach": 3, "avoid": 2, "lift": 1 }; @@ -186,9 +185,6 @@ const CarpetSensorModeControlCapabilitySelectListMenuItem = () => { case "detach": label = "Detach Mop"; break; - case "cross": - label = "Cross Carpet"; - break; } return {