mirror of
https://github.com/Hypfer/Valetudo.git
synced 2025-10-26 11:27:27 +00:00
feat(vendor.midea): J12 Ultra
This commit is contained in:
parent
64b0ee01fe
commit
8fc5a0ab29
@ -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 {
|
||||
|
||||
@ -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(() => {
|
||||
|
||||
@ -57,8 +57,7 @@ CarpetSensorModeControlCapability.MODE = Object.freeze({
|
||||
OFF: "off",
|
||||
AVOID: "avoid",
|
||||
LIFT: "lift",
|
||||
DETACH: "detach",
|
||||
CROSS: "cross",
|
||||
DETACH: "detach"
|
||||
});
|
||||
|
||||
|
||||
|
||||
@ -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];
|
||||
|
||||
@ -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
|
||||
});
|
||||
|
||||
|
||||
@ -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)}));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@ -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;
|
||||
|
||||
63
backend/lib/robots/midea/MideaJ12UltraValetudoRobot.js
Normal file
63
backend/lib/robots/midea/MideaJ12UltraValetudoRobot.js
Normal 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;
|
||||
@ -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),
|
||||
]
|
||||
}));
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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());
|
||||
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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(
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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"),
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
module.exports = {
|
||||
"MideaJ15ValetudoRobot": require("./MideaJ15ProUltraValetudoRobot")
|
||||
"MideaJ12UltraValetudoRobot": require("./MideaJ12UltraValetudoRobot"),
|
||||
"MideaJ15ProUltraValetudoRobot": require("./MideaJ15ProUltraValetudoRobot")
|
||||
};
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user