Valetudo/backend/lib/robots/dreame/Dreame1CValetudoRobot.js

620 lines
24 KiB
JavaScript

const capabilities = require("./capabilities");
const DreameMiotServices = require("./DreameMiotServices");
const DreameValetudoRobot = require("./DreameValetudoRobot");
const entities = require("../../entities");
const Logger = require("../../Logger");
const MiioValetudoRobot = require("../MiioValetudoRobot");
const ValetudoRestrictedZone = require("../../entities/core/ValetudoRestrictedZone");
const ValetudoSelectionPreset = require("../../entities/core/ValetudoSelectionPreset");
const stateAttrs = entities.state.attributes;
const MIOT_SERVICES = DreameMiotServices["1C"];
class Dreame1CValetudoRobot extends DreameValetudoRobot {
/**
*
* @param {object} options
* @param {import("../../Configuration")} options.config
* @param {import("../../ValetudoEventStore")} options.valetudoEventStore
*/
constructor(options) {
super(
Object.assign(
{},
options,
{
miotServices: {
MAP: MIOT_SERVICES.MAP
}
}
)
);
this.isCharging = false;
this.registerCapability(new capabilities.Dreame1CBasicControlCapability({
robot: this,
miot_actions: {
start: {
siid: MIOT_SERVICES.VACUUM_2.SIID,
aiid: MIOT_SERVICES.VACUUM_2.ACTIONS.START.AIID
},
stop: {
siid: MIOT_SERVICES.VACUUM_2.SIID,
aiid: MIOT_SERVICES.VACUUM_2.ACTIONS.PAUSE.AIID
},
pause: {
siid: MIOT_SERVICES.VACUUM_2.SIID,
aiid: MIOT_SERVICES.VACUUM_2.ACTIONS.PAUSE.AIID
},
home: {
siid: MIOT_SERVICES.BATTERY.SIID,
aiid: MIOT_SERVICES.BATTERY.ACTIONS.START_CHARGE.AIID
}
}
}));
this.registerCapability(new capabilities.Dreame1CManualControlCapability({
robot: this,
miot_actions: {
move: {
siid: MIOT_SERVICES.MANUAL_CONTROL.SIID,
aiid: MIOT_SERVICES.MANUAL_CONTROL.ACTIONS.MOVE.AIID
},
stop: {
siid: MIOT_SERVICES.MANUAL_CONTROL.SIID,
aiid: MIOT_SERVICES.MANUAL_CONTROL.ACTIONS.STOP.AIID
}
},
miot_properties: {
angle: {
piid: MIOT_SERVICES.MANUAL_CONTROL.PROPERTIES.ANGLE.PIID
},
velocity: {
piid: MIOT_SERVICES.MANUAL_CONTROL.PROPERTIES.VELOCITY.PIID
}
}
}));
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.DreameWaterUsageControlCapability({
robot: this,
presets: Object.keys(DreameValetudoRobot.WATER_GRADES).map(k => {
return new ValetudoSelectionPreset({name: k, value: DreameValetudoRobot.WATER_GRADES[k]});
}),
siid: MIOT_SERVICES.VACUUM_2.SIID,
piid: MIOT_SERVICES.VACUUM_2.PROPERTIES.WATER_USAGE.PIID
}));
this.registerCapability(new capabilities.DreameLocateCapability({
robot: this,
siid: MIOT_SERVICES.LOCATE.SIID,
aiid: MIOT_SERVICES.LOCATE.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: 1,
customOrderSupported: true
}));
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.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.consumableMonitoringCapability = new capabilities.Dreame1CConsumableMonitoringCapability({
robot: this,
miot_properties: {
main_brush: {
siid: MIOT_SERVICES.MAIN_BRUSH.SIID,
piid: MIOT_SERVICES.MAIN_BRUSH.PROPERTIES.TIME_LEFT.PIID
},
side_brush: {
siid: MIOT_SERVICES.SIDE_BRUSH.SIID,
piid: MIOT_SERVICES.SIDE_BRUSH.PROPERTIES.TIME_LEFT.PIID
},
filter: {
siid: MIOT_SERVICES.FILTER.SIID,
piid: MIOT_SERVICES.FILTER.PROPERTIES.TIME_LEFT.PIID
}
},
miot_actions: {
reset_main_brush: {
siid: MIOT_SERVICES.MAIN_BRUSH.SIID,
aiid: MIOT_SERVICES.MAIN_BRUSH.ACTIONS.RESET.AIID
},
reset_side_brush: {
siid: MIOT_SERVICES.SIDE_BRUSH.SIID,
aiid: MIOT_SERVICES.SIDE_BRUSH.ACTIONS.RESET.AIID
},
reset_filter: {
siid: MIOT_SERVICES.FILTER.SIID,
aiid: MIOT_SERVICES.FILTER.ACTIONS.RESET.AIID
}
}
});
this.registerCapability(this.consumableMonitoringCapability);
//Looks like this is always enabled for LIDAR robots but a toggle for vSlam ones?
this.registerCapability(new capabilities.DreamePersistentMapControlCapability({
robot: this,
siid: MIOT_SERVICES.VACUUM_2.SIID,
piid: MIOT_SERVICES.VACUUM_2.PROPERTIES.PERSISTENT_MAPS.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.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.Dreame1CZoneCleaningCapability({
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
}
},
zoneCleaningModeId: 19
}));
this.registerCapability(new capabilities.DreameSpeakerVolumeControlCapability({
robot: this,
siid: MIOT_SERVICES.VOICE.SIID,
piid: MIOT_SERVICES.VOICE.PROPERTIES.VOLUME.PIID
}));
this.registerCapability(new capabilities.DreameSpeakerTestCapability({
robot: this,
siid: MIOT_SERVICES.LOCATE.SIID,
aiid: MIOT_SERVICES.LOCATE.ACTIONS.VOLUME_TEST.AIID
}));
this.registerCapability(new capabilities.Dreame1CVoicePackManagementCapability({
robot: this,
siid: MIOT_SERVICES.VOICE.SIID,
aiid: MIOT_SERVICES.VOICE.ACTIONS.DOWNLOAD_VOICEPACK.AIID,
hash_piid: MIOT_SERVICES.VOICE.PROPERTIES.HASH.PIID,
url_piid: MIOT_SERVICES.VOICE.PROPERTIES.URL.PIID,
active_voicepack_piid: MIOT_SERVICES.VOICE.PROPERTIES.ACTIVE_VOICEPACK.PIID,
size_piid: MIOT_SERVICES.VOICE.PROPERTIES.SIZE.PIID
}));
this.registerCapability(new capabilities.DreameTotalStatisticsCapability({
robot: this,
miot_properties: {
time: {
siid: MIOT_SERVICES.VACUUM_2.SIID,
piid: MIOT_SERVICES.VACUUM_2.PROPERTIES.TOTAL_STATISTICS_TIME.PIID
},
area: {
siid: MIOT_SERVICES.VACUUM_2.SIID,
piid: MIOT_SERVICES.VACUUM_2.PROPERTIES.TOTAL_STATISTICS_AREA.PIID
},
count: {
siid: MIOT_SERVICES.VACUUM_2.SIID,
piid: MIOT_SERVICES.VACUUM_2.PROPERTIES.TOTAL_STATISTICS_COUNT.PIID
}
}
}));
this.registerCapability(new capabilities.DreameCurrentStatisticsCapability({
robot: this,
miot_properties: {
time: {
siid: MIOT_SERVICES.VACUUM_2.SIID,
piid: MIOT_SERVICES.VACUUM_2.PROPERTIES.CURRENT_STATISTICS_TIME.PIID
},
area: {
siid: MIOT_SERVICES.VACUUM_2.SIID,
piid: MIOT_SERVICES.VACUUM_2.PROPERTIES.CURRENT_STATISTICS_AREA.PIID
}
}
}));
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
}
}
}));
}
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:
//intentional since these will only be P-Frames which are unsupported (yet?)
break;
case MIOT_SERVICES.MAP.PROPERTIES.CLOUD_FILE_NAME.PIID:
//intentionally left blank since we don't care about this
break;
default:
Logger.debug("Unhandled Map property change ", e);
}
break;
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:
this.parseAndUpdateState([e]);
break;
default:
Logger.debug("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": {
switch (msg.params.siid) {
case MIOT_SERVICES.VACUUM_2.SIID:
this.parseAndUpdateState(msg.params.arguments.map(a => {
return {
siid: msg.params.siid,
piid: a.piid,
value: a.value
};
}));
this.sendCloud({id: msg.id, "result":"ok"}).catch((err) => {
Logger.warn("Error while sending cloud ack", err);
});
break;
default:
Logger.debug("Unhandled event", msg);
this.sendCloud({id: msg.id, "result":"ok"}).catch((err) => {
Logger.warn("Error while sending cloud ack", err);
});
}
return true;
}
}
return false;
}
async pollState() {
const response = await this.sendCommand("get_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_USAGE.PIID
},
{
siid: MIOT_SERVICES.VACUUM_2.SIID,
piid: MIOT_SERVICES.VACUUM_2.PROPERTIES.WATER_TANK_ATTACHMENT.PIID
},
{
siid: MIOT_SERVICES.ERROR.SIID,
piid: MIOT_SERVICES.ERROR.PROPERTIES.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
}
].map(e => {
e.did = this.deviceId;
return e;
}));
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.ERROR.SIID: {
this.errorCode = typeof elem.value === "number" ? elem.value.toString() : elem.value;
this.stateNeedsUpdate = true;
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.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;
});
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(DreameValetudoRobot.WATER_GRADES).find(key => {
return DreameValetudoRobot.WATER_GRADES[key] === 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: {
this.state.upsertFirstMatchingAttribute(new entities.state.attributes.AttachmentStateAttribute({
type: entities.state.attributes.AttachmentStateAttribute.TYPE.WATERTANK,
attached: elem.value === 1
}));
this.state.upsertFirstMatchingAttribute(new entities.state.attributes.AttachmentStateAttribute({
type: entities.state.attributes.AttachmentStateAttribute.TYPE.MOP,
attached: elem.value === 1
}));
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 and charging
2 = Not on Charger
4 = On Charger and fully charged
5 = Returning to Charger
*/
this.isCharging = elem.value === 4 || 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:
this.consumableMonitoringCapability.parseConsumablesMessage(elem);
break;
}
});
if (this.stateNeedsUpdate === true) {
let newState;
let statusValue;
let statusFlag;
let statusError;
let statusMetaData = {};
if (this.errorCode === "0" || this.errorCode === "" || this.errorCode === 0 || this.errorCode === undefined) {
statusValue = DreameValetudoRobot.STATUS_MAP[this.mode]?.value ?? stateAttrs.StatusStateAttribute.VALUE.IDLE;
statusFlag = DreameValetudoRobot.STATUS_MAP[this.mode]?.flag;
if (this.isCharging === true) {
statusValue = stateAttrs.StatusStateAttribute.VALUE.DOCKED;
statusFlag = undefined;
}
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 {
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();
}
getModelName() {
return "1C";
}
static IMPLEMENTATION_AUTO_DETECTION_HANDLER() {
const deviceConf = MiioValetudoRobot.READ_DEVICE_CONF(DreameValetudoRobot.DEVICE_CONF_PATH);
return !!(deviceConf && ["dreame.vacuum.ma1808", "dreame.vacuum.mb1808", "dreame.vacuum.mc1808"].includes(deviceConf.model));
}
}
module.exports = Dreame1CValetudoRobot;