mirror of https://github.com/Hypfer/Valetudo.git
803 lines
34 KiB
JavaScript
803 lines
34 KiB
JavaScript
const capabilities = require("./capabilities");
|
|
|
|
const ConsumableMonitoringCapability = require("../../core/capabilities/ConsumableMonitoringCapability");
|
|
const DreameConst = require("./DreameConst");
|
|
const DreameMiotServices = require("./DreameMiotServices");
|
|
const DreameUtils = require("./DreameUtils");
|
|
const DreameValetudoRobot = require("./DreameValetudoRobot");
|
|
const entities = require("../../entities");
|
|
const ErrorStateValetudoEvent = require("../../valetudo_events/events/ErrorStateValetudoEvent");
|
|
const LinuxTools = require("../../utils/LinuxTools");
|
|
const Logger = require("../../Logger");
|
|
const MopAttachmentReminderValetudoEvent = require("../../valetudo_events/events/MopAttachmentReminderValetudoEvent");
|
|
const ValetudoRestrictedZone = require("../../entities/core/ValetudoRestrictedZone");
|
|
const ValetudoSelectionPreset = require("../../entities/core/ValetudoSelectionPreset");
|
|
|
|
const stateAttrs = entities.state.attributes;
|
|
|
|
|
|
const MIOT_SERVICES = DreameMiotServices["GEN2"];
|
|
|
|
|
|
|
|
class DreameGen2ValetudoRobot extends DreameValetudoRobot {
|
|
/**
|
|
*
|
|
* @param {object} options
|
|
* @param {object} options.operationModes
|
|
* @param {boolean} [options.detailedAttachmentReport]
|
|
* @param {boolean} [options.highResolutionWaterGrades]
|
|
* @param {import("../../Configuration")} options.config
|
|
* @param {import("../../ValetudoEventStore")} options.valetudoEventStore
|
|
*/
|
|
constructor(options) {
|
|
super(
|
|
Object.assign(
|
|
{},
|
|
{
|
|
operationModes: DreameGen2ValetudoRobot.OPERATION_MODES,
|
|
miotServices: {
|
|
MAP: MIOT_SERVICES.MAP
|
|
}
|
|
},
|
|
options,
|
|
)
|
|
);
|
|
|
|
this.highResolutionWaterGrades = !!options.highResolutionWaterGrades;
|
|
this.waterGrades = this.highResolutionWaterGrades ? DreameGen2ValetudoRobot.HIGH_RESOLUTION_WATER_GRADES : DreameValetudoRobot.WATER_GRADES;
|
|
|
|
this.detailedAttachmentReport = options.detailedAttachmentReport === true;
|
|
|
|
/** @type {Array<{siid: number, piid: number, did: number}>} */
|
|
this.statePropertiesToPoll = this.getStatePropertiesToPoll().map(e => {
|
|
return {
|
|
siid: e.siid,
|
|
piid: e.piid,
|
|
did: this.deviceId
|
|
};
|
|
});
|
|
|
|
this.mode = 0; //Idle
|
|
this.isCharging = false;
|
|
this.errorCode = "0";
|
|
this.stateNeedsUpdate = false;
|
|
|
|
this.registerCapability(new capabilities.DreameBasicControlCapability({
|
|
robot: this,
|
|
miot_actions: {
|
|
start: {
|
|
siid: MIOT_SERVICES.VACUUM_1.SIID,
|
|
aiid: MIOT_SERVICES.VACUUM_1.ACTIONS.RESUME.AIID
|
|
},
|
|
stop: {
|
|
siid: MIOT_SERVICES.VACUUM_2.SIID,
|
|
aiid: MIOT_SERVICES.VACUUM_2.ACTIONS.STOP.AIID
|
|
},
|
|
pause: {
|
|
siid: MIOT_SERVICES.VACUUM_1.SIID,
|
|
aiid: MIOT_SERVICES.VACUUM_1.ACTIONS.PAUSE.AIID
|
|
},
|
|
home: {
|
|
siid: MIOT_SERVICES.BATTERY.SIID,
|
|
aiid: MIOT_SERVICES.BATTERY.ACTIONS.START_CHARGE.AIID
|
|
}
|
|
}
|
|
}));
|
|
|
|
this.registerCapability(new capabilities.DreameFanSpeedControlCapability({
|
|
robot: this,
|
|
presets: Object.keys(DreameValetudoRobot.FAN_SPEEDS).map(k => {
|
|
return new ValetudoSelectionPreset({name: k, value: DreameValetudoRobot.FAN_SPEEDS[k]});
|
|
}),
|
|
siid: MIOT_SERVICES.VACUUM_2.SIID,
|
|
piid: MIOT_SERVICES.VACUUM_2.PROPERTIES.FAN_SPEED.PIID
|
|
}));
|
|
|
|
this.registerCapability(new capabilities.DreameLocateCapability({
|
|
robot: this,
|
|
siid: MIOT_SERVICES.AUDIO.SIID,
|
|
aiid: MIOT_SERVICES.AUDIO.ACTIONS.LOCATE.AIID
|
|
}));
|
|
|
|
this.registerCapability(new capabilities.DreameMapSegmentationCapability({
|
|
robot: this,
|
|
miot_actions: {
|
|
start: {
|
|
siid: MIOT_SERVICES.VACUUM_2.SIID,
|
|
aiid: MIOT_SERVICES.VACUUM_2.ACTIONS.START.AIID
|
|
}
|
|
},
|
|
miot_properties: {
|
|
mode: {
|
|
piid: MIOT_SERVICES.VACUUM_2.PROPERTIES.MODE.PIID
|
|
},
|
|
additionalCleanupParameters: {
|
|
piid: MIOT_SERVICES.VACUUM_2.PROPERTIES.ADDITIONAL_CLEANUP_PROPERTIES.PIID
|
|
}
|
|
},
|
|
segmentCleaningModeId: 18,
|
|
iterationsSupported: 4,
|
|
customOrderSupported: true
|
|
}));
|
|
|
|
this.registerCapability(new capabilities.DreameMapSegmentEditCapability({
|
|
robot: this,
|
|
miot_actions: {
|
|
map_edit: {
|
|
siid: MIOT_SERVICES.MAP.SIID,
|
|
aiid: MIOT_SERVICES.MAP.ACTIONS.EDIT.AIID
|
|
}
|
|
},
|
|
miot_properties: {
|
|
mapDetails: {
|
|
piid: MIOT_SERVICES.MAP.PROPERTIES.MAP_DETAILS.PIID
|
|
},
|
|
actionResult: {
|
|
piid: MIOT_SERVICES.MAP.PROPERTIES.ACTION_RESULT.PIID
|
|
}
|
|
}
|
|
}));
|
|
|
|
this.registerCapability(new capabilities.DreameMapSegmentRenameCapability({
|
|
robot: this,
|
|
miot_actions: {
|
|
map_edit: {
|
|
siid: MIOT_SERVICES.MAP.SIID,
|
|
aiid: MIOT_SERVICES.MAP.ACTIONS.EDIT.AIID
|
|
}
|
|
},
|
|
miot_properties: {
|
|
mapDetails: {
|
|
piid: MIOT_SERVICES.MAP.PROPERTIES.MAP_DETAILS.PIID
|
|
},
|
|
actionResult: {
|
|
piid: MIOT_SERVICES.MAP.PROPERTIES.ACTION_RESULT.PIID
|
|
}
|
|
}
|
|
}));
|
|
|
|
this.registerCapability(new capabilities.DreameMapResetCapability({
|
|
robot: this,
|
|
miot_actions: {
|
|
map_edit: {
|
|
siid: MIOT_SERVICES.MAP.SIID,
|
|
aiid: MIOT_SERVICES.MAP.ACTIONS.EDIT.AIID
|
|
}
|
|
},
|
|
miot_properties: {
|
|
mapDetails: {
|
|
piid: MIOT_SERVICES.MAP.PROPERTIES.MAP_DETAILS.PIID
|
|
},
|
|
actionResult: {
|
|
piid: MIOT_SERVICES.MAP.PROPERTIES.ACTION_RESULT.PIID
|
|
}
|
|
}
|
|
}));
|
|
|
|
this.registerCapability(new capabilities.DreameCombinedVirtualRestrictionsCapability({
|
|
robot: this,
|
|
supportedRestrictedZoneTypes: [
|
|
ValetudoRestrictedZone.TYPE.REGULAR,
|
|
ValetudoRestrictedZone.TYPE.MOP
|
|
],
|
|
miot_actions: {
|
|
map_edit: {
|
|
siid: MIOT_SERVICES.MAP.SIID,
|
|
aiid: MIOT_SERVICES.MAP.ACTIONS.EDIT.AIID
|
|
}
|
|
},
|
|
miot_properties: {
|
|
mapDetails: {
|
|
piid: MIOT_SERVICES.MAP.PROPERTIES.MAP_DETAILS.PIID
|
|
},
|
|
actionResult: {
|
|
piid: MIOT_SERVICES.MAP.PROPERTIES.ACTION_RESULT.PIID
|
|
}
|
|
}
|
|
}));
|
|
|
|
this.registerCapability(new capabilities.DreameSpeakerVolumeControlCapability({
|
|
robot: this,
|
|
siid: MIOT_SERVICES.AUDIO.SIID,
|
|
piid: MIOT_SERVICES.AUDIO.PROPERTIES.VOLUME.PIID
|
|
}));
|
|
this.registerCapability(new capabilities.DreameSpeakerTestCapability({
|
|
robot: this,
|
|
siid: MIOT_SERVICES.AUDIO.SIID,
|
|
aiid: MIOT_SERVICES.AUDIO.ACTIONS.VOLUME_TEST.AIID
|
|
}));
|
|
|
|
this.registerCapability(new capabilities.DreamePendingMapChangeHandlingCapability({
|
|
robot: this,
|
|
miot_actions: {
|
|
map_edit: {
|
|
siid: MIOT_SERVICES.MAP.SIID,
|
|
aiid: MIOT_SERVICES.MAP.ACTIONS.EDIT.AIID
|
|
}
|
|
},
|
|
miot_properties: {
|
|
mapDetails: {
|
|
piid: MIOT_SERVICES.MAP.PROPERTIES.MAP_DETAILS.PIID
|
|
},
|
|
actionResult: {
|
|
piid: MIOT_SERVICES.MAP.PROPERTIES.ACTION_RESULT.PIID
|
|
}
|
|
}
|
|
}));
|
|
|
|
this.registerCapability(new capabilities.DreameTotalStatisticsCapability({
|
|
robot: this,
|
|
miot_properties: {
|
|
time: {
|
|
siid: MIOT_SERVICES.TOTAL_STATISTICS.SIID,
|
|
piid: MIOT_SERVICES.TOTAL_STATISTICS.PROPERTIES.TIME.PIID
|
|
},
|
|
area: {
|
|
siid: MIOT_SERVICES.TOTAL_STATISTICS.SIID,
|
|
piid: MIOT_SERVICES.TOTAL_STATISTICS.PROPERTIES.AREA.PIID
|
|
},
|
|
count: {
|
|
siid: MIOT_SERVICES.TOTAL_STATISTICS.SIID,
|
|
piid: MIOT_SERVICES.TOTAL_STATISTICS.PROPERTIES.COUNT.PIID
|
|
}
|
|
}
|
|
}));
|
|
|
|
this.registerCapability(new capabilities.DreameCurrentStatisticsCapability({
|
|
robot: this,
|
|
miot_properties: {
|
|
time: {
|
|
siid: MIOT_SERVICES.VACUUM_2.SIID,
|
|
piid: MIOT_SERVICES.VACUUM_2.PROPERTIES.CLEANING_TIME.PIID
|
|
},
|
|
area: {
|
|
siid: MIOT_SERVICES.VACUUM_2.SIID,
|
|
piid: MIOT_SERVICES.VACUUM_2.PROPERTIES.CLEANING_AREA.PIID
|
|
}
|
|
}
|
|
}));
|
|
|
|
[
|
|
capabilities.DreameVoicePackManagementCapability,
|
|
capabilities.DreameManualControlCapability,
|
|
capabilities.DreameDoNotDisturbCapability
|
|
].forEach(capability => {
|
|
this.registerCapability(new capability({robot: this}));
|
|
});
|
|
|
|
|
|
this.state.upsertFirstMatchingAttribute(new entities.state.attributes.AttachmentStateAttribute({
|
|
type: entities.state.attributes.AttachmentStateAttribute.TYPE.MOP,
|
|
attached: false
|
|
}));
|
|
}
|
|
|
|
onIncomingCloudMessage(msg) {
|
|
if (super.onIncomingCloudMessage(msg) === true) {
|
|
return true;
|
|
}
|
|
|
|
switch (msg.method) {
|
|
case "properties_changed": {
|
|
msg.params.forEach(e => {
|
|
switch (e.siid) {
|
|
case MIOT_SERVICES.MAP.SIID:
|
|
switch (e.piid) {
|
|
case MIOT_SERVICES.MAP.PROPERTIES.MAP_DATA.PIID:
|
|
/*
|
|
Most of the time, these will be P-Frames, which Valetudo ignores, however
|
|
sometimes, they may be I-Frames as well. Usually that's right when a new map
|
|
is being created, as then the map data is small enough to fit into a miio msg
|
|
*/
|
|
this.preprocessAndParseMap(e.value).catch(err => {
|
|
Logger.warn("Error while trying to parse map update", err);
|
|
});
|
|
break;
|
|
}
|
|
break;
|
|
case MIOT_SERVICES.VACUUM_1.SIID:
|
|
case MIOT_SERVICES.VACUUM_2.SIID:
|
|
case MIOT_SERVICES.BATTERY.SIID:
|
|
case MIOT_SERVICES.MAIN_BRUSH.SIID:
|
|
case MIOT_SERVICES.SIDE_BRUSH.SIID:
|
|
case MIOT_SERVICES.FILTER.SIID:
|
|
case MIOT_SERVICES.SENSOR.SIID:
|
|
case MIOT_SERVICES.MOP.SIID:
|
|
case MIOT_SERVICES.SECONDARY_FILTER.SIID:
|
|
case MIOT_SERVICES.DETERGENT.SIID:
|
|
case MIOT_SERVICES.MOP_EXPANSION.SIID:
|
|
case MIOT_SERVICES.MISC_STATES.SIID:
|
|
this.parseAndUpdateState([e]);
|
|
break;
|
|
case MIOT_SERVICES.DEVICE.SIID:
|
|
case 99: //This seems to be a duplicate of the device service
|
|
//Intentionally ignored
|
|
break;
|
|
case MIOT_SERVICES.AUDIO.SIID:
|
|
case MIOT_SERVICES.DND.SIID:
|
|
case MIOT_SERVICES.PERSISTENT_MAPS.SIID:
|
|
//Intentionally ignored since we only poll that info when required and therefore don't care about updates
|
|
break;
|
|
case MIOT_SERVICES.AUTO_EMPTY_DOCK.SIID:
|
|
case MIOT_SERVICES.TIMERS.SIID:
|
|
case MIOT_SERVICES.TOTAL_STATISTICS.SIID:
|
|
//Intentionally left blank (for now?)
|
|
break;
|
|
case MIOT_SERVICES.SILVER_ION.SIID:
|
|
case 21: //Something else that also seems to be some kind of consumable?
|
|
//Intentionally ignored for now, because I have no idea what that should be or where it could be located
|
|
//TODO: figure out
|
|
break;
|
|
case 10001:
|
|
/*
|
|
Seems to have something to do with the AI camera
|
|
Sample value: {"operType":"properties_changed","operation":"monitor","result":0,"status":0}
|
|
*/
|
|
//Intentionally ignored
|
|
break;
|
|
default:
|
|
Logger.warn("Unhandled property change ", e);
|
|
}
|
|
});
|
|
|
|
this.sendCloud({id: msg.id, "result":"ok"}).catch((err) => {
|
|
Logger.warn("Error while sending cloud ack", err);
|
|
});
|
|
return true;
|
|
}
|
|
case "props":
|
|
if (msg.params && msg.params.ota_state) {
|
|
this.sendCloud({id: msg.id, "result":"ok"}).catch((err) => {
|
|
Logger.warn("Error while sending cloud ack", err);
|
|
});
|
|
return true;
|
|
}
|
|
break;
|
|
case "event_occured": {
|
|
// This is sent by the robot after a cleanup has finished.
|
|
// It will contain the parameters of that past cleanup
|
|
// Therefore, we ignore it in our current status
|
|
|
|
this.sendCloud({id: msg.id, "result":"ok"}).catch((err) => {
|
|
Logger.warn("Error while sending cloud ack", err);
|
|
});
|
|
return true;
|
|
}
|
|
case "ali_lic": {
|
|
// ignore
|
|
return true;
|
|
}
|
|
case "vendor_lic": {
|
|
// ignore
|
|
return true;
|
|
}
|
|
case "lwt": {
|
|
// ignore
|
|
return true;
|
|
}
|
|
case "_sync.update_vacuum_mapinfo": {
|
|
// ignore
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* May be extended by children
|
|
*
|
|
* @return {Array<{piid: number, siid: number}>}
|
|
*/
|
|
getStatePropertiesToPoll() {
|
|
const properties = [
|
|
{
|
|
siid: MIOT_SERVICES.VACUUM_2.SIID,
|
|
piid: MIOT_SERVICES.VACUUM_2.PROPERTIES.MODE.PIID
|
|
},
|
|
{
|
|
siid: MIOT_SERVICES.VACUUM_2.SIID,
|
|
piid: MIOT_SERVICES.VACUUM_2.PROPERTIES.TASK_STATUS.PIID
|
|
},
|
|
{
|
|
siid: MIOT_SERVICES.VACUUM_2.SIID,
|
|
piid: MIOT_SERVICES.VACUUM_2.PROPERTIES.FAN_SPEED.PIID
|
|
},
|
|
{
|
|
siid: MIOT_SERVICES.VACUUM_2.SIID,
|
|
piid: MIOT_SERVICES.VACUUM_2.PROPERTIES.WATER_TANK_ATTACHMENT.PIID
|
|
},
|
|
{
|
|
siid: MIOT_SERVICES.VACUUM_2.SIID,
|
|
piid: MIOT_SERVICES.VACUUM_2.PROPERTIES.ERROR_CODE.PIID
|
|
},
|
|
{
|
|
siid: MIOT_SERVICES.BATTERY.SIID,
|
|
piid: MIOT_SERVICES.BATTERY.PROPERTIES.LEVEL.PIID
|
|
},
|
|
{
|
|
siid: MIOT_SERVICES.BATTERY.SIID,
|
|
piid: MIOT_SERVICES.BATTERY.PROPERTIES.CHARGING.PIID
|
|
}
|
|
];
|
|
|
|
if (this.highResolutionWaterGrades) {
|
|
properties.push({
|
|
siid: MIOT_SERVICES.MOP_EXPANSION.SIID,
|
|
piid: MIOT_SERVICES.MOP_EXPANSION.PROPERTIES.HIGH_RES_WATER_USAGE.PIID
|
|
});
|
|
} else {
|
|
properties.push({
|
|
siid: MIOT_SERVICES.VACUUM_2.SIID,
|
|
piid: MIOT_SERVICES.VACUUM_2.PROPERTIES.WATER_USAGE.PIID
|
|
});
|
|
}
|
|
|
|
return properties;
|
|
}
|
|
|
|
async pollState() {
|
|
const response = await this.sendCommand(
|
|
"get_properties",
|
|
this.statePropertiesToPoll
|
|
);
|
|
|
|
if (response) {
|
|
this.parseAndUpdateState(response);
|
|
}
|
|
|
|
return this.state;
|
|
}
|
|
|
|
|
|
parseAndUpdateState(data) {
|
|
if (!Array.isArray(data)) {
|
|
Logger.error("Received non-array state", data);
|
|
return;
|
|
}
|
|
|
|
data.forEach(elem => {
|
|
switch (elem.siid) {
|
|
case MIOT_SERVICES.VACUUM_1.SIID: {
|
|
//intentionally left blank since there's nothing here that isn't also in VACUUM_2
|
|
//
|
|
// Update 2024-06-04: Dreame repurposed PIID 1 on newer robots such as the X40.
|
|
// It now doesn't contain the same as VACUUM_2 MODE but instead a new and extended status enum
|
|
// with stuff such as "returning to the dock to install mops"
|
|
// At the time of writing, the "old" VACUUM_2 MODE still works, so no need to map these for now
|
|
break;
|
|
}
|
|
|
|
case MIOT_SERVICES.VACUUM_2.SIID: {
|
|
switch (elem.piid) {
|
|
case MIOT_SERVICES.VACUUM_2.PROPERTIES.MODE.PIID: {
|
|
this.mode = elem.value;
|
|
|
|
this.stateNeedsUpdate = true;
|
|
break;
|
|
}
|
|
case MIOT_SERVICES.VACUUM_2.PROPERTIES.ERROR_CODE.PIID: {
|
|
this.errorCode = elem.value ?? "";
|
|
|
|
this.stateNeedsUpdate = true;
|
|
break;
|
|
}
|
|
case MIOT_SERVICES.VACUUM_2.PROPERTIES.TASK_STATUS.PIID: {
|
|
this.taskStatus = elem.value;
|
|
|
|
this.stateNeedsUpdate = true;
|
|
break;
|
|
}
|
|
case MIOT_SERVICES.VACUUM_2.PROPERTIES.FAN_SPEED.PIID: {
|
|
let matchingFanSpeed = Object.keys(DreameValetudoRobot.FAN_SPEEDS).find(key => {
|
|
return DreameValetudoRobot.FAN_SPEEDS[key] === elem.value;
|
|
});
|
|
|
|
if (matchingFanSpeed === undefined) {
|
|
Logger.warn(`Received unknown fan speed ${elem.value}`);
|
|
}
|
|
|
|
this.state.upsertFirstMatchingAttribute(new stateAttrs.PresetSelectionStateAttribute({
|
|
metaData: {
|
|
rawValue: elem.value
|
|
},
|
|
type: stateAttrs.PresetSelectionStateAttribute.TYPE.FAN_SPEED,
|
|
value: matchingFanSpeed
|
|
}));
|
|
break;
|
|
}
|
|
|
|
case MIOT_SERVICES.VACUUM_2.PROPERTIES.WATER_USAGE.PIID: {
|
|
let matchingWaterGrade = Object.keys(this.waterGrades).find(key => {
|
|
return this.waterGrades[key] === elem.value;
|
|
});
|
|
|
|
if (matchingWaterGrade === undefined) {
|
|
Logger.warn(`Received unknown water grade ${elem.value}`);
|
|
}
|
|
|
|
this.state.upsertFirstMatchingAttribute(new stateAttrs.PresetSelectionStateAttribute({
|
|
metaData: {
|
|
rawValue: elem.value
|
|
},
|
|
type: stateAttrs.PresetSelectionStateAttribute.TYPE.WATER_GRADE,
|
|
value: matchingWaterGrade
|
|
}));
|
|
break;
|
|
}
|
|
case MIOT_SERVICES.VACUUM_2.PROPERTIES.WATER_TANK_ATTACHMENT.PIID: {
|
|
const supportedAttachments = this.getModelDetails().supportedAttachments;
|
|
const parsedAttachmentStates = {
|
|
[stateAttrs.AttachmentStateAttribute.TYPE.WATERTANK]: elem.value !== 0,
|
|
[stateAttrs.AttachmentStateAttribute.TYPE.MOP]: elem.value !== 0,
|
|
};
|
|
|
|
if (this.detailedAttachmentReport) {
|
|
parsedAttachmentStates[stateAttrs.AttachmentStateAttribute.TYPE.WATERTANK] = !!(elem.value & 0b01);
|
|
parsedAttachmentStates[stateAttrs.AttachmentStateAttribute.TYPE.MOP] = !!(elem.value & 0b10);
|
|
}
|
|
|
|
|
|
if (supportedAttachments.includes(stateAttrs.AttachmentStateAttribute.TYPE.WATERTANK)) {
|
|
this.state.upsertFirstMatchingAttribute(new entities.state.attributes.AttachmentStateAttribute({
|
|
type: stateAttrs.AttachmentStateAttribute.TYPE.WATERTANK,
|
|
attached: parsedAttachmentStates[stateAttrs.AttachmentStateAttribute.TYPE.WATERTANK]
|
|
}));
|
|
}
|
|
|
|
if (supportedAttachments.includes(stateAttrs.AttachmentStateAttribute.TYPE.MOP)) {
|
|
this.state.upsertFirstMatchingAttribute(new entities.state.attributes.AttachmentStateAttribute({
|
|
type: stateAttrs.AttachmentStateAttribute.TYPE.MOP,
|
|
attached: parsedAttachmentStates[stateAttrs.AttachmentStateAttribute.TYPE.MOP]
|
|
}));
|
|
}
|
|
|
|
break;
|
|
}
|
|
case MIOT_SERVICES.VACUUM_2.PROPERTIES.MOP_DOCK_STATUS.PIID: {
|
|
this.state.upsertFirstMatchingAttribute(new entities.state.attributes.DockStatusStateAttribute({
|
|
value: DreameValetudoRobot.MOP_DOCK_STATUS_MAP[elem.value]
|
|
}));
|
|
|
|
break;
|
|
}
|
|
case MIOT_SERVICES.VACUUM_2.PROPERTIES.MOP_DOCK_SETTINGS.PIID: {
|
|
const deserializedValue = DreameUtils.DESERIALIZE_MOP_DOCK_SETTINGS(elem.value);
|
|
|
|
let matchingOperationMode = Object.keys(this.operationModes).find(key => {
|
|
return this.operationModes[key] === deserializedValue.operationMode;
|
|
});
|
|
|
|
if (matchingOperationMode === undefined) {
|
|
Logger.warn(`Received unknown operation mode ${elem.value}`);
|
|
}
|
|
|
|
this.state.upsertFirstMatchingAttribute(new stateAttrs.PresetSelectionStateAttribute({
|
|
type: stateAttrs.PresetSelectionStateAttribute.TYPE.OPERATION_MODE,
|
|
value: matchingOperationMode
|
|
}));
|
|
break;
|
|
}
|
|
|
|
case DreameGen2ValetudoRobot.MIOT_SERVICES.VACUUM_2.PROPERTIES.MISC_TUNABLES.PIID: {
|
|
const deserializedTunables = DreameUtils.DESERIALIZE_MISC_TUNABLES(elem.value);
|
|
|
|
if (deserializedTunables.SmartHost > 0) {
|
|
Logger.info("Disabling CleanGenius");
|
|
// CleanGenius breaks most controls in Valetudo without any user feedback
|
|
// Thus, we just automatically disable it instead of making every functionality aware of it
|
|
|
|
this.helper.writeProperty(
|
|
DreameGen2ValetudoRobot.MIOT_SERVICES.VACUUM_2.SIID,
|
|
DreameGen2ValetudoRobot.MIOT_SERVICES.VACUUM_2.PROPERTIES.MISC_TUNABLES.PIID,
|
|
DreameUtils.SERIALIZE_MISC_TUNABLES_SINGLE_TUNABLE({
|
|
SmartHost: 0
|
|
})
|
|
).catch(e => {
|
|
Logger.warn("Error while disabling CleanGenius", e);
|
|
});
|
|
}
|
|
|
|
if (deserializedTunables.FluctuationConfirmResult > 0) {
|
|
if (deserializedTunables.FluctuationTestResult !== 6) { // 6 Means success
|
|
const errorString = DreameConst.WATER_HOOKUP_ERRORS[deserializedTunables.FluctuationTestResult];
|
|
|
|
this.valetudoEventStore.raise(new ErrorStateValetudoEvent({
|
|
message: `Water Hookup Error. ${errorString ?? "Unknown error " + deserializedTunables.FluctuationTestResult}`
|
|
}));
|
|
}
|
|
|
|
|
|
this.helper.writeProperty(
|
|
DreameGen2ValetudoRobot.MIOT_SERVICES.VACUUM_2.SIID,
|
|
DreameGen2ValetudoRobot.MIOT_SERVICES.VACUUM_2.PROPERTIES.MISC_TUNABLES.PIID,
|
|
DreameUtils.SERIALIZE_MISC_TUNABLES_SINGLE_TUNABLE({
|
|
FluctuationConfirmResult: 0
|
|
})
|
|
).catch(e => {
|
|
Logger.warn("Error while confirming water hookup test result", e);
|
|
});
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case MIOT_SERVICES.BATTERY.SIID: {
|
|
switch (elem.piid) {
|
|
case MIOT_SERVICES.BATTERY.PROPERTIES.LEVEL.PIID:
|
|
this.state.upsertFirstMatchingAttribute(new stateAttrs.BatteryStateAttribute({
|
|
level: elem.value
|
|
}));
|
|
break;
|
|
case MIOT_SERVICES.BATTERY.PROPERTIES.CHARGING.PIID:
|
|
/*
|
|
1 = On Charger
|
|
2 = Not on Charger
|
|
5 = Returning to Charger
|
|
*/
|
|
this.isCharging = elem.value === 1;
|
|
this.stateNeedsUpdate = true;
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
|
|
case MIOT_SERVICES.MAIN_BRUSH.SIID:
|
|
case MIOT_SERVICES.SIDE_BRUSH.SIID:
|
|
case MIOT_SERVICES.FILTER.SIID:
|
|
case MIOT_SERVICES.SENSOR.SIID:
|
|
case MIOT_SERVICES.MOP.SIID:
|
|
case MIOT_SERVICES.SECONDARY_FILTER.SIID:
|
|
case MIOT_SERVICES.DETERGENT.SIID:
|
|
if (this.capabilities[ConsumableMonitoringCapability.TYPE]) {
|
|
this.capabilities[ConsumableMonitoringCapability.TYPE].parseConsumablesMessage(elem);
|
|
}
|
|
break;
|
|
case MIOT_SERVICES.MOP_EXPANSION.SIID: {
|
|
switch (elem.piid) {
|
|
case MIOT_SERVICES.MOP_EXPANSION.PROPERTIES.HIGH_RES_WATER_USAGE.PIID: {
|
|
let matchingWaterGrade = Object.keys(this.waterGrades).find(key => {
|
|
return this.waterGrades[key] === elem.value;
|
|
});
|
|
|
|
if (matchingWaterGrade === undefined) {
|
|
Logger.warn(`Received unknown water grade ${elem.value}`);
|
|
}
|
|
|
|
this.state.upsertFirstMatchingAttribute(new stateAttrs.PresetSelectionStateAttribute({
|
|
metaData: {
|
|
rawValue: elem.value
|
|
},
|
|
type: stateAttrs.PresetSelectionStateAttribute.TYPE.WATER_GRADE,
|
|
value: matchingWaterGrade
|
|
}));
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case MIOT_SERVICES.MISC_STATES.SIID: {
|
|
// Ignored for now
|
|
break;
|
|
}
|
|
default:
|
|
Logger.warn("Unhandled property update", elem);
|
|
}
|
|
});
|
|
|
|
|
|
if (this.stateNeedsUpdate === true) {
|
|
let newState;
|
|
let statusValue;
|
|
let statusFlag;
|
|
let statusError;
|
|
let statusMetaData = {};
|
|
|
|
/*
|
|
Somewhere in 2022, Dreame firmwares gained the ability to report multiple error codes at once
|
|
only separated by a comma. At the time of writing, the Valetudo abstraction does not support that.
|
|
|
|
Most of the time, it's two codes with one code being a non-error reminder error code.
|
|
Additionally, in all reported cases where there were two actual error codes, both of them mapped
|
|
to the same error type and description in Valetudo.
|
|
|
|
We can therefore simply filter the non-error codes and pick the first remaining element.
|
|
*/
|
|
if (this.errorCode.includes(",")) {
|
|
let errorArray = this.errorCode.split(",");
|
|
|
|
errorArray = errorArray.filter(e => !["68", "114", "122"].includes(e));
|
|
|
|
this.errorCode = errorArray[0] ?? "";
|
|
}
|
|
|
|
|
|
if (this.errorCode === "0" || this.errorCode === "") {
|
|
statusValue = DreameValetudoRobot.STATUS_MAP[this.mode]?.value ?? stateAttrs.StatusStateAttribute.VALUE.IDLE;
|
|
statusFlag = DreameValetudoRobot.STATUS_MAP[this.mode]?.flag;
|
|
|
|
if (statusValue === stateAttrs.StatusStateAttribute.VALUE.DOCKED && this.taskStatus !== 0) {
|
|
// Robot has a pending task but is charging due to low battery and will resume when battery >= 80%
|
|
statusFlag = stateAttrs.StatusStateAttribute.FLAG.RESUMABLE;
|
|
} else if (statusValue === stateAttrs.StatusStateAttribute.VALUE.IDLE && statusFlag === undefined && this.isCharging === true) {
|
|
statusValue = stateAttrs.StatusStateAttribute.VALUE.DOCKED;
|
|
}
|
|
} else {
|
|
if (this.errorCode === "68") { // Docked with mop still attached. For some reason, dreame decided to have this as an error
|
|
statusValue = stateAttrs.StatusStateAttribute.VALUE.DOCKED;
|
|
|
|
if (!this.hasCapability(capabilities.DreameMopDockDryManualTriggerCapability.TYPE)) {
|
|
this.valetudoEventStore.raise(new MopAttachmentReminderValetudoEvent({}));
|
|
}
|
|
} else if (this.errorCode === "114") { // Reminder message to regularly clean the mop dock
|
|
statusValue = stateAttrs.StatusStateAttribute.VALUE.DOCKED;
|
|
} else if (this.errorCode === "122") { // Apparently just an info that the water hookup (kit) worked successfully?
|
|
statusValue = stateAttrs.StatusStateAttribute.VALUE.DOCKED;
|
|
} else {
|
|
statusValue = stateAttrs.StatusStateAttribute.VALUE.ERROR;
|
|
|
|
statusError = DreameValetudoRobot.MAP_ERROR_CODE(this.errorCode);
|
|
}
|
|
|
|
}
|
|
|
|
newState = new stateAttrs.StatusStateAttribute({
|
|
value: statusValue,
|
|
flag: statusFlag,
|
|
metaData: statusMetaData,
|
|
error: statusError
|
|
});
|
|
|
|
this.state.upsertFirstMatchingAttribute(newState);
|
|
|
|
if (newState.isActiveState) {
|
|
this.pollMap();
|
|
}
|
|
|
|
this.stateNeedsUpdate = false;
|
|
}
|
|
|
|
|
|
|
|
this.emitStateAttributesUpdated();
|
|
}
|
|
|
|
startup() {
|
|
super.startup();
|
|
|
|
if (this.config.get("embedded") === true) {
|
|
try {
|
|
const parsedCmdline = LinuxTools.READ_PROC_CMDLINE();
|
|
|
|
if (parsedCmdline.partitions[parsedCmdline.root]) {
|
|
Logger.info(`Current rootfs: ${parsedCmdline.partitions[parsedCmdline.root]} (${parsedCmdline.root})`);
|
|
}
|
|
} catch (e) {
|
|
Logger.warn("Unable to read /proc/cmdline", e);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
DreameGen2ValetudoRobot.MIOT_SERVICES = MIOT_SERVICES;
|
|
|
|
DreameGen2ValetudoRobot.OPERATION_MODES = Object.freeze({
|
|
[stateAttrs.PresetSelectionStateAttribute.MODE.VACUUM]: 0,
|
|
[stateAttrs.PresetSelectionStateAttribute.MODE.MOP]: 1,
|
|
[stateAttrs.PresetSelectionStateAttribute.MODE.VACUUM_AND_MOP]: 2,
|
|
});
|
|
|
|
DreameGen2ValetudoRobot.HIGH_RESOLUTION_WATER_GRADES = Object.freeze({
|
|
[stateAttrs.PresetSelectionStateAttribute.INTENSITY.MIN]: 1,
|
|
[stateAttrs.PresetSelectionStateAttribute.INTENSITY.LOW]: 8,
|
|
[stateAttrs.PresetSelectionStateAttribute.INTENSITY.MEDIUM]: 16,
|
|
[stateAttrs.PresetSelectionStateAttribute.INTENSITY.HIGH]: 24,
|
|
[stateAttrs.PresetSelectionStateAttribute.INTENSITY.MAX]: 32,
|
|
});
|
|
|
|
|
|
module.exports = DreameGen2ValetudoRobot;
|