mirror of
https://github.com/Hypfer/Valetudo.git
synced 2025-10-26 11:27:27 +00:00
feat(vendor.midea): Midea
This commit is contained in:
parent
eb213328de
commit
ae1ac479d7
@ -105,12 +105,12 @@ class RetryWrapper {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.mutex.take(() => {
|
||||
this.sendMessageHelper(msg, options).then(response => {
|
||||
|
||||
this.mutex.leave();
|
||||
|
||||
resolve(response);
|
||||
}).catch(err => {
|
||||
|
||||
this.mutex.leave();
|
||||
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
@ -205,7 +205,7 @@ class RetryWrapper {
|
||||
await this.handshake(true);
|
||||
}
|
||||
|
||||
//remove all remains of a previous attempt
|
||||
// remove all remains of a previous attempt
|
||||
delete(msg["id"]);
|
||||
}
|
||||
|
||||
|
||||
381
backend/lib/msmart/BEightParser.js
Normal file
381
backend/lib/msmart/BEightParser.js
Normal file
@ -0,0 +1,381 @@
|
||||
const Logger = require("../Logger");
|
||||
const MSmartConst = require("./MSmartConst");
|
||||
const MSmartPacket = require("./MSmartPacket");
|
||||
|
||||
const dtos = require("./dtos");
|
||||
|
||||
class BEightParser {
|
||||
/**
|
||||
* @param {MSmartPacket} packet
|
||||
* @returns {import("./dtos/MSmartDTO")|"SKIP"|undefined} - FIXME: remove SKIP
|
||||
*/
|
||||
static PARSE(packet) {
|
||||
const payload = packet.payload;
|
||||
|
||||
switch (packet.messageType) {
|
||||
case MSmartPacket.MESSAGE_TYPE.SETTING: {
|
||||
// 0xaa 0x01 <typeId>
|
||||
switch (payload[2]) {
|
||||
case 0xc4: // FIXME
|
||||
// No idea. Sample: aa 01 c4 04 00 00 00 00 5c 00 9d 10 01
|
||||
return "SKIP";
|
||||
case 0x9d: // FIXME
|
||||
// Network state? Sample: aa 01 9d 01 02 01
|
||||
return "SKIP";
|
||||
case 0x22: // FIXME
|
||||
// bSaveMapSwitch. Sample: aa 01 22 00
|
||||
return "SKIP";
|
||||
default: {
|
||||
Logger.warn(
|
||||
`Unhandled SETTING packet with typeId '${payload[2]}'`,
|
||||
packet.toHexString()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case MSmartPacket.MESSAGE_TYPE.ACTION: {
|
||||
// 0xaa 0x01 <typeId>
|
||||
switch (payload[2]) {
|
||||
case MSmartConst.ACTION.GET_STATUS: {
|
||||
const data = BEightParser._parse_status_payload(payload);
|
||||
|
||||
return new dtos.MSmartStatusDTO(data);
|
||||
}
|
||||
case MSmartConst.ACTION.LIST_MAPS: {
|
||||
if (payload.length < 9) {
|
||||
Logger.warn("Received invalid LIST_MAPS response. Payload too short.");
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const data = {
|
||||
currentMapId: payload[5],
|
||||
savedMapIds: []
|
||||
};
|
||||
|
||||
const mapBitfield = payload.readUInt16LE(7);
|
||||
|
||||
for (let i = 0; i < 16; i++) {
|
||||
if ((mapBitfield >> i) & 1) {
|
||||
data.savedMapIds.push(i + 1);
|
||||
}
|
||||
}
|
||||
|
||||
return new dtos.MSmartMapListDTO(data);
|
||||
}
|
||||
case MSmartConst.ACTION.GET_ACTIVE_ZONES: {
|
||||
const data = BEightParser._parse_active_zones_payload(payload);
|
||||
|
||||
return new dtos.MSmartActiveZonesDTO(data);
|
||||
}
|
||||
case MSmartConst.ACTION.GET_DND: {
|
||||
const data = BEightParser._parse_dnd_payload(payload);
|
||||
|
||||
return new dtos.MSmartDndConfigurationDTO(data);
|
||||
}
|
||||
case MSmartConst.ACTION.GET_CLEANING_SETTINGS_1: {
|
||||
const data = BEightParser._parse_cleaning_settings_1_payload(payload);
|
||||
|
||||
return new dtos.MSmartCleaningSettings1DTO(data);
|
||||
}
|
||||
default: {
|
||||
Logger.warn(
|
||||
`Unhandled ACTION packet with typeId '${payload[2]}'`,
|
||||
packet.toHexString()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case MSmartPacket.MESSAGE_TYPE.EVENT: {
|
||||
// 0xaa 0x01 <typeId>
|
||||
switch (payload[2]) {
|
||||
case MSmartConst.EVENT.STATUS: {
|
||||
const data = BEightParser._parse_status_payload(payload);
|
||||
|
||||
return new dtos.MSmartStatusDTO(data);
|
||||
}
|
||||
case MSmartConst.EVENT.ACTIVE_ZONES: {
|
||||
const data = BEightParser._parse_active_zones_payload(payload);
|
||||
|
||||
return new dtos.MSmartActiveZonesDTO(data);
|
||||
}
|
||||
case MSmartConst.EVENT.ERROR: {
|
||||
return new dtos.MSmartErrorDTO({
|
||||
error_type: payload[3],
|
||||
error_desc: payload[4],
|
||||
sta_index: payload[5],
|
||||
});
|
||||
}
|
||||
case MSmartConst.EVENT.CLEANING_SETTINGS_1: {
|
||||
const data = BEightParser._parse_cleaning_settings_1_payload(payload);
|
||||
|
||||
return new dtos.MSmartCleaningSettings1DTO(data);
|
||||
}
|
||||
case 0xA8: // FIXME
|
||||
// This seems to be relating to the dock state and what it is doing
|
||||
// There are also timers in here?
|
||||
|
||||
// payload[3]; // Mode?. 0x00:Idle?, 0x01:Clean?, 0x02:Empty, 0x03:Dry, 0x05:Wash, possibly hair cuttting?
|
||||
// payload.readUInt32LE(4); // unclear
|
||||
// payload.readUInt32LE(8); // timer. Seconds counting up
|
||||
// payload[12]; // unclear
|
||||
// payload.readUInt32LE(13); // possibly expected duration of the timer in seconds
|
||||
|
||||
return "SKIP";
|
||||
default: {
|
||||
Logger.warn(
|
||||
`Unhandled EVENT packet with typeId '${payload[2]}'`,
|
||||
packet.toHexString()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case MSmartPacket.MESSAGE_TYPE.DOCK: {
|
||||
if (
|
||||
payload[0] === 0x66 &&
|
||||
payload[1] === 0x06
|
||||
) {
|
||||
return new dtos.MSmartDockStatusDTO({
|
||||
dust_collection_count: payload[2],
|
||||
fluid_1_ok: !!payload[5],
|
||||
fluid_2_ok: !!payload[6],
|
||||
});
|
||||
} else {
|
||||
Logger.warn("Unhandled DOCK packet", packet.toHexString());
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
Logger.warn(
|
||||
`Unhandled packet with messageType '${packet.messageType}'. ${packet.payload.subarray(0, 3).toString("hex")}`,
|
||||
packet.toHexString()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @private
|
||||
* @param {Buffer} payload
|
||||
* @returns {object}
|
||||
*/
|
||||
static _parse_status_payload(payload) {
|
||||
const data = {};
|
||||
|
||||
data.work_status = payload[3]; // 1 - 23
|
||||
data.function_type = payload[4]; // 1 - 6
|
||||
data.control_type = payload[5]; // 0 - 2
|
||||
data.move_direction = payload[6]; // 0 - 8
|
||||
data.work_mode = payload[7]; // 0 - 15
|
||||
data.fan_level = payload[8]; // 0 - 5
|
||||
|
||||
// work_area and work_time each have +4 additional bits in payload[22]. - INSANE
|
||||
data.work_area = ((payload[22] & 0b00001111) << 8) + payload[9];
|
||||
|
||||
data.water_level = payload[10]; // 0 - 3, OR high-res starting at >= 100
|
||||
data.voice_level = payload[11]; // 0 - 100
|
||||
// 12 => have_reserve_tank ??
|
||||
data.battery_percent = payload[13]; // 0 - 100
|
||||
|
||||
// work_area and work_time each have +4 additional bits in payload[22]. - INSANE - TODO validate. It was [23] before the LLM suggested something else
|
||||
data.work_time = (((payload[22] & 0b11110000) >> 4) << 8) + payload[14];
|
||||
|
||||
data.uv_switch = !!(payload[15] & 0b00000001);
|
||||
data.wifi_switch = !!(payload[15] & 0b00000010);
|
||||
data.voice_switch = !!(payload[15] & 0b00000100);
|
||||
data.command_source = !!(payload[15] & 0b01000000);
|
||||
data.device_error = !!(payload[15] & 0b10000000);
|
||||
|
||||
data.error_type = payload[16];
|
||||
data.error_desc = payload[17];
|
||||
|
||||
const mopStatusByte = payload[18];
|
||||
data.has_mop = !!(mopStatusByte & 0b00000001); // Mops attached bool
|
||||
data.has_vibrate_mop = !!(mopStatusByte & 0b00000010);
|
||||
|
||||
data.carpet_switch = payload[19]; // bool, TODO: validate offset
|
||||
|
||||
// 20 is unknown
|
||||
|
||||
data.cleaning_type = payload[21]; // 0 - 6
|
||||
|
||||
data.vibrate_mode = payload[23] ? "careful" : "efficient"; // TODO: should this be string?
|
||||
data.vibrate_switch = !!payload[24];
|
||||
data.electrolyzed_water = !!payload[25];
|
||||
data.electrolyzed_water_status = payload[26];
|
||||
|
||||
data.dustDragSwitch = !!((payload[27] & 0x01));
|
||||
data.dustDragStatus = !!((payload[27] & 0x02));
|
||||
data.dustTimes = payload[28];
|
||||
data.dustedTimes = payload[29];
|
||||
data.chargeDockType = payload[30];
|
||||
|
||||
const stationStatusBits = payload[33];
|
||||
data.fluid_1_ok = !!(stationStatusBits & 0b00000100);
|
||||
data.fluid_2_ok = !!(stationStatusBits & 0b00001000);
|
||||
data.dustbag_installed = !!(stationStatusBits & 0b00100000);
|
||||
data.dustbag_full = !!(stationStatusBits & 0b00010000);
|
||||
|
||||
data.mopMode = payload[34];
|
||||
data.station_work_status = payload[36]; // 0 - 89 but with holes
|
||||
|
||||
data.job_state = payload[37];
|
||||
data.whole_process_state = payload[38];
|
||||
|
||||
data.continuous_clean_mode = !!payload[41];
|
||||
// 42 is unknown
|
||||
data.clean_sequence_switch = !!payload[43];
|
||||
|
||||
const childLockBits = payload[45];
|
||||
data.child_lock_enabled = !!(childLockBits & 0b00000001);
|
||||
data.child_lock_follows_dnd = !!(childLockBits & 0b00000010);
|
||||
|
||||
const generalSwitchBits1 = payload[50];
|
||||
data.personal_clean_prefer_switch = !!(generalSwitchBits1 & 0b00000001);
|
||||
data.station_inject_fluid_switch = !!(generalSwitchBits1 & 0b00000010);
|
||||
data.station_inject_soft_fluid_switch = !!(generalSwitchBits1 & 0b00000100);
|
||||
data.carpet_evade_switch = !!(generalSwitchBits1 & 0b00010000);
|
||||
data.station_first_fast_wash_switch = !!(generalSwitchBits1 & 0b01000000);
|
||||
data.pet_mode_switch = !!(generalSwitchBits1 & 0b10000000);
|
||||
|
||||
data.station_capability_flags = payload[52];
|
||||
|
||||
const generalSwitchBits2 = payload[53];
|
||||
data.stain_clean_switch = !!(generalSwitchBits2 & 0b00000001);
|
||||
data.ai_obstacle_switch = !!(generalSwitchBits2 & 0b00000010);
|
||||
data.cross_bridge_switch = !!(generalSwitchBits2 & 0b00000100);
|
||||
data.camera_led_switch = !!(generalSwitchBits2 & 0b00001000);
|
||||
data.map_3d_switch = !!(generalSwitchBits2 & 0b00010000);
|
||||
data.ai_recognition_switch = !!(generalSwitchBits2 & 0b00100000);
|
||||
|
||||
data.test_mode_type = payload[54];
|
||||
data.hot_water_wash_mode = payload[55];
|
||||
|
||||
const generalSwitchBits3 = payload[56];
|
||||
data.station_self_fluid_2_switch = !!(generalSwitchBits3 & 0b00000001);
|
||||
data.slam_version_switch = !!(generalSwitchBits3 & 0b00000010);
|
||||
data.hot_dry_charge_plate_switch = !!(generalSwitchBits3 & 0b00000100);
|
||||
data.telnet_switch = !!(generalSwitchBits3 & 0b00001000);
|
||||
data.mop_auto_dry_switch = !!(generalSwitchBits3 & 0b00010000);
|
||||
data.ai_grade_avoidance_mode = !!(generalSwitchBits3 & 0b00100000);
|
||||
data.pound_sign_switch = !!(generalSwitchBits3 & 0b10000000); // TODO: naming - this is the criss cross pattern with multiple iterations
|
||||
|
||||
data.stationCleanFrequency = payload[57];
|
||||
data.beautify_map_grade = payload[58];
|
||||
data.collect_dust_mode = payload[59];
|
||||
data.session_id = payload[61];
|
||||
data.transaction_id = payload[62];
|
||||
|
||||
const generalSwitchBits4 = payload[63];
|
||||
data.bridge_boost_switch = !!(generalSwitchBits4 & 0b00001000);
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
if (payload.length >= 71) {
|
||||
const generalSwitchBits7 = payload[70];
|
||||
data.big_object_detect_switch = !!(generalSwitchBits7 & 0b00000001);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {Buffer} payload
|
||||
* @returns {object}
|
||||
*/
|
||||
static _parse_active_zones_payload(payload) {
|
||||
const zoneCount = payload.readUInt8(3);
|
||||
const zones = [];
|
||||
let offset = 4;
|
||||
|
||||
for (let i = 0; i < zoneCount; i++) {
|
||||
if (offset + 10 > payload.length) {
|
||||
Logger.warn("Malformed ACTIVE_ZONES payload. Not enough data for all declared zones.");
|
||||
break;
|
||||
}
|
||||
|
||||
zones.push({
|
||||
index: payload.readUInt8(offset),
|
||||
passes: payload.readUInt8(offset + 1),
|
||||
pA: {
|
||||
x: payload.readUInt16LE(offset + 2),
|
||||
y: payload.readUInt16LE(offset + 4),
|
||||
},
|
||||
pC: {
|
||||
x: payload.readUInt16LE(offset + 6),
|
||||
y: payload.readUInt16LE(offset + 8),
|
||||
}
|
||||
});
|
||||
|
||||
offset += 10;
|
||||
}
|
||||
|
||||
return { zones: zones };
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @private
|
||||
* @param {Buffer} payload
|
||||
* @returns {object}
|
||||
*/
|
||||
static _parse_dnd_payload(payload) {
|
||||
return {
|
||||
enabled: payload[3] !== 0,
|
||||
start: {
|
||||
hour: payload[4],
|
||||
minute: payload[5],
|
||||
},
|
||||
end: {
|
||||
hour: payload[6],
|
||||
minute: payload[7]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {Buffer} payload
|
||||
* @returns {object}
|
||||
*/
|
||||
static _parse_cleaning_settings_1_payload(payload) {
|
||||
const data = {};
|
||||
|
||||
data.route_type = payload[3];
|
||||
data.cut_hair_level = payload[4];
|
||||
data.collect_suction_level = payload[7];
|
||||
data.exhibition_switch = !!payload[8];
|
||||
data.ai_grade_avoidance_mode = payload[9];
|
||||
data.cut_hair_super_switch = !!payload[10];
|
||||
data.turbidity_re_mop_switch = payload[11];
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BEightParser;
|
||||
46
backend/lib/msmart/MSmartConst.js
Normal file
46
backend/lib/msmart/MSmartConst.js
Normal file
@ -0,0 +1,46 @@
|
||||
|
||||
const SETTING = Object.freeze({
|
||||
SET_WORK_STATUS: 0x01, // This is the state, the high-level state machine is in. To get the robot to do things, you set it
|
||||
DO_MANUAL_CONTROL_CMD: 0x02,
|
||||
START_SEGMENT_CLEANUP: 0x03,
|
||||
START_ZONE_CLEANUP: 0x05,
|
||||
SET_VIRTUAL_WALLS: 0x20,
|
||||
SET_RESTRICTED_ZONES: 0x21,
|
||||
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_FAN_SPEED: 0x50,
|
||||
SET_WATER_GRADE: 0x51,
|
||||
SET_DOCK_INTERVALS: 0x56,
|
||||
SET_OPERATION_MODE: 0x58,
|
||||
TRIGGER_STATION_ACTION: 0x5A,
|
||||
SET_DND: 0x92,
|
||||
SET_VOLUME: 0x93,
|
||||
SET_VARIOUS_TOGGLES: 0x9C,
|
||||
SET_CLEANING_SETTINGS_1: 0xC9, // FIXME: naming
|
||||
});
|
||||
|
||||
const ACTION = Object.freeze({
|
||||
GET_STATUS: 0x01,
|
||||
LIST_MAPS: 0x20,
|
||||
POLL_MAP: 0x22,
|
||||
GET_DOCK_POSITION: 0x24,
|
||||
GET_ACTIVE_ZONES: 0x27,
|
||||
LOCATE: 0x57,
|
||||
GET_DND: 0x90,
|
||||
GET_CLEANING_SETTINGS_1: 0xAA, // FIXME: naming
|
||||
});
|
||||
|
||||
const EVENT = Object.freeze({
|
||||
STATUS: 0x01,
|
||||
ACTIVE_ZONES: 0x22,
|
||||
ERROR: 0xA3,
|
||||
CLEANING_SETTINGS_1: 0xAA // FIXME: naming
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
SETTING: SETTING,
|
||||
ACTION: ACTION,
|
||||
EVENT: EVENT
|
||||
};
|
||||
650
backend/lib/msmart/MSmartDummycloud.js
Normal file
650
backend/lib/msmart/MSmartDummycloud.js
Normal file
@ -0,0 +1,650 @@
|
||||
const crypto = require("crypto");
|
||||
const express = require("express");
|
||||
const https = require("https");
|
||||
const Logger = require("../Logger");
|
||||
const MSmartPacket = require("./MSmartPacket");
|
||||
const MSmartTimeoutError = require("./MSmartTimeoutError");
|
||||
const Semaphore = require("semaphore");
|
||||
const tls = require("tls");
|
||||
const {createBroker} = require("aedes");
|
||||
|
||||
// For this to work in all situations, we need to patch out the forced timesync within the firmware,
|
||||
// as otherwise it will never start up if access to the hardcoded NTP servers in the FW is blocked
|
||||
// J15PU FW 490 also added an additional check that pings either google or baidu and refuses to start up otherwise
|
||||
// That too needs to be patched out
|
||||
|
||||
class MSmartDummycloud {
|
||||
/**
|
||||
* @param {object} options
|
||||
* @param {import("../utils/DummyCloudCertManager")} options.dummyCloudCertManager
|
||||
* @param {string} options.bindIP
|
||||
* @param {number=} options.timeout timeout in milliseconds to wait for a response
|
||||
* @param {(packet: import("./MSmartPacket")) => boolean} options.onIncomingCloudMessage
|
||||
* @param {string} options.dummyClientCert
|
||||
* @param {string} options.dummyClientKey
|
||||
* @param {() => void} options.onConnected
|
||||
* @param {(req: any, res: any) => boolean} options.onHttpRequest
|
||||
* @param {(type: string, data: any) => void} options.onUpload - TODO naming
|
||||
* @param {(type: string, value: any) => void} options.onEvent - TODO naming
|
||||
*/
|
||||
constructor(options) {
|
||||
this.dummyCloudCertManager = options.dummyCloudCertManager;
|
||||
this.bindIP = options.bindIP;
|
||||
this.timeout = options.timeout ?? 5000;
|
||||
this.onConnected = options.onConnected;
|
||||
this.onIncomingCloudMessage = options.onIncomingCloudMessage;
|
||||
this.onHttpRequest = options.onHttpRequest;
|
||||
this.onUpload = options.onUpload;
|
||||
this.onEvent = options.onEvent;
|
||||
this.dummyClientCert = options.dummyClientCert;
|
||||
this.dummyClientKey = options.dummyClientKey;
|
||||
|
||||
this.sendCommandMutex = Semaphore(1);
|
||||
|
||||
this.mqttBroker = createBroker();
|
||||
this.mqttServer = tls.createServer({
|
||||
SNICallback: (hostname, callback) => {
|
||||
const { key, cert } = this.dummyCloudCertManager.getCertificate(hostname);
|
||||
callback(null, tls.createSecureContext({ key: key, cert: cert }));
|
||||
}
|
||||
});
|
||||
this.httpServer = https.createServer({
|
||||
SNICallback: (hostname, callback) => {
|
||||
const { key, cert } = this.dummyCloudCertManager.getCertificate(hostname);
|
||||
callback(null, tls.createSecureContext({ key: key, cert: cert }));
|
||||
}
|
||||
});
|
||||
|
||||
this.commandTopic = "device/unknown/down";
|
||||
this.aiCommandTopic = "ai/unknown/down";
|
||||
|
||||
/**
|
||||
* @type {Object.<string, {
|
||||
* timeout_id?: NodeJS.Timeout,
|
||||
* onTimeoutCallback: () => void,
|
||||
* resolve: (result: any) => void,
|
||||
* reject: (err: any) => void,
|
||||
* command: string
|
||||
* }>}
|
||||
*/
|
||||
this.pendingRequests = {};
|
||||
|
||||
this.setupMQTT();
|
||||
this.setupHTTP();
|
||||
}
|
||||
|
||||
setupMQTT() {
|
||||
this.mqttServer = tls.createServer({
|
||||
SNICallback: (hostname, callback) => {
|
||||
const { key, cert } = this.dummyCloudCertManager.getCertificate(hostname);
|
||||
callback(null, tls.createSecureContext({ key: key, cert: cert }));
|
||||
}
|
||||
}, this.mqttBroker.handle);
|
||||
|
||||
this.mqttServer.listen(MSmartDummycloud.MQTT_PORT, this.bindIP, () => {
|
||||
Logger.info(`MSmartDummycloud MQTT listening on ${this.bindIP}:${MSmartDummycloud.MQTT_PORT}`);
|
||||
});
|
||||
|
||||
this.mqttServer.on("error", (err) => {
|
||||
Logger.error("MSmartDummycloud MQTT Server Error:", err);
|
||||
});
|
||||
|
||||
this.mqttBroker.on("client", (client) => {
|
||||
Logger.info(`MSmartDummycloud MQTT client connected: ${client.id}`);
|
||||
|
||||
this.onConnected();
|
||||
});
|
||||
|
||||
this.mqttBroker.on("subscribe", (subscriptions) => {
|
||||
Logger.debug("Subscriptions", subscriptions);
|
||||
|
||||
subscriptions.forEach(subscription => {
|
||||
if (subscription.topic.endsWith("/down")) {
|
||||
if (subscription.topic.startsWith("device/")) {
|
||||
this.commandTopic = subscription.topic;
|
||||
Logger.info(`MSmartDummycloud device command topic: ${this.commandTopic}`);
|
||||
} else if (subscription.topic.startsWith("ai/")) {
|
||||
this.aiCommandTopic = subscription.topic;
|
||||
Logger.info(`MSmartDummycloud AI command topic: ${this.aiCommandTopic}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.mqttBroker.on("clientDisconnect", (client) => {
|
||||
Logger.info(`MSmartDummycloud MQTT client disconnected: ${client.id}`);
|
||||
});
|
||||
|
||||
this.mqttBroker.on("publish", async (packet, client) => {
|
||||
if (!client) {
|
||||
return; // messages without client are outgoing
|
||||
}
|
||||
|
||||
Logger.trace(`MSmartDummycloud MQTT Message on '${packet.topic}':`, packet.payload.toString());
|
||||
|
||||
try {
|
||||
const message = JSON.parse(packet.payload.toString());
|
||||
this.handleIncomingCloudMessage({topic: packet.topic, payload: message});
|
||||
} catch (e) {
|
||||
Logger.warn("MSmartDummycloud failed to parse incoming message", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setupHTTP() {
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
app.post("/acl/device/register", (req, res) => {
|
||||
const incomingHostname = req.hostname;
|
||||
Logger.info(`Handling provisioning request for hostname: ${incomingHostname}. ${JSON.stringify(req.body, null, 2)}`);
|
||||
|
||||
const responsePayload = {
|
||||
"errorCode": "0",
|
||||
"msg": "success",
|
||||
"reason": "success",
|
||||
"data": {
|
||||
"deviceId": req.body.uuid,
|
||||
"uuid": req.body.uuid,
|
||||
"productId": req.body.productId,
|
||||
"mac": req.body.mac,
|
||||
"sn": req.body.sn,
|
||||
"ip": req.ip,
|
||||
"key": "MOCK_DEVICE_KEY_" + Date.now(),
|
||||
"bindToken": "MOCK_BIND_TOKEN_" + Date.now(),
|
||||
"mqttInfo": {
|
||||
"clientId": req.body.uuid,
|
||||
"serverAddress": incomingHostname,
|
||||
"port": MSmartDummycloud.MQTT_PORT,
|
||||
"authType": 1,
|
||||
"certificatePem": this.dummyClientCert,
|
||||
"publicKey": this.dummyClientCert,
|
||||
"privateKey": this.dummyClientKey
|
||||
},
|
||||
"extra": {
|
||||
"mapHost": `http://${incomingHostname}`,
|
||||
"odmServiceHost": `https://${incomingHostname}`,
|
||||
"otaHost": `http://${incomingHostname}`,
|
||||
"logHost": `http://${incomingHostname}`,
|
||||
"voiceHost": `http://${incomingHostname}`,
|
||||
"videoHost": `https://${incomingHostname}`
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Logger.info("Constructed Response Payload:", JSON.stringify(responsePayload, null, 2));
|
||||
res.status(200).json(responsePayload);
|
||||
});
|
||||
|
||||
app.get("/m7-server/actuator/health/ping", (req, res) => {
|
||||
Logger.trace("Handling /m7-server/actuator/health/ping");
|
||||
res.status(200).json({"status": "UP"});
|
||||
});
|
||||
|
||||
app.get("/", (req, res, next) => {
|
||||
if (req.hostname.endsWith("ipify.org") || req.hostname.endsWith("ipify.cn")) {
|
||||
Logger.info(`Handling IP lookup request for ${req.hostname}.`);
|
||||
|
||||
res.status(200).send(req.ip);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/v1/dev2pro/m7/map/part/get", (req, res) => {
|
||||
Logger.debug(`Handling part get for: ${req.body.mapPart}`);
|
||||
Logger.debug(req.body);
|
||||
|
||||
res.status(200).json({ "data": {} });
|
||||
});
|
||||
|
||||
app.post("/v1/dev2pro/m7/map/list/:part", (req, res) => {
|
||||
if (req.body) {
|
||||
Logger.trace(`${req.url}: `, JSON.stringify(req.body, null, 2));
|
||||
}
|
||||
|
||||
this.onUpload(req.params.part, req.body.data); //TODO: perhaps validate types
|
||||
|
||||
res.status(200).send();
|
||||
});
|
||||
|
||||
app.post("/v1/dev2pro/m7/map/list/mop/:part", (req, res) => {
|
||||
if (req.body) {
|
||||
Logger.trace(`${req.url}: `, JSON.stringify(req.body, null, 2));
|
||||
}
|
||||
|
||||
this.onUpload(`mop_${req.params.part}`, req.body.data); //TODO: perhaps validate types
|
||||
|
||||
res.status(200).send();
|
||||
});
|
||||
|
||||
app.post("/v1/dev2pro/m7/map/part/upload", (req, res) => {
|
||||
if (req.body) {
|
||||
Logger.trace(`${req.url}: `, JSON.stringify(req.body, null, 2));
|
||||
}
|
||||
|
||||
this.onUpload(req.body.mapPart, req.body.data);
|
||||
|
||||
res.status(200).send();
|
||||
});
|
||||
|
||||
app.post("/v1/dev2pro/cruise/list/points", (req, res) => {
|
||||
if (req.body) {
|
||||
Logger.trace(`${req.url}: `, JSON.stringify(req.body, null, 2));
|
||||
}
|
||||
|
||||
this.onUpload("points", req.body.data);
|
||||
|
||||
res.status(200).send();
|
||||
});
|
||||
|
||||
app.post("/v1/dev2pro/m7/work/status/upload", (req, res) => {
|
||||
Logger.debug("Received a historical record for a finished or aborted cleanup.");
|
||||
|
||||
res.status(200).json({ msg: "OK", code: "0" });
|
||||
});
|
||||
|
||||
/*
|
||||
This is used to ask the cloud for a URL to a voice pack by id
|
||||
I think it also regularly checks for updates to voicepacks? Not sure.
|
||||
|
||||
Reference reply for request
|
||||
{
|
||||
"sn8": "750Y000R",
|
||||
"id": "561",
|
||||
"md5": "958386132015a99f300ee9a372273b4a"
|
||||
}
|
||||
|
||||
{
|
||||
"code": "0",
|
||||
"data": "https://<cdn_url>/m7-voice-full/750Y000R-561-18-<somestring>.zip",
|
||||
"md5": "052e67dd8843cdfc8f89fc130ea21db5",
|
||||
"msg": "OK",
|
||||
"nonce": "<nonce>",
|
||||
"voiceId": "1198"
|
||||
}
|
||||
*/
|
||||
|
||||
app.post("/v1/dev2pro/m7/voice/check", (req, res) => {
|
||||
Logger.debug(`Handling request for Voice with ID '${req.body.id}'`);
|
||||
|
||||
res.status(200).json({
|
||||
"code": "0",
|
||||
"data": "",
|
||||
"md5": "",
|
||||
"msg": "OK",
|
||||
"nonce": req.headers["nonce"] ?? "",
|
||||
"voiceId": ""
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
/*
|
||||
Always respond with "no update available".
|
||||
For reference, example reply for an available update:
|
||||
|
||||
{
|
||||
"code":"0",
|
||||
"msg":"OK",
|
||||
"nonce":"<nonce>",
|
||||
"data":{
|
||||
"id":91,
|
||||
"sn8":"750Y000R",
|
||||
"moduleBranchCode":63,
|
||||
"type":0,
|
||||
"name":"V2.0.0.20240927_rc",
|
||||
"md5":"9478bb7890c6cef753e484804c117e26",
|
||||
"url":"https://<cdn_url>/<filename>.zip",
|
||||
"size":32044516,
|
||||
"version":2,
|
||||
"minModuleVersion":240,
|
||||
"maxModuleVersion":9999,
|
||||
"releaseMode":0,
|
||||
"whitelistDeviceIds":[
|
||||
|
||||
],
|
||||
"historicalVersions":[
|
||||
|
||||
],
|
||||
"lastOperator":"lixin224",
|
||||
"updateTime":"2025-05-23 19:25:48"
|
||||
}
|
||||
}
|
||||
*/
|
||||
app.post("/package-management/v1/dev2pro/check", (req, res) => {
|
||||
Logger.debug(`Handling AI Model update check. Currently installed version: '${req.body.packageVersion}'`);
|
||||
|
||||
res.status(200).json({
|
||||
"code": "0",
|
||||
"msg": "OK",
|
||||
"nonce": req.headers["nonce"] ?? "",
|
||||
"data": null
|
||||
});
|
||||
});
|
||||
|
||||
app.post("/v1/ota/version/check", (req, res) => {
|
||||
const requestedModule = req.body.isModule || "0";
|
||||
Logger.debug(`Handling OTA check for module type: ${requestedModule}`);
|
||||
|
||||
const responsePayload = {
|
||||
"errorCode": "0",
|
||||
"msg": "success",
|
||||
"reason": "success",
|
||||
"data": {
|
||||
"isModule": requestedModule,
|
||||
"hasNew": "0",
|
||||
"md5": "",
|
||||
"productName": "",
|
||||
"sn8": "",
|
||||
"url": "",
|
||||
"version": "",
|
||||
"forceUpdate": "",
|
||||
"fwSign": "",
|
||||
"sh256": "",
|
||||
"rsaSign": ""
|
||||
}
|
||||
};
|
||||
|
||||
res.status(200).json(responsePayload);
|
||||
});
|
||||
|
||||
app.post("/v1/ota/status/update", (req, res) => {
|
||||
Logger.debug("Handling OTA status update request.");
|
||||
if (req.body) {
|
||||
Logger.debug("OTA Status Update Body:", JSON.stringify(req.body, null, 2));
|
||||
}
|
||||
|
||||
res.status(200).json({ msg: "OK", code: "0" });
|
||||
});
|
||||
|
||||
app.post("/logService/v1/dev/event-tracking", (req, res) => {
|
||||
if (req.body) {
|
||||
Logger.trace(`${req.url}: `, JSON.stringify(req.body, null, 2));
|
||||
}
|
||||
|
||||
this.onEvent(req.body.type, req.body.value);
|
||||
|
||||
res.status(200).send();
|
||||
});
|
||||
|
||||
app.post("/v3/dev2pro/login", (req, res) => {
|
||||
Logger.debug("Received login request");
|
||||
if (req.body) {
|
||||
Logger.debug("Body:", JSON.stringify(req.body, null, 2));
|
||||
}
|
||||
|
||||
res.status(200).json({ data: "i-am-not-a-token" });
|
||||
});
|
||||
|
||||
app.post("/v3/dev2pro/ability", (req, res) => {
|
||||
Logger.debug("Received ability request");
|
||||
if (req.body) {
|
||||
Logger.debug("Body:", JSON.stringify(req.body, null, 2));
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
data: {
|
||||
videoImageEnc: false
|
||||
// There could be more in this. So far, I just found the single key and didn't check what the cloud actually sends
|
||||
// TBD: is false the correct thing to set here?
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
This seems to be used by the firmware to pull a temporary AES encryption key on boot to.. maybe encrypt pictures with?
|
||||
The response itself, even though sent via HTTPS, is a json containing base64, which is the requested key + IV AES encrypted with a static one
|
||||
|
||||
Persistent might mean image uploads perhaps? Compared with its RTC sibling
|
||||
*/
|
||||
app.post("/v3/dev2pro/enc/persistent/key", (req, res) => {
|
||||
Logger.debug("Handling persistent key request");
|
||||
|
||||
const transportKey = "Midea@api-device";
|
||||
const algorithm = "aes-128-cbc";
|
||||
|
||||
// 16-byte key + 16-byte IV
|
||||
const plaintextPayload = Buffer.alloc(32, 0);
|
||||
|
||||
const transportIv = Buffer.alloc(16, 0);
|
||||
const cipher = crypto.createCipheriv(algorithm, transportKey, transportIv);
|
||||
const encryptedPayload = Buffer.concat([cipher.update(plaintextPayload), cipher.final()]);
|
||||
|
||||
res.status(200).json({
|
||||
code: "0",
|
||||
msg: "success",
|
||||
requestId: transportIv.toString("hex"),
|
||||
data: encryptedPayload.toString("base64"),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// FIXME: this is a duplicate of the persistent route.
|
||||
// I am just guessing that this might be the correct response as well
|
||||
// TODO: check internal logs of the firmware and see if anything complains
|
||||
app.post("/v3/dev2pro/enc/rtc/key", (req, res) => {
|
||||
Logger.debug("Handling rtc key request");
|
||||
|
||||
Logger.debug("Handling persistent key request");
|
||||
|
||||
const transportKey = "Midea@api-device";
|
||||
const algorithm = "aes-128-cbc";
|
||||
|
||||
// 16-byte key + 16-byte IV
|
||||
const plaintextPayload = Buffer.alloc(32, 0);
|
||||
|
||||
const transportIv = Buffer.alloc(16, 0);
|
||||
const cipher = crypto.createCipheriv(algorithm, transportKey, transportIv);
|
||||
const encryptedPayload = Buffer.concat([cipher.update(plaintextPayload), cipher.final()]);
|
||||
|
||||
res.status(200).json({
|
||||
code: "0",
|
||||
msg: "success",
|
||||
requestId: transportIv.toString("hex"),
|
||||
data: encryptedPayload.toString("base64"),
|
||||
});
|
||||
});
|
||||
|
||||
// This route receives events that might be protobufs(?) as multipart/form-data. They do seem debug-only? Not sure.
|
||||
// Let's see if we can get away without _actually_ handling them
|
||||
app.post("/v3/dev2pro/robot/event", (req, res) => {
|
||||
res.status(200).send();
|
||||
});
|
||||
|
||||
|
||||
app.all("*", (req, res) => {
|
||||
if (this.onHttpRequest) {
|
||||
const handled = this.onHttpRequest(req, res);
|
||||
if (handled) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Logger.info("Unhandled MSmartDummycloud HTTP Request", {
|
||||
protocol: req.secure ? "HTTPS" : "HTTP",
|
||||
host: req.headers.host,
|
||||
method: req.method,
|
||||
path: req.path,
|
||||
headers: req.headers,
|
||||
body: req.body ?? null
|
||||
});
|
||||
|
||||
res.status(200).json({ msg: "OK", code: "0" });
|
||||
});
|
||||
|
||||
this.httpServer.on("request", app);
|
||||
|
||||
this.httpServer.listen(MSmartDummycloud.HTTP_PORT, this.bindIP, () => {
|
||||
Logger.info(`MSmartDummycloud HTTPS listening on ${this.bindIP}:${MSmartDummycloud.HTTP_PORT}`);
|
||||
});
|
||||
|
||||
this.httpServer.on("error", (err) => {
|
||||
Logger.error("MSmartDummycloud HTTPS Server Error:", err);
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} command
|
||||
* @param {object} [options]
|
||||
* @param {number} [options.timeout] - milliseconds
|
||||
* @param {"device"|"ai"} [options.target] - defaults to "device"
|
||||
* @returns {Promise<import("./MSmartPacket")>}
|
||||
*/
|
||||
sendCommand(command, options) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.sendCommandMutex.take(() => {
|
||||
this.actualSendCommand(command, options).then(response => {
|
||||
this.sendCommandMutex.leave();
|
||||
|
||||
resolve(response);
|
||||
}).catch(err => {
|
||||
this.sendCommandMutex.leave();
|
||||
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @param {string} command
|
||||
* @param {object} [options]
|
||||
* @param {number} [options.timeout] - milliseconds
|
||||
* @param {"device"|"ai"} [options.target] - defaults to "device"
|
||||
* @returns {Promise<import("./MSmartPacket")>}
|
||||
*/
|
||||
actualSendCommand(command, options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const nonce = crypto.randomUUID();
|
||||
const payload = JSON.stringify({
|
||||
data: command,
|
||||
nonce: nonce,
|
||||
version: 1,
|
||||
timestamp: Math.round(Date.now() / 1000),
|
||||
productId: "valetudo"
|
||||
});
|
||||
|
||||
const target = options?.target ?? "device";
|
||||
const targetTopic = target === "ai" ? this.aiCommandTopic : this.commandTopic;
|
||||
|
||||
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}));
|
||||
}
|
||||
};
|
||||
|
||||
this.pendingRequests[nonce].timeout_id = setTimeout(
|
||||
() => {
|
||||
this.pendingRequests[nonce].onTimeoutCallback();
|
||||
},
|
||||
options?.timeout ?? this.timeout
|
||||
);
|
||||
|
||||
Logger.trace(`Sending command to ${targetTopic}`, payload);
|
||||
|
||||
this.mqttBroker.publish(
|
||||
{
|
||||
cmd: "publish",
|
||||
topic: targetTopic,
|
||||
payload: Buffer.from(payload),
|
||||
qos: 0,
|
||||
retain: false,
|
||||
dup: false,
|
||||
},
|
||||
(error) => {
|
||||
if (error) {
|
||||
Logger.error(`Error publishing message: ${error}`);
|
||||
|
||||
if (this.pendingRequests[nonce]) {
|
||||
clearTimeout(this.pendingRequests[nonce].timeout_id);
|
||||
delete this.pendingRequests[nonce];
|
||||
}
|
||||
|
||||
reject(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async shutdown() {
|
||||
Logger.debug("MSmartDummycloud shutdown in progress...");
|
||||
|
||||
await new Promise((resolve) => {
|
||||
this.httpServer.close(() => {
|
||||
Logger.info("MSmartDummycloud HTTPS server shut down");
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise((resolve) => {
|
||||
this.mqttBroker.close(() => {
|
||||
this.mqttServer.close(() => {
|
||||
Logger.info("MSmartDummycloud MQTT server shut down");
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Logger.debug("MSmartDummycloud shutdown done");
|
||||
}
|
||||
}
|
||||
|
||||
MSmartDummycloud.MQTT_PORT = 8883;
|
||||
MSmartDummycloud.HTTP_PORT = 443;
|
||||
|
||||
module.exports = MSmartDummycloud;
|
||||
127
backend/lib/msmart/MSmartPacket.js
Normal file
127
backend/lib/msmart/MSmartPacket.js
Normal file
@ -0,0 +1,127 @@
|
||||
class MSmartPacket {
|
||||
/**
|
||||
* @param {object} options
|
||||
* @param {number} [options.deviceType]
|
||||
* @param {number} [options.messageId]
|
||||
* @param {number} [options.protocolVersion]
|
||||
* @param {number} [options.deviceProtocolVersion]
|
||||
* @param {number} options.messageType
|
||||
* @param {Buffer} options.payload
|
||||
*/
|
||||
constructor(options) {
|
||||
this.deviceType = options.deviceType ?? MSmartPacket.DEVICE_TYPE.VACUUM;
|
||||
this.messageId = options.messageId ?? 0;
|
||||
this.protocolVersion = options.protocolVersion ?? 0;
|
||||
this.deviceProtocolVersion = options.deviceProtocolVersion ?? 0;
|
||||
this.messageType = options.messageType;
|
||||
this.payload = options.payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes the packet into a Buffer for transmission.
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
toBytes() {
|
||||
const length = 10 + this.payload.length;
|
||||
if (length > 255) {
|
||||
throw new Error(`Invalid MSmartPacket! Length ${length} > 255)`);
|
||||
}
|
||||
|
||||
const header = Buffer.alloc(10);
|
||||
header[0] = 0xAA;
|
||||
header[1] = length;
|
||||
header[2] = this.deviceType;
|
||||
header[3] = 0x00;
|
||||
header[4] = 0x00;
|
||||
header[5] = 0x00;
|
||||
header[6] = this.messageId;
|
||||
header[7] = this.protocolVersion;
|
||||
header[8] = this.deviceProtocolVersion;
|
||||
header[9] = this.messageType;
|
||||
|
||||
const dataToChecksum = Buffer.concat([header.subarray(1), this.payload]);
|
||||
const checksum = MSmartPacket.calculateChecksum(dataToChecksum);
|
||||
|
||||
return Buffer.concat([header, this.payload, Buffer.from([checksum])]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
toHexString() {
|
||||
return this.toBytes().toString("hex");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Buffer} bytes
|
||||
* @returns {MSmartPacket}
|
||||
*/
|
||||
static FROM_BYTES(bytes) {
|
||||
if (bytes.length < 11) {
|
||||
throw new Error(`Packet too short. Expected at least 11 bytes, got ${bytes.length}.`);
|
||||
}
|
||||
if (bytes[0] !== 0xAA) {
|
||||
throw new Error(`Invalid Magic Byte. Expected 0xAA, got 0x${bytes[0].toString(16)}.`);
|
||||
}
|
||||
if (bytes[1] !== bytes.length - 1) {
|
||||
throw new Error(`Length mismatch. Length byte is ${bytes[1]}, but packet length is ${bytes.length}.`);
|
||||
}
|
||||
|
||||
const dataToChecksum = bytes.subarray(1, bytes.length - 1);
|
||||
const expectedChecksum = MSmartPacket.calculateChecksum(dataToChecksum);
|
||||
const actualChecksum = bytes[bytes.length - 1];
|
||||
|
||||
if (actualChecksum !== expectedChecksum) {
|
||||
throw new Error(`Checksum mismatch. Calculated 0x${expectedChecksum.toString(16)}, but got 0x${actualChecksum.toString(16)}.`);
|
||||
}
|
||||
|
||||
return new MSmartPacket({
|
||||
deviceType: bytes[2],
|
||||
messageId: bytes[6],
|
||||
protocolVersion: bytes[7],
|
||||
deviceProtocolVersion: bytes[8],
|
||||
messageType: bytes[9],
|
||||
payload: bytes.subarray(10, bytes.length - 1)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Buffer} data
|
||||
* @returns {number}
|
||||
*/
|
||||
static calculateChecksum(data) {
|
||||
const sum = data.reduce((acc, val) => acc + val, 0);
|
||||
return (~sum + 1) & 0xFF;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} commandId
|
||||
* @param {Buffer} [actualPayload]
|
||||
* @return {Buffer}
|
||||
*/
|
||||
static buildPayload(commandId, actualPayload) {
|
||||
const header = Buffer.from([0xaa, 0x01, commandId]);
|
||||
if (actualPayload === undefined) {
|
||||
return header;
|
||||
}
|
||||
|
||||
return Buffer.concat([
|
||||
header,
|
||||
actualPayload
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
MSmartPacket.DEVICE_TYPE = Object.freeze({
|
||||
VACUUM: 0xb8
|
||||
});
|
||||
|
||||
MSmartPacket.MESSAGE_TYPE = Object.freeze({
|
||||
SETTING: 0x02,
|
||||
ACTION: 0x03,
|
||||
EVENT: 0x04,
|
||||
DOCK: 0x06
|
||||
});
|
||||
|
||||
module.exports = MSmartPacket;
|
||||
93
backend/lib/msmart/MSmartProvisioningPacket.js
Normal file
93
backend/lib/msmart/MSmartProvisioningPacket.js
Normal file
@ -0,0 +1,93 @@
|
||||
class MSmartProvisioningPacket {
|
||||
/**
|
||||
* @param {object} options
|
||||
* @param {number} options.commandId
|
||||
* @param {Buffer} options.payload
|
||||
*/
|
||||
constructor(options) {
|
||||
this.commandId = options.commandId;
|
||||
this.payload = options.payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
toBytes() {
|
||||
const coreCommand = Buffer.alloc(4 + this.payload.length);
|
||||
coreCommand.writeUInt16BE(this.commandId, 0);
|
||||
coreCommand.writeUInt16BE(this.payload.length, 2);
|
||||
this.payload.copy(coreCommand, 4);
|
||||
|
||||
const checksum = MSmartProvisioningPacket.calculateChecksum(coreCommand);
|
||||
|
||||
const finalPacket = Buffer.alloc(7 + this.payload.length);
|
||||
finalPacket[0] = 0xEE;
|
||||
finalPacket[1] = 0x01;
|
||||
coreCommand.copy(finalPacket, 2);
|
||||
finalPacket[finalPacket.length - 1] = checksum;
|
||||
|
||||
return finalPacket;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Buffer} bytes
|
||||
* @returns {MSmartProvisioningPacket}
|
||||
*/
|
||||
static FROM_BYTES(bytes) {
|
||||
if (bytes.length < 7) {
|
||||
throw new Error(`Packet too short. Expected at least 7 bytes, got ${bytes.length}.`);
|
||||
}
|
||||
if (bytes[0] !== 0xEE || bytes[1] !== 0x01) {
|
||||
throw new Error(`Invalid Magic Header. Expected 0xEE01, got 0x${bytes.toString("hex", 0, 2)}.`);
|
||||
}
|
||||
|
||||
const coreCommand = bytes.subarray(2, bytes.length - 1);
|
||||
const expectedChecksum = MSmartProvisioningPacket.calculateChecksum(coreCommand);
|
||||
const actualChecksum = bytes[bytes.length - 1];
|
||||
|
||||
if (actualChecksum !== expectedChecksum) {
|
||||
throw new Error(`Checksum mismatch. Calculated 0x${expectedChecksum.toString(16)}, but got 0x${actualChecksum.toString(16)}.`);
|
||||
}
|
||||
|
||||
const commandId = coreCommand.readUInt16BE(0);
|
||||
const payloadLength = coreCommand.readUInt16BE(2);
|
||||
|
||||
if (coreCommand.length !== 4 + payloadLength) {
|
||||
throw new Error(`Payload length mismatch. Header says ${payloadLength}, but actual was ${coreCommand.length - 4}.`);
|
||||
}
|
||||
|
||||
return new MSmartProvisioningPacket({
|
||||
commandId: commandId,
|
||||
payload: coreCommand.subarray(4)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Buffer} data
|
||||
* @returns {number}
|
||||
*/
|
||||
static calculateChecksum(data) {
|
||||
const sum = data.reduce((acc, val) => acc + val, 0);
|
||||
return sum & 0xFF;
|
||||
}
|
||||
}
|
||||
|
||||
MSmartProvisioningPacket.RESPONSE_ID_OFFSET = 0b1000000000000000; // FIXME: naming
|
||||
|
||||
MSmartProvisioningPacket.COMMAND_IDS = Object.freeze({
|
||||
CMD_ALL_INFO: 208,
|
||||
CMD_UUID_INFO: 213,
|
||||
|
||||
// The robot can also send "commands"
|
||||
CMD_NOTIFY_PROGRESS: 222,
|
||||
CMD_NOTIFY_RESULT: 223
|
||||
});
|
||||
|
||||
MSmartProvisioningPacket.RESPONSE_IDS = Object.freeze({
|
||||
CMD_ALL_INFO: MSmartProvisioningPacket.COMMAND_IDS.CMD_ALL_INFO + MSmartProvisioningPacket.RESPONSE_ID_OFFSET,
|
||||
CMD_UUID_INFO: MSmartProvisioningPacket.COMMAND_IDS.CMD_UUID_INFO + MSmartProvisioningPacket.RESPONSE_ID_OFFSET,
|
||||
CMD_NOTIFY_PROGRESS: MSmartProvisioningPacket.COMMAND_IDS.CMD_NOTIFY_PROGRESS + MSmartProvisioningPacket.RESPONSE_ID_OFFSET,
|
||||
CMD_NOTIFY_RESULT: MSmartProvisioningPacket.COMMAND_IDS.CMD_NOTIFY_RESULT + MSmartProvisioningPacket.RESPONSE_ID_OFFSET
|
||||
});
|
||||
|
||||
module.exports = MSmartProvisioningPacket;
|
||||
16
backend/lib/msmart/MSmartTimeoutError.js
Normal file
16
backend/lib/msmart/MSmartTimeoutError.js
Normal file
@ -0,0 +1,16 @@
|
||||
class MSmartTimeoutError extends Error {
|
||||
/**
|
||||
* @param {object} context
|
||||
* @param {string} context.nonce
|
||||
* @param {string} context.command
|
||||
*/
|
||||
constructor(context) {
|
||||
super(`Request with nonce ${context.nonce} timed out`);
|
||||
|
||||
this.name = "MSmartTimeoutError";
|
||||
this.nonce = context.nonce;
|
||||
this.command = context.command;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MSmartTimeoutError;
|
||||
41
backend/lib/msmart/dtos/MSmartActiveZonesDTO.js
Normal file
41
backend/lib/msmart/dtos/MSmartActiveZonesDTO.js
Normal file
@ -0,0 +1,41 @@
|
||||
const MSmartDTO = require("./MSmartDTO");
|
||||
|
||||
/**
|
||||
* @typedef {object} MideaMapPoint
|
||||
* @property {number} x
|
||||
* @property {number} y
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} MideaActiveZone
|
||||
* @property {number} index
|
||||
* @property {number} passes
|
||||
* @property {MideaMapPoint} pA - top-left
|
||||
* @property {MideaMapPoint} pC - bottom-right
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} MideaActiveZonesData
|
||||
* @property {MideaActiveZone[]} zones
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* @class MSmartActiveZonesDTO
|
||||
* @extends MSmartDTO
|
||||
*/
|
||||
class MSmartActiveZonesDTO extends MSmartDTO {
|
||||
/**
|
||||
* @param {MideaActiveZonesData} data
|
||||
*/
|
||||
constructor(data) {
|
||||
super();
|
||||
|
||||
/** @type {MideaActiveZone[]} */
|
||||
this.zones = data.zones;
|
||||
|
||||
Object.freeze(this);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MSmartActiveZonesDTO;
|
||||
31
backend/lib/msmart/dtos/MSmartCleaningSettings1DTO.js
Normal file
31
backend/lib/msmart/dtos/MSmartCleaningSettings1DTO.js
Normal file
@ -0,0 +1,31 @@
|
||||
const MSmartDTO = require("./MSmartDTO");
|
||||
|
||||
// FIXME: naming
|
||||
|
||||
class MSmartCleaningSettings1DTO extends MSmartDTO {
|
||||
/**
|
||||
* @param {object} data
|
||||
* @param {number} data.route_type
|
||||
* @param {number} data.cut_hair_level 0-2
|
||||
* @param {number} data.collect_suction_level 0-1
|
||||
* @param {boolean} data.exhibition_switch
|
||||
* @param {number} data.ai_grade_avoidance_mode
|
||||
* @param {boolean} data.cut_hair_super_switch
|
||||
* @param {number} data.turbidity_re_mop_switch
|
||||
*/
|
||||
constructor(data) {
|
||||
super();
|
||||
|
||||
this.route_type = data.route_type;
|
||||
this.cut_hair_level = data.cut_hair_level;
|
||||
this.collect_suction_level = data.collect_suction_level;
|
||||
this.exhibition_switch = data.exhibition_switch;
|
||||
this.ai_grade_avoidance_mode = data.ai_grade_avoidance_mode;
|
||||
this.cut_hair_super_switch = data.cut_hair_super_switch;
|
||||
this.turbidity_re_mop_switch = data.turbidity_re_mop_switch;
|
||||
|
||||
Object.freeze(this);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MSmartCleaningSettings1DTO;
|
||||
27
backend/lib/msmart/dtos/MSmartDNDConfigurationDTO.js
Normal file
27
backend/lib/msmart/dtos/MSmartDNDConfigurationDTO.js
Normal file
@ -0,0 +1,27 @@
|
||||
const MSmartDTO = require("./MSmartDTO");
|
||||
|
||||
class MSmartDndConfigurationDTO extends MSmartDTO {
|
||||
/**
|
||||
* @param {object} data
|
||||
* @param {boolean} data.enabled
|
||||
*
|
||||
* @param {object} data.start
|
||||
* @param {number} data.start.hour
|
||||
* @param {number} data.start.minute
|
||||
*
|
||||
* @param {object} data.end
|
||||
* @param {number} data.end.hour
|
||||
* @param {number} data.end.minute
|
||||
*/
|
||||
constructor(data) {
|
||||
super();
|
||||
|
||||
this.enabled = data.enabled;
|
||||
this.start = data.start;
|
||||
this.end = data.end;
|
||||
|
||||
Object.freeze(this);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MSmartDndConfigurationDTO;
|
||||
8
backend/lib/msmart/dtos/MSmartDTO.js
Normal file
8
backend/lib/msmart/dtos/MSmartDTO.js
Normal file
@ -0,0 +1,8 @@
|
||||
/**
|
||||
* @abstract
|
||||
*/
|
||||
class MSmartDTO {
|
||||
|
||||
}
|
||||
|
||||
module.exports = MSmartDTO;
|
||||
21
backend/lib/msmart/dtos/MSmartDockStatusDTO.js
Normal file
21
backend/lib/msmart/dtos/MSmartDockStatusDTO.js
Normal file
@ -0,0 +1,21 @@
|
||||
const MSmartDTO = require("./MSmartDTO");
|
||||
|
||||
class MSmartDockStatusDTO extends MSmartDTO {
|
||||
/**
|
||||
* @param {object} data
|
||||
* @param {number} data.dust_collection_count
|
||||
* @param {boolean} data.fluid_1_ok
|
||||
* @param {boolean} data.fluid_2_ok
|
||||
*/
|
||||
constructor(data) {
|
||||
super();
|
||||
|
||||
this.dust_collection_count = data.dust_collection_count;
|
||||
this.fluid_1_ok = data.fluid_1_ok;
|
||||
this.fluid_2_ok = data.fluid_2_ok;
|
||||
|
||||
Object.freeze(this);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MSmartDockStatusDTO;
|
||||
21
backend/lib/msmart/dtos/MSmartErrorDTO.js
Normal file
21
backend/lib/msmart/dtos/MSmartErrorDTO.js
Normal file
@ -0,0 +1,21 @@
|
||||
const MSmartDTO = require("./MSmartDTO");
|
||||
|
||||
class MSmartErrorDTO extends MSmartDTO {
|
||||
/**
|
||||
* @param {object} data
|
||||
* @param {number} data.error_type
|
||||
* @param {number} data.error_desc
|
||||
* @param {number} data.sta_index - FIXME: figure out what this means
|
||||
*/
|
||||
constructor(data) {
|
||||
super();
|
||||
|
||||
this.error_type = data.error_type;
|
||||
this.error_desc = data.error_desc;
|
||||
this.sta_index = data.sta_index;
|
||||
|
||||
Object.freeze(this);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MSmartErrorDTO;
|
||||
19
backend/lib/msmart/dtos/MSmartMapListDTO.js
Normal file
19
backend/lib/msmart/dtos/MSmartMapListDTO.js
Normal file
@ -0,0 +1,19 @@
|
||||
const MSmartDTO = require("./MSmartDTO");
|
||||
|
||||
class MSmartMapListDTO extends MSmartDTO {
|
||||
/**
|
||||
* @param {object} data
|
||||
* @param {number} data.currentMapId
|
||||
* @param {Array<number>} data.savedMapIds
|
||||
*/
|
||||
constructor(data) {
|
||||
super();
|
||||
|
||||
this.currentMapId = data.currentMapId;
|
||||
this.savedMapIds = data.savedMapIds;
|
||||
|
||||
Object.freeze(this);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MSmartMapListDTO;
|
||||
187
backend/lib/msmart/dtos/MSmartStatusDTO.js
Normal file
187
backend/lib/msmart/dtos/MSmartStatusDTO.js
Normal file
@ -0,0 +1,187 @@
|
||||
const MSmartDTO = require("./MSmartDTO");
|
||||
|
||||
class MSmartStatusDTO extends MSmartDTO {
|
||||
/**
|
||||
* @param {object} data
|
||||
* @param {number} [data.work_status]
|
||||
* @param {number} [data.function_type]
|
||||
* @param {number} [data.control_type]
|
||||
* @param {number} [data.move_direction]
|
||||
* @param {number} [data.work_mode]
|
||||
* @param {number} [data.fan_level]
|
||||
* @param {number} [data.work_area]
|
||||
* @param {number} [data.water_level]
|
||||
* @param {number} [data.voice_level]
|
||||
* @param {number} [data.battery_percent]
|
||||
* @param {number} [data.work_time]
|
||||
* @param {boolean} [data.uv_switch] - TODO: VALIDATE
|
||||
* @param {boolean} [data.wifi_switch] - TODO: VALIDATE
|
||||
* @param {boolean} [data.voice_switch] - TODO: VALIDATE
|
||||
* @param {boolean} [data.command_source] - TODO: VALIDATE
|
||||
* @param {boolean} [data.device_error] - TODO: VALIDATE
|
||||
* @param {number} [data.error_type]
|
||||
* @param {number} [data.error_desc]
|
||||
* @param {boolean} [data.has_mop]
|
||||
* @param {boolean} [data.has_vibrate_mop]
|
||||
* @param {number} [data.carpet_switch]
|
||||
* @param {number} [data.district_status]
|
||||
* @param {number} [data.cleaning_type]
|
||||
* @param {string} [data.vibrate_mode]
|
||||
* @param {boolean} [data.vibrate_switch]
|
||||
* @param {boolean} [data.electrolyzed_water]
|
||||
* @param {number} [data.electrolyzed_water_status]
|
||||
* @param {boolean} [data.dustDragSwitch]
|
||||
* @param {boolean} [data.dustDragStatus]
|
||||
* @param {number} [data.dustTimes]
|
||||
* @param {number} [data.dustedTimes]
|
||||
* @param {number} [data.chargeDockType]
|
||||
* @param {boolean} [data.fluid_1_ok]
|
||||
* @param {boolean} [data.fluid_2_ok]
|
||||
* @param {boolean} [data.dustbag_installed]
|
||||
* @param {boolean} [data.dustbag_full]
|
||||
* @param {number} [data.mopMode]
|
||||
* @param {number} [data.station_work_status]
|
||||
* @param {number} [data.job_state]
|
||||
* @param {number} [data.whole_process_state]
|
||||
* @param {boolean} [data.continuous_clean_mode]
|
||||
* @param {boolean} [data.clean_sequence_switch]
|
||||
* @param {boolean} [data.child_lock_enabled]
|
||||
* @param {boolean} [data.child_lock_follows_dnd]
|
||||
* @param {boolean} [data.personal_clean_prefer_switch]
|
||||
* @param {boolean} [data.station_inject_fluid_switch]
|
||||
* @param {boolean} [data.station_inject_soft_fluid_switch]
|
||||
* @param {boolean} [data.carpet_evade_switch]
|
||||
* @param {boolean} [data.station_first_fast_wash_switch]
|
||||
* @param {boolean} [data.pet_mode_switch]
|
||||
* @param {number} [data.station_capability_flags]
|
||||
* @param {boolean} [data.stain_clean_switch]
|
||||
* @param {boolean} [data.ai_obstacle_switch]
|
||||
* @param {boolean} [data.cross_bridge_switch]
|
||||
* @param {boolean} [data.camera_led_switch]
|
||||
* @param {boolean} [data.map_3d_switch]
|
||||
* @param {boolean} [data.ai_recognition_switch]
|
||||
* @param {number} [data.test_mode_type]
|
||||
* @param {number} [data.hot_water_wash_mode]
|
||||
* @param {boolean} [data.station_self_fluid_2_switch]
|
||||
* @param {boolean} [data.slam_version_switch]
|
||||
* @param {boolean} [data.hot_dry_charge_plate_switch]
|
||||
* @param {boolean} [data.telnet_switch]
|
||||
* @param {boolean} [data.mop_auto_dry_switch]
|
||||
* @param {boolean} [data.ai_grade_avoidance_mode]
|
||||
* @param {boolean} [data.pound_sign_switch]
|
||||
* @param {number} [data.stationCleanFrequency]
|
||||
* @param {number} [data.beautify_map_grade]
|
||||
* @param {number} [data.collect_dust_mode]
|
||||
* @param {number} [data.session_id]
|
||||
* @param {number} [data.transaction_id]
|
||||
* @param {boolean} [data.bridge_boost_switch]
|
||||
* @param {boolean} [data.narrow_zone_recharge_switch]
|
||||
* @param {boolean} [data.verification_map_switch]
|
||||
* @param {boolean} [data.wake_up_switch]
|
||||
* @param {boolean} [data.ai_carpet_avoid_switch]
|
||||
* @param {boolean} [data.carpet_evade_adaptive_switch]
|
||||
* @param {boolean} [data.stuck_mark_switch]
|
||||
* @param {boolean} [data.mop_extend_switch]
|
||||
* @param {boolean} [data.zigzag_to_end_switch]
|
||||
* @param {number} [data.remaining_area]
|
||||
* @param {boolean} [data.ai_avoidance_switch]
|
||||
* @param {boolean} [data.gap_deep_cleaning_switch]
|
||||
* @param {boolean} [data.furniture_legs_cleaning_switch]
|
||||
* @param {boolean} [data.edge_deep_vacuum_switch]
|
||||
* @param {boolean} [data.big_object_detect_switch]
|
||||
*/
|
||||
constructor(data) {
|
||||
super();
|
||||
|
||||
this.work_status = data.work_status;
|
||||
this.function_type = data.function_type;
|
||||
this.control_type = data.control_type;
|
||||
this.move_direction = data.move_direction;
|
||||
this.work_mode = data.work_mode;
|
||||
this.fan_level = data.fan_level;
|
||||
this.work_area = data.work_area;
|
||||
this.water_level = data.water_level;
|
||||
this.voice_level = data.voice_level;
|
||||
this.battery_percent = data.battery_percent;
|
||||
this.work_time = data.work_time;
|
||||
this.uv_switch = data.uv_switch;
|
||||
this.wifi_switch = data.wifi_switch;
|
||||
this.voice_switch = data.voice_switch;
|
||||
this.command_source = data.command_source;
|
||||
this.device_error = data.device_error;
|
||||
this.error_type = data.error_type;
|
||||
this.error_desc = data.error_desc;
|
||||
this.has_mop = data.has_mop;
|
||||
this.has_vibrate_mop = data.has_vibrate_mop;
|
||||
this.carpet_switch = data.carpet_switch;
|
||||
this.district_status = data.district_status;
|
||||
this.cleaning_type = data.cleaning_type;
|
||||
this.vibrate_mode = data.vibrate_mode;
|
||||
this.vibrate_switch = data.vibrate_switch;
|
||||
this.electrolyzed_water = data.electrolyzed_water;
|
||||
this.electrolyzed_water_status = data.electrolyzed_water_status;
|
||||
this.dustDragSwitch = data.dustDragSwitch;
|
||||
this.dustDragStatus = data.dustDragStatus;
|
||||
this.dustTimes = data.dustTimes;
|
||||
this.dustedTimes = data.dustedTimes;
|
||||
this.chargeDockType = data.chargeDockType;
|
||||
this.fluid_1_ok = data.fluid_1_ok;
|
||||
this.fluid_2_ok = data.fluid_2_ok;
|
||||
this.dustbag_installed = data.dustbag_installed;
|
||||
this.dustbag_full = data.dustbag_full;
|
||||
this.mopMode = data.mopMode;
|
||||
this.station_work_status = data.station_work_status;
|
||||
this.job_state = data.job_state;
|
||||
this.whole_process_state = data.whole_process_state;
|
||||
this.continuous_clean_mode = data.continuous_clean_mode;
|
||||
this.clean_sequence_switch = data.clean_sequence_switch;
|
||||
this.child_lock_enabled = data.child_lock_enabled;
|
||||
this.child_lock_follows_dnd = data.child_lock_follows_dnd;
|
||||
this.personal_clean_prefer_switch = data.personal_clean_prefer_switch;
|
||||
this.station_inject_fluid_switch = data.station_inject_fluid_switch;
|
||||
this.station_inject_soft_fluid_switch = data.station_inject_soft_fluid_switch;
|
||||
this.carpet_evade_switch = data.carpet_evade_switch;
|
||||
this.station_first_fast_wash_switch = data.station_first_fast_wash_switch;
|
||||
this.pet_mode_switch = data.pet_mode_switch;
|
||||
this.station_capability_flags = data.station_capability_flags;
|
||||
this.stain_clean_switch = data.stain_clean_switch;
|
||||
this.ai_obstacle_switch = data.ai_obstacle_switch;
|
||||
this.cross_bridge_switch = data.cross_bridge_switch;
|
||||
this.camera_led_switch = data.camera_led_switch;
|
||||
this.map_3d_switch = data.map_3d_switch;
|
||||
this.ai_recognition_switch = data.ai_recognition_switch;
|
||||
this.test_mode_type = data.test_mode_type;
|
||||
this.hot_water_wash_mode = data.hot_water_wash_mode;
|
||||
this.station_self_fluid_2_switch = data.station_self_fluid_2_switch;
|
||||
this.slam_version_switch = data.slam_version_switch;
|
||||
this.hot_dry_charge_plate_switch = data.hot_dry_charge_plate_switch;
|
||||
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.pound_sign_switch = data.pound_sign_switch;
|
||||
this.stationCleanFrequency = data.stationCleanFrequency;
|
||||
this.beautify_map_grade = data.beautify_map_grade;
|
||||
this.collect_dust_mode = data.collect_dust_mode;
|
||||
this.session_id = data.session_id;
|
||||
this.transaction_id = data.transaction_id;
|
||||
this.bridge_boost_switch = data.bridge_boost_switch;
|
||||
this.narrow_zone_recharge_switch = data.narrow_zone_recharge_switch;
|
||||
this.verification_map_switch = data.verification_map_switch;
|
||||
this.wake_up_switch = data.wake_up_switch;
|
||||
this.ai_carpet_avoid_switch = data.ai_carpet_avoid_switch;
|
||||
this.carpet_evade_adaptive_switch = data.carpet_evade_adaptive_switch;
|
||||
this.stuck_mark_switch = data.stuck_mark_switch;
|
||||
this.mop_extend_switch = data.mop_extend_switch;
|
||||
this.zigzag_to_end_switch = data.zigzag_to_end_switch;
|
||||
this.remaining_area = data.remaining_area;
|
||||
this.ai_avoidance_switch = data.ai_avoidance_switch;
|
||||
this.gap_deep_cleaning_switch = data.gap_deep_cleaning_switch;
|
||||
this.furniture_legs_cleaning_switch = data.furniture_legs_cleaning_switch;
|
||||
this.edge_deep_vacuum_switch = data.edge_deep_vacuum_switch;
|
||||
this.big_object_detect_switch = data.big_object_detect_switch;
|
||||
|
||||
Object.freeze(this);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MSmartStatusDTO;
|
||||
10
backend/lib/msmart/dtos/index.js
Normal file
10
backend/lib/msmart/dtos/index.js
Normal file
@ -0,0 +1,10 @@
|
||||
module.exports = {
|
||||
MSmartActiveZonesDTO: require("./MSmartActiveZonesDTO"),
|
||||
MSmartCleaningSettings1DTO: require("./MSmartCleaningSettings1DTO"),
|
||||
MSmartDTO: require("./MSmartDTO"),
|
||||
MSmartDndConfigurationDTO: require("./MSmartDNDConfigurationDTO"),
|
||||
MSmartDockStatusDTO: require("./MSmartDockStatusDTO"),
|
||||
MSmartErrorDTO: require("./MSmartErrorDTO"),
|
||||
MSmartMapListDTO: require("./MSmartMapListDTO"),
|
||||
MSmartStatusDTO: require("./MSmartStatusDTO"),
|
||||
};
|
||||
17
backend/lib/res/roodkcab.js
Normal file
17
backend/lib/res/roodkcab.js
Normal file
@ -0,0 +1,17 @@
|
||||
/*
|
||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⡀⠀⠀⠀⢀⣀⡠⠤⠴⠚⣿⠃
|
||||
⠀⠸⣿⡭⣭⣿⣽⣿⣿⣿⣿⣿⣿⣿⣽⣿⡿⠓⠚⠉⣉⣀⣤⡤⣴⠀⣿⠀
|
||||
⠀⠀⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠀⢰⠞⢩⠀⢻⡏⠀⡏⠀⣿⠄
|
||||
⠀⢠⣟⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠀⢸⠀⢸⠀⢸⡇⠀⠃⠀⣿⠂
|
||||
⠀⢘⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠀⢸⠀⢸⠀⢸⡇⠀⡇⠀⣿⡇
|
||||
⠀⠈⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠀⢸⠀⢸⠀⢸⡇⠀⣷⠀⣿⡇
|
||||
⠀⣠⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠀⢸⠀⢸⠀⢸⡇⠀⣿⣼⣿⡇
|
||||
⠀⡃⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠀⠘⠛⠛⠒⠛⠓⠛⠛⣿⣿⡇
|
||||
⠀⠀⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠀⢰⠦⢠⠀⢤⣤⣤⣄⠋⣿⡇
|
||||
⠀⠀⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠀⢸⠀⢸⠀⢸⡇⠈⣿⠀⣿⡇
|
||||
⠀⢸⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠀⢸⠀⢸⠀⢸⡇⠀⣿⠀⣿⡇
|
||||
⠀⠀⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠀⢸⣄⢸⠠⣼⡇⠀⣿⠀⣿⡇
|
||||
⠀⣸⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠀⠀⠉⠉⠀⠛⠚⠯⠿⠀⣿⡇
|
||||
⠠⢿⣿⣷⣶⣶⣶⠶⢶⡶⢶⣶⣶⣶⣶⢿⣶⣤⣄⣀⣀⠀⠀⠀⢨⠀⣿⡇
|
||||
⠀⠀⠀⠈⠀⠐⠒⠒⠀⠀⠀⠘⠁⠈⠀⠀⠀⠀⠉⠉⢛⠉⠑⠒⠠⠤⢿⠇
|
||||
*/
|
||||
@ -1,4 +1,5 @@
|
||||
const dreame = require("./dreame");
|
||||
const midea = require("./midea");
|
||||
const mock = require("./mock");
|
||||
const roborock = require("./roborock");
|
||||
const viomi = require("./viomi");
|
||||
@ -7,5 +8,6 @@ module.exports = Object.assign({},
|
||||
roborock,
|
||||
viomi,
|
||||
dreame,
|
||||
midea,
|
||||
mock
|
||||
);
|
||||
|
||||
59
backend/lib/robots/midea/MideaConst.js
Normal file
59
backend/lib/robots/midea/MideaConst.js
Normal file
@ -0,0 +1,59 @@
|
||||
|
||||
/*
|
||||
openssl req -x509 -newkey rsa:2048 -nodes -keyout dummy_client.key -out dummy_client.crt -sha256 -days 36500 -subj "/CN=ValetudoDummyClient"
|
||||
*/
|
||||
const DUMMY_CLIENT_KEY = `-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDcTZAsDj4J2iVy
|
||||
VJFEtz6xAZOLoAzAI02MRDgRhOb5Abbwg3GJ2nfM9GHBXGSyC1X5UPLFMSe4TI/C
|
||||
Woajcf67Ru7xlPkDwb1i5QkzmVenJmflWH/vmvlKU421asFsvUXO2T80EPxoQ5B5
|
||||
viffA6dujcPFmvu4IN+oKp+TcdXXI/fAleiT3UMKOubD8drWXSVWTDNMpN5BNE1j
|
||||
sPYN14rSCMCbcoPSa8tGzK1khMiClVWYRGXPz0Xk2D+4b1/fuFEunQ259pnneoa4
|
||||
ohe44NYfSo/iu92RLYHIia1SXQaU9MwqrsD5APT7zo+y9dMqVrLJSAXRJqCCCrsP
|
||||
DT1BQ4TNAgMBAAECggEAD38dPxwZXRQNQkeUmGLTdBwKRu4RN4rEL7O0xfa1UJrA
|
||||
RZbZa7sEZlRic/mN08BcYddB3IEirCImkqNPiTvBkWbh8/hos8zzB3vY89o7gjR/
|
||||
ZnCdPzuFgaby9un1hTKjMHOzsHPpbWQjS40GvPdC1dH/DW1je4ZEdU3aP8LoKePq
|
||||
Y6QEI3Wb6LqG/qM7x2TiFgsRbChkJeZNo8c5rl5puLd4cxjyLifeERTxtAoM6mCJ
|
||||
mD8gindVThFWxUY+O92DIX7T9JUwM059dBZfe9wSCXX84a5blMiBeq9Q7ilEwPUi
|
||||
jwpudJwJJ/7Q/9FjgRrSyPr/L7aeVFG2Uw11C4N0eQKBgQDnaR5vREUveFlAKTyy
|
||||
cttDo4jPriPpIjtG9OHSeyg5K/gQJ7X94IngroxW2qkKXR8hbStdTNRt0Rg7DQpS
|
||||
HFJwGnD21SWE7FBT30p1XyVpABAmyOL3MhfW+hEMOw3vxc/1k8mzBcjg7YY1zNl7
|
||||
KLMyAaiKWiOknemirXVcqqgMywKBgQDztkq6T9SHnKFcWYBEyTRHDBrTR8Yh+WiO
|
||||
0EY6eXfkvos86kYrlutYKT5LmYgroXA1PEW/dHAHM3pyQSmNJXsJcyIfz8Mn1ZZU
|
||||
6coNg2fNN6kfClc4KVNK5KO+N/NM4l0sithPa1yhjFctqR2aZQWT5LvLR5k/663j
|
||||
I75Mta1ZxwKBgGrZCohtiVRlyS/q2m+6wKr2c1ERItueRqh4oVxCKUxclOlArLNQ
|
||||
Xdk0PvBLfgme/aS9d2xY8SzTgtChMMbA9P919frCZ9R8GIrhasvO5sMYmFyQHNvu
|
||||
cTt9sylmiwTO3TqSxmq2nQ3eHj3xG+nV3QeV5HAdNp/nmdzXIn1q/rUJAoGAVPfs
|
||||
S9LDVViNhYYKy3Ce0lptC9aNRJERHCGPKpno7A5muyEuv8nJWZ5fgroPmK6bUWQn
|
||||
KR3uZQRUn3sKgpRbtiq27gJglwXHeOldsaJr0UejphfT2tfFm2nlkM8u+1I8i+gI
|
||||
jH/w9r3YMyowEQFBlZN8yd23l2qS4Is4sMPyoUcCgYEAi4ukkRVRM/PsM2v+MW0U
|
||||
uu0nXu44Lcq05RoO93f+LdcUibNRIBIpNDU7vH6RuPdmRVFRQCBvmhzslszusm4V
|
||||
lxTSqhOpa5h/S+cFspmPbgIOq1mwM1VEGU6KLrcl5mnz60VkBsVDFfKE0Ni9sN2T
|
||||
THzNXxIdC+dgFxqRGL6BELk=
|
||||
-----END PRIVATE KEY-----
|
||||
`;
|
||||
|
||||
const DUMMY_CLIENT_CERT = `-----BEGIN CERTIFICATE-----
|
||||
MIIDHzCCAgegAwIBAgIUVboURMYBW4WYeT/IQhL2uf9bomUwDQYJKoZIhvcNAQEL
|
||||
BQAwHjEcMBoGA1UEAwwTVmFsZXR1ZG9EdW1teUNsaWVudDAgFw0yNTA3MDkxODM2
|
||||
MjJaGA8yMTI1MDYxNTE4MzYyMlowHjEcMBoGA1UEAwwTVmFsZXR1ZG9EdW1teUNs
|
||||
aWVudDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANxNkCwOPgnaJXJU
|
||||
kUS3PrEBk4ugDMAjTYxEOBGE5vkBtvCDcYnad8z0YcFcZLILVflQ8sUxJ7hMj8Ja
|
||||
hqNx/rtG7vGU+QPBvWLlCTOZV6cmZ+VYf++a+UpTjbVqwWy9Rc7ZPzQQ/GhDkHm+
|
||||
J98Dp26Nw8Wa+7gg36gqn5Nx1dcj98CV6JPdQwo65sPx2tZdJVZMM0yk3kE0TWOw
|
||||
9g3XitIIwJtyg9Jry0bMrWSEyIKVVZhEZc/PReTYP7hvX9+4US6dDbn2med6hrii
|
||||
F7jg1h9Kj+K73ZEtgciJrVJdBpT0zCquwPkA9PvOj7L10ypWsslIBdEmoIIKuw8N
|
||||
PUFDhM0CAwEAAaNTMFEwHQYDVR0OBBYEFG0w3IIqzW+Wm8Eul+uwm96t1fNHMB8G
|
||||
A1UdIwQYMBaAFG0w3IIqzW+Wm8Eul+uwm96t1fNHMA8GA1UdEwEB/wQFMAMBAf8w
|
||||
DQYJKoZIhvcNAQELBQADggEBANJ4ejxINHp6Say0Q92ytpkHz27fwWWeCq1FRR2i
|
||||
X2MCPaSl6djg+AdoQ/LOXuOJ516OofCNxyGlTDCfOxguD6CV8ttA8jh4EGo+Jmen
|
||||
ezfmGR4I6DzMKTiOdu2Qd6EVajVXLMZZfBJUNNs1tmtWhtUk2P4IJqn/lqeib7un
|
||||
zzq/W47Y0MGPL4UmGyp0mwaVjj9LMN0NL8EEQhhG6SkLGE4SUXfeiqAfzrYP/PdB
|
||||
tkciowVpUEiXK0tg/t8EgHj+sW9vUqX6ovKDx5Hy2nczd2CruRxHUieSQKX+zajv
|
||||
8REhl/azGTE4XmSqFfs5sdm2dWIFudNMZoT+tkxR/pQB1ts=
|
||||
-----END CERTIFICATE-----
|
||||
`;
|
||||
|
||||
module.exports = {
|
||||
DUMMY_CLIENT_CERT: DUMMY_CLIENT_CERT,
|
||||
DUMMY_CLIENT_KEY: DUMMY_CLIENT_KEY
|
||||
};
|
||||
28
backend/lib/robots/midea/MideaJ15ValetudoRobot.js
Normal file
28
backend/lib/robots/midea/MideaJ15ValetudoRobot.js
Normal file
@ -0,0 +1,28 @@
|
||||
const fs = require("node:fs");
|
||||
const Logger = require("../../Logger");
|
||||
const MideaValetudoRobot = require("./MideaValetudoRobot");
|
||||
|
||||
class MideaJ15ValetudoRobot extends MideaValetudoRobot {
|
||||
getManufacturer() {
|
||||
return "Eureka";
|
||||
}
|
||||
|
||||
getModelName() {
|
||||
return "J15 Pro Ultra"; // TODO: how do you distinguish the ultra from the pro ultra? And what about the max?
|
||||
}
|
||||
|
||||
static IMPLEMENTATION_AUTO_DETECTION_HANDLER() {
|
||||
let productModel;
|
||||
|
||||
try {
|
||||
productModel = fs.readFileSync("/oem/product/product_model").toString().trim();
|
||||
} catch (e) {
|
||||
//This is intentionally failing if we're the wrong implementation
|
||||
Logger.trace("cannot read", "/oem/product/product_model", e);
|
||||
}
|
||||
|
||||
return !!(productModel && productModel === "j15_eureka");
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MideaJ15ValetudoRobot;
|
||||
558
backend/lib/robots/midea/MideaMapParser.js
Normal file
558
backend/lib/robots/midea/MideaMapParser.js
Normal file
@ -0,0 +1,558 @@
|
||||
const Logger = require("../../Logger");
|
||||
const mapEntities = require("../../entities/map");
|
||||
const zlib = require("zlib");
|
||||
|
||||
class MideaMapParser {
|
||||
constructor() {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.mapInfo = {
|
||||
height: 0,
|
||||
width: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
};
|
||||
this.dockPosition = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
angle: 0
|
||||
};
|
||||
|
||||
this.layers = [];
|
||||
this.entities = [];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @returns {{x: number, y: number}}
|
||||
*/
|
||||
convertToValetudoCoordinates(x, y) {
|
||||
// TODO: throw when not initialized
|
||||
|
||||
return {
|
||||
x: (x - this.mapInfo.left) * MideaMapParser.PIXEL_SIZE,
|
||||
y: (this.mapInfo.height - 1 - (y - this.mapInfo.bottom)) * MideaMapParser.PIXEL_SIZE
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @returns {{x: number, y: number}}
|
||||
*/
|
||||
convertToMideaCoordinates(x, y) {
|
||||
// TODO: throw when not initialized
|
||||
|
||||
return {
|
||||
x: Math.round(x / MideaMapParser.PIXEL_SIZE) + this.mapInfo.left,
|
||||
y: this.mapInfo.bottom + (this.mapInfo.height - 1 - Math.round(y / MideaMapParser.PIXEL_SIZE))
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} type
|
||||
* @param {any} data
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async update(type, data) {
|
||||
switch (type) {
|
||||
case "map":
|
||||
await this.handleInfoMapUpdate(data);
|
||||
break;
|
||||
case "track":
|
||||
await this.handleTrackUpdate(data);
|
||||
break;
|
||||
case "dockPosition":
|
||||
await this.handleDockPositionUpdate(data);
|
||||
break;
|
||||
case "virtual":
|
||||
await this.handleVirtualWallUpdate(data);
|
||||
break;
|
||||
case "forbidden":
|
||||
await this.handleVirtualRestrictionZoneUpdate(data, mapEntities.PolygonMapEntity.TYPE.NO_GO_AREA);
|
||||
break;
|
||||
case "mop_forbidden":
|
||||
await this.handleVirtualRestrictionZoneUpdate(data, mapEntities.PolygonMapEntity.TYPE.NO_MOP_AREA);
|
||||
break;
|
||||
|
||||
case "evt_active_zones":
|
||||
await this.handleActiveZonesUpdate(data);
|
||||
break;
|
||||
|
||||
case "threshold_area":
|
||||
case "points":
|
||||
case "bridge_data":
|
||||
case "user_defined_carpet":
|
||||
case "backup_map":
|
||||
case "3d":
|
||||
case "semantic_data":
|
||||
case "stain_area":
|
||||
case "partition":
|
||||
case "adjacent":
|
||||
case "user_deleted_detected_curtain":
|
||||
case "displayed_curtain":
|
||||
case "user_deleted_detected_door_sill":
|
||||
case "displayed_door_sill":
|
||||
// Ignored for now
|
||||
break;
|
||||
default:
|
||||
Logger.warn(`Unknown map update type '${type}'`);
|
||||
Logger.warn(data, data?.length); // TODO: remove
|
||||
}
|
||||
}
|
||||
|
||||
getCurrentMap() {
|
||||
const entities = [...this.entities];
|
||||
|
||||
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
|
||||
],
|
||||
metaData: {
|
||||
angle: dockAngle
|
||||
},
|
||||
type: mapEntities.PointMapEntity.TYPE.ROBOT_POSITION
|
||||
}));
|
||||
}
|
||||
|
||||
return new mapEntities.ValetudoMap({
|
||||
size: {
|
||||
x: this.mapInfo.width * MideaMapParser.PIXEL_SIZE,
|
||||
y: this.mapInfo.height * MideaMapParser.PIXEL_SIZE
|
||||
},
|
||||
pixelSize: MideaMapParser.PIXEL_SIZE,
|
||||
layers: this.layers,
|
||||
entities: entities
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} data
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async handleInfoMapUpdate(data) {
|
||||
const parsed = MideaMapParser.INFO_MAP_REGEX.exec(data);
|
||||
if (!parsed) {
|
||||
if (data !== "") {
|
||||
Logger.warn("Could not parse info_map.");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const left = parseInt(parsed.groups.left);
|
||||
const bottom = parseInt(parsed.groups.bottom);
|
||||
const width = parseInt(parsed.groups.right) - left + 1;
|
||||
const height = parseInt(parsed.groups.top) - bottom + 1;
|
||||
|
||||
const payload = await MideaMapParser.DECOMPRESS_PAYLOAD(parsed.groups.payload);
|
||||
|
||||
const pixels = {
|
||||
floor: [],
|
||||
wall: [],
|
||||
segments: {}
|
||||
};
|
||||
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
const idx = (y * width) + x;
|
||||
const val = payload[idx];
|
||||
|
||||
const coords = [
|
||||
x,
|
||||
height - y - 1
|
||||
];
|
||||
|
||||
switch (val) {
|
||||
case 0:
|
||||
// void
|
||||
break;
|
||||
case 255:
|
||||
pixels.wall.push(coords);
|
||||
break;
|
||||
case 99:
|
||||
case 100:
|
||||
case 101:
|
||||
pixels.floor.push(coords);
|
||||
break;
|
||||
case 170:
|
||||
// This is the magic byte at the start of the data. The format is a bit broken,
|
||||
// because this is treated as a pixel, otherwise the map is short by 1 byte
|
||||
// Thus, we just ignore it without slicing it away
|
||||
break;
|
||||
|
||||
default:
|
||||
if (val >= 1 && val <= 98) {
|
||||
if (!Array.isArray(pixels.segments[val])) {
|
||||
pixels.segments[val] = [];
|
||||
}
|
||||
|
||||
pixels.segments[val].push(coords);
|
||||
} else {
|
||||
Logger.warn(`Encountered unknown pixel type ${val}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const layers = [];
|
||||
|
||||
if (pixels.floor.length > 0) {
|
||||
layers.push(new mapEntities.MapLayer({
|
||||
pixels: pixels.floor.sort(mapEntities.MapLayer.COORDINATE_TUPLE_SORT).flat(),
|
||||
type: mapEntities.MapLayer.TYPE.FLOOR
|
||||
}));
|
||||
}
|
||||
|
||||
if (pixels.wall.length > 0) {
|
||||
layers.push(new mapEntities.MapLayer({
|
||||
pixels: pixels.wall.sort(mapEntities.MapLayer.COORDINATE_TUPLE_SORT).flat(),
|
||||
type: mapEntities.MapLayer.TYPE.WALL
|
||||
}));
|
||||
}
|
||||
|
||||
Object.keys(pixels.segments).forEach((segmentId) => {
|
||||
if (pixels.segments[segmentId].length > 0) {
|
||||
layers.push(new mapEntities.MapLayer({
|
||||
pixels: pixels.segments[segmentId].sort(mapEntities.MapLayer.COORDINATE_TUPLE_SORT).flat(),
|
||||
type: mapEntities.MapLayer.TYPE.SEGMENT,
|
||||
metaData: {
|
||||
segmentId: segmentId,
|
||||
// Segment names appear to be stored in the cloud and in the cloud only :(
|
||||
active: false, // This does not appear to be reported by the firmware ??
|
||||
}
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
this.mapInfo.width = width;
|
||||
this.mapInfo.height = height;
|
||||
this.mapInfo.left = left;
|
||||
this.mapInfo.bottom = bottom;
|
||||
this.layers = layers;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} data
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async handleTrackUpdate(data) {
|
||||
const payload = await MideaMapParser.DECOMPRESS_PAYLOAD(data);
|
||||
|
||||
this.entities = this.entities.filter(e => {
|
||||
return ![
|
||||
mapEntities.PathMapEntity.TYPE.PATH,
|
||||
mapEntities.PathMapEntity.TYPE.PREDICTED_PATH,
|
||||
|
||||
mapEntities.PointMapEntity.TYPE.ROBOT_POSITION
|
||||
].includes(e.type);
|
||||
});
|
||||
|
||||
if (payload.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// First 4 payload bytes were observed to be 00 00 00 00 and 00 02 00 00
|
||||
// TODO: figure out what that means
|
||||
let offset = 0;
|
||||
let currentType = undefined;
|
||||
let paths = [];
|
||||
let points = [];
|
||||
|
||||
|
||||
do {
|
||||
const x = payload.readUInt16BE(offset);
|
||||
const y = payload.readUInt16BE(offset + 2);
|
||||
const type = payload.readUInt16BE(offset + 4);
|
||||
|
||||
if (type !== currentType) {
|
||||
// @ts-ignore
|
||||
if (!Object.values(MideaMapParser.PATH_TYPES).includes(type)) {
|
||||
Logger.info(`Encountered unknown path type ${type}`); // TODO: debug loglevel
|
||||
}
|
||||
|
||||
|
||||
paths.push({ points: points, type: currentType });
|
||||
points = [];
|
||||
|
||||
currentType = type;
|
||||
}
|
||||
|
||||
points.push(x, y);
|
||||
offset = offset + 6;
|
||||
} while (offset < payload.length);
|
||||
|
||||
// Final path
|
||||
paths.push({ points: points, type: currentType });
|
||||
|
||||
|
||||
const entities = paths.filter(p => p.type !== 2400 && p.points.length > 0).map(path => {
|
||||
const transformedPoints = [];
|
||||
|
||||
for (let i = 0; i < path.points.length; i += 2) {
|
||||
const x = path.points[i];
|
||||
const y = path.points[i + 1];
|
||||
|
||||
const coords = this.convertToValetudoCoordinates(x, y);
|
||||
transformedPoints.push(coords.x, coords.y);
|
||||
}
|
||||
|
||||
return new mapEntities.PathMapEntity({
|
||||
points: transformedPoints,
|
||||
type: mapEntities.PathMapEntity.TYPE.PATH,
|
||||
metaData: {
|
||||
vendorPathType: path.type // todo: remove
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// Add the robot position entity based on the very last valid path
|
||||
if (entities.length > 0) {
|
||||
const lastPathEntity = entities[entities.length - 1];
|
||||
const lastPathPoints = lastPathEntity.points;
|
||||
|
||||
if (lastPathPoints.length >= 2) {
|
||||
const robotPositionCoordinates = [
|
||||
lastPathPoints[lastPathPoints.length - 2],
|
||||
lastPathPoints[lastPathPoints.length - 1]
|
||||
];
|
||||
|
||||
let robotAngle = 0;
|
||||
if (lastPathPoints.length >= 4) {
|
||||
robotAngle = (Math.round(Math.atan2(
|
||||
robotPositionCoordinates[1] - lastPathPoints[lastPathPoints.length - 3],
|
||||
robotPositionCoordinates[0] - lastPathPoints[lastPathPoints.length - 4]
|
||||
) * 180 / Math.PI) + 90) % 360; //TODO: No idea why
|
||||
}
|
||||
|
||||
entities.push(new mapEntities.PointMapEntity({
|
||||
points: robotPositionCoordinates,
|
||||
metaData: {
|
||||
angle: robotAngle
|
||||
},
|
||||
type: mapEntities.PointMapEntity.TYPE.ROBOT_POSITION
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
this.entities.push(...entities.filter(e => {
|
||||
// We do that quite late here, because we need them to calculate the robot position
|
||||
return ![
|
||||
MideaMapParser.PATH_TYPES.MAPPING,
|
||||
MideaMapParser.PATH_TYPES.MOVING,
|
||||
MideaMapParser.PATH_TYPES.RETURNING,
|
||||
MideaMapParser.PATH_TYPES.TAXIING,
|
||||
MideaMapParser.PATH_TYPES.TAXIING_ZONES
|
||||
].includes(e.metaData.vendorPathType);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {object} data
|
||||
* @param {number} data.x
|
||||
* @param {number} data.y
|
||||
* @param {number} data.angle
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async handleDockPositionUpdate(data) {
|
||||
this.dockPosition = data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Empty string means none. Otherwise, format: line 324 433 354 403 394 425 424 395 1
|
||||
* With 4 ints each being x,y; x,y and the final trailing number being unknown
|
||||
*
|
||||
* @param {string} data
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async handleVirtualWallUpdate(data) {
|
||||
this.entities = this.entities.filter(e =>
|
||||
e.type !== mapEntities.LineMapEntity.TYPE.VIRTUAL_WALL
|
||||
);
|
||||
|
||||
if (!data.startsWith("line")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const coordinates = data.split(" ").slice(1, -1).map(coord => parseInt(coord, 10));
|
||||
|
||||
if (coordinates.length % 4 !== 0) {
|
||||
Logger.warn("Invalid wall data format");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const entities = [];
|
||||
|
||||
for (let i = 0; i < coordinates.length; i += 4) {
|
||||
const [x1, y1, x2, y2] = coordinates.slice(i, i + 4);
|
||||
|
||||
const points = [
|
||||
...Object.values(this.convertToValetudoCoordinates(x1, y1)),
|
||||
...Object.values(this.convertToValetudoCoordinates(x2, y2))
|
||||
];
|
||||
|
||||
entities.push(new mapEntities.LineMapEntity({
|
||||
points: points,
|
||||
type: mapEntities.LineMapEntity.TYPE.VIRTUAL_WALL
|
||||
}));
|
||||
}
|
||||
|
||||
this.entities.push(...entities);
|
||||
}
|
||||
|
||||
/**
|
||||
* Empty string means none. Otherwise, format: forbid_zone 367 455 397 425 440 458 470 428 1
|
||||
*
|
||||
* @param {string} data
|
||||
* @param {string} entityType
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async handleVirtualRestrictionZoneUpdate(data, entityType) {
|
||||
this.entities = this.entities.filter(e => e.type !== entityType);
|
||||
|
||||
if (!data.startsWith("forbid_zone")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const coordinates = data.split(" ").slice(1, -1).map(coord => parseInt(coord, 10));
|
||||
|
||||
if (coordinates.length % 4 !== 0) {
|
||||
const zoneTypeName = entityType === mapEntities.PolygonMapEntity.TYPE.NO_GO_AREA ? "no-go" : "no-mop";
|
||||
Logger.warn(`Invalid ${zoneTypeName} zone data format`);
|
||||
return;
|
||||
}
|
||||
|
||||
const entities = [];
|
||||
|
||||
for (let i = 0; i < coordinates.length; i += 4) {
|
||||
const [x1, y1, x2, y2] = coordinates.slice(i, i + 4);
|
||||
|
||||
const pA = this.convertToValetudoCoordinates(x1, y1);
|
||||
const pC = this.convertToValetudoCoordinates(x2, y2);
|
||||
|
||||
const xCoords = [pA.x, pC.x].sort((a, b) => a - b);
|
||||
const yCoords = [pA.y, pC.y].sort((a, b) => a - b);
|
||||
|
||||
entities.push(new mapEntities.PolygonMapEntity({
|
||||
points: [
|
||||
xCoords[0], yCoords[0],
|
||||
xCoords[1], yCoords[0],
|
||||
xCoords[1], yCoords[1],
|
||||
xCoords[0], yCoords[1]
|
||||
],
|
||||
type: entityType
|
||||
}));
|
||||
}
|
||||
|
||||
this.entities.push(...entities);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import("../../msmart/dtos/MSmartActiveZonesDTO")} data
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async handleActiveZonesUpdate(data) {
|
||||
this.entities = this.entities.filter(e => e.type !== mapEntities.PolygonMapEntity.TYPE.ACTIVE_ZONE);
|
||||
|
||||
const entities = [];
|
||||
|
||||
for (const zone of data.zones) {
|
||||
const pA = this.convertToValetudoCoordinates(zone.pA.x, zone.pA.y);
|
||||
const pC = this.convertToValetudoCoordinates(zone.pC.x, zone.pC.y);
|
||||
|
||||
|
||||
const xCoords = [pA.x, pC.x].sort((a, b) => a - b);
|
||||
const yCoords = [pA.y, pC.y].sort((a, b) => a - b);
|
||||
|
||||
entities.push(new mapEntities.PolygonMapEntity({
|
||||
points: [
|
||||
xCoords[0], yCoords[0],
|
||||
xCoords[1], yCoords[0],
|
||||
xCoords[1], yCoords[1],
|
||||
xCoords[0], yCoords[1]
|
||||
],
|
||||
type: mapEntities.PolygonMapEntity.TYPE.ACTIVE_ZONE
|
||||
}));
|
||||
}
|
||||
|
||||
this.entities.push(...entities);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} data
|
||||
* @return {Promise<Buffer>}
|
||||
*/
|
||||
static async DECOMPRESS_PAYLOAD(data) {
|
||||
const compressedPayload = Buffer.from(data, "base64");
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (compressedPayload.length === 0) {
|
||||
return resolve(compressedPayload);
|
||||
}
|
||||
|
||||
zlib.inflate(compressedPayload, (err, decompressed) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(decompressed);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
MideaMapParser.PIXEL_SIZE = 5;
|
||||
MideaMapParser.INFO_MAP_REGEX = /^info_map (?<id>\d+) (?<left>\d+) (?<bottom>\d+) (?<right>\d+) (?<top>\d+) (?<payload>[a-zA-Z0-9+=/]+)$/;
|
||||
|
||||
MideaMapParser.PATH_TYPES = Object.freeze({
|
||||
"NONE": 0, // Probably not a real type?
|
||||
|
||||
"RETURNING": 10,
|
||||
"OUTLINE": 30,
|
||||
"TAXIING_ZONES": 40,
|
||||
"TAXIING_SEGMENT_CLEANING": 50,
|
||||
|
||||
"CLEANING_TURN": 80,
|
||||
"CLEANING": 100,
|
||||
|
||||
"MAPPING": 170,
|
||||
"TAXIING": 180,
|
||||
"MOVING": 190,
|
||||
|
||||
"HEADER": 2400, // Not a real type. Just the format header
|
||||
});
|
||||
|
||||
module.exports = MideaMapParser;
|
||||
134
backend/lib/robots/midea/MideaQuirkFactory.js
Normal file
134
backend/lib/robots/midea/MideaQuirkFactory.js
Normal file
@ -0,0 +1,134 @@
|
||||
const BEightParser = require("../../msmart/BEightParser");
|
||||
const MSmartCleaningSettings1DTO = require("../../msmart/dtos/MSmartCleaningSettings1DTO");
|
||||
const MSmartConst = require("../../msmart/MSmartConst");
|
||||
const MSmartPacket = require("../../msmart/MSmartPacket");
|
||||
const Quirk = require("../../core/Quirk");
|
||||
|
||||
class MideaQuirkFactory {
|
||||
/**
|
||||
*
|
||||
* @param {object} options
|
||||
* @param {import("./MideaValetudoRobot")} options.robot
|
||||
*/
|
||||
constructor(options) {
|
||||
this.robot = options.robot;
|
||||
}
|
||||
/**
|
||||
* @param {string} id
|
||||
*/
|
||||
getQuirk(id) {
|
||||
switch (id) {
|
||||
case MideaQuirkFactory.KNOWN_QUIRKS.HAIR_CUTTING:
|
||||
return new Quirk({
|
||||
id: id,
|
||||
title: "Hair Cutting",
|
||||
description: "Control the hair cutting cycle that runs once docked after a cleanup.",
|
||||
options: ["off", "normal", "strong"],
|
||||
getter: async () => {
|
||||
const packet = new MSmartPacket({
|
||||
messageType: MSmartPacket.MESSAGE_TYPE.ACTION,
|
||||
payload: MSmartPacket.buildPayload(MSmartConst.ACTION.GET_CLEANING_SETTINGS_1)
|
||||
});
|
||||
|
||||
const response = await this.robot.sendCommand(packet.toHexString());
|
||||
const parsedResponse = BEightParser.PARSE(response);
|
||||
|
||||
if (parsedResponse instanceof MSmartCleaningSettings1DTO) {
|
||||
switch (parsedResponse.cut_hair_level) {
|
||||
case 0:
|
||||
return "off";
|
||||
case 1:
|
||||
return "normal";
|
||||
case 2:
|
||||
return "strong";
|
||||
}
|
||||
} else {
|
||||
throw new Error("Invalid response from robot");
|
||||
}
|
||||
},
|
||||
setter: async (value) => {
|
||||
let val;
|
||||
|
||||
switch (value) {
|
||||
case "off":
|
||||
val = 0;
|
||||
break;
|
||||
case "normal":
|
||||
val = 1;
|
||||
break;
|
||||
case "strong":
|
||||
val = 2;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Invalid hair cutting value: ${value}`);
|
||||
}
|
||||
|
||||
const packet = new MSmartPacket({
|
||||
messageType: MSmartPacket.MESSAGE_TYPE.SETTING,
|
||||
payload: MSmartPacket.buildPayload(
|
||||
MSmartConst.SETTING.SET_CLEANING_SETTINGS_1,
|
||||
Buffer.from([0x01, val])
|
||||
)
|
||||
});
|
||||
|
||||
await this.robot.sendCommand(packet.toHexString());
|
||||
}
|
||||
});
|
||||
case MideaQuirkFactory.KNOWN_QUIRKS.HAIR_CUTTING_ONE_TIME_TURBO:
|
||||
return new Quirk({
|
||||
id: id,
|
||||
title: "Hair Cutting Turbo",
|
||||
description: "Enabling this will run an even stronger hair cutting cycle on the next cleanup. Disables itself afterwards.",
|
||||
options: ["off", "on"],
|
||||
getter: async () => {
|
||||
const packet = new MSmartPacket({
|
||||
messageType: MSmartPacket.MESSAGE_TYPE.ACTION,
|
||||
payload: MSmartPacket.buildPayload(MSmartConst.ACTION.GET_CLEANING_SETTINGS_1)
|
||||
});
|
||||
|
||||
const response = await this.robot.sendCommand(packet.toHexString());
|
||||
const parsedResponse = BEightParser.PARSE(response);
|
||||
|
||||
if (parsedResponse instanceof MSmartCleaningSettings1DTO) {
|
||||
return parsedResponse.cut_hair_super_switch ? "on" : "off";
|
||||
} else {
|
||||
throw new Error("Invalid response from robot");
|
||||
}
|
||||
},
|
||||
setter: async (value) => {
|
||||
let val;
|
||||
|
||||
switch (value) {
|
||||
case "off":
|
||||
val = 0;
|
||||
break;
|
||||
case "on":
|
||||
val = 1;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Invalid super hair cutting value: ${value}`);
|
||||
}
|
||||
|
||||
const packet = new MSmartPacket({
|
||||
messageType: MSmartPacket.MESSAGE_TYPE.SETTING,
|
||||
payload: MSmartPacket.buildPayload(
|
||||
MSmartConst.SETTING.SET_CLEANING_SETTINGS_1,
|
||||
Buffer.from([0x06, val])
|
||||
)
|
||||
});
|
||||
|
||||
await this.robot.sendCommand(packet.toHexString());
|
||||
}
|
||||
});
|
||||
default:
|
||||
throw new Error(`There's no quirk with id ${id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MideaQuirkFactory.KNOWN_QUIRKS = {
|
||||
HAIR_CUTTING: "afa83002-87db-43bb-b8ff-e4b38863a5d3",
|
||||
HAIR_CUTTING_ONE_TIME_TURBO: "224b6a0a-1a51-48d7-9d4d-61645399d368",
|
||||
};
|
||||
|
||||
module.exports = MideaQuirkFactory;
|
||||
1238
backend/lib/robots/midea/MideaValetudoRobot.js
Normal file
1238
backend/lib/robots/midea/MideaValetudoRobot.js
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,69 @@
|
||||
const AutoEmptyDockAutoEmptyControlCapability = require("../../../core/capabilities/AutoEmptyDockAutoEmptyControlCapability");
|
||||
const BEightParser = require("../../../msmart/BEightParser");
|
||||
const MSmartConst = require("../../../msmart/MSmartConst");
|
||||
const MSmartPacket = require("../../../msmart/MSmartPacket");
|
||||
const MSmartStatusDTO = require("../../../msmart/dtos/MSmartStatusDTO");
|
||||
|
||||
/**
|
||||
* @extends AutoEmptyDockAutoEmptyControlCapability<import("../MideaValetudoRobot")>
|
||||
*/
|
||||
class MideaAutoEmptyDockAutoEmptyControlCapability extends AutoEmptyDockAutoEmptyControlCapability {
|
||||
|
||||
/**
|
||||
*
|
||||
* @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.dustTimes >= 1; // could also be every 3 or every 5, but not supported
|
||||
} 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_DOCK_INTERVALS,
|
||||
Buffer.from([
|
||||
0x01, // Auto-empty
|
||||
0x01 // Every 1 cleanup
|
||||
])
|
||||
)
|
||||
});
|
||||
|
||||
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_DOCK_INTERVALS,
|
||||
Buffer.from([
|
||||
0x01, // Auto-empty
|
||||
0x00 // Disabled
|
||||
])
|
||||
)
|
||||
});
|
||||
|
||||
await this.robot.sendCommand(packet.toHexString());
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MideaAutoEmptyDockAutoEmptyControlCapability;
|
||||
@ -0,0 +1,28 @@
|
||||
const AutoEmptyDockManualTriggerCapability = require("../../../core/capabilities/AutoEmptyDockManualTriggerCapability");
|
||||
const MSmartConst = require("../../../msmart/MSmartConst");
|
||||
const MSmartPacket = require("../../../msmart/MSmartPacket");
|
||||
|
||||
/**
|
||||
* @extends AutoEmptyDockManualTriggerCapability<import("../MideaValetudoRobot")>
|
||||
*/
|
||||
class MideaAutoEmptyDockManualTriggerCapability extends AutoEmptyDockManualTriggerCapability {
|
||||
/**
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async triggerAutoEmpty() {
|
||||
const packet = new MSmartPacket({
|
||||
messageType: MSmartPacket.MESSAGE_TYPE.SETTING,
|
||||
payload: MSmartPacket.buildPayload(
|
||||
MSmartConst.SETTING.TRIGGER_STATION_ACTION,
|
||||
Buffer.from([
|
||||
0x02, // Mode: Dust Collection
|
||||
0x01 // Start
|
||||
])
|
||||
)
|
||||
});
|
||||
|
||||
await this.robot.sendCommand(packet.toHexString());
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MideaAutoEmptyDockManualTriggerCapability;
|
||||
@ -0,0 +1,86 @@
|
||||
const BasicControlCapability = require("../../../core/capabilities/BasicControlCapability");
|
||||
const MSmartConst = require("../../../msmart/MSmartConst");
|
||||
const MSmartPacket = require("../../../msmart/MSmartPacket");
|
||||
|
||||
const entities = require("../../../entities");
|
||||
|
||||
const stateAttrs = entities.state.attributes;
|
||||
|
||||
/**
|
||||
* @extends BasicControlCapability<import("../MideaValetudoRobot")>
|
||||
*/
|
||||
class MideaBasicControlCapability extends BasicControlCapability {
|
||||
/**
|
||||
* @param {object} options
|
||||
* @param {import("../MideaValetudoRobot")} options.robot
|
||||
*/
|
||||
constructor(options) {
|
||||
super(options);
|
||||
}
|
||||
|
||||
async start() {
|
||||
const StatusStateAttribute = this.robot.state.getFirstMatchingAttributeByConstructor(stateAttrs.StatusStateAttribute);
|
||||
let command = 4;
|
||||
|
||||
if (
|
||||
StatusStateAttribute &&
|
||||
StatusStateAttribute.value === stateAttrs.StatusStateAttribute.VALUE.PAUSED
|
||||
) {
|
||||
command = 6;
|
||||
}
|
||||
|
||||
const packet = new MSmartPacket({
|
||||
messageType: MSmartPacket.MESSAGE_TYPE.SETTING,
|
||||
payload: MSmartPacket.buildPayload(
|
||||
MSmartConst.SETTING.SET_WORK_STATUS,
|
||||
Buffer.from([
|
||||
command //4 for a new one, 6 when paused to resume
|
||||
])
|
||||
)
|
||||
});
|
||||
|
||||
await this.robot.sendCommand(packet.toHexString());
|
||||
await this.robot.pollState(); // for good measure
|
||||
}
|
||||
|
||||
async stop() {
|
||||
const packet = new MSmartPacket({
|
||||
messageType: MSmartPacket.MESSAGE_TYPE.SETTING,
|
||||
payload: MSmartPacket.buildPayload(
|
||||
MSmartConst.SETTING.SET_WORK_STATUS,
|
||||
Buffer.from([7])
|
||||
)
|
||||
});
|
||||
|
||||
await this.robot.sendCommand(packet.toHexString());
|
||||
await this.robot.pollState(); // for good measure
|
||||
}
|
||||
|
||||
async pause() {
|
||||
const packet = new MSmartPacket({
|
||||
messageType: MSmartPacket.MESSAGE_TYPE.SETTING,
|
||||
payload: MSmartPacket.buildPayload(
|
||||
MSmartConst.SETTING.SET_WORK_STATUS,
|
||||
Buffer.from([5])
|
||||
)
|
||||
});
|
||||
|
||||
await this.robot.sendCommand(packet.toHexString());
|
||||
await this.robot.pollState(); // for good measure
|
||||
}
|
||||
|
||||
async home() {
|
||||
const packet = new MSmartPacket({
|
||||
messageType: MSmartPacket.MESSAGE_TYPE.SETTING,
|
||||
payload: MSmartPacket.buildPayload(
|
||||
MSmartConst.SETTING.SET_WORK_STATUS,
|
||||
Buffer.from([1]) // TODO: perhaps pull into const or from const? These are the same as the status
|
||||
)
|
||||
});
|
||||
|
||||
await this.robot.sendCommand(packet.toHexString());
|
||||
await this.robot.pollState(); // for good measure
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MideaBasicControlCapability;
|
||||
@ -0,0 +1,70 @@
|
||||
const BEightParser = require("../../../msmart/BEightParser");
|
||||
const CameraLightControlCapability = require("../../../core/capabilities/CameraLightControlCapability");
|
||||
const MSmartConst = require("../../../msmart/MSmartConst");
|
||||
const MSmartPacket = require("../../../msmart/MSmartPacket");
|
||||
const MSmartStatusDTO = require("../../../msmart/dtos/MSmartStatusDTO");
|
||||
|
||||
/**
|
||||
* @extends CameraLightControlCapability<import("../MideaValetudoRobot")>
|
||||
*/
|
||||
class MideaCameraLightControlCapability extends CameraLightControlCapability {
|
||||
|
||||
/**
|
||||
* @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.camera_led_switch;
|
||||
} 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_VARIOUS_TOGGLES,
|
||||
Buffer.from([
|
||||
0x0b, // LED
|
||||
0x01 // true
|
||||
])
|
||||
)
|
||||
});
|
||||
|
||||
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_VARIOUS_TOGGLES,
|
||||
Buffer.from([
|
||||
0x0b, // LED
|
||||
0x00 // false
|
||||
])
|
||||
)
|
||||
});
|
||||
|
||||
await this.robot.sendCommand(packet.toHexString());
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MideaCameraLightControlCapability;
|
||||
@ -0,0 +1,70 @@
|
||||
const BEightParser = require("../../../msmart/BEightParser");
|
||||
const CollisionAvoidantNavigationControlCapability = require("../../../core/capabilities/CollisionAvoidantNavigationControlCapability");
|
||||
const MSmartConst = require("../../../msmart/MSmartConst");
|
||||
const MSmartPacket = require("../../../msmart/MSmartPacket");
|
||||
const MSmartStatusDTO = require("../../../msmart/dtos/MSmartStatusDTO");
|
||||
|
||||
/**
|
||||
* @extends CollisionAvoidantNavigationControlCapability<import("../MideaValetudoRobot")>
|
||||
*/
|
||||
class MideaCollisionAvoidantNavigationControlCapability extends CollisionAvoidantNavigationControlCapability {
|
||||
|
||||
/**
|
||||
* @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.edge_deep_vacuum_switch;
|
||||
} 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_VARIOUS_TOGGLES,
|
||||
Buffer.from([
|
||||
0x31, // Edge Deep
|
||||
0x01 // true
|
||||
])
|
||||
)
|
||||
});
|
||||
|
||||
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_VARIOUS_TOGGLES,
|
||||
Buffer.from([
|
||||
0x31, // Edge Deep
|
||||
0x00 // false
|
||||
])
|
||||
)
|
||||
});
|
||||
|
||||
await this.robot.sendCommand(packet.toHexString());
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MideaCollisionAvoidantNavigationControlCapability;
|
||||
@ -0,0 +1,97 @@
|
||||
/**
|
||||
* @typedef {import("../../../entities/core/ValetudoVirtualRestrictions")} ValetudoVirtualRestrictions
|
||||
*/
|
||||
|
||||
const CombinedVirtualRestrictionsCapability = require("../../../core/capabilities/CombinedVirtualRestrictionsCapability");
|
||||
const MSmartConst = require("../../../msmart/MSmartConst");
|
||||
const MSmartPacket = require("../../../msmart/MSmartPacket");
|
||||
const ValetudoRestrictedZone = require("../../../entities/core/ValetudoRestrictedZone");
|
||||
const {sleep} = require("../../../utils/misc");
|
||||
|
||||
/**
|
||||
* @extends CombinedVirtualRestrictionsCapability<import("../MideaValetudoRobot")>
|
||||
*/
|
||||
class MideaCombinedVirtualRestrictionsCapability extends CombinedVirtualRestrictionsCapability {
|
||||
/**
|
||||
* @param {ValetudoVirtualRestrictions} virtualRestrictions
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async setVirtualRestrictions(virtualRestrictions) {
|
||||
if (virtualRestrictions.virtualWalls.length > 10) {
|
||||
throw new Error("Can't store more than 10 virtual walls.");
|
||||
}
|
||||
if (
|
||||
virtualRestrictions.restrictedZones.filter(e => e.type === ValetudoRestrictedZone.TYPE.REGULAR).length > 10
|
||||
) {
|
||||
throw new Error("Can't store more than 10 no-go zones.");
|
||||
}
|
||||
if (
|
||||
virtualRestrictions.restrictedZones.filter(e => e.type === ValetudoRestrictedZone.TYPE.MOP).length > 10
|
||||
) {
|
||||
throw new Error("Can't store more than 10 no-mop zones.");
|
||||
}
|
||||
|
||||
|
||||
const wallDataPayload = Buffer.alloc(1 + (virtualRestrictions.virtualWalls.length * 9));
|
||||
|
||||
wallDataPayload[0] = virtualRestrictions.virtualWalls.length;
|
||||
virtualRestrictions.virtualWalls.forEach((wall, i) => {
|
||||
const offset = 1 + (i * 9);
|
||||
const pA = this.robot.mapParser.convertToMideaCoordinates(wall.points.pA.x, wall.points.pA.y);
|
||||
const pB = this.robot.mapParser.convertToMideaCoordinates(wall.points.pB.x, wall.points.pB.y);
|
||||
|
||||
wallDataPayload[offset] = i + 1; // ID
|
||||
wallDataPayload.writeUInt16LE(pA.x, offset + 1);
|
||||
wallDataPayload.writeUInt16LE(pA.y, offset + 3);
|
||||
wallDataPayload.writeUInt16LE(pB.x, offset + 5);
|
||||
wallDataPayload.writeUInt16LE(pB.y, offset + 7);
|
||||
});
|
||||
|
||||
|
||||
const zoneDataPayload = Buffer.alloc(1 + (virtualRestrictions.restrictedZones.length * 10));
|
||||
|
||||
zoneDataPayload[0] = virtualRestrictions.restrictedZones.length;
|
||||
virtualRestrictions.restrictedZones.forEach((zone, i) => {
|
||||
const offset = 1 + (i * 10);
|
||||
const pA = this.robot.mapParser.convertToMideaCoordinates(zone.points.pA.x, zone.points.pA.y);
|
||||
const pC = this.robot.mapParser.convertToMideaCoordinates(zone.points.pC.x, zone.points.pC.y);
|
||||
|
||||
zoneDataPayload[offset] = zone.type === ValetudoRestrictedZone.TYPE.REGULAR ? 0 : 1;
|
||||
zoneDataPayload[offset + 1] = i + 1; // ID
|
||||
zoneDataPayload.writeUInt16LE(pA.x, offset + 2);
|
||||
zoneDataPayload.writeUInt16LE(pA.y, offset + 4);
|
||||
zoneDataPayload.writeUInt16LE(pC.x, offset + 6);
|
||||
zoneDataPayload.writeUInt16LE(pC.y, offset + 8);
|
||||
});
|
||||
|
||||
|
||||
const wallPacket = new MSmartPacket({
|
||||
messageType: MSmartPacket.MESSAGE_TYPE.SETTING,
|
||||
payload: MSmartPacket.buildPayload(MSmartConst.SETTING.SET_VIRTUAL_WALLS, wallDataPayload)
|
||||
});
|
||||
const zonePacket = new MSmartPacket({
|
||||
messageType: MSmartPacket.MESSAGE_TYPE.SETTING,
|
||||
payload: MSmartPacket.buildPayload(MSmartConst.SETTING.SET_RESTRICTED_ZONES, zoneDataPayload)
|
||||
});
|
||||
|
||||
await this.robot.sendCommand(wallPacket.toHexString());
|
||||
await this.robot.sendCommand(zonePacket.toHexString());
|
||||
|
||||
this.robot.pollMap();
|
||||
await sleep(4_000); // TODO: is this enough?
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {import("../../../core/capabilities/CombinedVirtualRestrictionsCapability").CombinedVirtualRestrictionsCapabilityProperties}
|
||||
*/
|
||||
getProperties() {
|
||||
return {
|
||||
supportedRestrictedZoneTypes: [
|
||||
ValetudoRestrictedZone.TYPE.REGULAR,
|
||||
ValetudoRestrictedZone.TYPE.MOP,
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MideaCombinedVirtualRestrictionsCapability;
|
||||
@ -0,0 +1,49 @@
|
||||
const CurrentStatisticsCapability = require("../../../core/capabilities/CurrentStatisticsCapability");
|
||||
const ValetudoDataPoint = require("../../../entities/core/ValetudoDataPoint");
|
||||
|
||||
/**
|
||||
* @extends CurrentStatisticsCapability<import("../MideaValetudoRobot")>
|
||||
*/
|
||||
class MideaCurrentStatisticsCapability extends CurrentStatisticsCapability {
|
||||
/**
|
||||
* @param {object} options
|
||||
* @param {import("../MideaValetudoRobot")} options.robot
|
||||
*/
|
||||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
this.currentStatistics = {
|
||||
time: undefined,
|
||||
area: undefined
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Promise<Array<ValetudoDataPoint>>}
|
||||
*/
|
||||
async getStatistics() {
|
||||
await this.robot.pollState(); //fetching robot state populates the capability's internal state. somewhat spaghetti :(
|
||||
|
||||
return [
|
||||
new ValetudoDataPoint({
|
||||
type: ValetudoDataPoint.TYPES.TIME,
|
||||
value: this.currentStatistics.time
|
||||
}),
|
||||
new ValetudoDataPoint({
|
||||
type: ValetudoDataPoint.TYPES.AREA,
|
||||
value: this.currentStatistics.area
|
||||
})
|
||||
];
|
||||
}
|
||||
|
||||
getProperties() {
|
||||
return {
|
||||
availableStatistics: [
|
||||
ValetudoDataPoint.TYPES.TIME,
|
||||
ValetudoDataPoint.TYPES.AREA
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MideaCurrentStatisticsCapability;
|
||||
@ -0,0 +1,58 @@
|
||||
const BEightParser = require("../../../msmart/BEightParser");
|
||||
const DoNotDisturbCapability = require("../../../core/capabilities/DoNotDisturbCapability");
|
||||
const MSmartConst = require("../../../msmart/MSmartConst");
|
||||
const MSmartDndConfigurationDTO = require("../../../msmart/dtos/MSmartDNDConfigurationDTO");
|
||||
const MSmartPacket = require("../../../msmart/MSmartPacket");
|
||||
const ValetudoDNDConfiguration = require("../../../entities/core/ValetudoDNDConfiguration");
|
||||
|
||||
/**
|
||||
* @extends DoNotDisturbCapability<import("../MideaValetudoRobot")>
|
||||
*/
|
||||
class MideaDoNotDisturbCapability extends DoNotDisturbCapability {
|
||||
/**
|
||||
* @returns {Promise<ValetudoDNDConfiguration>}
|
||||
*/
|
||||
async getDndConfiguration() {
|
||||
const packet = new MSmartPacket({
|
||||
messageType: MSmartPacket.MESSAGE_TYPE.ACTION,
|
||||
payload: MSmartPacket.buildPayload(MSmartConst.ACTION.GET_DND)
|
||||
});
|
||||
|
||||
const responsePacket = await this.robot.sendCommand(packet.toHexString());
|
||||
const parsedResponse = BEightParser.PARSE(responsePacket);
|
||||
|
||||
if (!(parsedResponse instanceof MSmartDndConfigurationDTO)) {
|
||||
throw new Error("Failed to parse DND configuration response.");
|
||||
}
|
||||
|
||||
return new ValetudoDNDConfiguration({
|
||||
enabled: parsedResponse.enabled,
|
||||
start: parsedResponse.start,
|
||||
end: parsedResponse.end
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ValetudoDNDConfiguration} dndConfig
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async setDndConfiguration(dndConfig) {
|
||||
const payload = Buffer.from([
|
||||
dndConfig.enabled ? 1 : 0,
|
||||
dndConfig.start.hour,
|
||||
dndConfig.start.minute,
|
||||
dndConfig.end.hour,
|
||||
dndConfig.end.minute
|
||||
]);
|
||||
|
||||
const packet = new MSmartPacket({
|
||||
messageType: MSmartPacket.MESSAGE_TYPE.SETTING,
|
||||
payload: MSmartPacket.buildPayload(MSmartConst.SETTING.SET_DND, payload)
|
||||
});
|
||||
|
||||
await this.robot.sendCommand(packet.toHexString());
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MideaDoNotDisturbCapability;
|
||||
@ -0,0 +1,47 @@
|
||||
const FanSpeedControlCapability = require("../../../core/capabilities/FanSpeedControlCapability");
|
||||
|
||||
const MSmartConst = require("../../../msmart/MSmartConst");
|
||||
const MSmartPacket = require("../../../msmart/MSmartPacket");
|
||||
const MSmartStatusDTO = require("../../../msmart/dtos/MSmartStatusDTO");
|
||||
|
||||
/**
|
||||
* @extends FanSpeedControlCapability<import("../MideaValetudoRobot")>
|
||||
*/
|
||||
class MideaFanSpeedControlCapability extends FanSpeedControlCapability {
|
||||
/**
|
||||
* @param {string} preset
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async selectPreset(preset) {
|
||||
const matchedPreset = this.presets.find(p => {
|
||||
return p.name === preset;
|
||||
});
|
||||
|
||||
if (matchedPreset) {
|
||||
const packet = new MSmartPacket({
|
||||
messageType: MSmartPacket.MESSAGE_TYPE.SETTING,
|
||||
payload: MSmartPacket.buildPayload(
|
||||
MSmartConst.SETTING.SET_FAN_SPEED,
|
||||
Buffer.from([matchedPreset.value])
|
||||
)
|
||||
});
|
||||
|
||||
const response = await this.robot.sendCommand(packet.toHexString());
|
||||
|
||||
if (response?.payload?.[3] === 0x00) {
|
||||
this.robot.parseAndUpdateState(
|
||||
new MSmartStatusDTO({
|
||||
fan_level: matchedPreset.value
|
||||
})
|
||||
);
|
||||
} else {
|
||||
throw new Error("Fan speed change failed");
|
||||
}
|
||||
} else {
|
||||
throw new Error("Invalid Preset");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = MideaFanSpeedControlCapability;
|
||||
@ -0,0 +1,71 @@
|
||||
const BEightParser = require("../../../msmart/BEightParser");
|
||||
const KeyLockCapability = require("../../../core/capabilities/KeyLockCapability");
|
||||
const MSmartConst = require("../../../msmart/MSmartConst");
|
||||
const MSmartPacket = require("../../../msmart/MSmartPacket");
|
||||
const MSmartStatusDTO = require("../../../msmart/dtos/MSmartStatusDTO");
|
||||
|
||||
/**
|
||||
* @extends KeyLockCapability<import("../MideaValetudoRobot")>
|
||||
*/
|
||||
class MideaKeyLockCapability extends KeyLockCapability {
|
||||
|
||||
/**
|
||||
*
|
||||
* @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.child_lock_enabled;
|
||||
} 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_VARIOUS_TOGGLES,
|
||||
Buffer.from([
|
||||
0x01, // Key Lock
|
||||
0x01 // true
|
||||
])
|
||||
)
|
||||
});
|
||||
|
||||
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_VARIOUS_TOGGLES,
|
||||
Buffer.from([
|
||||
0x01, // Key Lock
|
||||
0x00 // false
|
||||
])
|
||||
)
|
||||
});
|
||||
|
||||
await this.robot.sendCommand(packet.toHexString());
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MideaKeyLockCapability;
|
||||
@ -0,0 +1,22 @@
|
||||
const LocateCapability = require("../../../core/capabilities/LocateCapability");
|
||||
const MSmartConst = require("../../../msmart/MSmartConst");
|
||||
const MSmartPacket = require("../../../msmart/MSmartPacket");
|
||||
|
||||
/**
|
||||
* @extends LocateCapability<import("../MideaValetudoRobot")>
|
||||
*/
|
||||
class MideaLocateCapability extends LocateCapability {
|
||||
/**
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async locate() {
|
||||
const packet = new MSmartPacket({
|
||||
messageType: MSmartPacket.MESSAGE_TYPE.ACTION,
|
||||
payload: MSmartPacket.buildPayload(MSmartConst.ACTION.LOCATE)
|
||||
});
|
||||
|
||||
await this.robot.sendCommand(packet.toHexString());
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MideaLocateCapability;
|
||||
@ -0,0 +1,166 @@
|
||||
const AttributeSubscriber = require("../../../entities/AttributeSubscriber");
|
||||
const CallbackAttributeSubscriber = require("../../../entities/CallbackAttributeSubscriber");
|
||||
const ManualControlCapability = require("../../../core/capabilities/ManualControlCapability");
|
||||
const MSmartConst = require("../../../msmart/MSmartConst");
|
||||
const MSmartPacket = require("../../../msmart/MSmartPacket");
|
||||
const StatusStateAttribute = require("../../../entities/state/attributes/StatusStateAttribute");
|
||||
const {sleep} = require("../../../utils/misc");
|
||||
|
||||
// FIXME: This doesn't feel good enough to be used
|
||||
|
||||
/**
|
||||
* @extends ManualControlCapability<import("../MideaValetudoRobot")>
|
||||
*/
|
||||
class MideaManualControlCapability extends ManualControlCapability {
|
||||
/**
|
||||
* @param {object} options
|
||||
* @param {import("../MideaValetudoRobot")} options.robot
|
||||
*/
|
||||
constructor(options) {
|
||||
super(Object.assign({}, options, {
|
||||
supportedMovementCommands: [
|
||||
ManualControlCapability.MOVEMENT_COMMAND_TYPE.FORWARD,
|
||||
ManualControlCapability.MOVEMENT_COMMAND_TYPE.ROTATE_CLOCKWISE,
|
||||
ManualControlCapability.MOVEMENT_COMMAND_TYPE.ROTATE_COUNTERCLOCKWISE,
|
||||
]
|
||||
}));
|
||||
|
||||
this.active = false;
|
||||
this.keepAliveTimeout = undefined;
|
||||
this.lastCommand = new Date(0).getTime();
|
||||
|
||||
this.robot.state.subscribe(
|
||||
new CallbackAttributeSubscriber((eventType, status, prevStatus) => {
|
||||
if (
|
||||
eventType === AttributeSubscriber.EVENT_TYPE.CHANGE &&
|
||||
//@ts-ignore
|
||||
status.value !== StatusStateAttribute.VALUE.MANUAL_CONTROL &&
|
||||
prevStatus &&
|
||||
//@ts-ignore
|
||||
prevStatus.value === StatusStateAttribute.VALUE.MANUAL_CONTROL
|
||||
) {
|
||||
this.disableManualControl().catch(() => {});
|
||||
}
|
||||
}),
|
||||
{attributeClass: StatusStateAttribute.name}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {number} direction The direction parameter byte (0x0D)
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async sendMovementCommand(direction) {
|
||||
const packet = new MSmartPacket({
|
||||
messageType: MSmartPacket.MESSAGE_TYPE.SETTING,
|
||||
payload: MSmartPacket.buildPayload(
|
||||
MSmartConst.SETTING.DO_MANUAL_CONTROL_CMD,
|
||||
Buffer.from([
|
||||
direction,
|
||||
MIDEA_MANUAL_CONTROL_MODE.CRUISE
|
||||
])
|
||||
)
|
||||
});
|
||||
|
||||
await this.robot.sendCommand(packet.toHexString());
|
||||
this.lastCommand = new Date().getTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async scheduleKeepAlive() {
|
||||
clearTimeout(this.keepAliveTimeout);
|
||||
|
||||
if (this.active) {
|
||||
if (new Date().getTime() - this.lastCommand >= KEEP_ALIVE_INTERVAL) {
|
||||
await this.sendMovementCommand(MIDEA_MANUAL_CONTROL_DIRECTION.STOP);
|
||||
}
|
||||
|
||||
this.keepAliveTimeout = setTimeout(() => {
|
||||
this.scheduleKeepAlive().catch(() => {
|
||||
/* intentional */
|
||||
});
|
||||
}, KEEP_ALIVE_INTERVAL);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async enableManualControl() {
|
||||
if (!this.active) {
|
||||
this.active = true;
|
||||
|
||||
await this.sendMovementCommand(MIDEA_MANUAL_CONTROL_DIRECTION.STOP);
|
||||
await this.robot.pollState();
|
||||
|
||||
await this.scheduleKeepAlive();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async disableManualControl() {
|
||||
if (this.active) {
|
||||
clearTimeout(this.keepAliveTimeout);
|
||||
this.keepAliveTimeout = undefined;
|
||||
this.active = false;
|
||||
|
||||
await this.robot.pollState();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async manualControlActive() {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("../../../core/capabilities/ManualControlCapability").ValetudoManualControlMovementCommandType} movementCommand
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async manualControl(movementCommand) {
|
||||
let direction;
|
||||
|
||||
switch (movementCommand) {
|
||||
case ManualControlCapability.MOVEMENT_COMMAND_TYPE.FORWARD:
|
||||
direction = MIDEA_MANUAL_CONTROL_DIRECTION.FORWARD;
|
||||
break;
|
||||
case ManualControlCapability.MOVEMENT_COMMAND_TYPE.ROTATE_CLOCKWISE:
|
||||
direction = MIDEA_MANUAL_CONTROL_DIRECTION.RIGHT;
|
||||
break;
|
||||
case ManualControlCapability.MOVEMENT_COMMAND_TYPE.ROTATE_COUNTERCLOCKWISE:
|
||||
direction = MIDEA_MANUAL_CONTROL_DIRECTION.LEFT;
|
||||
break;
|
||||
default:
|
||||
throw new Error("Invalid movementCommand");
|
||||
}
|
||||
|
||||
await this.sendMovementCommand(direction);
|
||||
await sleep(500);
|
||||
await this.sendMovementCommand(MIDEA_MANUAL_CONTROL_DIRECTION.STOP); // FIXME: this feels terrible but so does not having it
|
||||
}
|
||||
}
|
||||
|
||||
const MIDEA_MANUAL_CONTROL_DIRECTION = Object.freeze({
|
||||
STOP: 0x00,
|
||||
FORWARD: 0x01,
|
||||
LEFT: 0x03,
|
||||
RIGHT: 0x04
|
||||
});
|
||||
|
||||
const MIDEA_MANUAL_CONTROL_MODE = Object.freeze({
|
||||
CLEAN: 0x00,
|
||||
CRUISE: 0x01
|
||||
});
|
||||
|
||||
const KEEP_ALIVE_INTERVAL = 3000;
|
||||
|
||||
module.exports = MideaManualControlCapability;
|
||||
@ -0,0 +1,34 @@
|
||||
const MapResetCapability = require("../../../core/capabilities/MapResetCapability");
|
||||
const MSmartConst = require("../../../msmart/MSmartConst");
|
||||
const MSmartPacket = require("../../../msmart/MSmartPacket");
|
||||
const {sleep} = require("../../../utils/misc");
|
||||
|
||||
// After sending the bitset, the firmware will notice that it has maps the cloud doesn't know about, and,
|
||||
// if more than 5 minutes have passed since the last map upload, it will delete all of those.
|
||||
|
||||
// As that condition is not useful when using valetudo, we need to patch out that 5 minute check
|
||||
// It is found in CCentralController::SetMapHouseIDSet in manager_node
|
||||
|
||||
/**
|
||||
* @extends MapResetCapability<import("../MideaValetudoRobot")>
|
||||
*/
|
||||
class MideaMapResetCapability extends MapResetCapability {
|
||||
/**
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async reset() {
|
||||
const setMapIndexPacket = new MSmartPacket({
|
||||
messageType: MSmartPacket.MESSAGE_TYPE.SETTING,
|
||||
payload: MSmartPacket.buildPayload(
|
||||
MSmartConst.SETTING.SET_VALID_MAP_IDS,
|
||||
Buffer.from([0b00000000000000000000000000000000]) // bitset of all map IDs the cloud is aware of
|
||||
)
|
||||
});
|
||||
await this.robot.sendCommand(setMapIndexPacket.toHexString());
|
||||
await sleep(4_000); // for good measure
|
||||
|
||||
this.robot.clearValetudoMap();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MideaMapResetCapability;
|
||||
@ -0,0 +1,78 @@
|
||||
const MapSegmentEditCapability = require("../../../core/capabilities/MapSegmentEditCapability");
|
||||
const MSmartConst = require("../../../msmart/MSmartConst");
|
||||
const MSmartPacket = require("../../../msmart/MSmartPacket");
|
||||
const {sleep} = require("../../../utils/misc");
|
||||
|
||||
/**
|
||||
* @extends MapSegmentEditCapability<import("../MideaValetudoRobot")>
|
||||
*/
|
||||
class MideaMapSegmentEditCapability extends MapSegmentEditCapability {
|
||||
/**
|
||||
* @param {import("../../../entities/core/ValetudoMapSegment")} segmentA
|
||||
* @param {import("../../../entities/core/ValetudoMapSegment")} segmentB
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async joinSegments(segmentA, segmentB) {
|
||||
const packet = new MSmartPacket({
|
||||
messageType: MSmartPacket.MESSAGE_TYPE.SETTING,
|
||||
payload: MSmartPacket.buildPayload(
|
||||
MSmartConst.SETTING.JOIN_SEGMENTS,
|
||||
Buffer.from([
|
||||
2, // count
|
||||
parseInt(segmentA.id),
|
||||
parseInt(segmentB.id)
|
||||
])
|
||||
)
|
||||
});
|
||||
|
||||
const response = await this.robot.sendCommand(packet.toHexString());
|
||||
if (response.payload[3] !== 0x00) {
|
||||
throw new Error("Segment split failed.");
|
||||
} // TODO: should I do this with every command?
|
||||
|
||||
|
||||
this.robot.pollMap();
|
||||
await sleep(2_000);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("../../../entities/core/ValetudoMapSegment")} segment
|
||||
* @param {object} pA
|
||||
* @param {number} pA.x
|
||||
* @param {number} pA.y
|
||||
* @param {object} pB
|
||||
* @param {number} pB.x
|
||||
* @param {number} pB.y
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async splitSegment(segment, pA, pB) {
|
||||
const pAm = this.robot.mapParser.convertToMideaCoordinates(pA.x, pA.y);
|
||||
const pBm = this.robot.mapParser.convertToMideaCoordinates(pB.x, pB.y);
|
||||
|
||||
const payload = Buffer.alloc(9);
|
||||
payload.writeUInt16LE(pAm.x, 0);
|
||||
payload.writeUInt16LE(pAm.y, 2);
|
||||
payload.writeUInt16LE(pBm.x, 4);
|
||||
payload.writeUInt16LE(pBm.y, 6);
|
||||
|
||||
payload[8] = parseInt(segment.id);
|
||||
|
||||
const packet = new MSmartPacket({
|
||||
messageType: MSmartPacket.MESSAGE_TYPE.SETTING,
|
||||
payload: MSmartPacket.buildPayload(
|
||||
MSmartConst.SETTING.SPLIT_SEGMENT,
|
||||
payload
|
||||
)
|
||||
});
|
||||
|
||||
const response = await this.robot.sendCommand(packet.toHexString());
|
||||
if (response.payload[3] !== 0x00) {
|
||||
throw new Error("Segment split failed.");
|
||||
}
|
||||
|
||||
this.robot.pollMap();
|
||||
await sleep(2_000);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MideaMapSegmentEditCapability;
|
||||
@ -0,0 +1,87 @@
|
||||
const entities = require("../../../entities");
|
||||
const MapSegmentationCapability = require("../../../core/capabilities/MapSegmentationCapability");
|
||||
const MSmartConst = require("../../../msmart/MSmartConst");
|
||||
const MSmartPacket = require("../../../msmart/MSmartPacket");
|
||||
|
||||
/**
|
||||
* @extends MapSegmentationCapability<import("../MideaValetudoRobot")>
|
||||
*/
|
||||
class MideaMapSegmentationCapability extends MapSegmentationCapability {
|
||||
/**
|
||||
* @param {Array<import("../../../entities/core/ValetudoMapSegment")>} segments
|
||||
* @param {object} [options]
|
||||
* @param {number} [options.iterations]
|
||||
* @param {boolean} [options.customOrder]
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async executeSegmentAction(segments, options) {
|
||||
if (segments.length > 10) {
|
||||
throw new Error("This robot can only clean a maximum of 10 segments at a time.");
|
||||
}
|
||||
|
||||
const FanSpeedStateAttribute = this.robot.state.getFirstMatchingAttribute({
|
||||
attributeClass: entities.state.attributes.PresetSelectionStateAttribute.name,
|
||||
attributeType: entities.state.attributes.PresetSelectionStateAttribute.TYPE.FAN_SPEED
|
||||
});
|
||||
const WaterGradeAttribute = this.robot.state.getFirstMatchingAttribute({
|
||||
attributeClass: entities.state.attributes.PresetSelectionStateAttribute.name,
|
||||
attributeType: entities.state.attributes.PresetSelectionStateAttribute.TYPE.WATER_GRADE
|
||||
});
|
||||
const OperationModeStateAttribute = this.robot.state.getFirstMatchingAttribute({
|
||||
attributeClass: entities.state.attributes.PresetSelectionStateAttribute.name,
|
||||
attributeType: entities.state.attributes.PresetSelectionStateAttribute.TYPE.OPERATION_MODE
|
||||
});
|
||||
|
||||
const fanSpeed = FanSpeedStateAttribute?.metaData?.rawValue ?? 1;
|
||||
const waterGrade = WaterGradeAttribute?.metaData?.rawValue ?? 1;
|
||||
const operationMode = OperationModeStateAttribute?.metaData?.rawValue ?? 0;
|
||||
|
||||
/*
|
||||
The J15 fw 413 expects a fixed-size payload for 10 room slots because it just ignores the length provided.
|
||||
A shorter payload causes a buffer over-read, leading to garbage rooms being added to the plan.
|
||||
|
||||
The same bug does not exist in the zone payload handling, which does adhere to the length passed.
|
||||
*/
|
||||
const segmentDataPayload = Buffer.alloc(1 + 10 * 10); // 1-byte count + 10 rooms * 10 bytes/room
|
||||
|
||||
// On the J15 fw 413, this is being ignored by the robot :(
|
||||
segmentDataPayload[0] = segments.length;
|
||||
|
||||
segments.slice(0, 10).forEach((segment, i) => {
|
||||
const offset = 1 + i * 10;
|
||||
|
||||
segmentDataPayload[offset] = parseInt(segment.id);
|
||||
segmentDataPayload[offset + 1] = typeof options?.iterations === "number" ? options.iterations : 1;
|
||||
segmentDataPayload[offset + 2] = operationMode;
|
||||
// offset + 3 unknown
|
||||
segmentDataPayload[offset + 4] = fanSpeed;
|
||||
segmentDataPayload[offset + 5] = waterGrade;
|
||||
// remaining 4 bytes unknown
|
||||
});
|
||||
|
||||
const packet = new MSmartPacket({
|
||||
messageType: MSmartPacket.MESSAGE_TYPE.SETTING,
|
||||
payload: MSmartPacket.buildPayload(
|
||||
MSmartConst.SETTING.START_SEGMENT_CLEANUP,
|
||||
segmentDataPayload
|
||||
)
|
||||
});
|
||||
|
||||
await this.robot.sendCommand(packet.toHexString());
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {import("../../../core/capabilities/MapSegmentationCapability").MapSegmentationCapabilityProperties}
|
||||
*/
|
||||
getProperties() {
|
||||
return {
|
||||
iterationCount: {
|
||||
min: 1,
|
||||
max: 3
|
||||
},
|
||||
customOrderSupport: false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MideaMapSegmentationCapability;
|
||||
@ -0,0 +1,43 @@
|
||||
const BEightParser = require("../../../msmart/BEightParser");
|
||||
const MappingPassCapability = require("../../../core/capabilities/MappingPassCapability");
|
||||
const MSmartConst = require("../../../msmart/MSmartConst");
|
||||
const MSmartMapListDTO = require("../../../msmart/dtos/MSmartMapListDTO");
|
||||
const MSmartPacket = require("../../../msmart/MSmartPacket");
|
||||
|
||||
/**
|
||||
* @extends MappingPassCapability<import("../MideaValetudoRobot")>
|
||||
*/
|
||||
class MideaMappingPassCapability extends MappingPassCapability {
|
||||
/**
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async startMapping() {
|
||||
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 check for existing map.");
|
||||
}
|
||||
|
||||
if (parsedListMapsResponse.currentMapId !== 0) {
|
||||
throw new Error("A map already exists.");
|
||||
}
|
||||
|
||||
const startMappingPacket = new MSmartPacket({
|
||||
messageType: MSmartPacket.MESSAGE_TYPE.SETTING,
|
||||
payload: MSmartPacket.buildPayload(
|
||||
MSmartConst.SETTING.SET_WORK_STATUS,
|
||||
Buffer.from([10])
|
||||
)
|
||||
});
|
||||
|
||||
await this.robot.sendCommand(startMappingPacket.toHexString());
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MideaMappingPassCapability;
|
||||
@ -0,0 +1,46 @@
|
||||
const MopDockCleanManualTriggerCapability = require("../../../core/capabilities/MopDockCleanManualTriggerCapability");
|
||||
const MSmartConst = require("../../../msmart/MSmartConst");
|
||||
const MSmartPacket = require("../../../msmart/MSmartPacket");
|
||||
|
||||
/**
|
||||
* @extends MopDockCleanManualTriggerCapability<import("../MideaValetudoRobot")>
|
||||
*/
|
||||
class MideaMopDockCleanManualTriggerCapability extends MopDockCleanManualTriggerCapability {
|
||||
/**
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async startCleaning() {
|
||||
const packet = new MSmartPacket({
|
||||
messageType: MSmartPacket.MESSAGE_TYPE.SETTING,
|
||||
payload: MSmartPacket.buildPayload(
|
||||
MSmartConst.SETTING.TRIGGER_STATION_ACTION,
|
||||
Buffer.from([
|
||||
0x01, // Mode: Mop Clean (basic) - TODO: there is also 0x03 for station cleaning
|
||||
0x01 // Control: Start
|
||||
])
|
||||
)
|
||||
});
|
||||
|
||||
await this.robot.sendCommand(packet.toHexString());
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async stopCleaning() {
|
||||
const packet = new MSmartPacket({
|
||||
messageType: MSmartPacket.MESSAGE_TYPE.SETTING,
|
||||
payload: MSmartPacket.buildPayload(
|
||||
MSmartConst.SETTING.TRIGGER_STATION_ACTION,
|
||||
Buffer.from([
|
||||
0x00, // Mode: Ignored when stopping
|
||||
0x00 // Control: Stop
|
||||
])
|
||||
)
|
||||
});
|
||||
|
||||
await this.robot.sendCommand(packet.toHexString());
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MideaMopDockCleanManualTriggerCapability;
|
||||
@ -0,0 +1,48 @@
|
||||
const MopDockDryManualTriggerCapability = require("../../../core/capabilities/MopDockDryManualTriggerCapability");
|
||||
const MSmartConst = require("../../../msmart/MSmartConst");
|
||||
const MSmartPacket = require("../../../msmart/MSmartPacket");
|
||||
|
||||
// TODO: this doesn't seem to work?
|
||||
|
||||
/**
|
||||
* @extends MopDockDryManualTriggerCapability<import("../MideaValetudoRobot")>
|
||||
*/
|
||||
class MideaMopDockDryManualTriggerCapability extends MopDockDryManualTriggerCapability {
|
||||
/**
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async startDrying() {
|
||||
const packet = new MSmartPacket({
|
||||
messageType: MSmartPacket.MESSAGE_TYPE.SETTING,
|
||||
payload: MSmartPacket.buildPayload(
|
||||
MSmartConst.SETTING.TRIGGER_STATION_ACTION,
|
||||
Buffer.from([
|
||||
0x04, // Drying Mode
|
||||
0x01 // Start
|
||||
])
|
||||
)
|
||||
});
|
||||
|
||||
await this.robot.sendCommand(packet.toHexString());
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async stopDrying() {
|
||||
const packet = new MSmartPacket({
|
||||
messageType: MSmartPacket.MESSAGE_TYPE.SETTING,
|
||||
payload: MSmartPacket.buildPayload(
|
||||
MSmartConst.SETTING.TRIGGER_STATION_ACTION,
|
||||
Buffer.from([
|
||||
0x00, // Doesn't matter
|
||||
0x00 // Stop
|
||||
])
|
||||
)
|
||||
});
|
||||
|
||||
await this.robot.sendCommand(packet.toHexString());
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MideaMopDockDryManualTriggerCapability;
|
||||
@ -0,0 +1,70 @@
|
||||
const BEightParser = require("../../../msmart/BEightParser");
|
||||
const MopExtensionControlCapability = require("../../../core/capabilities/MopExtensionControlCapability");
|
||||
const MSmartConst = require("../../../msmart/MSmartConst");
|
||||
const MSmartPacket = require("../../../msmart/MSmartPacket");
|
||||
const MSmartStatusDTO = require("../../../msmart/dtos/MSmartStatusDTO");
|
||||
|
||||
/**
|
||||
* @extends MopExtensionControlCapability<import("../MideaValetudoRobot")>
|
||||
*/
|
||||
class MideaMopExtensionControlCapability extends MopExtensionControlCapability {
|
||||
|
||||
/**
|
||||
* @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.mop_extend_switch;
|
||||
} 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_VARIOUS_TOGGLES,
|
||||
Buffer.from([
|
||||
0x26, // Mop extension
|
||||
0x01 // true
|
||||
])
|
||||
)
|
||||
});
|
||||
|
||||
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_VARIOUS_TOGGLES,
|
||||
Buffer.from([
|
||||
0x26, // Mop extension
|
||||
0x00 // false
|
||||
])
|
||||
)
|
||||
});
|
||||
|
||||
await this.robot.sendCommand(packet.toHexString());
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MideaMopExtensionControlCapability;
|
||||
@ -0,0 +1,47 @@
|
||||
const OperationModeControlCapability = require("../../../core/capabilities/OperationModeControlCapability");
|
||||
|
||||
const MSmartConst = require("../../../msmart/MSmartConst");
|
||||
const MSmartPacket = require("../../../msmart/MSmartPacket");
|
||||
const MSmartStatusDTO = require("../../../msmart/dtos/MSmartStatusDTO");
|
||||
|
||||
|
||||
/**
|
||||
* @extends OperationModeControlCapability<import("../MideaValetudoRobot")>
|
||||
*/
|
||||
class MideaOperationModeControlCapability extends OperationModeControlCapability {
|
||||
/**
|
||||
* @param {string} preset
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async selectPreset(preset) {
|
||||
const matchedPreset = this.presets.find(p => {
|
||||
return p.name === preset;
|
||||
});
|
||||
|
||||
if (matchedPreset) {
|
||||
const packet = new MSmartPacket({
|
||||
messageType: MSmartPacket.MESSAGE_TYPE.SETTING,
|
||||
payload: MSmartPacket.buildPayload(
|
||||
MSmartConst.SETTING.SET_OPERATION_MODE,
|
||||
Buffer.from([matchedPreset.value])
|
||||
)
|
||||
});
|
||||
|
||||
const response = await this.robot.sendCommand(packet.toHexString());
|
||||
|
||||
if (response?.payload?.[3] === 0x00) {
|
||||
this.robot.parseAndUpdateState(
|
||||
new MSmartStatusDTO({
|
||||
station_work_status: matchedPreset.value
|
||||
})
|
||||
);
|
||||
} else {
|
||||
throw new Error("Operation mode change failed");
|
||||
}
|
||||
} else {
|
||||
throw new Error("Invalid Preset");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MideaOperationModeControlCapability;
|
||||
@ -0,0 +1,29 @@
|
||||
const Logger = require("../../../Logger");
|
||||
const SpeakerTestCapability = require("../../../core/capabilities/SpeakerTestCapability");
|
||||
const {exec} = require("child_process");
|
||||
const {promisify} = require("util");
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
/**
|
||||
* @extends SpeakerTestCapability<import("../MideaValetudoRobot")>
|
||||
*/
|
||||
class MideaSpeakerTestCapability extends SpeakerTestCapability {
|
||||
/**
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async playTestSound() {
|
||||
if (this.robot.config.get("embedded") === true) {
|
||||
try {
|
||||
await execAsync("mp3aplay /oem/sound/2.mp3");
|
||||
} catch (err) {
|
||||
Logger.warn("Error during speaker test", err);
|
||||
}
|
||||
} else {
|
||||
throw new Error("Only possible when embedded");
|
||||
}
|
||||
}
|
||||
}
|
||||
//TODO: I think I saw a command for this somewhere in some _node
|
||||
|
||||
module.exports = MideaSpeakerTestCapability;
|
||||
@ -0,0 +1,57 @@
|
||||
const BEightParser = require("../../../msmart/BEightParser");
|
||||
const MSmartConst = require("../../../msmart/MSmartConst");
|
||||
const MSmartPacket = require("../../../msmart/MSmartPacket");
|
||||
const MSmartStatusDTO = require("../../../msmart/dtos/MSmartStatusDTO");
|
||||
const SpeakerVolumeControlCapability = require("../../../core/capabilities/SpeakerVolumeControlCapability");
|
||||
|
||||
/**
|
||||
* @extends SpeakerVolumeControlCapability<import("../MideaValetudoRobot")>
|
||||
*/
|
||||
class MideaSpeakerVolumeControlCapability extends SpeakerVolumeControlCapability {
|
||||
|
||||
/**
|
||||
* Returns the current voice volume as percentage
|
||||
*
|
||||
* @returns {Promise<number>}
|
||||
*/
|
||||
async getVolume() {
|
||||
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.voice_level ?? 0;
|
||||
} else {
|
||||
throw new Error("Invalid response from robot");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the speaker volume
|
||||
*
|
||||
* @param {number} value - Volume level (0-100)
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async setVolume(value) {
|
||||
// TODO: validate 0-100?
|
||||
|
||||
const packet = new MSmartPacket({
|
||||
messageType: MSmartPacket.MESSAGE_TYPE.SETTING,
|
||||
payload: MSmartPacket.buildPayload(
|
||||
MSmartConst.SETTING.SET_VOLUME,
|
||||
Buffer.from([value])
|
||||
)
|
||||
});
|
||||
|
||||
|
||||
this.robot.sendCommand(packet.toHexString()).catch(e => {
|
||||
// FIXME: the robot never acknowledges this. Are we doing something wrong, or is it just like that?
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MideaSpeakerVolumeControlCapability;
|
||||
@ -0,0 +1,46 @@
|
||||
const MSmartConst = require("../../../msmart/MSmartConst");
|
||||
const MSmartPacket = require("../../../msmart/MSmartPacket");
|
||||
const MSmartStatusDTO = require("../../../msmart/dtos/MSmartStatusDTO");
|
||||
const WaterUsageControlCapability = require("../../../core/capabilities/WaterUsageControlCapability");
|
||||
|
||||
|
||||
/**
|
||||
* @extends WaterUsageControlCapability<import("../MideaValetudoRobot")>
|
||||
*/
|
||||
class MideaWaterUsageControlCapability extends WaterUsageControlCapability {
|
||||
/**
|
||||
* @param {string} preset
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async selectPreset(preset) {
|
||||
const matchedPreset = this.presets.find(p => {
|
||||
return p.name === preset;
|
||||
});
|
||||
|
||||
if (matchedPreset) {
|
||||
const packet = new MSmartPacket({
|
||||
messageType: MSmartPacket.MESSAGE_TYPE.SETTING,
|
||||
payload: MSmartPacket.buildPayload(
|
||||
MSmartConst.SETTING.SET_WATER_GRADE,
|
||||
Buffer.from([matchedPreset.value])
|
||||
)
|
||||
});
|
||||
|
||||
const response = await this.robot.sendCommand(packet.toHexString());
|
||||
|
||||
if (response?.payload?.[3] === 0x00) {
|
||||
this.robot.parseAndUpdateState(
|
||||
new MSmartStatusDTO({
|
||||
water_level: matchedPreset.value
|
||||
})
|
||||
);
|
||||
} else {
|
||||
throw new Error("Water grade change failed");
|
||||
}
|
||||
} else {
|
||||
throw new Error("Invalid Preset");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MideaWaterUsageControlCapability;
|
||||
@ -0,0 +1,188 @@
|
||||
const LinuxWifiConfigurationCapability = require("../../common/linuxCapabilities/LinuxWifiConfigurationCapability");
|
||||
const Logger = require("../../../Logger");
|
||||
const MSmartProvisioningPacket = require("../../../msmart/MSmartProvisioningPacket");
|
||||
const ValetudoWifiConfiguration = require("../../../entities/core/ValetudoWifiConfiguration");
|
||||
|
||||
const crypto = require("crypto");
|
||||
const net = require("net");
|
||||
|
||||
const ProvisioningState = Object.freeze({
|
||||
CONNECTING: "connecting",
|
||||
GETTING_UUID: "getting_uuid",
|
||||
PROVISIONING: "provisioning",
|
||||
DONE: "done"
|
||||
});
|
||||
|
||||
/**
|
||||
* The robot firmware is super fragile and does not at all handle us behaving any different from the real app
|
||||
* Hence, this got quite convoluted
|
||||
*
|
||||
* @extends LinuxWifiConfigurationCapability<import("../MideaValetudoRobot")>
|
||||
*/
|
||||
class MideaWifiConfigurationCapability extends LinuxWifiConfigurationCapability {
|
||||
/**
|
||||
* @param {import("../../../entities/core/ValetudoWifiConfiguration")} wifiConfig
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async setWifiConfiguration(wifiConfig) {
|
||||
if (
|
||||
wifiConfig?.ssid !== undefined &&
|
||||
wifiConfig.credentials?.type === ValetudoWifiConfiguration.CREDENTIALS_TYPE.WPA2_PSK &&
|
||||
wifiConfig.credentials.typeSpecificSettings?.password !== undefined
|
||||
) {
|
||||
await this.performFullProvisioningSequence(wifiConfig);
|
||||
} else {
|
||||
throw new Error("Invalid wifiConfig");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Not following this to the letter (including polling the UUID and waiting for the robot to close the connection)
|
||||
* bricks the firmware. It is _super_ brittle
|
||||
*
|
||||
* @private
|
||||
* @param {ValetudoWifiConfiguration} wifiConfig
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
performFullProvisioningSequence(wifiConfig) {
|
||||
const host = "127.0.0.1";
|
||||
const port = 9999;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const client = new net.Socket();
|
||||
let timeout;
|
||||
let state = ProvisioningState.CONNECTING;
|
||||
let dataBuffer = Buffer.alloc(0);
|
||||
|
||||
const cleanup = () => {
|
||||
clearTimeout(timeout);
|
||||
client.removeAllListeners();
|
||||
client.destroy();
|
||||
};
|
||||
|
||||
const fail = (err) => {
|
||||
Logger.warn("Provisioning failed", err);
|
||||
cleanup();
|
||||
reject(err);
|
||||
};
|
||||
|
||||
const succeed = () => {
|
||||
Logger.debug("Provisioning successful.");
|
||||
|
||||
cleanup();
|
||||
resolve();
|
||||
};
|
||||
|
||||
client.on("error", (err) => {
|
||||
fail(new Error(`Connection error to ${host}:${port}. Error: ${err.message}`));
|
||||
});
|
||||
|
||||
timeout = setTimeout(() => {
|
||||
// If we're not already done, fail with a timeout.
|
||||
if (state !== ProvisioningState.DONE) {
|
||||
fail(new Error(`Operation timed out after 15 seconds. Current state: ${state}`));
|
||||
}
|
||||
}, 15000);
|
||||
|
||||
client.on("close", () => {
|
||||
if (state !== ProvisioningState.DONE) {
|
||||
fail(new Error(`Connection closed unexpectedly during state: ${state}`));
|
||||
}
|
||||
});
|
||||
|
||||
client.connect(port, host, () => {
|
||||
Logger.debug(`WifiConfig State ${state} - Connected to ${host}:${port}. Transitioning to 'getting_uuid'.`);
|
||||
state = ProvisioningState.GETTING_UUID;
|
||||
|
||||
const getUuidPacket = new MSmartProvisioningPacket({
|
||||
commandId: MSmartProvisioningPacket.COMMAND_IDS.CMD_UUID_INFO,
|
||||
payload: Buffer.from([0x00])
|
||||
});
|
||||
|
||||
Logger.debug(`WifiConfig State ${state} - Sending 'Get UUID' request.`);
|
||||
client.write(getUuidPacket.toBytes());
|
||||
});
|
||||
|
||||
client.on("data", (data) => {
|
||||
if (state === ProvisioningState.DONE) {
|
||||
return;
|
||||
} // Don't process any further data after we're done
|
||||
|
||||
dataBuffer = Buffer.concat([dataBuffer, data]);
|
||||
|
||||
while (dataBuffer.length >= 7) {
|
||||
if (dataBuffer[0] !== 0xEE || dataBuffer[1] !== 0x01) {
|
||||
Logger.warn(`WifiConfig State ${state} - Desynchronized stream. Discarding byte ${dataBuffer[0].toString(16)}.`);
|
||||
dataBuffer = dataBuffer.subarray(1);
|
||||
continue;
|
||||
}
|
||||
|
||||
// FIXME: Not ideal here, as it is knowledge/logic that should be encapsulated by the MSmartProvisioningPacket
|
||||
const coreCommandLength = dataBuffer.readUInt16BE(4);
|
||||
const totalPacketLength = 7 + coreCommandLength;
|
||||
|
||||
if (dataBuffer.length < totalPacketLength) {
|
||||
break;
|
||||
}
|
||||
|
||||
const packetToProcess = dataBuffer.subarray(0, totalPacketLength);
|
||||
dataBuffer = dataBuffer.subarray(totalPacketLength);
|
||||
|
||||
try {
|
||||
const responsePacket = MSmartProvisioningPacket.FROM_BYTES(packetToProcess);
|
||||
Logger.debug(`WifiConfig State ${state} - Received and parsed packet with Command ID: ${responsePacket.commandId}`);
|
||||
|
||||
if (state === ProvisioningState.GETTING_UUID && responsePacket.commandId === MSmartProvisioningPacket.RESPONSE_IDS.CMD_UUID_INFO) {
|
||||
if (responsePacket.payload.length > 1) {
|
||||
Logger.debug(`WifiConfig State ${state} - Received full UUID response. Transitioning to 'provisioning'.`);
|
||||
state = ProvisioningState.PROVISIONING;
|
||||
|
||||
const payloadString = [
|
||||
wifiConfig.ssid,
|
||||
wifiConfig.credentials.typeSpecificSettings.password,
|
||||
"https://euprod.mzrobo.com/",
|
||||
"GMT+00:00",
|
||||
BigInt(`0x${crypto.randomBytes(8).toString("hex")}`).toString(),
|
||||
"1,13", // Wifi channel min-max
|
||||
"DE"
|
||||
].join("\n");
|
||||
|
||||
const provisioningPacket = new MSmartProvisioningPacket({
|
||||
commandId: MSmartProvisioningPacket.COMMAND_IDS.CMD_ALL_INFO,
|
||||
payload: Buffer.from(payloadString)
|
||||
});
|
||||
|
||||
Logger.debug(`WifiConfig State ${state} - Sending provisioning data.`);
|
||||
client.write(provisioningPacket.toBytes());
|
||||
} else {
|
||||
Logger.debug(`WifiConfig State ${state} - Ignoring ACK for UUID response.`);
|
||||
}
|
||||
} else if (state === ProvisioningState.PROVISIONING && responsePacket.commandId === MSmartProvisioningPacket.RESPONSE_IDS.CMD_ALL_INFO) {
|
||||
if (responsePacket.payload[0] === 0x00) {
|
||||
Logger.debug(`WifiConfig State ${state} - Received provisioning ack.`);
|
||||
state = ProvisioningState.DONE;
|
||||
|
||||
// Resolve this ASAP, so that the UI can still receive feedback before the robot transitions from AP to STA mode
|
||||
succeed();
|
||||
|
||||
return;
|
||||
} else {
|
||||
fail(new Error(`Robot acknowledged provisioning with error code: 0x${responsePacket.payload[0].toString(16)}`));
|
||||
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
Logger.debug(`WifiConfig State ${state} - Received unexpected but valid packet with ID ${responsePacket.commandId}. Ignoring.`);
|
||||
}
|
||||
} catch (e) {
|
||||
fail(new Error(`Failed to parse response from robot. Error: ${e.message}`));
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MideaWifiConfigurationCapability;
|
||||
@ -0,0 +1,65 @@
|
||||
const MSmartConst = require("../../../msmart/MSmartConst");
|
||||
const MSmartPacket = require("../../../msmart/MSmartPacket");
|
||||
const ZoneCleaningCapability = require("../../../core/capabilities/ZoneCleaningCapability");
|
||||
|
||||
/**
|
||||
* @extends ZoneCleaningCapability<import("../MideaValetudoRobot")>
|
||||
*/
|
||||
class MideaZoneCleaningCapability extends ZoneCleaningCapability {
|
||||
/**
|
||||
* @param {object} options
|
||||
* @param {Array<import("../../../entities/core/ValetudoZone")>} options.zones
|
||||
* @param {number} [options.iterations]
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async start(options) {
|
||||
if (options.zones.length > 5) {
|
||||
throw new Error("Cannot clean more than 5 zones at once.");
|
||||
}
|
||||
|
||||
const zoneDataPayload = Buffer.alloc(1 + options.zones.length * 10);
|
||||
|
||||
zoneDataPayload[0] = options.zones.length;
|
||||
options.zones.forEach((zone, i) => {
|
||||
const offset = 1 + i * 10;
|
||||
|
||||
const pA = this.robot.mapParser.convertToMideaCoordinates(zone.points.pA.x, zone.points.pA.y);
|
||||
const pC = this.robot.mapParser.convertToMideaCoordinates(zone.points.pC.x, zone.points.pC.y);
|
||||
|
||||
zoneDataPayload[offset] = i + 1;
|
||||
zoneDataPayload.writeUInt16LE(pA.x, offset + 1); // left
|
||||
zoneDataPayload.writeUInt16LE(pA.y, offset + 3); // top
|
||||
zoneDataPayload.writeUInt16LE(pC.x, offset + 5); // right
|
||||
zoneDataPayload.writeUInt16LE(pC.y, offset + 7); // bottom
|
||||
zoneDataPayload[offset + 9] = options.iterations ?? 1;
|
||||
});
|
||||
|
||||
const packet = new MSmartPacket({
|
||||
messageType: MSmartPacket.MESSAGE_TYPE.SETTING,
|
||||
payload: MSmartPacket.buildPayload(
|
||||
MSmartConst.SETTING.START_ZONE_CLEANUP,
|
||||
zoneDataPayload
|
||||
)
|
||||
});
|
||||
|
||||
await this.robot.sendCommand(packet.toHexString());
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {import("../../../core/capabilities/ZoneCleaningCapability").ZoneCleaningCapabilityProperties}
|
||||
*/
|
||||
getProperties() {
|
||||
return {
|
||||
zoneCount: {
|
||||
min: 1,
|
||||
max: 5
|
||||
},
|
||||
iterationCount: {
|
||||
min: 1,
|
||||
max: 3
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MideaZoneCleaningCapability;
|
||||
27
backend/lib/robots/midea/capabilities/index.js
Normal file
27
backend/lib/robots/midea/capabilities/index.js
Normal file
@ -0,0 +1,27 @@
|
||||
module.exports = {
|
||||
MideaAutoEmptyDockAutoEmptyControlCapability: require("./MideaAutoEmptyDockAutoEmptyControlCapability"),
|
||||
MideaAutoEmptyDockManualTriggerCapability: require("./MideaAutoEmptyDockManualTriggerCapability"),
|
||||
MideaBasicControlCapability: require("./MideaBasicControlCapability"),
|
||||
MideaCameraLightControlCapability: require("./MideaCameraLightControlCapability"),
|
||||
MideaCollisionAvoidantNavigationControlCapability: require("./MideaCollisionAvoidantNavigationControlCapability"),
|
||||
MideaCombinedVirtualRestrictionsCapability: require("./MideaCombinedVirtualRestrictionsCapability"),
|
||||
MideaCurrentStatisticsCapability: require("./MideaCurrentStatisticsCapability"),
|
||||
MideaDoNotDisturbCapability: require("./MideaDoNotDisturbCapability"),
|
||||
MideaFanSpeedControlCapability: require("./MideaFanSpeedControlCapability"),
|
||||
MideaKeyLockCapability: require("./MideaKeyLockCapability"),
|
||||
MideaLocateCapability: require("./MideaLocateCapability"),
|
||||
MideaManualControlCapability: require("./MideaManualControlCapability"),
|
||||
MideaMapResetCapability: require("./MideaMapResetCapability"),
|
||||
MideaMapSegmentEditCapability: require("./MideaMapSegmentEditCapability"),
|
||||
MideaMapSegmentationCapability: require("./MideaMapSegmentationCapability"),
|
||||
MideaMappingPassCapability: require("./MideaMappingPassCapability"),
|
||||
MideaMopDockCleanManualTriggerCapability: require("./MideaMopDockCleanManualTriggerCapability"),
|
||||
MideaMopDockDryManualTriggerCapability: require("./MideaMopDockDryManualTriggerCapability"),
|
||||
MideaMopExtensionControlCapability: require("./MideaMopExtensionControlCapability"),
|
||||
MideaOperationModeControlCapability: require("./MideaOperationModeControlCapability"),
|
||||
MideaSpeakerTestCapability: require("./MideaSpeakerTestCapability"),
|
||||
MideaSpeakerVolumeControlCapability: require("./MideaSpeakerVolumeControlCapability"),
|
||||
MideaWaterUsageControlCapability: require("./MideaWaterUsageControlCapability"),
|
||||
MideaWifiConfigurationCapability: require("./MideaWifiConfigurationCapability"),
|
||||
MideaZoneCleaningCapability: require("./MideaZoneCleaningCapability"),
|
||||
};
|
||||
3
backend/lib/robots/midea/index.js
Normal file
3
backend/lib/robots/midea/index.js
Normal file
@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
"MideaJ15ValetudoRobot": require("./MideaJ15ValetudoRobot")
|
||||
};
|
||||
70
backend/lib/utils/DummyCloudCertManager.js
Normal file
70
backend/lib/utils/DummyCloudCertManager.js
Normal file
@ -0,0 +1,70 @@
|
||||
const crypto = require("crypto");
|
||||
const forge = require("node-forge");
|
||||
const Logger = require("../Logger");
|
||||
|
||||
class DummyCloudCertManager {
|
||||
/**
|
||||
* @param {object} options
|
||||
* @param {forge.pki.PrivateKey} options.caKey
|
||||
* @param {forge.pki.Certificate} options.caCert
|
||||
*/
|
||||
constructor(options) {
|
||||
this.caKey = options.caKey;
|
||||
this.caCert = options.caCert;
|
||||
this.certCache = new Map();
|
||||
}
|
||||
|
||||
getCertificate(hostname) {
|
||||
if (this.certCache.has(hostname)) {
|
||||
const cachedEntry = this.certCache.get(hostname);
|
||||
|
||||
if (cachedEntry) {
|
||||
if (cachedEntry.validUntil > new Date(Date.now() + 60000)) {
|
||||
return cachedEntry;
|
||||
} else {
|
||||
Logger.info(`Certificate for '${hostname}' has expired or is about to expire. Regenerating.`);
|
||||
}
|
||||
} else {
|
||||
Logger.info(`No cached certificate available for '${hostname}'.`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const newCertData = this.generateCertificate(hostname);
|
||||
|
||||
this.certCache.set(hostname, newCertData);
|
||||
|
||||
return newCertData;
|
||||
}
|
||||
|
||||
generateCertificate(hostname) {
|
||||
Logger.info(`Generating new certificate for '${hostname}'.`);
|
||||
|
||||
const keys = forge.pki.rsa.generateKeyPair(2048);
|
||||
const cert = forge.pki.createCertificate();
|
||||
|
||||
cert.publicKey = keys.publicKey;
|
||||
cert.serialNumber = "01" + crypto.randomBytes(19).toString("hex"); // rfc5280 4.1.2.2
|
||||
cert.validity.notBefore = new Date(new Date().setDate(new Date().getDate() - 1));
|
||||
cert.validity.notAfter = new Date(new Date().setFullYear(new Date().getFullYear() + 30)); // Midea doesn't handle the cert expiring gracefully or at all
|
||||
|
||||
cert.setSubject([{ name: "commonName", value: hostname }]);
|
||||
cert.setIssuer(this.caCert.subject.attributes);
|
||||
cert.setExtensions([
|
||||
{ name: "basicConstraints", cA: false },
|
||||
{ name: "keyUsage", digitalSignature: true, keyEncipherment: true },
|
||||
{ name: "extKeyUsage", serverAuth: true },
|
||||
{ name: "subjectAltName", altNames: [{ type: 2, value: hostname }] }
|
||||
]);
|
||||
|
||||
cert.sign(this.caKey, forge.md.sha256.create());
|
||||
|
||||
return {
|
||||
key: forge.pki.privateKeyToPem(keys.privateKey),
|
||||
cert: forge.pki.certificateToPem(cert),
|
||||
validUntil: cert.validity.notAfter
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DummyCloudCertManager;
|
||||
@ -0,0 +1,22 @@
|
||||
const DismissibleValetudoEvent = require("./DismissibleValetudoEvent");
|
||||
|
||||
class MissingResourceValetudoEvent extends DismissibleValetudoEvent {
|
||||
/**
|
||||
*
|
||||
* @param {object} options
|
||||
* @param {string} options.message
|
||||
*
|
||||
* @param {string} [options.id]
|
||||
* @param {Date} [options.timestamp]
|
||||
* @param {boolean} [options.processed]
|
||||
* @param {object} [options.metaData]
|
||||
* @class
|
||||
*/
|
||||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
this.message = options.message;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MissingResourceValetudoEvent;
|
||||
@ -3,6 +3,7 @@ module.exports = {
|
||||
DismissibleValetudoEvent: require("./DismissibleValetudoEvent"),
|
||||
DustBinFullValetudoEvent: require("./DustBinFullValetudoEvent"),
|
||||
ErrorStateValetudoEvent: require("./ErrorStateValetudoEvent"),
|
||||
MissingResourceValetudoEvent: require("./MissingResourceValetudoEvent"),
|
||||
MopAttachmentReminderValetudoEvent: require("./MopAttachmentReminderValetudoEvent"),
|
||||
PendingMapChangeValetudoEvent: require("./PendingMapChangeValetudoEvent")
|
||||
PendingMapChangeValetudoEvent: require("./PendingMapChangeValetudoEvent"),
|
||||
};
|
||||
|
||||
@ -33,6 +33,7 @@
|
||||
"author": "",
|
||||
"dependencies": {
|
||||
"@destinationstransfers/ntp": "2.0.0",
|
||||
"aedes": "0.51.3",
|
||||
"ajv": "8.17.1",
|
||||
"async-mqtt": "2.6.3",
|
||||
"axios": "0.27.2",
|
||||
@ -50,6 +51,7 @@
|
||||
"mqtt": "5.10.1",
|
||||
"nested-object-assign": "1.0.4",
|
||||
"nested-property": "4.0.0",
|
||||
"node-forge": "1.3.1",
|
||||
"openapi-validator-middleware": "3.2.6",
|
||||
"semaphore": "1.1.0",
|
||||
"swagger-ui-express": "5.0.1",
|
||||
|
||||
116
backend/test/lib/msmart/MSmartPacket_spec.js
Normal file
116
backend/test/lib/msmart/MSmartPacket_spec.js
Normal file
@ -0,0 +1,116 @@
|
||||
const MSmartPacket = require("../../../lib/msmart/MSmartPacket");
|
||||
const should = require("should");
|
||||
|
||||
should.config.checkProtoEql = false;
|
||||
|
||||
describe("MSmartPacket", function () {
|
||||
|
||||
describe("Static FROM_BYTES Parser", function() {
|
||||
it("Should parse a short, valid B8 packet correctly", function() {
|
||||
const hexString = "aa0eb800000000000002aa0122006b";
|
||||
const buffer = Buffer.from(hexString, "hex");
|
||||
|
||||
const packet = MSmartPacket.FROM_BYTES(buffer);
|
||||
|
||||
packet.should.be.an.instanceOf(MSmartPacket);
|
||||
packet.deviceType.should.equal(0xB8);
|
||||
packet.messageId.should.equal(0x00);
|
||||
packet.protocolVersion.should.equal(0x00);
|
||||
packet.deviceProtocolVersion.should.equal(0x00);
|
||||
packet.messageType.should.equal(0x02);
|
||||
packet.payload.should.deepEqual(Buffer.from("aa012200", "hex"));
|
||||
});
|
||||
|
||||
it("Should parse locate command correctly", function() {
|
||||
const hexString = "aa0db800000000000003aa01018c";
|
||||
const buffer = Buffer.from(hexString, "hex");
|
||||
|
||||
const packet = MSmartPacket.FROM_BYTES(buffer);
|
||||
|
||||
packet.should.be.an.instanceOf(MSmartPacket);
|
||||
packet.deviceType.should.equal(0xB8);
|
||||
packet.messageId.should.equal(0x00);
|
||||
packet.protocolVersion.should.equal(0x00);
|
||||
packet.deviceProtocolVersion.should.equal(0x00);
|
||||
packet.messageType.should.equal(0x03);
|
||||
packet.payload.should.deepEqual(Buffer.from("aa0101", "hex"));
|
||||
});
|
||||
|
||||
it("Should parse a long, valid B8 packet correctly", function() {
|
||||
const hexString = "aa51b800000000000004aa0101120002ff00010078000062000100000000000100000000740001000400002e0000010000000001010020000000000f02003518000297010300000000300021000000030836";
|
||||
const payloadHexString = "aa0101120002ff00010078000062000100000000000100000000740001000400002e0000010000000001010020000000000f020035180002970103000000003000210000000308";
|
||||
const buffer = Buffer.from(hexString, "hex");
|
||||
|
||||
const packet = MSmartPacket.FROM_BYTES(buffer);
|
||||
|
||||
packet.should.be.an.instanceOf(MSmartPacket);
|
||||
packet.deviceType.should.equal(0xB8);
|
||||
packet.messageId.should.equal(0x00);
|
||||
packet.protocolVersion.should.equal(0x00);
|
||||
packet.deviceProtocolVersion.should.equal(0x00);
|
||||
packet.messageType.should.equal(0x04);
|
||||
packet.payload.should.deepEqual(Buffer.from(payloadHexString, "hex"));
|
||||
});
|
||||
|
||||
it("Should throw an error for a packet that is too short", function() {
|
||||
const invalidBuffer = Buffer.from("aa0102030405", "hex");
|
||||
|
||||
(() => {
|
||||
MSmartPacket.FROM_BYTES(invalidBuffer);
|
||||
}).should.throw(/Packet too short/);
|
||||
});
|
||||
|
||||
it("Should throw an error for a packet with an invalid magic byte", function() {
|
||||
const invalidBuffer = Buffer.from("bb0eb800000000000002aa0122006b", "hex");
|
||||
|
||||
(() => {
|
||||
MSmartPacket.FROM_BYTES(invalidBuffer);
|
||||
}).should.throw(/Invalid Magic Byte/);
|
||||
});
|
||||
|
||||
it("Should throw an error for a packet with a length mismatch", function() {
|
||||
// Length byte is 0x0D (13), but actual length is 15 bytes (so length byte should be 0x0E)
|
||||
const invalidBuffer = Buffer.from("aa0db800000000000002aa0122006b", "hex");
|
||||
|
||||
(() => {
|
||||
MSmartPacket.FROM_BYTES(invalidBuffer);
|
||||
}).should.throw(/Length mismatch/);
|
||||
});
|
||||
|
||||
it("Should throw an error for a packet with an invalid checksum", function() {
|
||||
// Valid checksum is 0x6b, we use 0x00
|
||||
const invalidBuffer = Buffer.from("aa0eb800000000000002aa01220000", "hex");
|
||||
|
||||
(() => {
|
||||
MSmartPacket.FROM_BYTES(invalidBuffer);
|
||||
}).should.throw(/Checksum mismatch/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Instance toBytes Serializer", function() {
|
||||
it("Should correctly serialize a packet that can be parsed again (round-trip)", function() {
|
||||
// 1. Create a packet from known data
|
||||
const originalPacket = new MSmartPacket({
|
||||
deviceType: 0xAC,
|
||||
messageId: 0x01,
|
||||
protocolVersion: 0x00,
|
||||
deviceProtocolVersion: 0x01,
|
||||
messageType: MSmartPacket.MESSAGE_TYPE.SETTING,
|
||||
payload: Buffer.from([0xB0, 0x01, 0x01])
|
||||
});
|
||||
|
||||
// 2. Serialize it to a buffer
|
||||
const buffer = originalPacket.toBytes();
|
||||
|
||||
// 3. Check if the buffer matches a known-good hex string
|
||||
const expectedHexString = "aa0dac00000001000102b0010191";
|
||||
buffer.toString("hex").should.equal(expectedHexString);
|
||||
|
||||
// 4. Parse it back
|
||||
const parsedPacket = MSmartPacket.FROM_BYTES(buffer);
|
||||
|
||||
// 5. Ensure the parsed packet is identical to the original
|
||||
parsedPacket.should.deepEqual(originalPacket);
|
||||
});
|
||||
});
|
||||
});
|
||||
107
backend/test/lib/msmart/MSmartProvisioningPacket_spec.js
Normal file
107
backend/test/lib/msmart/MSmartProvisioningPacket_spec.js
Normal file
@ -0,0 +1,107 @@
|
||||
const MSmartProvisioningPacket = require("../../../lib/msmart/MSmartProvisioningPacket");
|
||||
const should = require("should");
|
||||
|
||||
should.config.checkProtoEql = false;
|
||||
|
||||
describe("MSmartProvisioningPacket", function () {
|
||||
|
||||
describe("Static FROM_BYTES Parser", function() {
|
||||
it("Should parse a valid 'Get UUID' response correctly", function() {
|
||||
const hexString = "ee0180d500183138333239353934333937373939313337353561616161614c";
|
||||
const buffer = Buffer.from(hexString, "hex");
|
||||
|
||||
const packet = MSmartProvisioningPacket.FROM_BYTES(buffer);
|
||||
|
||||
packet.should.be.an.instanceOf(MSmartProvisioningPacket);
|
||||
packet.commandId.should.equal(MSmartProvisioningPacket.RESPONSE_IDS.CMD_UUID_INFO);
|
||||
packet.payload.should.deepEqual(Buffer.from("313833323935393433393737393931333735356161616161", "hex"));
|
||||
});
|
||||
|
||||
it("Should parse a valid 'Acknowledge Provisioning' response correctly", function() {
|
||||
const hexString = "ee0180d000010051";
|
||||
const buffer = Buffer.from(hexString, "hex");
|
||||
|
||||
const packet = MSmartProvisioningPacket.FROM_BYTES(buffer);
|
||||
|
||||
packet.should.be.an.instanceOf(MSmartProvisioningPacket);
|
||||
packet.commandId.should.equal(MSmartProvisioningPacket.RESPONSE_IDS.CMD_ALL_INFO);
|
||||
packet.payload.should.deepEqual(Buffer.from("00", "hex"));
|
||||
});
|
||||
|
||||
it("Should throw an error for a packet that is too short", function() {
|
||||
const invalidBuffer = Buffer.from("ee0100d5", "hex");
|
||||
|
||||
(() => {
|
||||
MSmartProvisioningPacket.FROM_BYTES(invalidBuffer);
|
||||
}).should.throw(/Packet too short/);
|
||||
});
|
||||
|
||||
it("Should throw an error for a packet with an invalid magic header", function() {
|
||||
const invalidBuffer = Buffer.from("ff0180d000010051", "hex");
|
||||
|
||||
(() => {
|
||||
MSmartProvisioningPacket.FROM_BYTES(invalidBuffer);
|
||||
}).should.throw(/Invalid Magic Header/);
|
||||
});
|
||||
|
||||
it("Should throw an error for a packet with a length mismatch", function() {
|
||||
// Length field is 0x0002, but actual payload length is 1 byte
|
||||
const invalidBuffer = Buffer.from("ee0180d000020052", "hex");
|
||||
|
||||
(() => {
|
||||
MSmartProvisioningPacket.FROM_BYTES(invalidBuffer);
|
||||
}).should.throw(/Payload length mismatch/);
|
||||
});
|
||||
|
||||
it("Should throw an error for a packet with an invalid checksum", function() {
|
||||
// Valid checksum is 0x51, we use 0x00
|
||||
const invalidBuffer = Buffer.from("ee0180d000010000", "hex");
|
||||
|
||||
(() => {
|
||||
MSmartProvisioningPacket.FROM_BYTES(invalidBuffer);
|
||||
}).should.throw(/Checksum mismatch/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Instance toBytes Serializer", function() {
|
||||
it("Should correctly serialize a packet that can be parsed again (round-trip)", function() {
|
||||
// 1. Create a packet for a "Get UUID" command
|
||||
const originalPacket = new MSmartProvisioningPacket({
|
||||
commandId: MSmartProvisioningPacket.COMMAND_IDS.CMD_UUID_INFO,
|
||||
payload: Buffer.from([0x00])
|
||||
});
|
||||
|
||||
// 2. Serialize it to a buffer
|
||||
const buffer = originalPacket.toBytes();
|
||||
|
||||
// 3. Check if the buffer matches the known-good hex string (checksum may vary based on exact implementation details but should be consistent)
|
||||
const expectedHexString = "ee0100d5000100d6";
|
||||
buffer.toString("hex").should.equal(expectedHexString);
|
||||
|
||||
// 4. Parse it back
|
||||
const parsedPacket = MSmartProvisioningPacket.FROM_BYTES(buffer);
|
||||
|
||||
// 5. Ensure the parsed packet is identical to the original
|
||||
parsedPacket.should.deepEqual(originalPacket);
|
||||
});
|
||||
|
||||
it("Should correctly serialize a packet with a larger payload (round-trip)", function() {
|
||||
const payload = Buffer.from("SSID\nPASSWORD\nhttps://server.com/\nGMT+00:00\nTOKEN\n-60\nDE");
|
||||
|
||||
// 1. Create a packet for a "Send All Info" command
|
||||
const originalPacket = new MSmartProvisioningPacket({
|
||||
commandId: MSmartProvisioningPacket.COMMAND_IDS.CMD_ALL_INFO,
|
||||
payload: payload
|
||||
});
|
||||
|
||||
// 2. Serialize it to a buffer
|
||||
const buffer = originalPacket.toBytes();
|
||||
|
||||
// 3. Parse it back
|
||||
const parsedPacket = MSmartProvisioningPacket.FROM_BYTES(buffer);
|
||||
|
||||
// 4. Ensure the parsed packet is identical to the original
|
||||
parsedPacket.should.deepEqual(originalPacket);
|
||||
});
|
||||
});
|
||||
});
|
||||
BIN
docs/_pages/general/img/robots/eureka/eureka_j15pu.jpg
Normal file
BIN
docs/_pages/general/img/robots/eureka/eureka_j15pu.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 244 KiB |
@ -103,15 +103,17 @@ You can use Ctrl + F to look for your model of robot.<br/>
|
||||
5. [Viomi](#viomi)
|
||||
1. [V6](#viomi_v6)
|
||||
2. [SE](#viomi_se)
|
||||
6. [Cecotec](#cecotec)
|
||||
6. [Eureka](#eureka)
|
||||
1. [J15 Pro Ultra](#eureka_j15pu)
|
||||
7. [Cecotec](#cecotec)
|
||||
1. [Conga 3290](#conga_3290)
|
||||
2. [Conga 3790](#conga_3790)
|
||||
7. [Proscenic](#proscenic)
|
||||
8. [Proscenic](#proscenic)
|
||||
1. [M6 Pro](#proscenic_m6pro)
|
||||
8. [Commodore](#commodore)
|
||||
9. [Commodore](#commodore)
|
||||
1. [CVR 200](#commodore_cvr200)
|
||||
9. [IKOHS](#ikohs)
|
||||
1. [Netbot LS22](#ikohs_ls22)
|
||||
10. [IKOHS](#ikohs)
|
||||
1. [Netbot LS22](#ikohs_ls22)
|
||||
|
||||
## Xiaomi<a id="xiaomi"></a>
|
||||
|
||||
@ -1024,6 +1026,43 @@ It might be required to remove the battery but that can be done without touching
|
||||
- [ADB](https://github.com/Hypfer/valetudo-crl200s-root)
|
||||
|
||||
|
||||
## Eureka<a id="eureka"></a>
|
||||
|
||||
Eureka is a brand of Midea.
|
||||
<div class="alert alert-warning" role="alert">
|
||||
As of now (2025-08-29), support for these is experimental, beta, barely tested and full of dragons.
|
||||
</div>
|
||||
|
||||
### Eureka J15 Pro Ultra<a id="eureka_j15pu"></a>
|
||||
|
||||
<img src="./img/robots/eureka/eureka_j15pu.jpg" width="1300" height="325"/>
|
||||
|
||||
The Eureka J15 Pro Ultra might be similar to the J15 Ultra, but frankly I currently (2025-08-29) have no idea.
|
||||
|
||||
#### Comments
|
||||
|
||||
**WARNING**<br/>
|
||||
|
||||
At the time of writing (2025-08-29), this robot is completely new on this list.<br/>
|
||||
I've been playing around with it for a few months, but the sample size is 1 and I have not used it extensively.
|
||||
|
||||
Because of that, there is also no rooting guide yet, as I still lack both data and experience to come up with one that matches the quality of the other guides.
|
||||
|
||||
**You should very likely not be buying this if you just want a robot that works.**<br/>
|
||||
**The experience will be worse for equal or more money than other supported robots.**
|
||||
|
||||
For now, there is a Telegram Group for it, so if you have one or are interested, you can join that and we'll see if/how you can get it rooted (with no commitment to timeframes, suitability, success, usability, reliability, functionality, etc.).<br/>
|
||||
Anything beyond that I shall figure out as I go (or not, if I should lose interest)
|
||||
|
||||
|
||||
<a href="https://t.me/+F00lFE1NVUc2NTAy" data-si="34097f03527c7c0375540b07132a652161373c7c0c2f29446177627c62615d705318331a1b1c0b7a">Valetudo on Midea Telegram Group</a>
|
||||
|
||||
#### Details
|
||||
|
||||
**Valetudo Binary**: `aarch64`
|
||||
**Secure Boot**: `no`
|
||||
|
||||
|
||||
## Cecotec<a id="cecotec"></a>
|
||||
|
||||
Conga is a brand that uses existing robot designs with a slightly customized cloud.<br/>
|
||||
|
||||
@ -169,6 +169,36 @@ const CreateDismissableEventControl = (message: string) : FunctionComponent<Vale
|
||||
};
|
||||
};
|
||||
|
||||
const MissingResourceEventControl: FunctionComponent<ValetudoEventRenderProps> =
|
||||
({event, interact}) => {
|
||||
const color = event.processed ? "textSecondary" : "textPrimary";
|
||||
const textStyle = event.processed ? {textDecoration: "line-through"} : {};
|
||||
|
||||
return (
|
||||
<EventRow>
|
||||
<Stack>
|
||||
<EventTimestamp timestamp={event.timestamp}/>
|
||||
<Typography color={color} style={textStyle} sx={{mr: 1}}>
|
||||
{event.message!}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Button
|
||||
size="small"
|
||||
variant={"contained"}
|
||||
disabled={event.processed}
|
||||
onClick={() => {
|
||||
interact({
|
||||
interaction: "ok"
|
||||
});
|
||||
}}
|
||||
color="warning"
|
||||
>
|
||||
Dismiss
|
||||
</Button>
|
||||
</EventRow>
|
||||
);
|
||||
};
|
||||
|
||||
const UnknownEventControl: FunctionComponent<ValetudoEventRenderProps> =
|
||||
({event}) => {
|
||||
return (
|
||||
@ -184,5 +214,6 @@ export const eventControls: Record<string, React.ComponentType<ValetudoEventRend
|
||||
PendingMapChangeValetudoEvent: PendingMapChangeEventControl,
|
||||
DustBinFullValetudoEvent: CreateDismissableEventControl("The dust bin is full. Please empty it."),
|
||||
MopAttachmentReminderValetudoEvent: CreateDismissableEventControl("The mop is still attached to the robot."),
|
||||
MissingResourceValetudoEvent: MissingResourceEventControl,
|
||||
Default: UnknownEventControl,
|
||||
};
|
||||
|
||||
375
package-lock.json
generated
375
package-lock.json
generated
@ -39,6 +39,7 @@
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@destinationstransfers/ntp": "2.0.0",
|
||||
"aedes": "0.51.3",
|
||||
"ajv": "8.17.1",
|
||||
"async-mqtt": "2.6.3",
|
||||
"axios": "0.27.2",
|
||||
@ -56,6 +57,7 @@
|
||||
"mqtt": "5.10.1",
|
||||
"nested-object-assign": "1.0.4",
|
||||
"nested-property": "4.0.0",
|
||||
"node-forge": "1.3.1",
|
||||
"openapi-validator-middleware": "3.2.6",
|
||||
"semaphore": "1.1.0",
|
||||
"swagger-ui-express": "5.0.1",
|
||||
@ -5517,6 +5519,111 @@
|
||||
"node": ">=8.9"
|
||||
}
|
||||
},
|
||||
"node_modules/aedes": {
|
||||
"version": "0.51.3",
|
||||
"resolved": "https://registry.npmjs.org/aedes/-/aedes-0.51.3.tgz",
|
||||
"integrity": "sha512-aQfiI9w3RbqnowNCdcGMmCtxBFXN9bhJFcuZm24U5/NU06V3MCl42jWK2GUnu8rOypR2Ahi/aEcgq3w7CMcycg==",
|
||||
"dependencies": {
|
||||
"aedes-packet": "^3.0.0",
|
||||
"aedes-persistence": "^9.1.2",
|
||||
"end-of-stream": "^1.4.4",
|
||||
"fastfall": "^1.5.1",
|
||||
"fastparallel": "^2.4.1",
|
||||
"fastseries": "^2.0.0",
|
||||
"hyperid": "^3.2.0",
|
||||
"mqemitter": "^6.0.0",
|
||||
"mqtt-packet": "^9.0.0",
|
||||
"retimer": "^4.0.0",
|
||||
"reusify": "^1.0.4",
|
||||
"uuid": "^10.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/aedes"
|
||||
}
|
||||
},
|
||||
"node_modules/aedes-packet": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/aedes-packet/-/aedes-packet-3.0.0.tgz",
|
||||
"integrity": "sha512-swASey0BxGs4/npZGWoiVDmnEyPvVFIRY6l2LVKL4rbiW8IhcIGDLfnb20Qo8U20itXlitAKPQ3MVTEbOGG5ZA==",
|
||||
"dependencies": {
|
||||
"mqtt-packet": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/aedes-packet/node_modules/bl": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
||||
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
|
||||
"dependencies": {
|
||||
"buffer": "^5.5.0",
|
||||
"inherits": "^2.0.4",
|
||||
"readable-stream": "^3.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/aedes-packet/node_modules/buffer": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.1.13"
|
||||
}
|
||||
},
|
||||
"node_modules/aedes-packet/node_modules/mqtt-packet": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-7.1.2.tgz",
|
||||
"integrity": "sha512-FFZbcZ2omsf4c5TxEQfcX9hI+JzDpDKPT46OmeIBpVA7+t32ey25UNqlqNXTmeZOr5BLsSIERpQQLsFWJS94SQ==",
|
||||
"dependencies": {
|
||||
"bl": "^4.0.2",
|
||||
"debug": "^4.1.1",
|
||||
"process-nextick-args": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/aedes-packet/node_modules/readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/aedes-persistence": {
|
||||
"version": "9.1.2",
|
||||
"resolved": "https://registry.npmjs.org/aedes-persistence/-/aedes-persistence-9.1.2.tgz",
|
||||
"integrity": "sha512-2Wlr5pwIK0eQOkiTwb8ZF6C20s8UPUlnsJ4kXYePZ3JlQl0NbBA176mzM8wY294BJ5wybpNc9P5XEQxqadRNcQ==",
|
||||
"dependencies": {
|
||||
"aedes-packet": "^3.0.0",
|
||||
"qlobber": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||
@ -10128,6 +10235,26 @@
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.1.tgz",
|
||||
"integrity": "sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw=="
|
||||
},
|
||||
"node_modules/fastfall": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/fastfall/-/fastfall-1.5.1.tgz",
|
||||
"integrity": "sha512-KH6p+Z8AKPXnmA7+Iz2Lh8ARCMr+8WNPVludm1LGkZoD2MjY6LVnRMtTKhkdzI+jr0RzQWXKzKyBJm1zoHEL4Q==",
|
||||
"dependencies": {
|
||||
"reusify": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fastparallel": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/fastparallel/-/fastparallel-2.4.1.tgz",
|
||||
"integrity": "sha512-qUmhxPgNHmvRjZKBFUNI0oZuuH9OlSIOXmJ98lhKPxMZZ7zS/Fi0wRHOihDSz0R1YiIOjxzOY4bq65YTcdBi2Q==",
|
||||
"dependencies": {
|
||||
"reusify": "^1.0.4",
|
||||
"xtend": "^4.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/fastq": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
|
||||
@ -10136,6 +10263,11 @@
|
||||
"reusify": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/fastseries": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fastseries/-/fastseries-2.0.0.tgz",
|
||||
"integrity": "sha512-XBU9RXeoYc2/VnvMhplAxEmZLfIk7cvTBu+xwoBuTI8pL19E03cmca17QQycKIdxgwCeFA/a4u27gv1h3ya5LQ=="
|
||||
},
|
||||
"node_modules/faye-websocket": {
|
||||
"version": "0.11.4",
|
||||
"resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz",
|
||||
@ -11438,6 +11570,47 @@
|
||||
"node": ">=10.17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/hyperid": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/hyperid/-/hyperid-3.3.0.tgz",
|
||||
"integrity": "sha512-7qhCVT4MJIoEsNcbhglhdmBKb09QtcmJNiIQGq7js/Khf5FtQQ9bzcAuloeqBeee7XD7JqDeve9KNlQya5tSGQ==",
|
||||
"dependencies": {
|
||||
"buffer": "^5.2.1",
|
||||
"uuid": "^8.3.2",
|
||||
"uuid-parse": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/hyperid/node_modules/buffer": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.1.13"
|
||||
}
|
||||
},
|
||||
"node_modules/hyperid/node_modules/uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.4.24",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||
@ -14866,6 +15039,26 @@
|
||||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/mqemitter": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/mqemitter/-/mqemitter-6.0.2.tgz",
|
||||
"integrity": "sha512-8RGlznQx/Nb1xC3xKUFXHWov7pn7JdH++YVwlr6SLT6k3ft1h+ImGqZdVudbdKruFckIq9wheq9s4hgCivJDow==",
|
||||
"dependencies": {
|
||||
"fastparallel": "^2.4.1",
|
||||
"qlobber": "^8.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/mqemitter/node_modules/qlobber": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/qlobber/-/qlobber-8.0.1.tgz",
|
||||
"integrity": "sha512-O+Wd1chXj5YE1DwmD+ae0bXiSLehmnS3czlC1R9FL/Nt/3q8uMS1bIHmg2lJfCoiimCxClWM8AAuJrF0EvNiog==",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
}
|
||||
},
|
||||
"node_modules/mqtt": {
|
||||
"version": "5.10.1",
|
||||
"resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.10.1.tgz",
|
||||
@ -17287,6 +17480,14 @@
|
||||
"teleport": ">=0.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/qlobber": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/qlobber/-/qlobber-7.0.1.tgz",
|
||||
"integrity": "sha512-FsFg9lMuMEFNKmTO9nV7tlyPhx8BmskPPjH2akWycuYVTtWaVwhW5yCHLJQ6Q+3mvw5cFX2vMfW2l9z2SiYAbg==",
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.13.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||
@ -18363,6 +18564,14 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/retimer": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/retimer/-/retimer-4.0.0.tgz",
|
||||
"integrity": "sha512-fZIVtvbOsQsxNSDhpdPOX4lx5Ss2ni+S72AUBitARpFhtA3UzrAjQ6gDtypB2/+l7L+1VQgAgpvAKY66mElH0w==",
|
||||
"dependencies": {
|
||||
"worker-timers": "^7.0.75"
|
||||
}
|
||||
},
|
||||
"node_modules/retry": {
|
||||
"version": "0.13.1",
|
||||
"resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
|
||||
@ -21044,6 +21253,11 @@
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/uuid-parse": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid-parse/-/uuid-parse-1.1.0.tgz",
|
||||
"integrity": "sha512-OdmXxA8rDsQ7YpNVbKSJkNzTw2I+S5WsbMDnCtIWSQaosNAcWtFuI/YK1TjzUI6nbkgiqEyh8gWngfcv8Asd9A=="
|
||||
},
|
||||
"node_modules/v8-to-istanbul": {
|
||||
"version": "8.1.1",
|
||||
"resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz",
|
||||
@ -25795,6 +26009,83 @@
|
||||
"regex-parser": "^2.2.11"
|
||||
}
|
||||
},
|
||||
"aedes": {
|
||||
"version": "0.51.3",
|
||||
"resolved": "https://registry.npmjs.org/aedes/-/aedes-0.51.3.tgz",
|
||||
"integrity": "sha512-aQfiI9w3RbqnowNCdcGMmCtxBFXN9bhJFcuZm24U5/NU06V3MCl42jWK2GUnu8rOypR2Ahi/aEcgq3w7CMcycg==",
|
||||
"requires": {
|
||||
"aedes-packet": "^3.0.0",
|
||||
"aedes-persistence": "^9.1.2",
|
||||
"end-of-stream": "^1.4.4",
|
||||
"fastfall": "^1.5.1",
|
||||
"fastparallel": "^2.4.1",
|
||||
"fastseries": "^2.0.0",
|
||||
"hyperid": "^3.2.0",
|
||||
"mqemitter": "^6.0.0",
|
||||
"mqtt-packet": "^9.0.0",
|
||||
"retimer": "^4.0.0",
|
||||
"reusify": "^1.0.4",
|
||||
"uuid": "^10.0.0"
|
||||
}
|
||||
},
|
||||
"aedes-packet": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/aedes-packet/-/aedes-packet-3.0.0.tgz",
|
||||
"integrity": "sha512-swASey0BxGs4/npZGWoiVDmnEyPvVFIRY6l2LVKL4rbiW8IhcIGDLfnb20Qo8U20itXlitAKPQ3MVTEbOGG5ZA==",
|
||||
"requires": {
|
||||
"mqtt-packet": "^7.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"bl": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
||||
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
|
||||
"requires": {
|
||||
"buffer": "^5.5.0",
|
||||
"inherits": "^2.0.4",
|
||||
"readable-stream": "^3.4.0"
|
||||
}
|
||||
},
|
||||
"buffer": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||
"requires": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.1.13"
|
||||
}
|
||||
},
|
||||
"mqtt-packet": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-7.1.2.tgz",
|
||||
"integrity": "sha512-FFZbcZ2omsf4c5TxEQfcX9hI+JzDpDKPT46OmeIBpVA7+t32ey25UNqlqNXTmeZOr5BLsSIERpQQLsFWJS94SQ==",
|
||||
"requires": {
|
||||
"bl": "^4.0.2",
|
||||
"debug": "^4.1.1",
|
||||
"process-nextick-args": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||
"requires": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"aedes-persistence": {
|
||||
"version": "9.1.2",
|
||||
"resolved": "https://registry.npmjs.org/aedes-persistence/-/aedes-persistence-9.1.2.tgz",
|
||||
"integrity": "sha512-2Wlr5pwIK0eQOkiTwb8ZF6C20s8UPUlnsJ4kXYePZ3JlQl0NbBA176mzM8wY294BJ5wybpNc9P5XEQxqadRNcQ==",
|
||||
"requires": {
|
||||
"aedes-packet": "^3.0.0",
|
||||
"qlobber": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"agent-base": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||
@ -29066,6 +29357,23 @@
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.1.tgz",
|
||||
"integrity": "sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw=="
|
||||
},
|
||||
"fastfall": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/fastfall/-/fastfall-1.5.1.tgz",
|
||||
"integrity": "sha512-KH6p+Z8AKPXnmA7+Iz2Lh8ARCMr+8WNPVludm1LGkZoD2MjY6LVnRMtTKhkdzI+jr0RzQWXKzKyBJm1zoHEL4Q==",
|
||||
"requires": {
|
||||
"reusify": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"fastparallel": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/fastparallel/-/fastparallel-2.4.1.tgz",
|
||||
"integrity": "sha512-qUmhxPgNHmvRjZKBFUNI0oZuuH9OlSIOXmJ98lhKPxMZZ7zS/Fi0wRHOihDSz0R1YiIOjxzOY4bq65YTcdBi2Q==",
|
||||
"requires": {
|
||||
"reusify": "^1.0.4",
|
||||
"xtend": "^4.0.2"
|
||||
}
|
||||
},
|
||||
"fastq": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
|
||||
@ -29074,6 +29382,11 @@
|
||||
"reusify": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"fastseries": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fastseries/-/fastseries-2.0.0.tgz",
|
||||
"integrity": "sha512-XBU9RXeoYc2/VnvMhplAxEmZLfIk7cvTBu+xwoBuTI8pL19E03cmca17QQycKIdxgwCeFA/a4u27gv1h3ya5LQ=="
|
||||
},
|
||||
"faye-websocket": {
|
||||
"version": "0.11.4",
|
||||
"resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz",
|
||||
@ -30012,6 +30325,32 @@
|
||||
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
|
||||
"integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="
|
||||
},
|
||||
"hyperid": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/hyperid/-/hyperid-3.3.0.tgz",
|
||||
"integrity": "sha512-7qhCVT4MJIoEsNcbhglhdmBKb09QtcmJNiIQGq7js/Khf5FtQQ9bzcAuloeqBeee7XD7JqDeve9KNlQya5tSGQ==",
|
||||
"requires": {
|
||||
"buffer": "^5.2.1",
|
||||
"uuid": "^8.3.2",
|
||||
"uuid-parse": "^1.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"buffer": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||
"requires": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.1.13"
|
||||
}
|
||||
},
|
||||
"uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"iconv-lite": {
|
||||
"version": "0.4.24",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||
@ -32407,6 +32746,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"mqemitter": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/mqemitter/-/mqemitter-6.0.2.tgz",
|
||||
"integrity": "sha512-8RGlznQx/Nb1xC3xKUFXHWov7pn7JdH++YVwlr6SLT6k3ft1h+ImGqZdVudbdKruFckIq9wheq9s4hgCivJDow==",
|
||||
"requires": {
|
||||
"fastparallel": "^2.4.1",
|
||||
"qlobber": "^8.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"qlobber": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/qlobber/-/qlobber-8.0.1.tgz",
|
||||
"integrity": "sha512-O+Wd1chXj5YE1DwmD+ae0bXiSLehmnS3czlC1R9FL/Nt/3q8uMS1bIHmg2lJfCoiimCxClWM8AAuJrF0EvNiog=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"mqtt": {
|
||||
"version": "5.10.1",
|
||||
"resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.10.1.tgz",
|
||||
@ -33959,6 +34314,11 @@
|
||||
"resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
|
||||
"integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw=="
|
||||
},
|
||||
"qlobber": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/qlobber/-/qlobber-7.0.1.tgz",
|
||||
"integrity": "sha512-FsFg9lMuMEFNKmTO9nV7tlyPhx8BmskPPjH2akWycuYVTtWaVwhW5yCHLJQ6Q+3mvw5cFX2vMfW2l9z2SiYAbg=="
|
||||
},
|
||||
"qs": {
|
||||
"version": "6.13.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||
@ -34755,6 +35115,14 @@
|
||||
"signal-exit": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"retimer": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/retimer/-/retimer-4.0.0.tgz",
|
||||
"integrity": "sha512-fZIVtvbOsQsxNSDhpdPOX4lx5Ss2ni+S72AUBitARpFhtA3UzrAjQ6gDtypB2/+l7L+1VQgAgpvAKY66mElH0w==",
|
||||
"requires": {
|
||||
"worker-timers": "^7.0.75"
|
||||
}
|
||||
},
|
||||
"retry": {
|
||||
"version": "0.13.1",
|
||||
"resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
|
||||
@ -36737,6 +37105,11 @@
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
|
||||
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="
|
||||
},
|
||||
"uuid-parse": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid-parse/-/uuid-parse-1.1.0.tgz",
|
||||
"integrity": "sha512-OdmXxA8rDsQ7YpNVbKSJkNzTw2I+S5WsbMDnCtIWSQaosNAcWtFuI/YK1TjzUI6nbkgiqEyh8gWngfcv8Asd9A=="
|
||||
},
|
||||
"v8-to-istanbul": {
|
||||
"version": "8.1.1",
|
||||
"resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz",
|
||||
@ -36768,6 +37141,7 @@
|
||||
"@types/semaphore": "1.1.4",
|
||||
"@types/uuid": "10.0.0",
|
||||
"@yao-pkg/pkg": "6.6.0",
|
||||
"aedes": "0.51.3",
|
||||
"ajv": "8.17.1",
|
||||
"async-mqtt": "2.6.3",
|
||||
"axios": "0.27.2",
|
||||
@ -36787,6 +37161,7 @@
|
||||
"mqtt": "5.10.1",
|
||||
"nested-object-assign": "1.0.4",
|
||||
"nested-property": "4.0.0",
|
||||
"node-forge": "1.3.1",
|
||||
"openapi-validator-middleware": "3.2.6",
|
||||
"semaphore": "1.1.0",
|
||||
"should": "13.2.3",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user