diff --git a/backend/lib/robots/cecotec/CecotecCongaRobot.js b/backend/lib/robots/cecotec/CecotecCongaRobot.js new file mode 100644 index 00000000..5ad6f6f8 --- /dev/null +++ b/backend/lib/robots/cecotec/CecotecCongaRobot.js @@ -0,0 +1,691 @@ +const capabilities = require("./capabilities"); +const fs = require("fs"); +const Logger = require("../../Logger"); +const PendingMapChangeValetudoEvent = require("../../valetudo_events/events/PendingMapChangeValetudoEvent"); +const SelectionPreset = require("../../entities/core/ValetudoSelectionPreset"); +const ValetudoRobot = require("../../core/ValetudoRobot"); +const { + BatteryStateAttribute, + StatusStateAttribute, + OperationModeStateAttribute, + PresetSelectionStateAttribute, + AttachmentStateAttribute, +} = require("../../entities/state/attributes"); +const { + CloudServer, + DeviceFanSpeed, + DeviceWaterLevel, + DeviceState, +} = require("@agnoc/core"); +const { + ValetudoMap, + PointMapEntity, + PathMapEntity, + MapLayer, + PolygonMapEntity, + LineMapEntity, +} = require("../../entities/map"); +const { DeviceMode } = require("@agnoc/core"); + +const DEVICE_MODE_TO_STATUS_STATE_FLAG = { + [DeviceMode.VALUE.NONE]: StatusStateAttribute.FLAG.NONE, + [DeviceMode.VALUE.SPOT]: StatusStateAttribute.FLAG.SPOT, + [DeviceMode.VALUE.ZONE]: StatusStateAttribute.FLAG.ZONE, +}; + +function throttle(callback, wait = 1000, immediate = true) { + let timeout = null; + let initialCall = true; + + return function () { + const callNow = immediate && initialCall; + const next = () => { + callback.apply(this, arguments); + timeout = null; + }; + + if (callNow) { + initialCall = false; + next(); + } + + if (!timeout) { + timeout = setTimeout(next, wait); + } + }; +} + +module.exports = class CecotecCongaRobot extends ValetudoRobot { + constructor(options) { + super(options); + + this.pathPoints = []; + this.server = new CloudServer(); + this.emitStateUpdated = throttle(this.emitStateUpdated.bind(this)); + this.emitStateAttributesUpdated = throttle( + this.emitStateAttributesUpdated.bind(this) + ); + this.emitMapUpdated = throttle(this.emitMapUpdated.bind(this)); + + this.registerCapability( + new capabilities.CecotecBasicControlCapability({ + robot: this, + }) + ); + this.registerCapability( + new capabilities.CecotecCarpetModeControlCapability({ + robot: this, + }) + ); + this.registerCapability( + new capabilities.CecotecCombinedVirtualRestrictionsCapability({ + robot: this, + }) + ); + this.registerCapability( + new capabilities.CecotecConsumableMonitoringCapability({ + robot: this, + }) + ); + this.registerCapability( + new capabilities.CecotecCurrentStatisticsCapability({ + robot: this, + }) + ); + this.registerCapability( + new capabilities.CecotecDoNotDisturbCapability({ + robot: this, + }) + ); + this.registerCapability( + new capabilities.CecotecFanSpeedControlCapability({ + robot: this, + presets: Object.values(DeviceFanSpeed.VALUE) + .filter((k) => { + return typeof k === "string"; + }) + .map((k) => { + return new SelectionPreset({ name: String(k), value: k }); + }), + }) + ); + this.registerCapability( + new capabilities.CecotecGoToLocationCapability({ + robot: this, + }) + ); + this.registerCapability( + new capabilities.CecotecLocateCapability({ + robot: this, + }) + ); + this.registerCapability( + new capabilities.CecotecManualControlCapability({ + robot: this, + }) + ); + this.registerCapability( + new capabilities.CecotecMapResetCapability({ + robot: this, + }) + ); + this.registerCapability( + new capabilities.CecotecMapSegmentationCapability({ + robot: this, + }) + ); + this.registerCapability( + new capabilities.CecotecMapSegmentEditCapability({ + robot: this, + }) + ); + this.registerCapability( + new capabilities.CecotecMapSegmentRenameCapability({ + robot: this, + }) + ); + this.registerCapability( + new capabilities.CecotecPendingMapChangeHandlingCapability({ + robot: this, + }) + ); + this.registerCapability( + new capabilities.CecotecPersistentMapControlCapability({ + robot: this, + }) + ); + this.registerCapability( + new capabilities.CecotecSpeakerTestCapability({ + robot: this, + }) + ); + this.registerCapability( + new capabilities.CecotecSpeakerVolumeControlCapability({ + robot: this, + }) + ); + this.registerCapability( + new capabilities.CecotecWaterUsageControlCapability({ + robot: this, + presets: Object.values(DeviceWaterLevel.VALUE).map((k) => { + return new SelectionPreset({ name: String(k), value: k }); + }), + }) + ); + if (this.config.get("embedded") === true) { + this.registerCapability( + new capabilities.CecotecWifiConfigurationCapability({ + robot: this, + networkInterface: "wlan0", + }) + ); + } + this.registerCapability( + new capabilities.CecotecZoneCleaningCapability({ + robot: this, + }) + ); + + this.server.on("error", this.onError.bind(this)); + this.server.on("addRobot", this.onAddRobot.bind(this)); + + void this.server.listen("0.0.0.0"); + } + + /** + * @returns {string} + */ + getManufacturer() { + return "Cecotec"; + } + + /** + * @returns {string} + */ + getModelName() { + return "Conga"; + } + + async shutdown() { + await this.server.close(); + } + + /** + * @type {import("@agnoc/core").Robot|undefined} + */ + get robot() { + return this.server.getRobots().find((robot) => { + return robot.isConnected; + }); + } + + onError(err) { + Logger.error(err); + } + + /** + * @param {import("@agnoc/core").Robot} robot + */ + onAddRobot(robot) { + Logger.info(`Added new robot with id '${robot.device.id}'`); + + robot.on("updateDevice", () => { + return this.onUpdateDevice(robot); + }); + robot.on("updateMap", () => { + return this.onUpdateMap(robot); + }); + robot.on("updateRobotPosition", () => { + return this.onUpdateRobotPosition(robot); + }); + robot.on("updateChargerPosition", () => { + return this.onUpdateChargerPosition(robot); + }); + } + + /** + * @param {import("@agnoc/core").Robot} robot + */ + onUpdateDevice(robot) { + const oldStatus = + this.state.getFirstMatchingAttributeByConstructor(StatusStateAttribute); + const newStatus = this.getStatusState(robot); + + // Reset path points when robot goes from docked to another state. + if ( + oldStatus && + oldStatus.value !== newStatus.value && + oldStatus.value === StatusStateAttribute.VALUE.DOCKED + ) { + this.pathPoints = []; + } + + this.state.upsertFirstMatchingAttribute(this.getBatteryState(robot)); + this.state.upsertFirstMatchingAttribute(this.getStatusState(robot)); + this.state.upsertFirstMatchingAttribute(this.getIntensityState(robot)); + this.state.upsertFirstMatchingAttribute(this.getWaterUsageState(robot)); + + this.state.upsertFirstMatchingAttribute( + new AttachmentStateAttribute({ + type: AttachmentStateAttribute.TYPE.DUSTBIN, + attached: !robot.device.hasMopAttached, + }) + ); + this.state.upsertFirstMatchingAttribute( + new AttachmentStateAttribute({ + type: AttachmentStateAttribute.TYPE.WATERTANK, + attached: robot.device.hasMopAttached, + }) + ); + this.state.upsertFirstMatchingAttribute( + new AttachmentStateAttribute({ + type: AttachmentStateAttribute.TYPE.MOP, + attached: robot.device.hasMopAttached, + }) + ); + this.state.upsertFirstMatchingAttribute( + new OperationModeStateAttribute({ + value: robot.device.hasMopAttached ? + OperationModeStateAttribute.VALUE.VACUUM_AND_MOP : + OperationModeStateAttribute.VALUE.VACUUM, + }) + ); + + this.emitStateAttributesUpdated(); + + if (robot.device.hasWaitingMap) { + const event = new PendingMapChangeValetudoEvent({}); + + if (!this.valetudoEventStore.getById(event.id)) { + this.valetudoEventStore.raise(event); + } + } + } + + /** + * @param {import("@agnoc/core").DeviceMap} map + */ + getRobotEntity(map) { + if (!map.robot) { + return; + } + + const offset = map.size.y; + const { x, y } = map.toPixel(map.robot.toCoordinates()); + + return new PointMapEntity({ + type: PointMapEntity.TYPE.ROBOT_POSITION, + points: [x, offset - y], + metaData: { + angle: map.robot.degrees, + }, + }); + } + + /** + * @param {import("@agnoc/core").DeviceMap} map + */ + getChargerEntity(map) { + if (!map.charger) { + return; + } + + const offset = map.size.y; + const { x, y } = map.toPixel(map.charger.toCoordinates()); + + return new PointMapEntity({ + type: PointMapEntity.TYPE.CHARGER_LOCATION, + points: [x, offset - y], + metaData: { + angle: map.charger.degrees, + }, + }); + } + + /** + * @param {import("@agnoc/core").DeviceMap} map + */ + getGoToTarget(map) { + if (!map.currentSpot) { + return; + } + + const offset = map.size.y; + const { x, y } = map.toPixel(map.currentSpot.toCoordinates()); + + return new PointMapEntity({ + type: PointMapEntity.TYPE.GO_TO_TARGET, + points: [x, offset - y], + metaData: { + angle: map.currentSpot.degrees, + }, + }); + } + + /** + * @param {import("@agnoc/core").DeviceMap} map + */ + updatePathPoints(map) { + if (!map.robot || map.robotPath.length > 0) { + return; + } + + const offset = map.size.y; + const { x, y } = map.toPixel(map.robot.toCoordinates()); + + this.pathPoints.push(x, offset - y); + } + + /** + * @param {import("@agnoc/core").DeviceMap} map + */ + getPathEntity(map) { + const offset = map.size.y; + const robotPath = + map.robotPath && + map.robotPath + .map((coordinate) => { + const { x, y } = map.toPixel(coordinate); + + return [x, offset - y]; + }) + .flat(); + + const points = + robotPath && robotPath.length > 0 ? robotPath : this.pathPoints; + + if (points.length === 0) { + return; + } + + return new PathMapEntity({ + type: PathMapEntity.TYPE.PATH, + points, + }); + } + + /** + * @param {import("@agnoc/core").DeviceMap} map + */ + getMapEntities(map) { + const { size, grid } = map; + const offset = 5; + const walls = []; + const floor = []; + + // apply offset to remove fake bottom line. + for (let x = offset; x < size.x - offset; x++) { + for (let y = offset; y < size.y - offset; y++) { + const coord = (size.y - y) * size.y + x; + const point = grid[coord]; + + if (point === 255) { + walls.push(x, y); + } else if (point !== 0) { + floor.push(x, y); + } + } + } + + return { + floor: floor.length ? + new MapLayer({ + type: MapLayer.TYPE.FLOOR, + pixels: floor, + }) : + null, + walls: walls.length ? + new MapLayer({ + type: MapLayer.TYPE.WALL, + pixels: walls, + }) : + null, + }; + } + + /** + * @param {import("@agnoc/core").DeviceMap} map + * @param {import("@agnoc/core").Room} room + */ + getSegmentEntity(map, room) { + const offset = map.size.y; + const pixels = room.pixels.map(({ x, y }) => { + return [x, offset - y]; + }); + + return pixels.length ? + new MapLayer({ + type: MapLayer.TYPE.SEGMENT, + pixels: pixels.flat(), + metaData: { + segmentId: room.id.value, + active: room.isEnabled, + name: room.name, + }, + }) : + null; + } + + /** + * @param {import("@agnoc/core").DeviceMap} map + */ + getSegmentEntities(map) { + const { rooms } = map; + + return ( + rooms.map((room) => { + return this.getSegmentEntity(map, room); + }) || [] + ); + } + + /** + * @param {import("@agnoc/core").DeviceMap} map + */ + getRestrictedZoneEntities(map) { + const offset = map.size.y; + const { restrictedZones } = map; + + return restrictedZones.map((zone) => { + const points = zone.coordinates.map((coordinate) => { + const { x, y } = map.toPixel(coordinate); + + return [x, offset - y]; + }); + + if ( + points[0].join() === points[2].join() && + points[1].join() === points[3].join() + ) { + return new LineMapEntity({ + type: LineMapEntity.TYPE.VIRTUAL_WALL, + points: [points[0], points[1]].flat(), + }); + } + + return new PolygonMapEntity({ + type: PolygonMapEntity.TYPE.NO_GO_AREA, + points: [points[0], points[3], points[2], points[1]].flat(), + }); + }); + } + + /** + * @param {import("@agnoc/core").Robot} robot + */ + onUpdateMap(robot) { + const { map } = robot.device; + + if (!map) { + return; + } + + const { floor, walls } = this.getMapEntities(map); + + this.updatePathPoints(map); + + this.state.map = new ValetudoMap({ + pixelSize: 1, // ? + entities: [ + this.getChargerEntity(map), + this.getRobotEntity(map), + this.getGoToTarget(map), + this.getPathEntity(map), + ...this.getRestrictedZoneEntities(map), + ].filter(Boolean), + layers: [floor, walls, ...this.getSegmentEntities(map)].filter(Boolean), + metaData: {}, + size: { + x: map.size.x, + y: map.size.y, + }, + }); + + this.emitMapUpdated(); + } + + /** + * @param {import("@agnoc/core").Robot} robot + */ + onUpdateChargerPosition(robot) { + const { map } = robot.device; + + if (!map || !this.state.map) { + return; + } + + const entity = this.getChargerEntity(map); + + this.state.map.entities = [ + ...this.state.map.entities.filter((entity) => { + return entity.type !== PointMapEntity.TYPE.CHARGER_LOCATION; + }), + entity, + ]; + + this.emitMapUpdated(); + } + + /** + * @param {import("@agnoc/core").Robot} robot + */ + onUpdateRobotPosition(robot) { + const { map } = robot.device; + + if (!map || !this.state.map) { + return; + } + + this.updatePathPoints(map); + + this.state.map.entities = [ + ...this.state.map.entities.filter((entity) => { + return entity.type !== PointMapEntity.TYPE.ROBOT_POSITION; + }), + this.getRobotEntity(map), + ]; + + this.emitMapUpdated(); + } + + /** + * @param {import("@agnoc/core").Robot} robot + */ + getBatteryState(robot) { + const { state, battery } = robot.device; + let flag = BatteryStateAttribute.FLAG.DISCHARGING; + + if (battery && state && state.value === DeviceState.VALUE.DOCKED) { + flag = + battery.value === 100 ? + BatteryStateAttribute.FLAG.CHARGED : + BatteryStateAttribute.FLAG.CHARGING; + } + + return new BatteryStateAttribute({ + level: battery ? battery.value : 0, + flag, + }); + } + + /** + * @param {import("@agnoc/core").Robot} robot + */ + getStatusState(robot) { + const { state, mode, error } = robot.device; + const flag = + DEVICE_MODE_TO_STATUS_STATE_FLAG[mode?.value] || + StatusStateAttribute.FLAG.NONE; + + return new StatusStateAttribute({ + value: state ? state.value : StatusStateAttribute.VALUE.DOCKED, + flag, + metaData: { + error_description: error && error.value, + }, + }); + } + + /** + * @param {import("@agnoc/core").Robot} robot + */ + getIntensityState(robot) { + const { fanSpeed } = robot.device; + + return new PresetSelectionStateAttribute({ + type: PresetSelectionStateAttribute.TYPE.FAN_SPEED, + // TODO: this should have a mapper. + value: fanSpeed ? + fanSpeed.value : + PresetSelectionStateAttribute.INTENSITY.OFF, + }); + } + + /** + * @param {import("@agnoc/core").Robot} robot + */ + getWaterUsageState(robot) { + const { waterLevel } = robot.device; + + return new PresetSelectionStateAttribute({ + type: PresetSelectionStateAttribute.TYPE.WATER_GRADE, + // TODO: this should have a mapper. + value: waterLevel ? + waterLevel.value : + PresetSelectionStateAttribute.INTENSITY.OFF, + }); + } + + static IMPLEMENTATION_AUTO_DETECTION_HANDLER() { + const path = "/mnt/UDISK/config/device_config.ini"; + let deviceConf; + + Logger.trace("Trying to open device.conf at " + path); + + try { + deviceConf = fs.readFileSync(path); + } catch (e) { + Logger.trace("cannot read", path, e); + + return false; + } + + let result = {}; + + if (deviceConf) { + deviceConf + .toString() + .split(/\n/) + .map((line) => { + return line.split(/=/, 2); + }) + .map(([k, v]) => { + return (result[k] = v); + }); + } + + Logger.trace("Software version: " + result.software_version); + + return Boolean(result.software_version); + } +}; diff --git a/backend/lib/robots/cecotec/capabilities/CecotecBasicControlCapability.js b/backend/lib/robots/cecotec/capabilities/CecotecBasicControlCapability.js new file mode 100644 index 00000000..a2b73d8d --- /dev/null +++ b/backend/lib/robots/cecotec/capabilities/CecotecBasicControlCapability.js @@ -0,0 +1,38 @@ +const BasicControlCapability = require("../../../core/capabilities/BasicControlCapability"); + +/** + * @extends BasicControlCapability + */ +module.exports = class CecotecBasicControlCapability extends BasicControlCapability { + async start() { + if (!this.robot.robot) { + throw new Error("There is no robot connected to server"); + } + + await this.robot.robot.start(); + } + + async stop() { + if (!this.robot.robot) { + throw new Error("There is no robot connected to server"); + } + + await this.robot.robot.stop(); + } + + async pause() { + if (!this.robot.robot) { + throw new Error("There is no robot connected to server"); + } + + await this.robot.robot.pause(); + } + + async home() { + if (!this.robot.robot) { + throw new Error("There is no robot connected to server"); + } + + await this.robot.robot.home(); + } +}; diff --git a/backend/lib/robots/cecotec/capabilities/CecotecCarpetModeControlCapability.js b/backend/lib/robots/cecotec/capabilities/CecotecCarpetModeControlCapability.js new file mode 100644 index 00000000..a030cfa9 --- /dev/null +++ b/backend/lib/robots/cecotec/capabilities/CecotecCarpetModeControlCapability.js @@ -0,0 +1,46 @@ +const CarpetModeControlCapability = require("../../../core/capabilities/CarpetModeControlCapability"); + +/** + * @extends CarpetModeControlCapability + */ +class CecotecCarpetModeControlCapability extends CarpetModeControlCapability { + /** + * This function polls the current carpet mode state and stores the attributes in our robostate + * + * @abstract + * @returns {Promise} + */ + async isEnabled() { + if (!this.robot.robot) { + throw new Error("There is no robot connected to server"); + } + + return this.robot.robot.device.config?.isCarpetModeEnabled || false; + } + + /** + * @abstract + * @returns {Promise} + */ + async enable() { + if (!this.robot.robot) { + throw new Error("There is no robot connected to server"); + } + + await this.robot.robot.setCarpetMode(true); + } + + /** + * @abstract + * @returns {Promise} + */ + async disable() { + if (!this.robot.robot) { + throw new Error("There is no robot connected to server"); + } + + await this.robot.robot.setCarpetMode(false); + } +} + +module.exports = CecotecCarpetModeControlCapability; diff --git a/backend/lib/robots/cecotec/capabilities/CecotecCombinedVirtualRestrictionsCapability.js b/backend/lib/robots/cecotec/capabilities/CecotecCombinedVirtualRestrictionsCapability.js new file mode 100644 index 00000000..70a02d2e --- /dev/null +++ b/backend/lib/robots/cecotec/capabilities/CecotecCombinedVirtualRestrictionsCapability.js @@ -0,0 +1,76 @@ +/** + * @typedef {import("../../../entities/core/ValetudoVirtualRestrictions")} ValetudoVirtualRestrictions + */ + +const CombinedVirtualRestrictionsCapability = require("../../../core/capabilities/CombinedVirtualRestrictionsCapability"); +const {Pixel} = require("@agnoc/core"); + +/** + * @extends CombinedVirtualRestrictionsCapability + */ +class CecotecCombinedVirtualRestrictionsCapability extends CombinedVirtualRestrictionsCapability { + /** + * @param {ValetudoVirtualRestrictions} virtualRestrictions + * @returns {Promise} + */ + async setVirtualRestrictions({ virtualWalls, restrictedZones }) { + if (!this.robot.robot) { + throw new Error("There is no robot connected to server"); + } + + const map = this.robot.robot.device.map; + + if (!map) { + return; + } + + const offset = map.size.y; + const areas = [ + ...virtualWalls.map(({ points }) => { + return [ + map.toCoordinate(new Pixel({ + x: points.pA.x, + y: offset - points.pA.y, + })), + map.toCoordinate(new Pixel({ + x: points.pB.x, + y: offset - points.pB.y, + })), + map.toCoordinate(new Pixel({ + x: points.pA.x, + y: offset - points.pA.y, + })), + map.toCoordinate(new Pixel({ + x: points.pB.x, + y: offset - points.pB.y, + })), + ]; + }), + ...restrictedZones.map(({ points }) => { + return [ + map.toCoordinate(new Pixel({ + x: points.pA.x, + y: offset - points.pA.y, + })), + map.toCoordinate(new Pixel({ + x: points.pD.x, + y: offset - points.pD.y, + })), + map.toCoordinate(new Pixel({ + x: points.pC.x, + y: offset - points.pC.y, + })), + map.toCoordinate(new Pixel({ + x: points.pB.x, + y: offset - points.pB.y, + })), + ]; + }) + ]; + + await this.robot.robot.setRestrictedZones(areas); + await this.robot.robot.updateMap(); + } +} + +module.exports = CecotecCombinedVirtualRestrictionsCapability; diff --git a/backend/lib/robots/cecotec/capabilities/CecotecConsumableMonitoringCapability.js b/backend/lib/robots/cecotec/capabilities/CecotecConsumableMonitoringCapability.js new file mode 100644 index 00000000..04d19486 --- /dev/null +++ b/backend/lib/robots/cecotec/capabilities/CecotecConsumableMonitoringCapability.js @@ -0,0 +1,101 @@ +const ConsumableMonitoringCapability = require("../../../core/capabilities/ConsumableMonitoringCapability"); +const ConsumableStateAttribute = require("../../../entities/state/attributes/ConsumableStateAttribute"); +const {CONSUMABLE_TYPE} = require("@agnoc/core"); + +const { TYPE, SUB_TYPE, UNITS } = ConsumableStateAttribute; +const MAIN_BRUSH_LIFE_TIME = 320 * 60; +const SIDE_BRUSH_LIFE_TIME = 220 * 60; +const FILTER_LIFE_TIME = 160 * 60; +const DISHCLOTH_LIFE_TIME = 100 * 60; + +/** + * @extends ConsumableMonitoringCapability + */ +module.exports = class CecotecConsumableMonitoringCapability extends ConsumableMonitoringCapability { + async getConsumables() { + if (!this.robot.robot) { + return []; + } + + const list = await this.robot.robot.getConsumables(); + const consumables = list.map(({ type, used }) => { + switch (type) { + case CONSUMABLE_TYPE.MAIN_BRUSH: + return new ConsumableStateAttribute({ + type: TYPE.BRUSH, + subType: SUB_TYPE.MAIN, + remaining: { + unit: UNITS.MINUTES, + value: Math.max(MAIN_BRUSH_LIFE_TIME - used, 0) + } + }); + case CONSUMABLE_TYPE.SIDE_BRUSH: + return new ConsumableStateAttribute({ + type: TYPE.BRUSH, + subType: SUB_TYPE.SIDE_RIGHT, + remaining: { + unit: UNITS.MINUTES, + value: Math.max(SIDE_BRUSH_LIFE_TIME - used, 0) + } + }); + case CONSUMABLE_TYPE.FILTER: + return new ConsumableStateAttribute({ + type: TYPE.FILTER, + subType: SUB_TYPE.MAIN, + remaining: { + unit: UNITS.MINUTES, + value: Math.max(FILTER_LIFE_TIME - used, 0) + } + }); + case CONSUMABLE_TYPE.DISHCLOTH: + return new ConsumableStateAttribute({ + type: TYPE.MOP, + subType: SUB_TYPE.MAIN, + remaining: { + unit: UNITS.MINUTES, + value: Math.max(DISHCLOTH_LIFE_TIME - used, 0) + } + }); + } + }); + + consumables.forEach(c => { + return this.robot.state.upsertFirstMatchingAttribute(c); + }); + + // @ts-ignore + this.robot.emitStateUpdated(); + + return consumables; + } + + /** + * @param {string} type + * @param {string} [subType] + * @returns {Promise} + */ + async resetConsumable(type, subType) { + if (!this.robot.robot) { + throw new Error("There is no robot connected to server"); + } + + /** + * @type {import("@agnoc/core").ConsumableType} + */ + let consumable; + + if (type === TYPE.BRUSH && subType === SUB_TYPE.MAIN) { + consumable = CONSUMABLE_TYPE.MAIN_BRUSH; + } else if (type === TYPE.BRUSH && subType === SUB_TYPE.SIDE_RIGHT) { + consumable = CONSUMABLE_TYPE.SIDE_BRUSH; + } else if (type === TYPE.FILTER) { + consumable = CONSUMABLE_TYPE.FILTER; + } else if (type === TYPE.MOP) { + consumable = CONSUMABLE_TYPE.DISHCLOTH; + } + + if (consumable) { + await this.robot.robot.resetConsumable(consumable); + } + } +}; diff --git a/backend/lib/robots/cecotec/capabilities/CecotecCurrentStatisticsCapability.js b/backend/lib/robots/cecotec/capabilities/CecotecCurrentStatisticsCapability.js new file mode 100644 index 00000000..bb576793 --- /dev/null +++ b/backend/lib/robots/cecotec/capabilities/CecotecCurrentStatisticsCapability.js @@ -0,0 +1,34 @@ +const CurrentStatisticsCapability = require("../../../core/capabilities/CurrentStatisticsCapability"); +const ValetudoDataPoint = require("../../../entities/core/ValetudoDataPoint"); + +/** + * @extends CurrentStatisticsCapability + */ +class CecotecCurrentStatisticsCapability extends CurrentStatisticsCapability { + /** + * @return {Promise>} + */ + async getStatistics() { + return [ + new ValetudoDataPoint({ + type: ValetudoDataPoint.TYPES.TIME, + value: (this.robot.robot?.device.currentClean?.time || 0) * 60, + }), + new ValetudoDataPoint({ + type: ValetudoDataPoint.TYPES.AREA, + value: (this.robot.robot?.device.currentClean?.size || 0) * 100, + }), + ]; + } + + getProperties() { + return { + availableStatistics: [ + ValetudoDataPoint.TYPES.TIME, + ValetudoDataPoint.TYPES.AREA, + ], + }; + } +} + +module.exports = CecotecCurrentStatisticsCapability; diff --git a/backend/lib/robots/cecotec/capabilities/CecotecDoNotDisturbCapability.js b/backend/lib/robots/cecotec/capabilities/CecotecDoNotDisturbCapability.js new file mode 100644 index 00000000..9314312f --- /dev/null +++ b/backend/lib/robots/cecotec/capabilities/CecotecDoNotDisturbCapability.js @@ -0,0 +1,54 @@ +const DoNotDisturbCapability = require("../../../core/capabilities/DoNotDisturbCapability"); +const ValetudoDNDConfiguration = require("../../../entities/core/ValetudoDNDConfiguration"); +const {DeviceQuietHours, DeviceTime} = require("@agnoc/core"); + +/** + * @extends DoNotDisturbCapability + */ +module.exports = class CecotecDoNotDisturbCapability extends DoNotDisturbCapability { + + /** + * @returns {Promise} + */ + async getDndConfiguration() { + if (!this.robot.robot) { + throw new Error("There is no robot connected to server"); + } + + const quietHours = await this.robot.robot.getQuietHours(); + + return new ValetudoDNDConfiguration({ + enabled: quietHours.isEnabled, + start: { + hour: quietHours.begin.hour, + minute: quietHours.begin.minute + }, + end: { + hour: quietHours.end.hour, + minute: quietHours.end.minute + } + }); + } + + /** + * @param {import("../../../entities/core/ValetudoDNDConfiguration")} dndConfig + * @returns {Promise} + */ + async setDndConfiguration(dndConfig) { + if (!this.robot.robot) { + throw new Error("There is no robot connected to server"); + } + + await this.robot.robot.setQuietHours(new DeviceQuietHours({ + isEnabled: dndConfig.enabled, + begin: new DeviceTime({ + hour: dndConfig.start.hour, + minute: dndConfig.start.minute, + }), + end: new DeviceTime({ + hour: dndConfig.end.hour, + minute: dndConfig.end.minute, + }), + })); + } +}; diff --git a/backend/lib/robots/cecotec/capabilities/CecotecFanSpeedControlCapability.js b/backend/lib/robots/cecotec/capabilities/CecotecFanSpeedControlCapability.js new file mode 100644 index 00000000..fc94161c --- /dev/null +++ b/backend/lib/robots/cecotec/capabilities/CecotecFanSpeedControlCapability.js @@ -0,0 +1,32 @@ +const FanSpeedControlCapability = require("../../../core/capabilities/FanSpeedControlCapability"); +const {DeviceFanSpeed} = require("@agnoc/core"); + +/** + * @extends FanSpeedControlCapability + */ +module.exports = class CecotecFanSpeedControlCapability extends FanSpeedControlCapability { + /** + * @returns {Array} + */ + getFanSpeedPresets() { + return this.presets.map(p => { + return p.name; + }); + } + + async selectPreset(preset) { + if (!this.robot.robot) { + throw new Error("There is no robot connected to server"); + } + + const matchedPreset = this.presets.find(p => { + return p.name === preset; + }); + + if (!matchedPreset) { + throw new Error("Invalid Preset"); + } + + await this.robot.robot.setFanSpeed(new DeviceFanSpeed({ value: matchedPreset.value })); + } +}; diff --git a/backend/lib/robots/cecotec/capabilities/CecotecGoToLocationCapability.js b/backend/lib/robots/cecotec/capabilities/CecotecGoToLocationCapability.js new file mode 100644 index 00000000..df4158e4 --- /dev/null +++ b/backend/lib/robots/cecotec/capabilities/CecotecGoToLocationCapability.js @@ -0,0 +1,29 @@ +const GoToLocationCapability = require("../../../core/capabilities/GoToLocationCapability"); +const { Position, Pixel } = require("@agnoc/core"); + +/** + * @extends GoToLocationCapability + */ +module.exports = class CecotecGoToLocationCapability extends GoToLocationCapability { + async goTo({ coordinates: { x, y } }) { + if (!this.robot.robot) { + throw new Error("There is no robot connected to server"); + } + + const map = this.robot.robot.device.map; + + if (!map) { + throw new Error("There is no map in connected robot"); + } + + const offset = map.size.y; + const absolute = map.toCoordinate(new Pixel({ x, y: offset - y })); + const position = new Position({ + x: absolute.x, + y: absolute.y, + phi: 0.0 + }); + + await this.robot.robot.cleanPosition(position); + } +}; diff --git a/backend/lib/robots/cecotec/capabilities/CecotecLocateCapability.js b/backend/lib/robots/cecotec/capabilities/CecotecLocateCapability.js new file mode 100644 index 00000000..cf48d8ea --- /dev/null +++ b/backend/lib/robots/cecotec/capabilities/CecotecLocateCapability.js @@ -0,0 +1,14 @@ +const LocateCapability = require("../../../core/capabilities/LocateCapability"); + +/** + * @extends LocateCapability + */ +module.exports = class CecotecLocateCapability extends LocateCapability { + async locate() { + if (!this.robot.robot) { + throw new Error("There is no robot connected to server"); + } + + await this.robot.robot.locate(); + } +}; diff --git a/backend/lib/robots/cecotec/capabilities/CecotecManualControlCapability.js b/backend/lib/robots/cecotec/capabilities/CecotecManualControlCapability.js new file mode 100644 index 00000000..976a5a42 --- /dev/null +++ b/backend/lib/robots/cecotec/capabilities/CecotecManualControlCapability.js @@ -0,0 +1,86 @@ +const ManualControlCapability = require("../../../core/capabilities/ManualControlCapability"); +const { FORWARD, BACKWARD, ROTATE_CLOCKWISE, ROTATE_COUNTERCLOCKWISE } = + ManualControlCapability.MOVEMENT_COMMAND_TYPE; +const { MANUAL_MODE } = require("@agnoc/core"); + +const COMMAND_MAP = { + [FORWARD]: MANUAL_MODE.forward, + [BACKWARD]: MANUAL_MODE.backward, + [ROTATE_CLOCKWISE]: MANUAL_MODE.right, + [ROTATE_COUNTERCLOCKWISE]: MANUAL_MODE.left, +}; + +/** + * @extends ManualControlCapability + */ +class CecotecManualControlCapability extends ManualControlCapability { + /** + * + * @param {object} options + * @param {import("../CecotecCongaRobot")} options.robot + * @class + */ + constructor(options) { + super( + Object.assign({}, options, { + supportedMovementCommands: [ + ManualControlCapability.MOVEMENT_COMMAND_TYPE.FORWARD, + ManualControlCapability.MOVEMENT_COMMAND_TYPE.BACKWARD, + ManualControlCapability.MOVEMENT_COMMAND_TYPE.ROTATE_CLOCKWISE, + ManualControlCapability.MOVEMENT_COMMAND_TYPE.ROTATE_COUNTERCLOCKWISE, + ], + }) + ); + + this.active = false; + } + + async enableManualControl() { + if (!this.robot.robot) { + throw new Error("There is no robot connected to server"); + } + + await this.robot.robot.enterManualMode(); + + this.active = true; + } + + /** + * @returns {Promise} + */ + async disableManualControl() { + if (!this.robot.robot) { + throw new Error("There is no robot connected to server"); + } + + await this.robot.robot.leaveManualMode(); + + this.active = false; + } + + /** + * @returns {Promise} + */ + async manualControlActive() { + return this.active; + } + /** + * @param {import("../../../core/capabilities/ManualControlCapability").ValetudoManualControlMovementCommandType} movementCommand + * @returns {Promise} + */ + async manualControl(movementCommand) { + if (!this.robot.robot) { + throw new Error("There is no robot connected to server"); + } + + const mode = COMMAND_MAP[movementCommand]; + + if (!mode) { + throw new Error("Unknown manual control model"); + } + + await this.robot.robot.setManualMode(mode); + } +} + +module.exports = CecotecManualControlCapability; diff --git a/backend/lib/robots/cecotec/capabilities/CecotecMapResetCapability.js b/backend/lib/robots/cecotec/capabilities/CecotecMapResetCapability.js new file mode 100644 index 00000000..38d1d807 --- /dev/null +++ b/backend/lib/robots/cecotec/capabilities/CecotecMapResetCapability.js @@ -0,0 +1,20 @@ +const MapResetCapability = require("../../../core/capabilities/MapResetCapability"); + +/** + * @extends MapResetCapability + */ +class CecotecMapResetCapability extends MapResetCapability { + /** + * @returns {Promise} + */ + async reset() { + if (!this.robot.robot) { + throw new Error("There is no robot connected to server"); + } + + await this.robot.robot.resetMap(); + await this.robot.robot.updateMap(); + } +} + +module.exports = CecotecMapResetCapability; diff --git a/backend/lib/robots/cecotec/capabilities/CecotecMapSegmentEditCapability.js b/backend/lib/robots/cecotec/capabilities/CecotecMapSegmentEditCapability.js new file mode 100644 index 00000000..31b80c8c --- /dev/null +++ b/backend/lib/robots/cecotec/capabilities/CecotecMapSegmentEditCapability.js @@ -0,0 +1,74 @@ +const MapSegmentEditCapability = require("../../../core/capabilities/MapSegmentEditCapability"); +const {Pixel} = require("@agnoc/core"); + +/** + * @extends MapSegmentEditCapability + */ +class CecotecMapSegmentEditCapability extends MapSegmentEditCapability { + /** + * @param {import("../../../entities/core/ValetudoMapSegment")} segmentA + * @param {import("../../../entities/core/ValetudoMapSegment")} segmentB + * @returns {Promise} + */ + async joinSegments(segmentA, segmentB) { + if (!this.robot.robot) { + throw new Error("There is no robot connected to server"); + } + + const map = this.robot.robot.device.map; + + if (!map) { + throw new Error("There is no map in connected robot"); + } + + const segmentIds = [segmentA, segmentB].map(segment => { + return String(segment.id); + }); + const rooms = map.rooms.filter(room => { + return segmentIds.includes(room.id.toString()); + }); + + await this.robot.robot.joinRooms(rooms); + await this.robot.robot.updateMap(); + } + + /** + * @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} + */ + async splitSegment(segment, pA, pB) { + if (!this.robot.robot) { + throw new Error("There is no robot connected to server"); + } + + const map = this.robot.robot.device.map; + + if (!map) { + throw new Error("There is no map in connected robot"); + } + + const offset = map.size.y; + const room = map.rooms.find(room => { + return room.id.toString() === String(segment.id); + }); + const pointA = map.toCoordinate(new Pixel({ + x: pA.x, + y: offset - pA.y + })); + const pointB = map.toCoordinate(new Pixel({ + x: pB.x, + y: offset - pB.y + })); + + await this.robot.robot.splitRoom(room, pointA, pointB); + await this.robot.robot.updateMap(); + } +} + +module.exports = CecotecMapSegmentEditCapability; diff --git a/backend/lib/robots/cecotec/capabilities/CecotecMapSegmentRenameCapability.js b/backend/lib/robots/cecotec/capabilities/CecotecMapSegmentRenameCapability.js new file mode 100644 index 00000000..ce38a054 --- /dev/null +++ b/backend/lib/robots/cecotec/capabilities/CecotecMapSegmentRenameCapability.js @@ -0,0 +1,35 @@ +const MapSegmentRenameCapability = require("../../../core/capabilities/MapSegmentRenameCapability"); + +/** + * @extends MapSegmentRenameCapability + */ +class CecotecMapSegmentRenameCapability extends MapSegmentRenameCapability { + /** + * @param {import("../../../entities/core/ValetudoMapSegment")} segment + * @param {string} name + */ + async renameSegment(segment, name) { + if (!this.robot.robot) { + throw new Error("There is no robot connected to server"); + } + + const map = this.robot.robot.device.map; + + if (!map) { + throw new Error("There is no map in connected robot"); + } + + const room = map.rooms.find(room => { + return room.id.toString() === String(segment.id); + }); + + if (!room) { + throw new Error(`There is no room with id '${segment.id}' in current map`); + } + + this.robot.robot.updateRoom(room.clone({ name })); + await this.robot.robot.updateMap(); + } +} + +module.exports = CecotecMapSegmentRenameCapability; diff --git a/backend/lib/robots/cecotec/capabilities/CecotecMapSegmentationCapability.js b/backend/lib/robots/cecotec/capabilities/CecotecMapSegmentationCapability.js new file mode 100644 index 00000000..c71f145e --- /dev/null +++ b/backend/lib/robots/cecotec/capabilities/CecotecMapSegmentationCapability.js @@ -0,0 +1,36 @@ +const MapSegmentationCapability = require("../../../core/capabilities/MapSegmentationCapability"); + +/** + * @extends MapSegmentationCapability + */ +class CecotecMapSegmentationCapability extends MapSegmentationCapability { + /** + * Could be phrased as "cleanSegments" for vacuums or "mowSegments" for lawnmowers + * + * + * @param {Array} segments + * @returns {Promise} + */ + async executeSegmentAction(segments) { + if (!this.robot.robot) { + throw new Error("There is no robot connected to server"); + } + + const map = this.robot.robot.device.map; + + if (!map) { + throw new Error("There is no map in connected robot"); + } + + const segmentIds = segments.map(segment => { + return String(segment.id); + }); + const rooms = map.rooms.filter(room => { + return segmentIds.includes(room.id.toString()); + }); + + await this.robot.robot.cleanRooms(rooms); + } +} + +module.exports = CecotecMapSegmentationCapability; diff --git a/backend/lib/robots/cecotec/capabilities/CecotecPendingMapChangeHandlingCapability.js b/backend/lib/robots/cecotec/capabilities/CecotecPendingMapChangeHandlingCapability.js new file mode 100644 index 00000000..b382f98c --- /dev/null +++ b/backend/lib/robots/cecotec/capabilities/CecotecPendingMapChangeHandlingCapability.js @@ -0,0 +1,38 @@ +const PendingMapChangeHandlingCapability = require("../../../core/capabilities/PendingMapChangeHandlingCapability"); + + +/** + * @extends PendingMapChangeHandlingCapability + */ +class CecotecPendingMapChangeHandlingCapability extends PendingMapChangeHandlingCapability { + /** + * @returns {Promise} + */ + async hasPendingChange() { + return this.robot?.robot.device.hasWaitingMap; + } + + /** + * @returns {Promise} + */ + async acceptChange() { + if (!this.robot.robot) { + throw new Error("There is no robot connected to server"); + } + + return this.robot.robot.saveWaitingMap(true); + } + + /** + * @returns {Promise} + */ + async rejectChange() { + if (!this.robot.robot) { + throw new Error("There is no robot connected to server"); + } + + return this.robot.robot.saveWaitingMap(false); + } +} + +module.exports = CecotecPendingMapChangeHandlingCapability; diff --git a/backend/lib/robots/cecotec/capabilities/CecotecPersistentMapControlCapability.js b/backend/lib/robots/cecotec/capabilities/CecotecPersistentMapControlCapability.js new file mode 100644 index 00000000..056b14b7 --- /dev/null +++ b/backend/lib/robots/cecotec/capabilities/CecotecPersistentMapControlCapability.js @@ -0,0 +1,43 @@ +const PersistentMapControlCapability = require("../../../core/capabilities/PersistentMapControlCapability"); + +/** + * @extends PersistentMapControlCapability + */ +class CecotecPersistentMapControlCapability extends PersistentMapControlCapability { + /** + * @returns {Promise} + */ + async isEnabled() { + if (!this.robot.robot || !this.robot.robot.device.config) { + return false; + } + + return this.robot.robot.device.config.isHistoryMapEnabled; + } + + /** + * @returns {Promise} + */ + async enable() { + if (!this.robot.robot) { + throw new Error("There is no robot connected to server"); + } + + await this.robot.robot.setHistoryMap(true); + await this.robot.robot.updateMap(); + } + + /** + * @returns {Promise} + */ + async disable() { + if (!this.robot.robot) { + throw new Error("There is no robot connected to server"); + } + + await this.robot.robot.setHistoryMap(false); + await this.robot.robot.updateMap(); + } +} + +module.exports = CecotecPersistentMapControlCapability; diff --git a/backend/lib/robots/cecotec/capabilities/CecotecSpeakerTestCapability.js b/backend/lib/robots/cecotec/capabilities/CecotecSpeakerTestCapability.js new file mode 100644 index 00000000..d088f09d --- /dev/null +++ b/backend/lib/robots/cecotec/capabilities/CecotecSpeakerTestCapability.js @@ -0,0 +1,19 @@ +const SpeakerTestCapability = require("../../../core/capabilities/SpeakerTestCapability"); + +/** + * @extends SpeakerTestCapability + */ +class CecotecSpeakerTestCapability extends SpeakerTestCapability { + /** + * @returns {Promise} + */ + async playTestSound() { + if (!this.robot.robot) { + throw new Error("There is no robot connected to server"); + } + + await this.robot.robot.locate(); + } +} + +module.exports = CecotecSpeakerTestCapability; diff --git a/backend/lib/robots/cecotec/capabilities/CecotecSpeakerVolumeControlCapability.js b/backend/lib/robots/cecotec/capabilities/CecotecSpeakerVolumeControlCapability.js new file mode 100644 index 00000000..b1b496e7 --- /dev/null +++ b/backend/lib/robots/cecotec/capabilities/CecotecSpeakerVolumeControlCapability.js @@ -0,0 +1,45 @@ +const SpeakerVolumeControlCapability = require("../../../core/capabilities/SpeakerVolumeControlCapability"); +const { DeviceVoice } = require("@agnoc/core"); + +/** + * @extends SpeakerVolumeControlCapability + */ +class CecotecSpeakerVolumeControlCapability extends SpeakerVolumeControlCapability { + /** + * Returns the current voice volume as percentage + * + * @returns {Promise} + */ + async getVolume() { + if (!this.robot.robot || !this.robot.robot.device.config) { + return 0; + } + + return this.robot.robot.device.config.voice.volume; + } + + /** + * Sets the speaker volume + * + * @param {number} value + * @returns {Promise} + */ + async setVolume(value) { + if (!this.robot.robot) { + throw new Error("There is no robot connected to server"); + } + + await this.robot.robot.setVoice( + new DeviceVoice({ + isEnabled: true, + volume: value, + }) + ); + } + + getAlsaControlName() { + return "Lineout volume control"; + } +} + +module.exports = CecotecSpeakerVolumeControlCapability; diff --git a/backend/lib/robots/cecotec/capabilities/CecotecWaterUsageControlCapability.js b/backend/lib/robots/cecotec/capabilities/CecotecWaterUsageControlCapability.js new file mode 100644 index 00000000..1c1e28c3 --- /dev/null +++ b/backend/lib/robots/cecotec/capabilities/CecotecWaterUsageControlCapability.js @@ -0,0 +1,38 @@ +const WaterUsageControlCapability = require("../../../core/capabilities/WaterUsageControlCapability"); +const {DeviceWaterLevel} = require("@agnoc/core"); + +/** + * @extends WaterUsageControlCapability + */ +class CecotecWaterUsageControlCapability extends WaterUsageControlCapability { + /** + * @returns {Array} + */ + getPresets() { + return this.presets.map(p => { + return p.name; + }); + } + + /** + * @param {string} preset + * @returns {Promise} + */ + async selectPreset(preset) { + if (!this.robot.robot) { + throw new Error("There is no robot connected to server"); + } + + const matchedPreset = this.presets.find(p => { + return p.name === preset; + }); + + if (!matchedPreset) { + throw new Error("Invalid Preset"); + } + + await this.robot.robot.setWaterLevel(new DeviceWaterLevel({ value: matchedPreset.value })); + } +} + +module.exports = CecotecWaterUsageControlCapability; diff --git a/backend/lib/robots/cecotec/capabilities/CecotecWifiConfigurationCapability.js b/backend/lib/robots/cecotec/capabilities/CecotecWifiConfigurationCapability.js new file mode 100644 index 00000000..ffc5de8c --- /dev/null +++ b/backend/lib/robots/cecotec/capabilities/CecotecWifiConfigurationCapability.js @@ -0,0 +1,27 @@ +const LinuxWifiConfigurationCapability = require("../../common/linuxCapabilities/LinuxWifiConfigurationCapability"); +const ValetudoWifiStatus = require("../../../entities/core/ValetudoWifiStatus"); + +/** + * @extends LinuxWifiConfigurationCapability + */ +class CecotecWifiConfigurationCapability extends LinuxWifiConfigurationCapability { + /** + * @returns {Promise} + */ + async getWifiStatus() { + if (this.robot.config.get("embedded") === true) { + return await super.getWifiStatus(); + } + + throw new Error("Cannot get Wi-Fi configuration for Cecotec vacuums"); + } + + /** + * @returns {Promise} + */ + async setWifiConfiguration() { + throw new Error("Cannot set Wi-Fi configuration for Cecotec vacuums"); + } +} + +module.exports = CecotecWifiConfigurationCapability; diff --git a/backend/lib/robots/cecotec/capabilities/CecotecZoneCleaningCapability.js b/backend/lib/robots/cecotec/capabilities/CecotecZoneCleaningCapability.js new file mode 100644 index 00000000..12720c05 --- /dev/null +++ b/backend/lib/robots/cecotec/capabilities/CecotecZoneCleaningCapability.js @@ -0,0 +1,47 @@ +const ZoneCleaningCapability = require("../../../core/capabilities/ZoneCleaningCapability"); +const {Pixel} = require("@agnoc/core"); + +/** + * @extends ZoneCleaningCapability + */ +module.exports = class CecotecZoneCleaningCapability extends ZoneCleaningCapability { + /** + * @param {Array} valetudoZones + * @returns {Promise} + */ + async start(valetudoZones) { + if (!this.robot.robot) { + throw new Error("There is no robot connected to server"); + } + + const map = this.robot.robot.device.map; + + if (!map) { + throw new Error("There is no map in connected robot"); + } + + const offset = map.size.y; + const areas = valetudoZones.map(({ points }) => { + return [ + map.toCoordinate(new Pixel({ + x: points.pA.x, + y: offset - points.pA.y, + })), + map.toCoordinate(new Pixel({ + x: points.pD.x, + y: offset - points.pD.y, + })), + map.toCoordinate(new Pixel({ + x: points.pC.x, + y: offset - points.pC.y, + })), + map.toCoordinate(new Pixel({ + x: points.pB.x, + y: offset - points.pB.y, + })), + ]; + }); + + await this.robot.robot.cleanAreas(areas); + } +}; diff --git a/backend/lib/robots/cecotec/capabilities/index.js b/backend/lib/robots/cecotec/capabilities/index.js new file mode 100644 index 00000000..58c67c56 --- /dev/null +++ b/backend/lib/robots/cecotec/capabilities/index.js @@ -0,0 +1,23 @@ +module.exports = { + CecotecBasicControlCapability: require("./CecotecBasicControlCapability"), + CecotecCarpetModeControlCapability: require("./CecotecCarpetModeControlCapability"), + CecotecCombinedVirtualRestrictionsCapability: require("./CecotecCombinedVirtualRestrictionsCapability"), + CecotecConsumableMonitoringCapability: require("./CecotecConsumableMonitoringCapability"), + CecotecCurrentStatisticsCapability: require("./CecotecCurrentStatisticsCapability"), + CecotecDoNotDisturbCapability: require("./CecotecDoNotDisturbCapability"), + CecotecFanSpeedControlCapability: require("./CecotecFanSpeedControlCapability"), + CecotecGoToLocationCapability: require("./CecotecGoToLocationCapability"), + CecotecLocateCapability: require("./CecotecLocateCapability"), + CecotecManualControlCapability: require("./CecotecManualControlCapability"), + CecotecMapResetCapability: require("./CecotecMapResetCapability"), + CecotecMapSegmentEditCapability: require("./CecotecMapSegmentEditCapability"), + CecotecMapSegmentRenameCapability: require("./CecotecMapSegmentRenameCapability"), + CecotecMapSegmentationCapability: require("./CecotecMapSegmentationCapability"), + CecotecPendingMapChangeHandlingCapability: require("./CecotecPendingMapChangeHandlingCapability"), + CecotecPersistentMapControlCapability: require("./CecotecPersistentMapControlCapability"), + CecotecSpeakerTestCapability: require("./CecotecSpeakerTestCapability"), + CecotecSpeakerVolumeControlCapability: require("./CecotecSpeakerVolumeControlCapability"), + CecotecWaterUsageControlCapability: require("./CecotecWaterUsageControlCapability"), + CecotecWifiConfigurationCapability: require("./CecotecWifiConfigurationCapability"), + CecotecZoneCleaningCapability: require("./CecotecZoneCleaningCapability"), +}; diff --git a/backend/lib/robots/cecotec/index.js b/backend/lib/robots/cecotec/index.js new file mode 100644 index 00000000..44b6ca5e --- /dev/null +++ b/backend/lib/robots/cecotec/index.js @@ -0,0 +1,3 @@ +module.exports = { + "CecotecCongaRobot": require("./CecotecCongaRobot"), +}; diff --git a/backend/lib/robots/index.js b/backend/lib/robots/index.js index 350e7f74..07457b71 100644 --- a/backend/lib/robots/index.js +++ b/backend/lib/robots/index.js @@ -1,9 +1,11 @@ +const cecotec = require("./cecotec"); const dreame = require("./dreame"); const mock = require("./mock"); const roborock = require("./roborock"); const viomi = require("./viomi"); module.exports = Object.assign({}, + cecotec, roborock, viomi, dreame, diff --git a/backend/lib/updater/update_provider/GithubValetudoUpdateProvider.js b/backend/lib/updater/update_provider/GithubValetudoUpdateProvider.js index bb92e5df..1024117c 100644 --- a/backend/lib/updater/update_provider/GithubValetudoUpdateProvider.js +++ b/backend/lib/updater/update_provider/GithubValetudoUpdateProvider.js @@ -91,7 +91,7 @@ class GithubValetudoUpdateProvider extends ValetudoUpdateProvider { } -GithubValetudoUpdateProvider.RELEASES_URL = "https://api.github.com/repos/Hypfer/Valetudo/releases"; +GithubValetudoUpdateProvider.RELEASES_URL = "https://api.github.com/repos/freeconga/Valetudo/releases"; GithubValetudoUpdateProvider.MANIFEST_NAME = "valetudo_release_manifest.json"; diff --git a/backend/package.json b/backend/package.json index 9c096dc1..e8642739 100644 --- a/backend/package.json +++ b/backend/package.json @@ -33,6 +33,7 @@ }, "author": "", "dependencies": { + "@agnoc/core": "~0.16.0-next.7", "@destinationstransfers/ntp": "2.0.0", "ajv": "8.8.2", "async-mqtt": "2.6.1", diff --git a/docs/_pages/general/supported-robots.md b/docs/_pages/general/supported-robots.md index f332c732..5bef7add 100644 --- a/docs/_pages/general/supported-robots.md +++ b/docs/_pages/general/supported-robots.md @@ -32,7 +32,9 @@ Don't take this as "Everything listed here will be 100% available and work all t ## Table of Contents 1. [Overview](#Overview) -2. [Dreame](#dreame) +2. [Cecotec](#cecotec) + 1. [Conga](#cecotec_conga) +3. [Dreame](#dreame) 1. [1C](#dreame_1c) 2. [1T](#dreame_1t) 3. [D9 Pro](#dreame_d9pro) @@ -41,7 +43,7 @@ Don't take this as "Everything listed here will be 100% available and work all t 6. [L10 Pro](#dreame_l10pro) 7. [MOVA Z500](#dreame_movaz500) 8. [Z10 Pro](#dreame_z10pro) -3. [Roborock](#roborock) +4. [Roborock](#roborock) 1. [S4 Max](#roborock_s4max) 2. [S4](#roborock_s4) 3. [S5 Max](#roborock_s5max) @@ -49,48 +51,111 @@ Don't take this as "Everything listed here will be 100% available and work all t 5. [S6 Pure](#roborock_s6pure) 6. [S6](#roborock_s6) 7. [V1](#roborock_v1) -4. [Viomi](#viomi) +5. [Viomi](#viomi) 1. [V7](#viomi_v7)
## Overview -Capability | 1C | 1T | D9 Pro | D9 | F9 | L10 Pro | MOVA Z500 | Z10 Pro | S4 Max | S4 | S5 Max | S5 | S6 Pure | S6 | V1 | V7 ----- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- -[AutoEmptyDockAutoEmptyControlCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#AutoEmptyDockAutoEmptyControlCapability) | No | No | No | No | No | No | No | Yes | No | No | No | No | No | No | No | No -[AutoEmptyDockManualTriggerCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#AutoEmptyDockManualTriggerCapability) | No | No | No | No | No | No | No | Yes | No | No | No | No | No | No | No | No -[BasicControlCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#BasicControlCapability) | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes -[CarpetModeControlCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#CarpetModeControlCapability) | No | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes -[CombinedVirtualRestrictionsCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#CombinedVirtualRestrictionsCapability) | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | No | Yes -[ConsumableMonitoringCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#ConsumableMonitoringCapability) | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes -[CurrentStatisticsCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#CurrentStatisticsCapability) | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes -[DoNotDisturbCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#DoNotDisturbCapability) | No | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes -[FanSpeedControlCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#FanSpeedControlCapability) | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes -[GoToLocationCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#GoToLocationCapability) | No | No | No | No | No | No | No | No | Yes | Yes | Yes | Yes | Yes | Yes | Yes | No -[KeyLockCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#KeyLockCapability) | No | No | No | No | No | Yes | No | Yes | No | No | No | No | No | No | No | No -[LocateCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#LocateCapability) | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes -[ManualControlCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#ManualControlCapability) | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes -[MapResetCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#MapResetCapability) | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | No | Yes -[MapSegmentEditCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#MapSegmentEditCapability) | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | No | Yes -[MapSegmentRenameCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#MapSegmentRenameCapability) | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | No | Yes -[MapSegmentationCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#MapSegmentationCapability) | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | No | Yes -[MapSnapshotCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#MapSnapshotCapability) | No | No | No | No | No | No | No | No | No | No | No | Yes | No | Yes | No | No -[MappingPassCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#MappingPassCapability) | No | Yes | Yes | Yes | Yes | Yes | Yes | Yes | No | No | No | No | No | No | No | No -[PendingMapChangeHandlingCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#PendingMapChangeHandlingCapability) | No | Yes | Yes | Yes | Yes | Yes | Yes | Yes | No | No | No | No | No | No | No | No -[PersistentMapControlCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#PersistentMapControlCapability) | Yes | Yes | No | No | Yes | No | Yes | No | Yes | Yes | Yes | Yes | Yes | Yes | No | Yes -[QuirksCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#QuirksCapability) | No | No | No | No | No | Yes | No | Yes | Yes | Yes | Yes | Yes | Yes | Yes | No | Yes -[SpeakerTestCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#SpeakerTestCapability) | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes -[SpeakerVolumeControlCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#SpeakerVolumeControlCapability) | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes -[TotalStatisticsCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#TotalStatisticsCapability) | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | No -[VoicePackManagementCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#VoicePackManagementCapability) | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes -[WaterUsageControlCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#WaterUsageControlCapability) | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | No | No | Yes | No | No | No | No | Yes -[WifiConfigurationCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#WifiConfigurationCapability) | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes -[ZoneCleaningCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#ZoneCleaningCapability) | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes +Capability | Conga | 1C | 1T | D9 Pro | D9 | F9 | L10 Pro | MOVA Z500 | Z10 Pro | S4 Max | S4 | S5 Max | S5 | S6 Pure | S6 | V1 | V7 +---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- +[AutoEmptyDockAutoEmptyControlCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#AutoEmptyDockAutoEmptyControlCapability) | No | No | No | No | No | No | No | No | Yes | No | No | No | No | No | No | No | No +[AutoEmptyDockManualTriggerCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#AutoEmptyDockManualTriggerCapability) | No | No | No | No | No | No | No | No | Yes | No | No | No | No | No | No | No | No +[BasicControlCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#BasicControlCapability) | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes +[CarpetModeControlCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#CarpetModeControlCapability) | Yes | No | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes +[CombinedVirtualRestrictionsCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#CombinedVirtualRestrictionsCapability) | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | No | Yes +[ConsumableMonitoringCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#ConsumableMonitoringCapability) | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes +[CurrentStatisticsCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#CurrentStatisticsCapability) | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes +[DoNotDisturbCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#DoNotDisturbCapability) | Yes | No | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes +[FanSpeedControlCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#FanSpeedControlCapability) | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes +[GoToLocationCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#GoToLocationCapability) | Yes | No | No | No | No | No | No | No | No | Yes | Yes | Yes | Yes | Yes | Yes | Yes | No +[KeyLockCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#KeyLockCapability) | No | No | No | No | No | No | Yes | No | Yes | No | No | No | No | No | No | No | No +[LocateCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#LocateCapability) | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes +[ManualControlCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#ManualControlCapability) | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes +[MapResetCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#MapResetCapability) | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | No | Yes +[MapSegmentEditCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#MapSegmentEditCapability) | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | No | Yes +[MapSegmentRenameCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#MapSegmentRenameCapability) | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | No | Yes +[MapSegmentationCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#MapSegmentationCapability) | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | No | Yes +[MapSnapshotCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#MapSnapshotCapability) | No | No | No | No | No | No | No | No | No | No | No | No | Yes | No | Yes | No | No +[MappingPassCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#MappingPassCapability) | No | No | Yes | Yes | Yes | Yes | Yes | Yes | Yes | No | No | No | No | No | No | No | No +[PendingMapChangeHandlingCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#PendingMapChangeHandlingCapability) | Yes | No | Yes | Yes | Yes | Yes | Yes | Yes | Yes | No | No | No | No | No | No | No | No +[PersistentMapControlCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#PersistentMapControlCapability) | Yes | Yes | Yes | No | No | Yes | No | Yes | No | Yes | Yes | Yes | Yes | Yes | Yes | No | Yes +[QuirksCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#QuirksCapability) | No | No | No | No | No | No | Yes | No | Yes | Yes | Yes | Yes | Yes | Yes | Yes | No | Yes +[SpeakerTestCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#SpeakerTestCapability) | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes +[SpeakerVolumeControlCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#SpeakerVolumeControlCapability) | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes +[TotalStatisticsCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#TotalStatisticsCapability) | No | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | No +[VoicePackManagementCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#VoicePackManagementCapability) | No | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes +[WaterUsageControlCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#WaterUsageControlCapability) | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | No | No | Yes | No | No | No | No | Yes +[WifiConfigurationCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#WifiConfigurationCapability) | No | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes +[ZoneCleaningCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#ZoneCleaningCapability) | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes
+## Cecotec + +### Conga + +#### Valetudo Support + +good + + + +#### Developer Support + +best effort + + + +#### Tested Working + +✔ + + + +#### Recommended + +Can't go wrong with this model + + + +#### Recommended Valetudo binary to use + +armv7 + + + +#### Comment + +This adaptation only supports Conga models that works with Conga 3000 retail app + + + +#### This model supports the following capabilities: + - [BasicControlCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#BasicControlCapability) + - [CarpetModeControlCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#CarpetModeControlCapability) + - [CombinedVirtualRestrictionsCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#CombinedVirtualRestrictionsCapability) + - [ConsumableMonitoringCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#ConsumableMonitoringCapability) + - [CurrentStatisticsCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#CurrentStatisticsCapability) + - [DoNotDisturbCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#DoNotDisturbCapability) + - [FanSpeedControlCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#FanSpeedControlCapability) + - [GoToLocationCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#GoToLocationCapability) + - [LocateCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#LocateCapability) + - [ManualControlCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#ManualControlCapability) + - [MapResetCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#MapResetCapability) + - [MapSegmentEditCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#MapSegmentEditCapability) + - [MapSegmentRenameCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#MapSegmentRenameCapability) + - [MapSegmentationCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#MapSegmentationCapability) + - [PendingMapChangeHandlingCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#PendingMapChangeHandlingCapability) + - [PersistentMapControlCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#PersistentMapControlCapability) + - [SpeakerTestCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#SpeakerTestCapability) + - [SpeakerVolumeControlCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#SpeakerVolumeControlCapability) + - [WaterUsageControlCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#WaterUsageControlCapability) + - [ZoneCleaningCapability](https://valetudo.cloud/pages/general/capabilities-overview.html#ZoneCleaningCapability) + + ## Dreame ### 1C @@ -1118,4 +1183,4 @@ Overall, it's just weird and annoying.




This page has been autogenerated.
-Autogeneration timestamp: 2022-01-28T10:30:56.549Z +Autogeneration timestamp: 2022-01-28T11:13:37.930Z diff --git a/docs/_pages/integrations/mqtt.md b/docs/_pages/integrations/mqtt.md index 9678e3a9..1a5d293b 100644 --- a/docs/_pages/integrations/mqtt.md +++ b/docs/_pages/integrations/mqtt.md @@ -555,7 +555,7 @@ Sample value: Sample value: ```json --54 +-25 ``` diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index fb230ac4..ebc47fd1 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -54,7 +54,7 @@ import { floorObject } from "./utils"; import {preprocessMap} from "./mapUtils"; export const valetudoAPI = axios.create({ - baseURL: "../api/v2", + baseURL: "./api/v2", }); let currentCommitId = "unknown"; diff --git a/frontend/src/components/ValetudoAppBar.tsx b/frontend/src/components/ValetudoAppBar.tsx index 604b2310..ea03126e 100644 --- a/frontend/src/components/ValetudoAppBar.tsx +++ b/frontend/src/components/ValetudoAppBar.tsx @@ -417,6 +417,17 @@ const ValetudoAppBar: React.FunctionComponent<{ paletteMode: PaletteMode, setPal + + + + + + Find
Go to - + Zones Segments
Unknown power diff --git a/old_frontend/lib/home.js b/old_frontend/lib/home.js index 59f3d95e..c6176e50 100644 --- a/old_frontend/lib/home.js +++ b/old_frontend/lib/home.js @@ -9,7 +9,7 @@ var pauseButton = document.getElementById("pause-button"); var stopButton = document.getElementById("stop-button"); var spotButton = document.getElementById("spot-button"); var goToButton = document.getElementById("go-to-button"); -//var areaButton = document.getElementById("area-button"); +var areaButton = document.getElementById("area-button"); var segmentsButton = document.getElementById("segments-button"); var fanspeedButton = document.getElementById("fanspeed-button"); var watergradeButton = document.getElementById("watergrade-button"); @@ -342,7 +342,7 @@ async function updateHomePage() { spot: spotButton, find: findRobotButton, go_to: goToButton, - //zones: areaButton, + zones: areaButton, segments: segmentsButton }; @@ -354,7 +354,7 @@ async function updateHomePage() { spot: false, // not ported to capability, discussed @Hypfer to disable it for now find: robotCapabilities.includes("LocateCapability"), go_to: robotCapabilities.includes("GoToLocationCapability"), - //zones: robotCapabilities.includes("ZoneCleaningCapability"), + zones: robotCapabilities.includes("ZoneCleaningCapability"), segments: robotCapabilities.includes("MapSegmentationCapability") }; @@ -400,7 +400,7 @@ async function updateHomePage() { buttonStateMap.start = false; buttonStateMap.home = false; buttonStateMap.go_to = false; - //buttonStateMap.zones = false; + buttonStateMap.zones = false; buttonStateMap.segments = false; break; case "cleaning": @@ -408,7 +408,7 @@ async function updateHomePage() { buttonStateMap.home = false; buttonStateMap.spot = false; buttonStateMap.go_to = false; - //buttonStateMap.zones = false; + buttonStateMap.zones = false; buttonStateMap.segments = false; break; case "paused": @@ -498,7 +498,7 @@ async function homeInit() { } } - /* + try { zones = await ApiService.getZones(); } catch (e) { @@ -509,7 +509,6 @@ async function homeInit() { areaButton.removeAttribute("disabled"); } - */ try { segments = await ApiService.getSegments().then((res) => res.filter((segment) => !!segment.name)); diff --git a/old_frontend/lib/services/api.service.js b/old_frontend/lib/services/api.service.js index 9246e39b..aeadbc6a 100644 --- a/old_frontend/lib/services/api.service.js +++ b/old_frontend/lib/services/api.service.js @@ -6,14 +6,14 @@ export class ApiService { * @param {*=} body */ static async fetch(method, url, body) { - // @ts-ignore + // @ts-ignore let response = await fetch(url, { method: method, headers: { - "Content-Type": "application/json" + "Content-Type": "application/json", }, body: JSON.stringify(body), - cache: "no-store" + cache: "no-store", }); if (!response.ok) { throw Error(await response.text()); @@ -26,32 +26,48 @@ export class ApiService { } static async startCleaning() { - await this.fetch("PUT", "../api/v2/robot/capabilities/BasicControlCapability", { - action: "start" - }); + await this.fetch( + "PUT", + "../api/v2/robot/capabilities/BasicControlCapability", + { + action: "start", + } + ); } static async pauseCleaning() { - await this.fetch("PUT", "../api/v2/robot/capabilities/BasicControlCapability", { - action: "pause" - }); + await this.fetch( + "PUT", + "../api/v2/robot/capabilities/BasicControlCapability", + { + action: "pause", + } + ); } static async stopCleaning() { - await this.fetch("PUT", "../api/v2/robot/capabilities/BasicControlCapability", { - action: "stop" - }); + await this.fetch( + "PUT", + "../api/v2/robot/capabilities/BasicControlCapability", + { + action: "stop", + } + ); } static async driveHome() { - await this.fetch("PUT", "../api/v2/robot/capabilities/BasicControlCapability", { - action: "home" - }); + await this.fetch( + "PUT", + "../api/v2/robot/capabilities/BasicControlCapability", + { + action: "home", + } + ); } static async findRobot() { await this.fetch("PUT", "../api/v2/robot/capabilities/LocateCapability", { - action: "locate" + action: "locate", }); } @@ -64,69 +80,101 @@ export class ApiService { * @param {number} y */ static async goto(x, y) { - await this.fetch("PUT", "../api/v2/robot/capabilities/GoToLocationCapability", { - action: "goto", - coordinates: { - x: x, - y: y + await this.fetch( + "PUT", + "../api/v2/robot/capabilities/GoToLocationCapability", + { + action: "goto", + coordinates: { + x: x, + y: y, + }, } - }); + ); } /** * @param {number[]} zoneIds */ + static async startCleaningZonesById(zoneIds) { - await this.fetch("PUT", "../api/v2/robot/capabilities/ZoneCleaningCapability/presets", { - action: "clean", - ids: zoneIds - }); + await this.fetch( + "PUT", + "../api/v2/robot/capabilities/ZoneCleaningCapability/presets", + { + action: "clean", + zones: zoneIds, + } + ); } static async startCleaningZoneByCoords(zones) { - await this.fetch("PUT", "../api/v2/robot/capabilities/ZoneCleaningCapability", { - action: "clean", - zones: zones - }); + await this.fetch( + "PUT", + "../api/v2/robot/capabilities/ZoneCleaningCapability", + { + action: "clean", + zones: zones, + } + ); } static async getSegments() { - return this.fetch("GET", "../api/v2/robot/capabilities/MapSegmentationCapability"); + return this.fetch( + "GET", + "../api/v2/robot/capabilities/MapSegmentationCapability" + ); } /** * @param {number[]} segmentIds */ static async startCleaningSegments(segmentIds) { - await this.fetch("PUT", "../api/v2/robot/capabilities/MapSegmentationCapability", { - action: "start_segment_action", - segment_ids: segmentIds - }); + await this.fetch( + "PUT", + "../api/v2/robot/capabilities/MapSegmentationCapability", + { + action: "start_segment_action", + segment_ids: segmentIds, + } + ); } static async splitSegment(pA, pB, segment_id) { - await this.fetch("PUT", "../api/v2/robot/capabilities/MapSegmentEditCapability", { - action: "split_segment", - pA: pA, - pB: pB, - segment_id: segment_id - }); + await this.fetch( + "PUT", + "../api/v2/robot/capabilities/MapSegmentEditCapability", + { + action: "split_segment", + pA: pA, + pB: pB, + segment_id: segment_id, + } + ); } static async joinSegments(segment_a_id, segment_b_id) { - await this.fetch("PUT", "../api/v2/robot/capabilities/MapSegmentEditCapability", { - action: "join_segments", - segment_a_id: segment_a_id, - segment_b_id: segment_b_id - }); + await this.fetch( + "PUT", + "../api/v2/robot/capabilities/MapSegmentEditCapability", + { + action: "join_segments", + segment_a_id: segment_a_id, + segment_b_id: segment_b_id, + } + ); } static async renameSegment(segment_id, name) { - await this.fetch("PUT", "../api/v2/robot/capabilities/MapSegmentRenameCapability", { - action: "rename_segment", - segment_id: segment_id, - name: name - }); + await this.fetch( + "PUT", + "../api/v2/robot/capabilities/MapSegmentRenameCapability", + { + action: "rename_segment", + segment_id: segment_id, + name: name, + } + ); } static async getVacuumState() { @@ -138,97 +186,122 @@ export class ApiService { } static async getFanSpeeds() { - return await this.fetch("GET", "../api/v2/robot/capabilities/FanSpeedControlCapability/presets"); + return await this.fetch( + "GET", + "../api/v2/robot/capabilities/FanSpeedControlCapability/presets" + ); } static async getWaterGradePresets() { - return await this.fetch("GET", "../api/v2/robot/capabilities/WaterUsageControlCapability/presets"); + return await this.fetch( + "GET", + "../api/v2/robot/capabilities/WaterUsageControlCapability/presets" + ); } /** * @param {string} level */ static async setFanspeed(level) { - await this.fetch("PUT", "../api/v2/robot/capabilities/FanSpeedControlCapability/preset", { - name: level - }); + await this.fetch( + "PUT", + "../api/v2/robot/capabilities/FanSpeedControlCapability/preset", + { + name: level, + } + ); } /** * @param {string} level */ static async setWaterGrade(level) { - await this.fetch("PUT", "../api/v2/robot/capabilities/WaterUsageControlCapability/preset", { - name: level - }); + await this.fetch( + "PUT", + "../api/v2/robot/capabilities/WaterUsageControlCapability/preset", + { + name: level, + } + ); } static async getSupportedVirtualRestrictions() { - return await this.fetch("GET", "../api/v2/robot/capabilities/CombinedVirtualRestrictionsCapability/properties"); + return await this.fetch( + "GET", + "../api/v2/robot/capabilities/CombinedVirtualRestrictionsCapability/properties" + ); } static async setPersistentData(virtualWalls, no_go_areas, no_mop_zones) { - await this.fetch("PUT", "../api/v2/robot/capabilities/CombinedVirtualRestrictionsCapability", { - virtualWalls: virtualWalls.map(w => { - return { - points: { - pA: { - x: w[0], - y: w[1], + await this.fetch( + "PUT", + "../api/v2/robot/capabilities/CombinedVirtualRestrictionsCapability", + { + virtualWalls: virtualWalls.map((w) => { + return { + points: { + pA: { + x: w[0], + y: w[1], + }, + pB: { + x: w[2], + y: w[3], + }, }, - pB: { - x: w[2], - y: w[3], - }, - } - }; - }), - restrictedZones: no_go_areas.map(a => { - return { - points: { - pA: { - x: a[0], - y: a[1], - }, - pB: { - x: a[2], - y: a[3], - }, - pC: { - x: a[4], - y: a[5], - }, - pD: { - x: a[6], - y: a[7], - }, - }, - type: "regular" - }; - }).concat(no_mop_zones.map(a => { - return { - points: { - pA: { - x: a[0], - y: a[1], - }, - pB: { - x: a[2], - y: a[3], - }, - pC: { - x: a[4], - y: a[5], - }, - pD: { - x: a[6], - y: a[7], - }, - }, - type: "mop" - }; - })) - }); + }; + }), + restrictedZones: no_go_areas + .map((a) => { + return { + points: { + pA: { + x: a[0], + y: a[1], + }, + pB: { + x: a[2], + y: a[3], + }, + pC: { + x: a[4], + y: a[5], + }, + pD: { + x: a[6], + y: a[7], + }, + }, + type: "regular", + }; + }) + .concat( + no_mop_zones.map((a) => { + return { + points: { + pA: { + x: a[0], + y: a[1], + }, + pB: { + x: a[2], + y: a[3], + }, + pC: { + x: a[4], + y: a[5], + }, + pD: { + x: a[6], + y: a[7], + }, + }, + type: "mop", + }; + }) + ), + } + ); } static async getLatestMap() { @@ -236,19 +309,40 @@ export class ApiService { } static async getSpots() { - return await this.fetch("GET", "../api/v2/robot/capabilities/GoToLocationCapability/presets_legacy"); + return await this.fetch( + "GET", + "../api/v2/robot/capabilities/GoToLocationCapability/presets_legacy" + ); } static async getZones() { - return await this.fetch("GET", "../api/v2/robot/capabilities/ZoneCleaningCapability/presets_legacy"); + return await this.fetch( + "GET", + "../api/v2/robot/capabilities/ZoneCleaningCapability/presets_legacy" + ); + } + + static async getZonesOpenAPI() { + return await this.fetch( + "GET", + "api/v2/robot/capabilities/ZoneCleaningCapability/presets" + ); } static async saveSpots(spotConfig) { - await this.fetch("POST", "../api/v2/robot/capabilities/GoToLocationCapability/presets_legacy", spotConfig); + await this.fetch( + "POST", + "../api/v2/robot/capabilities/GoToLocationCapability/presets_legacy", + spotConfig + ); } static async saveZones(zonesConfig) { - await this.fetch("POST", "../api/v2/robot/capabilities/ZoneCleaningCapability/presets_legacy", zonesConfig); + await this.fetch( + "POST", + "../api/v2/robot/capabilities/ZoneCleaningCapability/presets_legacy", + zonesConfig + ); } static async startManualControl() { @@ -264,7 +358,7 @@ export class ApiService { angle: angle, velocity: velocity, duration: duration, - sequenceId: sequenceId + sequenceId: sequenceId, }); } @@ -281,7 +375,7 @@ export class ApiService { } static async setValetudoLogLevel(level) { - await this.fetch("PUT", "../api/v2/valetudo/log/level", {level: level}); + await this.fetch("PUT", "../api/v2/valetudo/log/level", { level: level }); } static async getRobot() { @@ -297,11 +391,11 @@ export class ApiService { } static async addTimer(cron) { - await this.fetch("POST", "api/timers", {cron: cron}); + await this.fetch("POST", "api/timers", { cron: cron }); } static async toggleTimer(id, enabled) { - await this.fetch("PUT", "api/timers/" + id, {enabled: enabled}); + await this.fetch("PUT", "api/timers/" + id, { enabled: enabled }); } static async deleteTimer(id) { @@ -309,33 +403,57 @@ export class ApiService { } static async getDndConfiguration() { - return await this.fetch("GET", "../api/v2/robot/capabilities/DoNotDisturbCapability"); - } - - static async setDndConfiguration(enabled, start_hour, start_minute, end_hour, end_minute) { - await this.fetch("PUT", "../api/v2/robot/capabilities/DoNotDisturbCapability", { - enabled: enabled, - start: { - hour: start_hour, - minute: start_minute - }, - end: { - hour: end_hour, - minute: end_minute + return await this.fetch( + "GET", + "../api/v2/robot/capabilities/DoNotDisturbCapability" + ); + } + + static async setDndConfiguration( + enabled, + start_hour, + start_minute, + end_hour, + end_minute + ) { + await this.fetch( + "PUT", + "../api/v2/robot/capabilities/DoNotDisturbCapability", + { + enabled: enabled, + start: { + hour: start_hour, + minute: start_minute, + }, + end: { + hour: end_hour, + minute: end_minute, + }, } - }); + ); } static async getCarpetModeStatus() { - return await this.fetch("GET", "../api/v2/robot/capabilities/CarpetModeControlCapability"); + return await this.fetch( + "GET", + "../api/v2/robot/capabilities/CarpetModeControlCapability" + ); } static async enableCarpetMode() { - await this.fetch("PUT", "../api/v2/robot/capabilities/CarpetModeControlCapability", {action: "enable"}); + await this.fetch( + "PUT", + "../api/v2/robot/capabilities/CarpetModeControlCapability", + { action: "enable" } + ); } static async disableCarpetMode() { - await this.fetch("PUT", "../api/v2/robot/capabilities/CarpetModeControlCapability", {action: "disable"}); + await this.fetch( + "PUT", + "../api/v2/robot/capabilities/CarpetModeControlCapability", + { action: "disable" } + ); } static async getCapabilities() { @@ -343,33 +461,49 @@ export class ApiService { } static async getPersistentMapCapabilityStatus() { - return await this.fetch("GET", "../api/v2/robot/capabilities/PersistentMapControlCapability"); + return await this.fetch( + "GET", + "../api/v2/robot/capabilities/PersistentMapControlCapability" + ); } static async enablePersistentMaps() { - await this.fetch("PUT", "../api/v2/robot/capabilities/PersistentMapControlCapability", {action: "enable"}); + await this.fetch( + "PUT", + "../api/v2/robot/capabilities/PersistentMapControlCapability", + { action: "enable" } + ); } static async disablePersistentMaps() { - await this.fetch("PUT", "../api/v2/robot/capabilities/PersistentMapControlCapability", {action: "disable"}); + await this.fetch( + "PUT", + "../api/v2/robot/capabilities/PersistentMapControlCapability", + { action: "disable" } + ); } static async resetPersistentMaps() { - await this.fetch("PUT", "../api/v2/robot/capabilities/MapResetCapability", {action: "reset"}); + await this.fetch("PUT", "../api/v2/robot/capabilities/MapResetCapability", { + action: "reset", + }); } - static async resetConsumable(type, subType) { - var url = "../api/v2/robot/capabilities/ConsumableMonitoringCapability/" + type; + var url = + "../api/v2/robot/capabilities/ConsumableMonitoringCapability/" + type; if (subType) { url += "/" + subType; } - await this.fetch("PUT", url, {action: "reset"}); + await this.fetch("PUT", url, { action: "reset" }); } static async getConsumableStatus() { - return await this.fetch("GET", "../api/v2/robot/capabilities/ConsumableMonitoringCapability"); + return await this.fetch( + "GET", + "../api/v2/robot/capabilities/ConsumableMonitoringCapability" + ); } static async getCleanSummary() { @@ -377,27 +511,36 @@ export class ApiService { } static async setTimezone(newTimezone) { - return await this.fetch("POST", "api/set_timezone", {new_zone: newTimezone}); + return await this.fetch("POST", "api/set_timezone", { + new_zone: newTimezone, + }); } static async retrieveCleanRecord(recordId) { - return await this.fetch("PUT", "api/clean_record", {recordId: recordId}); + return await this.fetch("PUT", "api/clean_record", { recordId: recordId }); } static async getWifiStatus() { - return await this.fetch("GET", "../api/v2/robot/capabilities/WifiConfigurationCapability"); + return await this.fetch( + "GET", + "../api/v2/robot/capabilities/WifiConfigurationCapability" + ); } static async saveWifiConfig(ssid, password) { - await this.fetch("PUT", "../api/v2/robot/capabilities/WifiConfigurationCapability", { - ssid: ssid, - credentials: { - type: "wpa2_psk", - typeSpecificSettings: { - password: password - } + await this.fetch( + "PUT", + "../api/v2/robot/capabilities/WifiConfigurationCapability", + { + ssid: ssid, + credentials: { + type: "wpa2_psk", + typeSpecificSettings: { + password: password, + }, + }, } - }); + ); } static async getMqttConfig() { @@ -405,34 +548,55 @@ export class ApiService { } static async saveMqttConfig(mqttConfig) { - await this.fetch("PUT", "../api/v2/valetudo/config/interfaces/mqtt", mqttConfig); + await this.fetch( + "PUT", + "../api/v2/valetudo/config/interfaces/mqtt", + mqttConfig + ); } static async getToken() { return await this.fetch("GET", "api/token"); } - static async getSpeakerVolume() { - return await this.fetch("GET", "../api/v2/robot/capabilities/SpeakerVolumeControlCapability"); + return await this.fetch( + "GET", + "../api/v2/robot/capabilities/SpeakerVolumeControlCapability" + ); } static async setSpeakerVolume(volume) { - await this.fetch("PUT", "../api/v2/robot/capabilities/SpeakerVolumeControlCapability", {action: "set_volume", value: volume}); + await this.fetch( + "PUT", + "../api/v2/robot/capabilities/SpeakerVolumeControlCapability", + { action: "set_volume", value: volume } + ); } static async testSpeakerVolume() { - await this.fetch("PUT", "../api/v2/robot/capabilities/SpeakerTestCapability", {action: "play_test_sound"}); + await this.fetch( + "PUT", + "../api/v2/robot/capabilities/SpeakerTestCapability", + { action: "play_test_sound" } + ); } static async getInstallVoicePackStatus() { return await this.fetch("GET", "api/install_voice_pack_status"); } static async getHttpAuthConfig() { - return await this.fetch("GET", "../api/v2/valetudo/config/interfaces/http/auth/basic"); + return await this.fetch( + "GET", + "../api/v2/valetudo/config/interfaces/http/auth/basic" + ); } static async saveHttpAuthConfig(httpAuthConfig) { - await this.fetch("PUT", "../api/v2/valetudo/config/interfaces/http/auth/basic", httpAuthConfig); + await this.fetch( + "PUT", + "../api/v2/valetudo/config/interfaces/http/auth/basic", + httpAuthConfig + ); } } diff --git a/package-lock.json b/package-lock.json index 90418cb2..e37ee9f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,7 @@ "packages": { "": { "name": "valetudo", - "version": "2022.01.0", + "version": "2022.02.0", "license": "Apache-2.0", "workspaces": [ "backend", @@ -37,6 +37,7 @@ "name": "valetudo-backend", "license": "Apache-2.0", "dependencies": { + "@agnoc/core": "~0.16.0-next.7", "@destinationstransfers/ntp": "2.0.0", "ajv": "8.8.2", "async-mqtt": "2.6.1", @@ -649,6 +650,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@agnoc/core": { + "version": "0.16.0-next.7", + "resolved": "https://registry.npmjs.org/@agnoc/core/-/core-0.16.0-next.7.tgz", + "integrity": "sha512-sA9M8laq+UmYYTMww1s1GPja9JSqVivIoQ/eesXxh0rWo0JPnU/qPEUgyOsShczGGPnNqRUMhE4t8BmZuQLV9Q==", + "dependencies": { + "debug": "^4.3.1", + "protobufjs": "^6.11.2", + "tiny-typed-emitter": "^2.0.3" + }, + "engines": { + "node": ">=12.3" + } + }, "node_modules/@apidevtools/json-schema-ref-parser": { "version": "9.0.9", "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.9.tgz", @@ -3771,6 +3785,60 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" + }, "node_modules/@rollup/plugin-node-resolve": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-7.1.3.tgz", @@ -4291,6 +4359,11 @@ "integrity": "sha512-BLhCvUm5deaeDJb162gQoWAaPNa0fFdxbUwcWa5Noc6kFMmqfAEPbySaypbltl0H6FiCkyhfvs91UK2uFFCfEQ==", "dev": true }, + "node_modules/@types/long": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", + "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" + }, "node_modules/@types/mdast": { "version": "3.0.10", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.10.tgz", @@ -14091,6 +14164,11 @@ "url": "https://tidelift.com/funding/github/npm/loglevel" } }, + "node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + }, "node_modules/longest-streak": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.0.1.tgz", @@ -18788,6 +18866,31 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/protobufjs": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.2.tgz", + "integrity": "sha512-4BQJoPooKJl2G9j3XftkIXjoC9C0Av2NOrWmbLWT1vH32GcSUHjM0Arra6UfTsVyfMAuFzaLucXn1sadxJydAw==", + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/long": "^4.0.1", + "@types/node": ">=13.7.0", + "long": "^4.0.0" + }, + "bin": { + "pbjs": "bin/pbjs", + "pbts": "bin/pbts" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -22750,6 +22853,11 @@ "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.2.0.tgz", "integrity": "sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg==" }, + "node_modules/tiny-typed-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz", + "integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==" + }, "node_modules/tiny-warning": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", @@ -25909,6 +26017,16 @@ } }, "dependencies": { + "@agnoc/core": { + "version": "0.16.0-next.7", + "resolved": "https://registry.npmjs.org/@agnoc/core/-/core-0.16.0-next.7.tgz", + "integrity": "sha512-sA9M8laq+UmYYTMww1s1GPja9JSqVivIoQ/eesXxh0rWo0JPnU/qPEUgyOsShczGGPnNqRUMhE4t8BmZuQLV9Q==", + "requires": { + "debug": "^4.3.1", + "protobufjs": "^6.11.2", + "tiny-typed-emitter": "^2.0.3" + } + }, "@apidevtools/json-schema-ref-parser": { "version": "9.0.9", "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.9.tgz", @@ -28079,6 +28197,60 @@ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.0.tgz", "integrity": "sha512-zrsUxjLOKAzdewIDRWy9nsV1GQsKBCWaGwsZQlCgr6/q+vjyZhFgqedLfFBuI9anTPEUT4APq9Mu0SZBTzIcGQ==" }, + "@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=" + }, + "@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=" + }, + "@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", + "requires": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=" + }, + "@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=" + }, + "@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=" + }, + "@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=" + }, + "@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" + }, "@rollup/plugin-node-resolve": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-7.1.3.tgz", @@ -28484,6 +28656,11 @@ "integrity": "sha512-BLhCvUm5deaeDJb162gQoWAaPNa0fFdxbUwcWa5Noc6kFMmqfAEPbySaypbltl0H6FiCkyhfvs91UK2uFFCfEQ==", "dev": true }, + "@types/long": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", + "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" + }, "@types/mdast": { "version": "3.0.10", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.10.tgz", @@ -36127,6 +36304,11 @@ "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.8.0.tgz", "integrity": "sha512-G6A/nJLRgWOuuwdNuA6koovfEV1YpqqAG4pRUlFaz3jj2QNZ8M4vBqnVA+HBTmU/AMNUtlOsMmSpF6NyOjztbA==" }, + "long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + }, "longest-streak": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.0.1.tgz", @@ -39759,6 +39941,26 @@ "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.1.1.tgz", "integrity": "sha512-hrzC564QIl0r0vy4l6MvRLhafmUowhO/O3KgVSoXIbbA2Sz4j8HGpJc6T2cubRVwMwpdiG/vKGfhT4IixmKN9w==" }, + "protobufjs": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.2.tgz", + "integrity": "sha512-4BQJoPooKJl2G9j3XftkIXjoC9C0Av2NOrWmbLWT1vH32GcSUHjM0Arra6UfTsVyfMAuFzaLucXn1sadxJydAw==", + "requires": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/long": "^4.0.1", + "@types/node": ">=13.7.0", + "long": "^4.0.0" + } + }, "proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -42914,6 +43116,11 @@ "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.2.0.tgz", "integrity": "sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg==" }, + "tiny-typed-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz", + "integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==" + }, "tiny-warning": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", @@ -43538,6 +43745,7 @@ "valetudo-backend": { "version": "file:backend", "requires": { + "@agnoc/core": "~0.16.0-next.7", "@destinationstransfers/ntp": "2.0.0", "@types/compression": "1.7.2", "@types/express": "4.17.13", diff --git a/package.json b/package.json index 22c51cb3..7a88dffb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "valetudo", - "version": "2022.02.0", + "version": "2022.02.0-conga.0", "description": "Self-contained control webinterface for vacuum robots", "license": "Apache-2.0", "engines": { diff --git a/util/build_openapi_schema.mjs b/util/build_openapi_schema.mjs index 212d1231..1b55e0ea 100644 --- a/util/build_openapi_schema.mjs +++ b/util/build_openapi_schema.mjs @@ -6,102 +6,174 @@ import * as path from "path"; const __dirname = path.resolve(); process.on("uncaughtException", function (err) { - // eslint-disable-next-line no-console - console.log(err); - process.exit(1); + // eslint-disable-next-line no-console + console.log(err); + process.exit(1); }); const options = { - failOnErrors: false, - definition: { - openapi: "3.0.0", - info: { - title: "Valetudo REST API", - version: Tools.GET_VALETUDO_VERSION() - }, - tags: [ //Swagger UI respects the order of these - {name: "Valetudo", description: "Valetudo management API"}, - {name: "ValetudoEvents", description: "Valetudo Events"}, - {name: "Robot", description: "Robot API"}, - {name: "System", description: "System API"}, - {name: "NTP", description: "NTP Client API"}, - {name: "Timers", description: "Timers API"}, - {name: "Updater", description: "Update Valetudo using Valetudo"}, + failOnErrors: false, + definition: { + openapi: "3.0.0", + info: { + title: "Valetudo REST API", + version: Tools.GET_VALETUDO_VERSION(), + }, + servers: [{ url: "../" }], + tags: [ + //Swagger UI respects the order of these + { name: "Valetudo", description: "Valetudo management API" }, + { name: "ValetudoEvents", description: "Valetudo Events" }, + { name: "Robot", description: "Robot API" }, + { name: "System", description: "System API" }, + { name: "NTP", description: "NTP Client API" }, + { name: "Timers", description: "Timers API" }, + { name: "Updater", description: "Update Valetudo using Valetudo" }, - {name: "BasicControlCapability", description: "Basic control capability"}, - {name: "FanSpeedControlCapability", description: "Fan speed control capability"}, - {name: "WaterUsageControlCapability", description: "Water usage control capability"}, - {name: "WifiConfigurationCapability", description: "Wi-Fi configuration capability"}, - {name: "WifiScanCapability", description: "Wi-Fi scan capability"}, - {name: "ZoneCleaningCapability", description: "Zone cleaning capability"}, - {name: "MapSegmentationCapability", description: "Map segment cleaning capability"}, - {name: "ManualControlCapability", description: "Manual control capability"}, - {name: "DoNotDisturbCapability", description: "Do-not-disturb configuration capability"}, - {name: "ConsumableMonitoringCapability", description: "Consumable monitoring capability"}, - {name: "LocateCapability", description: "Robot locate capability"}, - {name: "GoToLocationCapability", description: "Go-to location capability"}, - {name: "CarpetModeControlCapability", description: "Carpet mode settings capability"}, - {name: "MapResetCapability", description: "Map reset capability"}, - {name: "MapSegmentEditCapability", description: "Map segment edit capability"}, - {name: "MapSegmentRenameCapability", description: "Map segment rename capability"}, - {name: "MapSnapshotCapability", description: "Map snapshots capability"}, - {name: "PersistentMapControlCapability", description: "Persistent map control capability"}, - {name: "SensorCalibrationCapability", description: "Sensor calibration capability"}, - {name: "SpeakerTestCapability", description: "Speaker test capability"}, - {name: "SpeakerVolumeControlCapability", description: "Speaker volume control capability"}, - {name: "VoicePackManagementCapability", description: "Voice pack management capability"}, - {name: "CombinedVirtualRestrictionsCapability", description: "Combined virtual restrictions capability"}, - {name: "PendingMapChangeHandlingCapability", description: "Pending map change handling capability"}, - {name: "MappingPassCapability", description: "Mapping pass capability"}, - {name: "KeyLockCapability", description: "Key lock capability"} - ], - components: { - responses: { - "200": {description: "Ok"}, - "201": {description: "Created"}, - "202": {description: "Accepted"}, - "400": {description: "Bad request"}, - "403": {description: "Forbidden"}, - "404": {description: "Not found"}, - "500": {description: "Internal server error"}, - }, - parameters: {}, - securitySchemes: { - BasicAuth: { - type: "http", - scheme: "basic" - } - } + { + name: "BasicControlCapability", + description: "Basic control capability", + }, + { + name: "FanSpeedControlCapability", + description: "Fan speed control capability", + }, + { + name: "WaterUsageControlCapability", + description: "Water usage control capability", + }, + { + name: "WifiConfigurationCapability", + description: "Wi-Fi configuration capability", + }, + { name: "WifiScanCapability", description: "Wi-Fi scan capability" }, + { + name: "ZoneCleaningCapability", + description: "Zone cleaning capability", + }, + { + name: "MapSegmentationCapability", + description: "Map segment cleaning capability", + }, + { + name: "ManualControlCapability", + description: "Manual control capability", + }, + { + name: "DoNotDisturbCapability", + description: "Do-not-disturb configuration capability", + }, + { + name: "ConsumableMonitoringCapability", + description: "Consumable monitoring capability", + }, + { name: "LocateCapability", description: "Robot locate capability" }, + { + name: "GoToLocationCapability", + description: "Go-to location capability", + }, + { + name: "CarpetModeControlCapability", + description: "Carpet mode settings capability", + }, + { name: "MapResetCapability", description: "Map reset capability" }, + { + name: "MapSegmentEditCapability", + description: "Map segment edit capability", + }, + { + name: "MapSegmentRenameCapability", + description: "Map segment rename capability", + }, + { + name: "MapSnapshotCapability", + description: "Map snapshots capability", + }, + { + name: "PersistentMapControlCapability", + description: "Persistent map control capability", + }, + { + name: "SensorCalibrationCapability", + description: "Sensor calibration capability", + }, + { name: "SpeakerTestCapability", description: "Speaker test capability" }, + { + name: "SpeakerVolumeControlCapability", + description: "Speaker volume control capability", + }, + { + name: "VoicePackManagementCapability", + description: "Voice pack management capability", + }, + { + name: "CombinedVirtualRestrictionsCapability", + description: "Combined virtual restrictions capability", + }, + { + name: "PendingMapChangeHandlingCapability", + description: "Pending map change handling capability", + }, + { name: "MappingPassCapability", description: "Mapping pass capability" }, + { name: "KeyLockCapability", description: "Key lock capability" }, + ], + components: { + responses: { + 200: { description: "Ok" }, + 201: { description: "Created" }, + 202: { description: "Accepted" }, + 400: { description: "Bad request" }, + 403: { description: "Forbidden" }, + 404: { description: "Not found" }, + 500: { description: "Internal server error" }, + }, + parameters: {}, + securitySchemes: { + BasicAuth: { + type: "http", + scheme: "basic", }, - security: [ - {BasicAuth: []} - ], + }, }, - apis: [ - path.join(__dirname, "./backend/util/openapi_defs/*.openapi.json"), - path.join(__dirname, "./backend/lib/doc/*.openapi.json"), - path.join(__dirname, "./backend/lib/webserver/doc/*.openapi.json"), - path.join(__dirname, "./backend/lib/webserver/capabilityRouters/doc/*.openapi.json"), - path.join(__dirname, "./backend/lib/webserver/middlewares/doc/*.openapi.json"), - path.join(__dirname, "./backend/lib/entities/doc/*.openapi.json"), - path.join(__dirname, "./backend/lib/entities/map/doc/*.openapi.json"), - path.join(__dirname, "./backend/lib/entities/core/doc/*.openapi.json"), - path.join(__dirname, "./backend/lib/entities/core/ntpClient/doc/*.openapi.json"), - path.join(__dirname, "./backend/lib/entities/core/updater/doc/*.openapi.json"), - path.join(__dirname, "./backend/lib/entities/state/doc/*.openapi.json"), - path.join(__dirname, "./backend/lib/entities/state/attributes/doc/*.openapi.json"), - path.join(__dirname, "./backend/lib/core/capabilities/doc/*.openapi.json") - ] + security: [{ BasicAuth: [] }], + }, + apis: [ + path.join(__dirname, "./backend/util/openapi_defs/*.openapi.json"), + path.join(__dirname, "./backend/lib/doc/*.openapi.json"), + path.join(__dirname, "./backend/lib/webserver/doc/*.openapi.json"), + path.join( + __dirname, + "./backend/lib/webserver/capabilityRouters/doc/*.openapi.json" + ), + path.join( + __dirname, + "./backend/lib/webserver/middlewares/doc/*.openapi.json" + ), + path.join(__dirname, "./backend/lib/entities/doc/*.openapi.json"), + path.join(__dirname, "./backend/lib/entities/map/doc/*.openapi.json"), + path.join(__dirname, "./backend/lib/entities/core/doc/*.openapi.json"), + path.join( + __dirname, + "./backend/lib/entities/core/ntpClient/doc/*.openapi.json" + ), + path.join( + __dirname, + "./backend/lib/entities/core/updater/doc/*.openapi.json" + ), + path.join(__dirname, "./backend/lib/entities/state/doc/*.openapi.json"), + path.join( + __dirname, + "./backend/lib/entities/state/attributes/doc/*.openapi.json" + ), + path.join(__dirname, "./backend/lib/core/capabilities/doc/*.openapi.json"), + ], }; const spec = await swaggerJsdoc(options); await SwaggerParser.validate(spec); - fs.writeFileSync( - path.join(__dirname, "./backend/lib/res/valetudo.openapi.schema.json"), - JSON.stringify(spec) + path.join(__dirname, "./backend/lib/res/valetudo.openapi.schema.json"), + JSON.stringify(spec) ); - - diff --git a/util/generate_robot_docs.js b/util/generate_robot_docs.js index 115ce45a..1be22652 100644 --- a/util/generate_robot_docs.js +++ b/util/generate_robot_docs.js @@ -7,246 +7,279 @@ const Configuration = require("../backend/lib/Configuration"); const ValetudoEventStore = require("valetudo-backend/lib/ValetudoEventStore"); function generateAnchor(str) { - return str.replace(/[^0-9a-z-A-Z]/g, "").toLowerCase() + return str.replace(/[^0-9a-z-A-Z]/g, "").toLowerCase(); } function generateCapabilityLink(capability) { - return "[" + capability + "](https://valetudo.cloud/pages/general/capabilities-overview.html#" + capability + ")"; + return ( + "[" + + capability + + "](https://valetudo.cloud/pages/general/capabilities-overview.html#" + + capability + + ")" + ); } function generateTable(models, tableData) { - let ret = "## Overview\n\nCapability | "; - ret += models.map((m) => { - return "" + m[0] + ""; - }).join(" | "); - ret += "\n----"; - models.forEach(() => { - ret += " | ----"; + let ret = "## Overview\n\nCapability | "; + ret += models + .map((m) => { + return "" + m[0] + ""; }) - ret += "\n"; - Object.keys(tableData).sort().forEach(capability => { - ret += generateCapabilityLink(capability); - models.forEach(m => { - ret += " | "; - if (tableData[capability].indexOf(m[0]) !== -1) { - ret += "Yes"; - } else { - ret += "No"; - } - }); - ret += "\n"; + .join(" | "); + ret += "\n----"; + models.forEach(() => { + ret += " | ----"; + }); + ret += "\n"; + Object.keys(tableData) + .sort() + .forEach((capability) => { + ret += generateCapabilityLink(capability); + models.forEach((m) => { + ret += " | "; + if (tableData[capability].indexOf(m[0]) !== -1) { + ret += 'Yes'; + } else { + ret += 'No'; + } + }); + ret += "\n"; }); - return ret; + return ret; } -process.on("uncaughtException", function(err) { - if (err.errno === "EADDRINUSE") { - //lol - } else { - console.log(err); - process.exit(1); - } +process.on("uncaughtException", function (err) { + if (err.errno === "EADDRINUSE") { + //lol + } else { + console.log(err); + process.exit(1); + } }); const VALETUDO_SUPPORT_GRADES = { - GREAT: "great", - GOOD: "good", - OKAY: "okay", - MEH: "meh", - BAD: "bad" -} + GREAT: "great", + GOOD: "good", + OKAY: "okay", + MEH: "meh", + BAD: "bad", +}; const DEVELOPER_SUPPORT_GRADES = { - YES: "yes", - BEST_EFFORT: "best effort", - SOME_EFFORT: "some effort", - NONE: "none" -} + YES: "yes", + BEST_EFFORT: "best effort", + SOME_EFFORT: "some effort", + NONE: "none", +}; const BUY_GRADES = { - GET_IT_RIGHT_NOW: "get it right now!", - OKAY: "Can't go wrong with this model", - OKAY_ISH: "This model is okay but has some issues that keep it from being fully recommendable", - NOT_OKAY: "This model has issues and therefore isn't recommended (see comment)", - OUTDATED_OKAY: "outdated but still okay-ish", - OUTDATED_NOT_OKAY: "outdated. not recommended (anymore)" -} + GET_IT_RIGHT_NOW: "get it right now!", + OKAY: "Can't go wrong with this model", + OKAY_ISH: + "This model is okay but has some issues that keep it from being fully recommendable", + NOT_OKAY: + "This model has issues and therefore isn't recommended (see comment)", + OUTDATED_OKAY: "outdated but still okay-ish", + OUTDATED_NOT_OKAY: "outdated. not recommended (anymore)", +}; const VALETUDO_ARCHITECTURES = { - ARM: "armv7", - ARM_LOWMEM: "armv7-lowmem", - AARCH64: "aarch64", -} + ARM: "armv7", + ARM_LOWMEM: "armv7-lowmem", + AARCH64: "aarch64", +}; const ModelDescriptions = { - "Dreame": { - "1C": { - valetudoSupport: VALETUDO_SUPPORT_GRADES.OKAY, - developerSupport: DEVELOPER_SUPPORT_GRADES.BEST_EFFORT, - testedWorking: true, - recommended: BUY_GRADES.OKAY_ISH, - comment: "vSLAM and a small battery, though there are persistent maps and everything seems to work", - architecture: VALETUDO_ARCHITECTURES.ARM, - }, - "1T": { - valetudoSupport: VALETUDO_SUPPORT_GRADES.GOOD, - developerSupport: DEVELOPER_SUPPORT_GRADES.BEST_EFFORT, - testedWorking: true, - recommended: BUY_GRADES.OKAY_ISH, - comment: "vSLAM :(", - architecture: VALETUDO_ARCHITECTURES.AARCH64, - }, - "D9": { - valetudoSupport: VALETUDO_SUPPORT_GRADES.GOOD, - developerSupport: DEVELOPER_SUPPORT_GRADES.YES, - testedWorking: true, - recommended: BUY_GRADES.OKAY_ISH, - comment: "256 MB RAM are problematic when dealing with large floorplans", - architecture: VALETUDO_ARCHITECTURES.ARM_LOWMEM, - }, - "D9 Pro": { - valetudoSupport: VALETUDO_SUPPORT_GRADES.GOOD, - developerSupport: DEVELOPER_SUPPORT_GRADES.YES, - testedWorking: true, - recommended: BUY_GRADES.OKAY_ISH, - comment: "256 MB RAM are problematic when dealing with large floorplans\n\nBasically the same as the D9", - architecture: VALETUDO_ARCHITECTURES.ARM_LOWMEM, - }, - "F9": { - valetudoSupport: VALETUDO_SUPPORT_GRADES.GOOD, - developerSupport: DEVELOPER_SUPPORT_GRADES.BEST_EFFORT, - testedWorking: true, - recommended: BUY_GRADES.OKAY_ISH, - comment: "vSLAM :(", - architecture: VALETUDO_ARCHITECTURES.ARM, - }, - "L10 Pro": { - valetudoSupport: VALETUDO_SUPPORT_GRADES.GOOD, - developerSupport: DEVELOPER_SUPPORT_GRADES.YES, - testedWorking: true, - recommended: BUY_GRADES.OKAY, - comment: "None", - architecture: VALETUDO_ARCHITECTURES.AARCH64, - }, - "MOVA Z500": { - valetudoSupport: VALETUDO_SUPPORT_GRADES.GOOD, - developerSupport: DEVELOPER_SUPPORT_GRADES.BEST_EFFORT, - testedWorking: true, - recommended: BUY_GRADES.OKAY_ISH, - comment: "vSLAM :(", - architecture: VALETUDO_ARCHITECTURES.ARM, - }, - "Z10 Pro": { - valetudoSupport: VALETUDO_SUPPORT_GRADES.GOOD, - developerSupport: DEVELOPER_SUPPORT_GRADES.YES, - testedWorking: true, - recommended: BUY_GRADES.GET_IT_RIGHT_NOW, - comment: "The auto-empty-dock is a neat addition", - architecture: VALETUDO_ARCHITECTURES.AARCH64, - }, + Dreame: { + "1C": { + valetudoSupport: VALETUDO_SUPPORT_GRADES.OKAY, + developerSupport: DEVELOPER_SUPPORT_GRADES.BEST_EFFORT, + testedWorking: true, + recommended: BUY_GRADES.OKAY_ISH, + comment: + "vSLAM and a small battery, though there are persistent maps and everything seems to work", + architecture: VALETUDO_ARCHITECTURES.ARM, }, - "Roborock": { - "S4 Max": { - valetudoSupport: VALETUDO_SUPPORT_GRADES.OKAY, - developerSupport: DEVELOPER_SUPPORT_GRADES.SOME_EFFORT, - testedWorking: true, - recommended: BUY_GRADES.OKAY_ISH, - comment: "Root requires full disassembly and soldering. \n\nAlso, 256 MB RAM and NAND are pretty bad HW specs, which can cause issues.", - architecture: VALETUDO_ARCHITECTURES.ARM_LOWMEM, - }, - "S4": { - valetudoSupport: VALETUDO_SUPPORT_GRADES.OKAY, - developerSupport: DEVELOPER_SUPPORT_GRADES.SOME_EFFORT, - testedWorking: true, - recommended: BUY_GRADES.OKAY_ISH, - comment: "Root requires full disassembly and soldering.", - architecture: VALETUDO_ARCHITECTURES.ARM, - }, - "S5 Max": { - valetudoSupport: VALETUDO_SUPPORT_GRADES.OKAY, - developerSupport: DEVELOPER_SUPPORT_GRADES.SOME_EFFORT, - testedWorking: true, - recommended: BUY_GRADES.OKAY_ISH, - comment: "Root requires full disassembly and soldering. \n\nAlso, 256 MB RAM and NAND are pretty bad HW specs, which can cause issues.", - architecture: VALETUDO_ARCHITECTURES.ARM_LOWMEM, - }, - "S5": { - valetudoSupport: VALETUDO_SUPPORT_GRADES.OKAY, - developerSupport: DEVELOPER_SUPPORT_GRADES.YES, - testedWorking: true, - recommended: BUY_GRADES.OUTDATED_OKAY, - comment: "Still works finefor most use-cases", - architecture: VALETUDO_ARCHITECTURES.ARM, - }, - "S6 MaxV": { - valetudoSupport: VALETUDO_SUPPORT_GRADES.BAD, - developerSupport: DEVELOPER_SUPPORT_GRADES.NONE, - testedWorking: true, - recommended: BUY_GRADES.NOT_OKAY, - comment: "It's basically impossible to root this thing.", - architecture: VALETUDO_ARCHITECTURES.ARM, - }, - "S6 Pure": { - valetudoSupport: VALETUDO_SUPPORT_GRADES.OKAY, - developerSupport: DEVELOPER_SUPPORT_GRADES.SOME_EFFORT, - testedWorking: true, - recommended: BUY_GRADES.OKAY_ISH, - comment: "Root requires full disassembly and soldering. \n\nAlso, 256 MB RAM and NAND are pretty bad HW specs, which can cause issues.", - architecture: VALETUDO_ARCHITECTURES.ARM_LOWMEM, - }, - "S6": { - valetudoSupport: VALETUDO_SUPPORT_GRADES.OKAY, - developerSupport: DEVELOPER_SUPPORT_GRADES.YES, - testedWorking: true, - recommended: BUY_GRADES.OUTDATED_OKAY, - comment: "Still works fine for most use-cases", - architecture: VALETUDO_ARCHITECTURES.ARM, - }, - "S7": { - valetudoSupport: VALETUDO_SUPPORT_GRADES.BAD, - developerSupport: DEVELOPER_SUPPORT_GRADES.NONE, - testedWorking: true, - recommended: BUY_GRADES.NOT_OKAY, - comment: "Rooting requires full disassembly and soldering. \n\nFurthermore, 256 MB RAM and NAND are pretty bad HW specs, which can cause issues and are frankly unacceptable considering the price of this robot. It's simply not worth it.", - architecture: VALETUDO_ARCHITECTURES.ARM_LOWMEM, - }, - "V1": { - valetudoSupport: VALETUDO_SUPPORT_GRADES.OKAY, - developerSupport: DEVELOPER_SUPPORT_GRADES.SOME_EFFORT, - testedWorking: true, - recommended: BUY_GRADES.OUTDATED_NOT_OKAY, - comment: "Unfortunately, this model is lacking basic features such as a persistent map which is insufficient in 2021+", - architecture: VALETUDO_ARCHITECTURES.ARM, - } + "1T": { + valetudoSupport: VALETUDO_SUPPORT_GRADES.GOOD, + developerSupport: DEVELOPER_SUPPORT_GRADES.BEST_EFFORT, + testedWorking: true, + recommended: BUY_GRADES.OKAY_ISH, + comment: "vSLAM :(", + architecture: VALETUDO_ARCHITECTURES.AARCH64, }, - "Viomi": { - "V7": { - valetudoSupport: VALETUDO_SUPPORT_GRADES.MEH, - developerSupport: DEVELOPER_SUPPORT_GRADES.NONE, - testedWorking: true, - recommended: BUY_GRADES.NOT_OKAY, - comment: "This model is actually just a White-Label Product with a custom Miio Software stack which is EOL and therefore doesn't receive any meaningful software updates.\n\nOverall, it's just weird and annoying.", - architecture: VALETUDO_ARCHITECTURES.ARM, - } - } -} + D9: { + valetudoSupport: VALETUDO_SUPPORT_GRADES.GOOD, + developerSupport: DEVELOPER_SUPPORT_GRADES.YES, + testedWorking: true, + recommended: BUY_GRADES.OKAY_ISH, + comment: "256 MB RAM are problematic when dealing with large floorplans", + architecture: VALETUDO_ARCHITECTURES.ARM_LOWMEM, + }, + "D9 Pro": { + valetudoSupport: VALETUDO_SUPPORT_GRADES.GOOD, + developerSupport: DEVELOPER_SUPPORT_GRADES.YES, + testedWorking: true, + recommended: BUY_GRADES.OKAY_ISH, + comment: + "256 MB RAM are problematic when dealing with large floorplans\n\nBasically the same as the D9", + architecture: VALETUDO_ARCHITECTURES.ARM_LOWMEM, + }, + F9: { + valetudoSupport: VALETUDO_SUPPORT_GRADES.GOOD, + developerSupport: DEVELOPER_SUPPORT_GRADES.BEST_EFFORT, + testedWorking: true, + recommended: BUY_GRADES.OKAY_ISH, + comment: "vSLAM :(", + architecture: VALETUDO_ARCHITECTURES.ARM, + }, + "L10 Pro": { + valetudoSupport: VALETUDO_SUPPORT_GRADES.GOOD, + developerSupport: DEVELOPER_SUPPORT_GRADES.YES, + testedWorking: true, + recommended: BUY_GRADES.OKAY, + comment: "None", + architecture: VALETUDO_ARCHITECTURES.AARCH64, + }, + "MOVA Z500": { + valetudoSupport: VALETUDO_SUPPORT_GRADES.GOOD, + developerSupport: DEVELOPER_SUPPORT_GRADES.BEST_EFFORT, + testedWorking: true, + recommended: BUY_GRADES.OKAY_ISH, + comment: "vSLAM :(", + architecture: VALETUDO_ARCHITECTURES.ARM, + }, + "Z10 Pro": { + valetudoSupport: VALETUDO_SUPPORT_GRADES.GOOD, + developerSupport: DEVELOPER_SUPPORT_GRADES.YES, + testedWorking: true, + recommended: BUY_GRADES.GET_IT_RIGHT_NOW, + comment: "The auto-empty-dock is a neat addition", + architecture: VALETUDO_ARCHITECTURES.AARCH64, + }, + }, + Roborock: { + "S4 Max": { + valetudoSupport: VALETUDO_SUPPORT_GRADES.OKAY, + developerSupport: DEVELOPER_SUPPORT_GRADES.SOME_EFFORT, + testedWorking: true, + recommended: BUY_GRADES.OKAY_ISH, + comment: + "Root requires full disassembly and soldering. \n\nAlso, 256 MB RAM and NAND are pretty bad HW specs, which can cause issues.", + architecture: VALETUDO_ARCHITECTURES.ARM_LOWMEM, + }, + S4: { + valetudoSupport: VALETUDO_SUPPORT_GRADES.OKAY, + developerSupport: DEVELOPER_SUPPORT_GRADES.SOME_EFFORT, + testedWorking: true, + recommended: BUY_GRADES.OKAY_ISH, + comment: "Root requires full disassembly and soldering.", + architecture: VALETUDO_ARCHITECTURES.ARM, + }, + "S5 Max": { + valetudoSupport: VALETUDO_SUPPORT_GRADES.OKAY, + developerSupport: DEVELOPER_SUPPORT_GRADES.SOME_EFFORT, + testedWorking: true, + recommended: BUY_GRADES.OKAY_ISH, + comment: + "Root requires full disassembly and soldering. \n\nAlso, 256 MB RAM and NAND are pretty bad HW specs, which can cause issues.", + architecture: VALETUDO_ARCHITECTURES.ARM_LOWMEM, + }, + S5: { + valetudoSupport: VALETUDO_SUPPORT_GRADES.OKAY, + developerSupport: DEVELOPER_SUPPORT_GRADES.YES, + testedWorking: true, + recommended: BUY_GRADES.OUTDATED_OKAY, + comment: "Still works finefor most use-cases", + architecture: VALETUDO_ARCHITECTURES.ARM, + }, + "S6 MaxV": { + valetudoSupport: VALETUDO_SUPPORT_GRADES.BAD, + developerSupport: DEVELOPER_SUPPORT_GRADES.NONE, + testedWorking: true, + recommended: BUY_GRADES.NOT_OKAY, + comment: "It's basically impossible to root this thing.", + architecture: VALETUDO_ARCHITECTURES.ARM, + }, + "S6 Pure": { + valetudoSupport: VALETUDO_SUPPORT_GRADES.OKAY, + developerSupport: DEVELOPER_SUPPORT_GRADES.SOME_EFFORT, + testedWorking: true, + recommended: BUY_GRADES.OKAY_ISH, + comment: + "Root requires full disassembly and soldering. \n\nAlso, 256 MB RAM and NAND are pretty bad HW specs, which can cause issues.", + architecture: VALETUDO_ARCHITECTURES.ARM_LOWMEM, + }, + S6: { + valetudoSupport: VALETUDO_SUPPORT_GRADES.OKAY, + developerSupport: DEVELOPER_SUPPORT_GRADES.YES, + testedWorking: true, + recommended: BUY_GRADES.OUTDATED_OKAY, + comment: "Still works fine for most use-cases", + architecture: VALETUDO_ARCHITECTURES.ARM, + }, + S7: { + valetudoSupport: VALETUDO_SUPPORT_GRADES.BAD, + developerSupport: DEVELOPER_SUPPORT_GRADES.NONE, + testedWorking: true, + recommended: BUY_GRADES.NOT_OKAY, + comment: + "Rooting requires full disassembly and soldering. \n\nFurthermore, 256 MB RAM and NAND are pretty bad HW specs, which can cause issues and are frankly unacceptable considering the price of this robot. It's simply not worth it.", + architecture: VALETUDO_ARCHITECTURES.ARM_LOWMEM, + }, + V1: { + valetudoSupport: VALETUDO_SUPPORT_GRADES.OKAY, + developerSupport: DEVELOPER_SUPPORT_GRADES.SOME_EFFORT, + testedWorking: true, + recommended: BUY_GRADES.OUTDATED_NOT_OKAY, + comment: + "Unfortunately, this model is lacking basic features such as a persistent map which is insufficient in 2021+", + architecture: VALETUDO_ARCHITECTURES.ARM, + }, + }, + Viomi: { + V7: { + valetudoSupport: VALETUDO_SUPPORT_GRADES.MEH, + developerSupport: DEVELOPER_SUPPORT_GRADES.NONE, + testedWorking: true, + recommended: BUY_GRADES.NOT_OKAY, + comment: + "This model is actually just a White-Label Product with a custom Miio Software stack which is EOL and therefore doesn't receive any meaningful software updates.\n\nOverall, it's just weird and annoying.", + architecture: VALETUDO_ARCHITECTURES.ARM, + }, + }, + Cecotec: { + Conga: { + valetudoSupport: VALETUDO_SUPPORT_GRADES.GOOD, + developerSupport: DEVELOPER_SUPPORT_GRADES.BEST_EFFORT, + testedWorking: true, + recommended: BUY_GRADES.OKAY, + comment: + "This adaptation only supports Conga models that works with Conga 3000 retail app", + architecture: VALETUDO_ARCHITECTURES.ARM, + }, + }, +}; function getModelDescription(vendor, model) { - const description = ModelDescriptions[vendor]?.[model]; - - if (!description) { - throw new Error(`Missing description for ${vendor} ${model}`) - } - - return [ - `#### Valetudo Support\n\n${description.valetudoSupport}\n\n`, - `#### Developer Support\n\n${description.developerSupport}\n\n`, - "#### Tested Working\n\n" + (description.testedWorking ? "✔" : "❌") + "\n\n", - `#### Recommended\n\n${description.recommended}\n\n`, - `#### Recommended Valetudo binary to use\n\n${description.architecture}\n\n`, - `#### Comment\n\n${description.comment}\n\n` - ] + const description = ModelDescriptions[vendor]?.[model]; + + if (!description) { + throw new Error(`Missing description for ${vendor} ${model}`); + } + + return [ + `#### Valetudo Support\n\n${description.valetudoSupport}\n\n`, + `#### Developer Support\n\n${description.developerSupport}\n\n`, + "#### Tested Working\n\n" + + (description.testedWorking ? "✔" : "❌") + + "\n\n", + `#### Recommended\n\n${description.recommended}\n\n`, + `#### Recommended Valetudo binary to use\n\n${description.architecture}\n\n`, + `#### Comment\n\n${description.comment}\n\n`, + ]; } /** @@ -258,38 +291,39 @@ function getModelDescription(vendor, model) { * @type {string[]} */ const HIDDEN_IMPLEMENTATIONS = [ - "RoborockS6MaxVValetudoRobot", - "RoborockS7ValetudoRobot" + "RoborockS6MaxVValetudoRobot", + "RoborockS7ValetudoRobot", ]; - const vendors = {}; -Object.values(Robots).forEach(robotClass => { - if (HIDDEN_IMPLEMENTATIONS.includes(robotClass.name)) { - return; - } +Object.values(Robots).forEach((robotClass) => { + if (HIDDEN_IMPLEMENTATIONS.includes(robotClass.name)) { + return; + } - const config = new Configuration(); - config.set("embedded", false); - const eventStore = new ValetudoEventStore(); + const config = new Configuration(); + config.set("embedded", false); + const eventStore = new ValetudoEventStore(); - try { - const instance = new robotClass({ - config: config, - valetudoEventStore: eventStore - }); - - vendors[instance.getManufacturer()] = vendors[instance.getManufacturer()] ? vendors[instance.getManufacturer()] : {}; + try { + const instance = new robotClass({ + config: config, + valetudoEventStore: eventStore, + }); - vendors[instance.getManufacturer()][instance.constructor.name] = { - vendorName: instance.getManufacturer(), - modelName: instance.getModelName(), - capabilities: Object.keys(instance.capabilities).sort() - } - } catch (e) { - console.error(e); - } + vendors[instance.getManufacturer()] = vendors[instance.getManufacturer()] + ? vendors[instance.getManufacturer()] + : {}; + + vendors[instance.getManufacturer()][instance.constructor.name] = { + vendorName: instance.getManufacturer(), + modelName: instance.getModelName(), + capabilities: Object.keys(instance.capabilities).sort(), + }; + } catch (e) { + console.error(e); + } }); const header = `--- @@ -325,75 +359,79 @@ Don't take this as "Everything listed here will be 100% available and work all t `; -const ToC = [ - "## Table of Contents", - "1. [Overview](#Overview)" -]; +const ToC = ["## Table of Contents", "1. [Overview](#Overview)"]; const VendorSections = []; const SummaryTable = {}; const RobotModels = []; -Object.keys(vendors).filter(v => v !== "Valetudo").sort().forEach((vendor, i) => { +Object.keys(vendors) + .filter((v) => v !== "Valetudo") + .sort() + .forEach((vendor, i) => { let vendorTocEntry = [ - (i+2) + ". [" + vendor +"](#" + generateAnchor(vendor) + ")" + i + 2 + ". [" + vendor + "](#" + generateAnchor(vendor) + ")", ]; // noinspection JSMismatchedCollectionQueryUpdate let vendorSection = [ - "## " + vendor + '', - "" - ] - + "## " + vendor + '', + "", + ]; const vendorRobots = vendors[vendor]; - Object.keys(vendorRobots).sort().forEach((robotImplName, i) => { + Object.keys(vendorRobots) + .sort() + .forEach((robotImplName, i) => { const robot = vendorRobots[robotImplName]; - const robotAnchor = generateAnchor(vendor) + "_" + generateAnchor(robot.modelName); + const robotAnchor = + generateAnchor(vendor) + "_" + generateAnchor(robot.modelName); RobotModels.push([robot.modelName, robotAnchor]); - vendorTocEntry.push(" " + (i+1) + ". [" + robot.modelName + "](#" + robotAnchor + ")"); + vendorTocEntry.push( + " " + (i + 1) + ". [" + robot.modelName + "](#" + robotAnchor + ")" + ); vendorSection.push( - "### " + robot.modelName + '', - "", - getModelDescription(robot.vendorName, robot.modelName).join("\n\n"), - "", - "#### This model supports the following capabilities:" + "### " + robot.modelName + '', + "", + getModelDescription(robot.vendorName, robot.modelName).join("\n\n"), + "", + "#### This model supports the following capabilities:" ); - robot.capabilities.forEach(capability => { - vendorSection.push(" - " + generateCapabilityLink(capability)); - if (!SummaryTable.hasOwnProperty(capability)) { - SummaryTable[capability] = [robot.modelName] - } else { - SummaryTable[capability].push(robot.modelName); - } + robot.capabilities.forEach((capability) => { + vendorSection.push(" - " + generateCapabilityLink(capability)); + if (!SummaryTable.hasOwnProperty(capability)) { + SummaryTable[capability] = [robot.modelName]; + } else { + SummaryTable[capability].push(robot.modelName); + } }); vendorSection.push("", ""); - }) - + }); ToC.push(vendorTocEntry.join("\n")); VendorSections.push(vendorSection.join("\n")); -}); - - + }); const page = [ - header, - ToC.join("\n"), - "\n
\n", - generateTable(RobotModels, SummaryTable), - "\n
\n", - VendorSections.join("\n"), - "




", - "This page has been autogenerated.
", - "Autogeneration timestamp: " + new Date().toISOString() -] - -fs.writeFileSync(path.join(__dirname, "../docs/_pages/general/supported-robots.md"), page.join("\n") + "\n") + header, + ToC.join("\n"), + "\n
\n", + generateTable(RobotModels, SummaryTable), + "\n
\n", + VendorSections.join("\n"), + "




", + "This page has been autogenerated.
", + "Autogeneration timestamp: " + new Date().toISOString(), +]; + +fs.writeFileSync( + path.join(__dirname, "../docs/_pages/general/supported-robots.md"), + page.join("\n") + "\n" +); process.exit(0);