refactor(core): Move map polling orchestration logic into ValetudoRobot base class

This commit is contained in:
Sören Beye 2025-08-17 16:52:03 +02:00
parent 453ac70f53
commit b12fb03136
6 changed files with 112 additions and 110 deletions

View File

@ -7,9 +7,13 @@ const entities = require("../entities");
const ErrorStateValetudoEvent = require("../valetudo_events/events/ErrorStateValetudoEvent");
const Logger = require("../Logger");
const NotImplementedError = require("./NotImplementedError");
const Semaphore = require("semaphore");
const Tools = require("../utils/Tools");
const {ConsumableStateAttribute, StatusStateAttribute} = require("../entities/state/attributes");
/**
* @abstract
*/
class ValetudoRobot {
/**
*
@ -33,6 +37,10 @@ class ValetudoRobot {
hugeMap: false
};
this.mapPollMutex = Semaphore(1);
this.mapPollTimeout = undefined;
this.postActiveStateMapPollCooldownCredits = 0;
this.initInternalSubscriptions();
}
@ -133,18 +141,18 @@ class ValetudoRobot {
/*
This will be displayed only once after a map larger than 120 has been uploaded to a new Valetudo process
It should serve as an unobtrusive reminder that while you can use Valetudo in a commercial environment
without any limitations whatsoever, doing so and saving money because of that without giving anything
back is simply not a very nice thing to do.
While there would be the option to introduce something like license keys or a paid version, not only
would that be futile in an open source project, but it would also likely harm perfectly fine non-commercial
uses of Valetudo in e.g., your local hackerspace, art installations, etc.
In the end, I'd rather have some people take advantage of this permissive system than making
the project worse for all of its users to prevent that.
You're welcome
*/
Logger.info("Based on your map size, it looks like you might be using Valetudo in a commercial environment.");
@ -164,6 +172,86 @@ class ValetudoRobot {
//intentional
}
/**
*
* @protected
* @abstract
* @returns {Promise<any>}
*/
async executeMapPoll() {
throw new NotImplementedError();
}
/**
* @protected
* @param {any} pollResponse Implementation specific
* @return {number} seconds
*/
determineNextMapPollInterval(pollResponse) {
let repollSeconds = ValetudoRobot.MAP_POLLING_INTERVALS.DEFAULT;
let statusStateAttribute = this.state.getFirstMatchingAttribute({
attributeClass: StatusStateAttribute.name
});
let isActive = false;
if (statusStateAttribute && statusStateAttribute.isActiveState) {
isActive = true;
this.postActiveStateMapPollCooldownCredits = 3;
}
if (!isActive && this.postActiveStateMapPollCooldownCredits > 0) {
// Pretend that we're still in an active state to ensure that we catch map updates e.g. after docking
isActive = true;
this.postActiveStateMapPollCooldownCredits--;
}
if (isActive) {
repollSeconds = ValetudoRobot.MAP_POLLING_INTERVALS.ACTIVE;
if (this.flags.lowmemHost) {
repollSeconds *= 2;
}
if (this.flags.hugeMap) {
repollSeconds *= 2;
}
}
return repollSeconds;
}
/**
* @public
* @returns {void}
*/
pollMap() {
this.mapPollMutex.take(() => {
let repollSeconds = ValetudoRobot.MAP_POLLING_INTERVALS.DEFAULT;
// Clear pending timeout, since were starting a new poll right now.
if (this.mapPollTimeout) {
clearTimeout(this.mapPollTimeout);
this.mapPollTimeout = undefined;
}
this.executeMapPoll().then((response) => {
repollSeconds = this.determineNextMapPollInterval(response);
}).catch(() => {
repollSeconds = ValetudoRobot.MAP_POLLING_INTERVALS.ERROR;
}).finally(() => {
this.mapPollTimeout = setTimeout(() => {
this.pollMap();
}, repollSeconds * 1000);
this.mapPollMutex.leave();
});
});
}
async shutdown() {
//intentional
@ -283,6 +371,13 @@ ValetudoRobot.WELL_KNOWN_PROPERTIES = {
FIRMWARE_VERSION: "firmwareVersion"
};
const HUGE_MAP_THRESHOLD = 145 * 10000; //145m² in cm²
ValetudoRobot.MAP_POLLING_INTERVALS = Object.freeze({
DEFAULT: 60,
ACTIVE: 2,
ERROR: 30
});
const HUGE_MAP_THRESHOLD = 145 * 10_000; //145m² in cm²
module.exports = ValetudoRobot;

View File

@ -9,18 +9,17 @@ const Semaphore = require("semaphore");
const Dummycloud = require("../miio/Dummycloud");
const Logger = require("../Logger");
const MiioDummycloudNotConnectedError = require("../miio/MiioDummycloudNotConnectedError");
const MiioSocket = require("../miio/MiioSocket");
const NotImplementedError = require("../core/NotImplementedError");
const RetryWrapper = require("../miio/RetryWrapper");
const ValetudoRobot = require("../core/ValetudoRobot");
const entities = require("../entities");
const MiioDummycloudNotConnectedError = require("../miio/MiioDummycloudNotConnectedError");
const stateAttrs = entities.state.attributes;
/**
* @abstract
*/
class MiioValetudoRobot extends ValetudoRobot {
/**
*
* @param {object} options
* @param {import("../Configuration")} options.config
* @param {import("../ValetudoEventStore")} options.valetudoEventStore
@ -33,7 +32,7 @@ class MiioValetudoRobot extends ValetudoRobot {
this.robotConfig = this.config.get("robot");
this.implConfig = (this.robotConfig && this.robotConfig.implementationSpecificConfig) ?? {};
this.ip = this.implConfig.ip ?? "127.0.0.1";
this.ip = this.implConfig["ip"] ?? "127.0.0.1";
this.embeddedDummycloudIp = this.implConfig["dummycloudIp"] ?? "127.0.0.1";
this.dummycloudBindIp = this.implConfig["dummycloudBindIp"] ?? (this.config.get("embedded") ? "127.0.0.1" : "0.0.0.0");
@ -73,9 +72,6 @@ class MiioValetudoRobot extends ValetudoRobot {
});
this.fdsUploadSemaphore = Semaphore(2);
this.mapPollMutex = Semaphore(1);
this.mapPollTimeout = undefined;
this.postActiveStateMapPollCooldownCredits = 0;
this.expressApp = express();
this.fdsMockServer = http.createServer(this.expressApp);
@ -448,90 +444,10 @@ class MiioValetudoRobot extends ValetudoRobot {
*/
onCloudConnected() {
Logger.info("Dummycloud connected");
// start polling the map after a brief delay of 3.5s
// start polling the map after a brief delay of 5s
setTimeout(() => {
return this.pollMap();
}, 3500);
}
/**
* @public
* @returns {void}
*/
pollMap() {
this.mapPollMutex.take(() => {
let repollSeconds = MiioValetudoRobot.MAP_POLLING_INTERVALS.DEFAULT;
// Clear pending timeout, since were starting a new poll right now.
if (this.mapPollTimeout) {
clearTimeout(this.mapPollTimeout);
this.mapPollTimeout = undefined;
}
this.executeMapPoll().then((response) => {
repollSeconds = this.determineNextMapPollInterval(response);
}).catch(() => {
repollSeconds = MiioValetudoRobot.MAP_POLLING_INTERVALS.ERROR;
}).finally(() => {
this.mapPollTimeout = setTimeout(() => {
this.pollMap();
}, repollSeconds * 1000);
this.mapPollMutex.leave();
});
});
}
/**
*
* @protected
* @abstract
* @returns {Promise<any>}
*/
async executeMapPoll() {
throw new NotImplementedError();
}
/**
* @protected
* @param {any} pollResponse Implementation specific
* @return {number} seconds
*/
determineNextMapPollInterval(pollResponse) {
let repollSeconds = MiioValetudoRobot.MAP_POLLING_INTERVALS.DEFAULT;
let StatusStateAttribute = this.state.getFirstMatchingAttribute({
attributeClass: stateAttrs.StatusStateAttribute.name
});
let isActive = false;
if (StatusStateAttribute && StatusStateAttribute.isActiveState) {
isActive = true;
this.postActiveStateMapPollCooldownCredits = 3;
}
if (!isActive && this.postActiveStateMapPollCooldownCredits > 0) {
// Pretend that we're still in an active state to ensure that we catch map updates e.g. after docking
isActive = true;
this.postActiveStateMapPollCooldownCredits--;
}
if (isActive) {
repollSeconds = MiioValetudoRobot.MAP_POLLING_INTERVALS.ACTIVE;
if (this.flags.lowmemHost) {
repollSeconds *= 2;
}
if (this.flags.hugeMap) {
repollSeconds *= 2;
}
}
return repollSeconds;
}, 5000);
}
/**
@ -630,10 +546,4 @@ class MiioValetudoRobot extends ValetudoRobot {
const DEVICE_CONF_KEY_VALUE_REGEX = /^(?<key>[A-Za-z\d:.]+)=(?<value>[A-Za-z\d:.]+)$/;
const MAX_UPLOAD_FILESIZE = 4 * 1024 * 1024; // 4 MiB
MiioValetudoRobot.MAP_POLLING_INTERVALS = Object.freeze({
DEFAULT: 60,
ACTIVE: 2,
ERROR: 30
});
module.exports = MiioValetudoRobot;

View File

@ -17,7 +17,6 @@ const MiioValetudoRobot = require("../MiioValetudoRobot");
const MopAttachmentReminderValetudoEvent = require("../../valetudo_events/events/MopAttachmentReminderValetudoEvent");
const PendingMapChangeValetudoEvent = require("../../valetudo_events/events/PendingMapChangeValetudoEvent");
const ValetudoMap = require("../../entities/map/ValetudoMap");
const ValetudoRobot = require("../../core/ValetudoRobot");
const ValetudoRobotError = require("../../entities/core/ValetudoRobotError");
const stateAttrs = entities.state.attributes;
@ -284,7 +283,7 @@ class DreameValetudoRobot extends MiioValetudoRobot {
const firmwareVersion = this.getFirmwareVersion();
if (firmwareVersion.valid) {
ourProps[ValetudoRobot.WELL_KNOWN_PROPERTIES.FIRMWARE_VERSION] = firmwareVersion.arm;
ourProps[DreameValetudoRobot.WELL_KNOWN_PROPERTIES.FIRMWARE_VERSION] = firmwareVersion.arm;
}
}

View File

@ -94,7 +94,7 @@ class MockRobot extends ValetudoRobot {
getProperties() {
const superProps = super.getProperties();
const ourProps = {
[ValetudoRobot.WELL_KNOWN_PROPERTIES.FIRMWARE_VERSION]: Tools.GET_VALETUDO_VERSION()
[MockRobot.WELL_KNOWN_PROPERTIES.FIRMWARE_VERSION]: Tools.GET_VALETUDO_VERSION()
};
return Object.assign(

View File

@ -13,7 +13,6 @@ const MiioDummycloudNotConnectedError = require("../../miio/MiioDummycloudNotCon
const MiioValetudoRobot = require("../MiioValetudoRobot");
const PendingMapChangeValetudoEvent = require("../../valetudo_events/events/PendingMapChangeValetudoEvent");
const ValetudoMap = require("../../entities/map/ValetudoMap");
const ValetudoRobot = require("../../core/ValetudoRobot");
const ValetudoRobotError = require("../../entities/core/ValetudoRobotError");
const ValetudoSelectionPreset = require("../../entities/core/ValetudoSelectionPreset");
@ -485,7 +484,7 @@ class RoborockValetudoRobot extends MiioValetudoRobot {
repollSeconds += 1;
} else {
// This fixes the map not being available on boot for another 60 seconds
repollSeconds = MiioValetudoRobot.MAP_POLLING_INTERVALS.ACTIVE;
repollSeconds = RoborockValetudoRobot.MAP_POLLING_INTERVALS.ACTIVE;
}
}
}
@ -596,7 +595,7 @@ class RoborockValetudoRobot extends MiioValetudoRobot {
const firmwareVersion = this.getFirmwareVersion();
if (firmwareVersion) {
ourProps[ValetudoRobot.WELL_KNOWN_PROPERTIES.FIRMWARE_VERSION] = firmwareVersion;
ourProps[RoborockValetudoRobot.WELL_KNOWN_PROPERTIES.FIRMWARE_VERSION] = firmwareVersion;
}
}

View File

@ -7,7 +7,6 @@ const Logger = require("../../Logger");
const miioCapabilities = require("../common/miioCapabilities");
const MiioValetudoRobot = require("../MiioValetudoRobot");
const ThreeIRobotixMapParser = require("../3irobotix/ThreeIRobotixMapParser");
const ValetudoRobot = require("../../core/ValetudoRobot");
const ValetudoRobotError = require("../../entities/core/ValetudoRobotError");
const ValetudoSelectionPreset = require("../../entities/core/ValetudoSelectionPreset");
@ -561,7 +560,7 @@ class ViomiValetudoRobot extends MiioValetudoRobot {
const firmwareVersion = this.getFirmwareVersion();
if (firmwareVersion) {
ourProps[ValetudoRobot.WELL_KNOWN_PROPERTIES.FIRMWARE_VERSION] = firmwareVersion;
ourProps[ViomiValetudoRobot.WELL_KNOWN_PROPERTIES.FIRMWARE_VERSION] = firmwareVersion;
}
}