feat(vendor.midea): J12 Ultra

This commit is contained in:
Sören Beye 2025-10-12 17:43:23 +02:00
parent 64b0ee01fe
commit 8fc5a0ab29
27 changed files with 710 additions and 164 deletions

View File

@ -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 {

View File

@ -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(() => {

View File

@ -57,8 +57,7 @@ CarpetSensorModeControlCapability.MODE = Object.freeze({
OFF: "off",
AVOID: "avoid",
LIFT: "lift",
DETACH: "detach",
CROSS: "cross",
DETACH: "detach"
});

View File

@ -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];

View File

@ -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
});

View File

@ -57,6 +57,7 @@ class MSmartDummycloud {
this.commandTopic = "device/unknown/down";
this.aiCommandTopic = "ai/unknown/down";
this.mapCommandTopic = "map/unknown/down";
/**
* @type {Object.<string, {
@ -102,10 +103,16 @@ class MSmartDummycloud {
if (subscription.topic.endsWith("/down")) {
if (subscription.topic.startsWith("device/")) {
this.commandTopic = subscription.topic;
Logger.info(`MSmartDummycloud device command topic: ${this.commandTopic}`);
Logger.debug(`MSmartDummycloud device command topic: ${this.commandTopic}`);
} else if (subscription.topic.startsWith("ai/")) {
this.aiCommandTopic = subscription.topic;
Logger.info(`MSmartDummycloud AI command topic: ${this.aiCommandTopic}`);
Logger.debug(`MSmartDummycloud AI command topic: ${this.aiCommandTopic}`);
} else if (subscription.topic.startsWith("map/")) { // J12 (and older?)
this.mapCommandTopic = subscription.topic;
Logger.debug(`MSmartDummycloud map command topic: ${this.mapCommandTopic}`);
}
}
});
@ -507,54 +514,81 @@ class MSmartDummycloud {
handleIncomingCloudMessage(msg) {
const { topic, payload } = msg;
try {
// Parse the payload.data as MSmartPacket
const responseHex = payload.data;
const responseBuffer = Buffer.from(responseHex, "hex");
const responsePacket = MSmartPacket.FROM_BYTES(responseBuffer);
if (typeof payload.data === "string") {
try {
const responseBuffer = Buffer.from(payload.data, "hex");
const responsePacket = MSmartPacket.FROM_BYTES(responseBuffer);
Logger.trace("Parsed incoming message:", {
Logger.trace("Parsed incoming message:", {
topic: topic,
nonce: payload.nonce,
deviceType: responsePacket.deviceType,
messageType: responsePacket.messageType,
payload: responsePacket.payload,
payloadLength: responsePacket.payload.length
});
if (payload.nonce && this.pendingRequests[payload.nonce]) {
const pendingRequest = this.pendingRequests[payload.nonce];
clearTimeout(pendingRequest.timeout_id);
pendingRequest.resolve(responsePacket);
delete this.pendingRequests[payload.nonce];
Logger.debug(`MSmartDummycloud received response for nonce ${payload.nonce}`);
return;
}
if (!this.onIncomingCloudMessage(responsePacket)) {
Logger.info("Unhandled message received:", responsePacket);
}
} catch (parseError) {
Logger.warn("Failed to parse incoming message:", parseError);
if (payload.nonce && this.pendingRequests[payload.nonce]) {
const pendingRequest = this.pendingRequests[payload.nonce];
clearTimeout(pendingRequest.timeout_id);
pendingRequest.reject(new Error(`Failed to parse MSmart response: ${parseError.message}`));
delete this.pendingRequests[payload.nonce];
}
}
} else if (payload.protocol === "map") { // Observed on the J12
Logger.trace("Received map-type message:", {
topic: topic,
nonce: payload.nonce,
deviceType: responsePacket.deviceType,
messageType: responsePacket.messageType,
payload: responsePacket.payload,
payloadLength: responsePacket.payload.length
data: payload.data
});
if (payload.nonce && this.pendingRequests[payload.nonce]) {
const pendingRequest = this.pendingRequests[payload.nonce];
clearTimeout(pendingRequest.timeout_id);
pendingRequest.resolve(responsePacket);
delete this.pendingRequests[payload.nonce];
Logger.debug(`MSmartDummycloud received response for nonce ${payload.nonce}`);
return;
if (typeof payload.data.fullMap === "string") {
this.onUpload("map", payload.data.fullMap);
} else {
Logger.warn("Unhandled map-type message");
}
} else if (payload.protocol === "track") { // Observed on the J12
Logger.trace("Received track-type message:", {
topic: topic,
nonce: payload.nonce,
data: payload.data
});
if (!this.onIncomingCloudMessage(responsePacket)) {
Logger.info("Unhandled message received:", responsePacket);
}
} catch (parseError) {
Logger.warn("Failed to parse incoming message:", parseError);
if (payload.nonce && this.pendingRequests[payload.nonce]) {
const pendingRequest = this.pendingRequests[payload.nonce];
clearTimeout(pendingRequest.timeout_id);
pendingRequest.reject(new Error(`Failed to parse MSmart response: ${parseError.message}`));
delete this.pendingRequests[payload.nonce];
if (typeof payload.data.fullTrack === "string") {
this.onUpload("track", payload.data.fullTrack);
} else {
Logger.warn("Unhandled track-type message");
}
} else {
Logger.warn("Unhandled MQTT message");
}
}
/**
* @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<import("./MSmartPacket")>}
*/
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<import("./MSmartPacket")>}
*/
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)}));
}
}
);

View File

@ -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;

View File

@ -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;

View File

@ -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),
]
}));

View File

@ -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,

View File

@ -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;

View File

@ -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<import("../../msmart/MSmartPacket")>}
*/
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());

View File

@ -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<import("../MideaValetudoRobot")>
*/
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;

View File

@ -7,7 +7,7 @@ const MSmartStatusDTO = require("../../../msmart/dtos/MSmartStatusDTO");
/**
* @extends AutoEmptyDockAutoEmptyIntervalControlCapability<import("../MideaValetudoRobot")>
*/
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;

View File

@ -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<import("../MideaValetudoRobot")>
*/
class MideaCarpetModeControlCapabilityV1 extends CarpetModeControlCapability {
/**
* @returns {Promise<boolean>}
*/
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<void>}
*/
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<void>}
*/
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;

View File

@ -8,7 +8,7 @@ const MSmartPacket = require("../../../msmart/MSmartPacket");
/**
* @extends CarpetModeControlCapability<import("../MideaValetudoRobot")>
*/
class MideaCarpetModeControlCapability extends CarpetModeControlCapability {
class MideaCarpetModeControlCapabilityV2 extends CarpetModeControlCapability {
/**
* @private
* @returns {Promise<MSmartCarpetBehaviorSettingsDTO>}
@ -81,4 +81,4 @@ class MideaCarpetModeControlCapability extends CarpetModeControlCapability {
}
}
module.exports = MideaCarpetModeControlCapability;
module.exports = MideaCarpetModeControlCapabilityV2;

View File

@ -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<import("../MideaValetudoRobot")>
*/
class MideaCarpetSensorModeControlCapabilityV1 extends CarpetSensorModeControlCapability {
/**
* @returns {Promise<string>}
*/
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;

View File

@ -7,7 +7,7 @@ const MSmartPacket = require("../../../msmart/MSmartPacket");
/**
* @extends CarpetSensorModeControlCapability<import("../MideaValetudoRobot")>
*/
class MideaCarpetSensorModeControlCapability extends CarpetSensorModeControlCapability {
class MideaCarpetSensorModeControlCapabilityV2 extends CarpetSensorModeControlCapability {
/**
* @private
* @returns {Promise<MSmartCarpetBehaviorSettingsDTO>}
@ -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;

View File

@ -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<void>}
*/
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(

View File

@ -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<import("../MideaValetudoRobot")>
*/
class MideaMopTwistControlCapabilityV1 extends MopTwistControlCapability {
/**
* @returns {Promise<boolean>}
*/
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<void>}
*/
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<void>}
*/
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;

View File

@ -7,7 +7,7 @@ const MSmartStatusDTO = require("../../../msmart/dtos/MSmartStatusDTO");
/**
* @extends MopTwistControlCapability<import("../MideaValetudoRobot")>
*/
class MideaMopTwistControlCapability extends MopTwistControlCapability {
class MideaMopTwistControlCapabilityV2 extends MopTwistControlCapability {
/**
* @returns {Promise<boolean>}
@ -59,4 +59,4 @@ class MideaMopTwistControlCapability extends MopTwistControlCapability {
}
}
module.exports = MideaMopTwistControlCapability;
module.exports = MideaMopTwistControlCapabilityV2;

View File

@ -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"),

View File

@ -1,3 +1,4 @@
module.exports = {
"MideaJ15ValetudoRobot": require("./MideaJ15ProUltraValetudoRobot")
"MideaJ12UltraValetudoRobot": require("./MideaJ12UltraValetudoRobot"),
"MideaJ15ProUltraValetudoRobot": require("./MideaJ15ProUltraValetudoRobot")
};

View File

@ -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;

View File

@ -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"
]
}
}

View File

@ -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

View File

@ -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 {