Skip to content

Commit e6091e5

Browse files
committed
feat(vendor.cecotec): add support for cecotec conga
1 parent dde8f2e commit e6091e5

38 files changed

Lines changed: 2794 additions & 585 deletions

backend/lib/robots/cecotec/CecotecCongaRobot.js

Lines changed: 691 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
const BasicControlCapability = require("../../../core/capabilities/BasicControlCapability");
2+
3+
/**
4+
* @extends BasicControlCapability<import("../CecotecCongaRobot")>
5+
*/
6+
module.exports = class CecotecBasicControlCapability extends BasicControlCapability {
7+
async start() {
8+
if (!this.robot.robot) {
9+
throw new Error("There is no robot connected to server");
10+
}
11+
12+
await this.robot.robot.start();
13+
}
14+
15+
async stop() {
16+
if (!this.robot.robot) {
17+
throw new Error("There is no robot connected to server");
18+
}
19+
20+
await this.robot.robot.stop();
21+
}
22+
23+
async pause() {
24+
if (!this.robot.robot) {
25+
throw new Error("There is no robot connected to server");
26+
}
27+
28+
await this.robot.robot.pause();
29+
}
30+
31+
async home() {
32+
if (!this.robot.robot) {
33+
throw new Error("There is no robot connected to server");
34+
}
35+
36+
await this.robot.robot.home();
37+
}
38+
};
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
const CarpetModeControlCapability = require("../../../core/capabilities/CarpetModeControlCapability");
2+
3+
/**
4+
* @extends CarpetModeControlCapability<import("../CecotecCongaRobot")>
5+
*/
6+
class CecotecCarpetModeControlCapability extends CarpetModeControlCapability {
7+
/**
8+
* This function polls the current carpet mode state and stores the attributes in our robostate
9+
*
10+
* @abstract
11+
* @returns {Promise<boolean>}
12+
*/
13+
async isEnabled() {
14+
if (!this.robot.robot) {
15+
throw new Error("There is no robot connected to server");
16+
}
17+
18+
return this.robot.robot.device.config?.isCarpetModeEnabled || false;
19+
}
20+
21+
/**
22+
* @abstract
23+
* @returns {Promise<void>}
24+
*/
25+
async enable() {
26+
if (!this.robot.robot) {
27+
throw new Error("There is no robot connected to server");
28+
}
29+
30+
await this.robot.robot.setCarpetMode(true);
31+
}
32+
33+
/**
34+
* @abstract
35+
* @returns {Promise<void>}
36+
*/
37+
async disable() {
38+
if (!this.robot.robot) {
39+
throw new Error("There is no robot connected to server");
40+
}
41+
42+
await this.robot.robot.setCarpetMode(false);
43+
}
44+
}
45+
46+
module.exports = CecotecCarpetModeControlCapability;
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/**
2+
* @typedef {import("../../../entities/core/ValetudoVirtualRestrictions")} ValetudoVirtualRestrictions
3+
*/
4+
5+
const CombinedVirtualRestrictionsCapability = require("../../../core/capabilities/CombinedVirtualRestrictionsCapability");
6+
const {Pixel} = require("@agnoc/core");
7+
8+
/**
9+
* @extends CombinedVirtualRestrictionsCapability<import("../CecotecCongaRobot")>
10+
*/
11+
class CecotecCombinedVirtualRestrictionsCapability extends CombinedVirtualRestrictionsCapability {
12+
/**
13+
* @param {ValetudoVirtualRestrictions} virtualRestrictions
14+
* @returns {Promise<void>}
15+
*/
16+
async setVirtualRestrictions({ virtualWalls, restrictedZones }) {
17+
if (!this.robot.robot) {
18+
throw new Error("There is no robot connected to server");
19+
}
20+
21+
const map = this.robot.robot.device.map;
22+
23+
if (!map) {
24+
return;
25+
}
26+
27+
const offset = map.size.y;
28+
const areas = [
29+
...virtualWalls.map(({ points }) => {
30+
return [
31+
map.toCoordinate(new Pixel({
32+
x: points.pA.x,
33+
y: offset - points.pA.y,
34+
})),
35+
map.toCoordinate(new Pixel({
36+
x: points.pB.x,
37+
y: offset - points.pB.y,
38+
})),
39+
map.toCoordinate(new Pixel({
40+
x: points.pA.x,
41+
y: offset - points.pA.y,
42+
})),
43+
map.toCoordinate(new Pixel({
44+
x: points.pB.x,
45+
y: offset - points.pB.y,
46+
})),
47+
];
48+
}),
49+
...restrictedZones.map(({ points }) => {
50+
return [
51+
map.toCoordinate(new Pixel({
52+
x: points.pA.x,
53+
y: offset - points.pA.y,
54+
})),
55+
map.toCoordinate(new Pixel({
56+
x: points.pD.x,
57+
y: offset - points.pD.y,
58+
})),
59+
map.toCoordinate(new Pixel({
60+
x: points.pC.x,
61+
y: offset - points.pC.y,
62+
})),
63+
map.toCoordinate(new Pixel({
64+
x: points.pB.x,
65+
y: offset - points.pB.y,
66+
})),
67+
];
68+
})
69+
];
70+
71+
await this.robot.robot.setRestrictedZones(areas);
72+
await this.robot.robot.updateMap();
73+
}
74+
}
75+
76+
module.exports = CecotecCombinedVirtualRestrictionsCapability;
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
const ConsumableMonitoringCapability = require("../../../core/capabilities/ConsumableMonitoringCapability");
2+
const ConsumableStateAttribute = require("../../../entities/state/attributes/ConsumableStateAttribute");
3+
const {CONSUMABLE_TYPE} = require("@agnoc/core");
4+
5+
const { TYPE, SUB_TYPE, UNITS } = ConsumableStateAttribute;
6+
const MAIN_BRUSH_LIFE_TIME = 320 * 60;
7+
const SIDE_BRUSH_LIFE_TIME = 220 * 60;
8+
const FILTER_LIFE_TIME = 160 * 60;
9+
const DISHCLOTH_LIFE_TIME = 100 * 60;
10+
11+
/**
12+
* @extends ConsumableMonitoringCapability<import("../CecotecCongaRobot")>
13+
*/
14+
module.exports = class CecotecConsumableMonitoringCapability extends ConsumableMonitoringCapability {
15+
async getConsumables() {
16+
if (!this.robot.robot) {
17+
return [];
18+
}
19+
20+
const list = await this.robot.robot.getConsumables();
21+
const consumables = list.map(({ type, used }) => {
22+
switch (type) {
23+
case CONSUMABLE_TYPE.MAIN_BRUSH:
24+
return new ConsumableStateAttribute({
25+
type: TYPE.BRUSH,
26+
subType: SUB_TYPE.MAIN,
27+
remaining: {
28+
unit: UNITS.MINUTES,
29+
value: Math.max(MAIN_BRUSH_LIFE_TIME - used, 0)
30+
}
31+
});
32+
case CONSUMABLE_TYPE.SIDE_BRUSH:
33+
return new ConsumableStateAttribute({
34+
type: TYPE.BRUSH,
35+
subType: SUB_TYPE.SIDE_RIGHT,
36+
remaining: {
37+
unit: UNITS.MINUTES,
38+
value: Math.max(SIDE_BRUSH_LIFE_TIME - used, 0)
39+
}
40+
});
41+
case CONSUMABLE_TYPE.FILTER:
42+
return new ConsumableStateAttribute({
43+
type: TYPE.FILTER,
44+
subType: SUB_TYPE.MAIN,
45+
remaining: {
46+
unit: UNITS.MINUTES,
47+
value: Math.max(FILTER_LIFE_TIME - used, 0)
48+
}
49+
});
50+
case CONSUMABLE_TYPE.DISHCLOTH:
51+
return new ConsumableStateAttribute({
52+
type: TYPE.MOP,
53+
subType: SUB_TYPE.MAIN,
54+
remaining: {
55+
unit: UNITS.MINUTES,
56+
value: Math.max(DISHCLOTH_LIFE_TIME - used, 0)
57+
}
58+
});
59+
}
60+
});
61+
62+
consumables.forEach(c => {
63+
return this.robot.state.upsertFirstMatchingAttribute(c);
64+
});
65+
66+
// @ts-ignore
67+
this.robot.emitStateUpdated();
68+
69+
return consumables;
70+
}
71+
72+
/**
73+
* @param {string} type
74+
* @param {string} [subType]
75+
* @returns {Promise<void>}
76+
*/
77+
async resetConsumable(type, subType) {
78+
if (!this.robot.robot) {
79+
throw new Error("There is no robot connected to server");
80+
}
81+
82+
/**
83+
* @type {import("@agnoc/core").ConsumableType}
84+
*/
85+
let consumable;
86+
87+
if (type === TYPE.BRUSH && subType === SUB_TYPE.MAIN) {
88+
consumable = CONSUMABLE_TYPE.MAIN_BRUSH;
89+
} else if (type === TYPE.BRUSH && subType === SUB_TYPE.SIDE_RIGHT) {
90+
consumable = CONSUMABLE_TYPE.SIDE_BRUSH;
91+
} else if (type === TYPE.FILTER) {
92+
consumable = CONSUMABLE_TYPE.FILTER;
93+
} else if (type === TYPE.MOP) {
94+
consumable = CONSUMABLE_TYPE.DISHCLOTH;
95+
}
96+
97+
if (consumable) {
98+
await this.robot.robot.resetConsumable(consumable);
99+
}
100+
}
101+
};
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
const CurrentStatisticsCapability = require("../../../core/capabilities/CurrentStatisticsCapability");
2+
const ValetudoDataPoint = require("../../../entities/core/ValetudoDataPoint");
3+
4+
/**
5+
* @extends CurrentStatisticsCapability<import("../CecotecCongaRobot")>
6+
*/
7+
class CecotecCurrentStatisticsCapability extends CurrentStatisticsCapability {
8+
/**
9+
* @return {Promise<Array<ValetudoDataPoint>>}
10+
*/
11+
async getStatistics() {
12+
return [
13+
new ValetudoDataPoint({
14+
type: ValetudoDataPoint.TYPES.TIME,
15+
value: (this.robot.robot?.device.currentClean?.time || 0) * 60,
16+
}),
17+
new ValetudoDataPoint({
18+
type: ValetudoDataPoint.TYPES.AREA,
19+
value: (this.robot.robot?.device.currentClean?.size || 0) * 100,
20+
}),
21+
];
22+
}
23+
24+
getProperties() {
25+
return {
26+
availableStatistics: [
27+
ValetudoDataPoint.TYPES.TIME,
28+
ValetudoDataPoint.TYPES.AREA,
29+
],
30+
};
31+
}
32+
}
33+
34+
module.exports = CecotecCurrentStatisticsCapability;
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
const DoNotDisturbCapability = require("../../../core/capabilities/DoNotDisturbCapability");
2+
const ValetudoDNDConfiguration = require("../../../entities/core/ValetudoDNDConfiguration");
3+
const {DeviceQuietHours, DeviceTime} = require("@agnoc/core");
4+
5+
/**
6+
* @extends DoNotDisturbCapability<import("../CecotecCongaRobot")>
7+
*/
8+
module.exports = class CecotecDoNotDisturbCapability extends DoNotDisturbCapability {
9+
10+
/**
11+
* @returns {Promise<import("../../../entities/core/ValetudoDNDConfiguration")>}
12+
*/
13+
async getDndConfiguration() {
14+
if (!this.robot.robot) {
15+
throw new Error("There is no robot connected to server");
16+
}
17+
18+
const quietHours = await this.robot.robot.getQuietHours();
19+
20+
return new ValetudoDNDConfiguration({
21+
enabled: quietHours.isEnabled,
22+
start: {
23+
hour: quietHours.begin.hour,
24+
minute: quietHours.begin.minute
25+
},
26+
end: {
27+
hour: quietHours.end.hour,
28+
minute: quietHours.end.minute
29+
}
30+
});
31+
}
32+
33+
/**
34+
* @param {import("../../../entities/core/ValetudoDNDConfiguration")} dndConfig
35+
* @returns {Promise<void>}
36+
*/
37+
async setDndConfiguration(dndConfig) {
38+
if (!this.robot.robot) {
39+
throw new Error("There is no robot connected to server");
40+
}
41+
42+
await this.robot.robot.setQuietHours(new DeviceQuietHours({
43+
isEnabled: dndConfig.enabled,
44+
begin: new DeviceTime({
45+
hour: dndConfig.start.hour,
46+
minute: dndConfig.start.minute,
47+
}),
48+
end: new DeviceTime({
49+
hour: dndConfig.end.hour,
50+
minute: dndConfig.end.minute,
51+
}),
52+
}));
53+
}
54+
};
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
const FanSpeedControlCapability = require("../../../core/capabilities/FanSpeedControlCapability");
2+
const {DeviceFanSpeed} = require("@agnoc/core");
3+
4+
/**
5+
* @extends FanSpeedControlCapability<import("../CecotecCongaRobot")>
6+
*/
7+
module.exports = class CecotecFanSpeedControlCapability extends FanSpeedControlCapability {
8+
/**
9+
* @returns {Array<string>}
10+
*/
11+
getFanSpeedPresets() {
12+
return this.presets.map(p => {
13+
return p.name;
14+
});
15+
}
16+
17+
async selectPreset(preset) {
18+
if (!this.robot.robot) {
19+
throw new Error("There is no robot connected to server");
20+
}
21+
22+
const matchedPreset = this.presets.find(p => {
23+
return p.name === preset;
24+
});
25+
26+
if (!matchedPreset) {
27+
throw new Error("Invalid Preset");
28+
}
29+
30+
await this.robot.robot.setFanSpeed(new DeviceFanSpeed({ value: matchedPreset.value }));
31+
}
32+
};

0 commit comments

Comments
 (0)