homebridge-kobold
Version:
A Vorwerk Kobold vacuum robot plugin for homebridge.
809 lines (700 loc) • 30.1 kB
text/typescript
import type { Characteristic, CharacteristicValue, PlatformAccessory, Service } from 'homebridge';
import type { WithUUID } from 'hap-nodejs';
import type { KoboldRobot } from 'node-kobold-control';
import Debug from 'debug';
import 'colors';
import type { KoboldBoundary, KoboldHomebridgePlatform, RobotRecord } from './platform.js';
const debug = Debug('homebridge-kobold');
const dictionaries = {
en: {
clean: 'Clean',
'clean the': 'Clean the',
goToDock: 'Go to Dock',
dockState: 'Dock',
eco: 'Eco Mode',
noGoLines: 'NoGo Lines',
extraCare: 'Extra Care',
schedule: 'Schedule',
findMe: 'Find me',
cleanSpot: 'Clean Spot',
battery: 'Battery',
},
de: {
clean: 'Sauge',
'clean the': 'Sauge',
goToDock: 'Zur Basis',
dockState: 'In der Basis',
eco: 'Eco Modus',
noGoLines: 'NoGo Linien',
extraCare: 'Extra Care',
schedule: 'Zeitplan',
findMe: 'Finde mich',
cleanSpot: 'Spot Reinigung',
battery: 'Batterie',
},
fr: {
clean: 'Aspirer',
'clean the': 'Aspirer',
goToDock: 'Retour à la base',
dockState: 'Sur la base',
eco: 'Eco mode',
noGoLines: 'Lignes NoGo',
extraCare: 'Extra Care',
schedule: 'Planifier',
findMe: 'Me retrouver',
cleanSpot: 'Nettoyage local',
battery: 'Batterie',
},
} as const;
type Dictionary = (typeof dictionaries)[keyof typeof dictionaries];
interface SpotSettings {
width: number | null;
height: number | null;
repeat: boolean;
}
type HapServiceConstructor = (typeof Service) & WithUUID<typeof Service>;
type HapCharacteristicConstructor = WithUUID<new () => Characteristic>;
export class KoboldVacuumAccessory {
private readonly robotObject: RobotRecord;
private readonly robot: KoboldRobot;
private readonly meta: Record<string, unknown>;
private readonly dict: Dictionary;
private readonly cleanService: Service;
private readonly batteryService?: Service;
private readonly goToDockService?: Service;
private readonly dockStateService?: Service;
private readonly ecoService?: Service;
private readonly noGoLinesService?: Service;
private readonly extraCareService?: Service;
private readonly scheduleService?: Service;
private readonly findMeService?: Service;
private readonly spotCleanService?: Service;
private spotWidthCharacteristic?: Characteristic;
private spotHeightCharacteristic?: Characteristic;
private spotRepeatCharacteristic?: Characteristic;
private readonly spotPlusFeatures: boolean;
private readonly refreshSetting;
private nextRoom: string | null = null;
private readonly name: string;
private readonly boundaryServices: Map<string, Service> = new Map();
private readonly boundaryLabels: Map<string, string> = new Map();
private readonly pendingBoundaryStates: Map<string, boolean | null> = new Map();
constructor(
private readonly platform: KoboldHomebridgePlatform,
private readonly accessory: PlatformAccessory,
robotObject: RobotRecord,
) {
this.robotObject = robotObject;
this.robot = robotObject.device;
this.meta = robotObject.meta;
this.refreshSetting = this.platform.refresh;
this.dict = dictionaries[this.platform.language as keyof typeof dictionaries] ?? dictionaries.en;
this.spotPlusFeatures = Array.isArray(this.robotObject.availableServices?.spotCleaning)
? this.robotObject.availableServices.spotCleaning.includes('basic')
: false;
this.name = this.robot.name;
this.accessory.displayName = this.name;
this.accessory.context.displayName = this.name;
this.accessory.context.robotSerial = this.robot._serial;
this.accessory.context.boundaryId = null;
const modelName = typeof this.meta.modelName === 'string' ? this.meta.modelName : 'Unknown Model';
const firmware = typeof this.meta.firmware === 'string' ? this.meta.firmware : 'Unknown';
this.informationService()
.setCharacteristic(this.platform.Characteristic.Manufacturer, 'Vorwerk Deutschland Stiftung & Co. KG')
.setCharacteristic(this.platform.Characteristic.Model, modelName)
.setCharacteristic(this.platform.Characteristic.SerialNumber, this.robot._serial)
.setCharacteristic(this.platform.Characteristic.FirmwareRevision, firmware)
.setCharacteristic(this.platform.Characteristic.Name, this.robot.name);
this.cleanService = this.createService(
this.platform.Service.Switch,
`${this.name} ${this.dict.clean}`,
'clean',
);
this.cleanService.getCharacteristic(this.platform.Characteristic.On)
.onSet(value => this.setClean(value))
.onGet(() => this.getClean());
this.batteryService = this.createService(
this.platform.Service.Battery,
`${this.name} ${this.dict.battery}`,
'battery',
true,
);
if (this.batteryService) {
this.batteryService.getCharacteristic(this.platform.Characteristic.BatteryLevel)
.onGet(this.getBatteryLevel.bind(this));
this.batteryService.getCharacteristic(this.platform.Characteristic.ChargingState)
.onGet(this.getBatteryChargingState.bind(this));
}
const exposeDock = !this.platform.isServiceHidden('dock');
this.goToDockService = this.createService(
this.platform.Service.Switch,
`${this.name} ${this.dict.goToDock}`,
'goToDock',
exposeDock,
);
if (exposeDock && this.goToDockService) {
this.goToDockService.getCharacteristic(this.platform.Characteristic.On)
.onSet(this.setGoToDock.bind(this))
.onGet(this.getGoToDock.bind(this));
}
const exposeDockState = !this.platform.isServiceHidden('dockstate');
this.dockStateService = this.createService(
this.platform.Service.OccupancySensor,
`${this.name} ${this.dict.dockState}`,
'dockState',
exposeDockState,
);
if (exposeDockState && this.dockStateService) {
this.dockStateService.getCharacteristic(this.platform.Characteristic.OccupancyDetected)
.onGet(this.getDock.bind(this));
}
const exposeEco = !this.platform.isServiceHidden('eco');
this.ecoService = this.createService(
this.platform.Service.Switch,
`${this.name} ${this.dict.eco}`,
'eco',
exposeEco,
);
if (exposeEco && this.ecoService) {
this.ecoService.getCharacteristic(this.platform.Characteristic.On)
.onSet(this.setEco.bind(this))
.onGet(this.getEco.bind(this));
}
const exposeNoGo = !this.platform.isServiceHidden('nogolines');
this.noGoLinesService = this.createService(
this.platform.Service.Switch,
`${this.name} ${this.dict.noGoLines}`,
'noGoLines',
exposeNoGo,
);
if (exposeNoGo && this.noGoLinesService) {
this.noGoLinesService.getCharacteristic(this.platform.Characteristic.On)
.onSet(this.setNoGoLines.bind(this))
.onGet(this.getNoGoLines.bind(this));
}
const exposeExtraCare = !this.platform.isServiceHidden('extracare');
this.extraCareService = this.createService(
this.platform.Service.Switch,
`${this.name} ${this.dict.extraCare}`,
'extraCare',
exposeExtraCare,
);
if (exposeExtraCare && this.extraCareService) {
this.extraCareService.getCharacteristic(this.platform.Characteristic.On)
.onSet(this.setExtraCare.bind(this))
.onGet(this.getExtraCare.bind(this));
}
const exposeSchedule = !this.platform.isServiceHidden('schedule');
this.scheduleService = this.createService(
this.platform.Service.Switch,
`${this.name} ${this.dict.schedule}`,
'schedule',
exposeSchedule,
);
if (exposeSchedule && this.scheduleService) {
this.scheduleService.getCharacteristic(this.platform.Characteristic.On)
.onSet(this.setSchedule.bind(this))
.onGet(this.getSchedule.bind(this));
}
const exposeFind = !this.platform.isServiceHidden('find');
this.findMeService = this.createService(
this.platform.Service.Switch,
`${this.name} ${this.dict.findMe}`,
'findMe',
exposeFind,
);
if (exposeFind && this.findMeService) {
this.findMeService.getCharacteristic(this.platform.Characteristic.On)
.onSet(this.setFindMe.bind(this))
.onGet(this.getFindMe.bind(this));
}
const exposeSpot = !this.platform.isServiceHidden('spot');
this.spotCleanService = this.createService(
this.platform.Service.Switch,
`${this.name} ${this.dict.cleanSpot}`,
'cleanSpot',
exposeSpot,
);
if (exposeSpot && this.spotCleanService) {
this.spotCleanService.getCharacteristic(this.platform.Characteristic.On)
.onSet(this.setSpotClean.bind(this))
.onGet(this.getSpotClean.bind(this));
this.spotRepeatCharacteristic = this.getOrAddCharacteristic(
this.spotCleanService,
this.platform.spotCharacteristics.SpotRepeatCharacteristic,
);
this.spotRepeatCharacteristic
?.onSet(this.setSpotRepeat.bind(this))
.onGet(this.getSpotRepeat.bind(this));
if (this.spotPlusFeatures) {
this.spotWidthCharacteristic = this.getOrAddCharacteristic(
this.spotCleanService,
this.platform.spotCharacteristics.SpotWidthCharacteristic,
);
this.spotHeightCharacteristic = this.getOrAddCharacteristic(
this.spotCleanService,
this.platform.spotCharacteristics.SpotHeightCharacteristic,
);
this.spotWidthCharacteristic
?.onSet(this.setSpotWidth.bind(this))
.onGet(this.getSpotWidth.bind(this));
this.spotHeightCharacteristic
?.onSet(this.setSpotHeight.bind(this))
.onGet(this.getSpotHeight.bind(this));
}
}
this.setupBoundaryServices();
}
updated(): void {
const currentClean = this.cleanService.getCharacteristic(this.platform.Characteristic.On).value as boolean | undefined;
const robotCanPause = !!this.robot.canPause;
if ((currentClean ?? false) !== robotCanPause) {
this.cleanService.updateCharacteristic(this.platform.Characteristic.On, robotCanPause);
}
if (this.goToDockService) {
const dockValue = this.goToDockService.getCharacteristic(this.platform.Characteristic.On).value as boolean | undefined;
if (dockValue === true && !!this.robot.dockHasBeenSeen) {
this.goToDockService.updateCharacteristic(this.platform.Characteristic.On, false);
}
}
if (this.scheduleService) {
const scheduleValue = this.scheduleService.getCharacteristic(this.platform.Characteristic.On).value as boolean | undefined;
const scheduleEnabled = !!this.robot.isScheduleEnabled;
if ((scheduleValue ?? false) !== scheduleEnabled) {
this.scheduleService.updateCharacteristic(
this.platform.Characteristic.On,
scheduleEnabled,
);
}
}
const isDocked = !!this.robot.isDocked;
this.dockStateService?.updateCharacteristic(
this.platform.Characteristic.OccupancyDetected,
isDocked ? 1 : 0,
);
this.ecoService?.updateCharacteristic(this.platform.Characteristic.On, !!this.robot.eco);
this.noGoLinesService?.updateCharacteristic(this.platform.Characteristic.On, !!this.robot.noGoLines);
this.extraCareService?.updateCharacteristic(
this.platform.Characteristic.On,
this.robot.navigationMode === 2,
);
const repeatValue = this.robot.spotRepeat ?? false;
this.spotRepeatCharacteristic?.updateValue(repeatValue);
if (this.spotPlusFeatures && this.spotWidthCharacteristic && this.spotHeightCharacteristic) {
const widthProps = this.spotWidthCharacteristic.props;
const heightProps = this.spotHeightCharacteristic.props;
const widthValid = this.robot.spotWidth !== undefined
&& this.robot.spotWidth >= (widthProps.minValue ?? 0)
&& this.robot.spotWidth <= (widthProps.maxValue ?? Number.MAX_SAFE_INTEGER)
? this.robot.spotWidth
: widthProps.minValue ?? this.robot.spotWidth ?? widthProps.minValue ?? 0;
const heightValid = this.robot.spotHeight !== undefined
&& this.robot.spotHeight >= (heightProps.minValue ?? 0)
&& this.robot.spotHeight <= (heightProps.maxValue ?? Number.MAX_SAFE_INTEGER)
? this.robot.spotHeight
: heightProps.minValue ?? this.robot.spotHeight ?? heightProps.minValue ?? 0;
this.spotWidthCharacteristic.updateValue(widthValid);
this.spotHeightCharacteristic.updateValue(heightValid);
}
this.boundaryServices.forEach((service, boundaryId) => {
const isCleaningBoundary = !!this.robot.canPause && this.robot.cleaningBoundaryId === boundaryId;
const boundaryValue = service.getCharacteristic(this.platform.Characteristic.On).value as boolean | undefined;
if ((boundaryValue ?? false) !== isCleaningBoundary) {
service.updateCharacteristic(this.platform.Characteristic.On, isCleaningBoundary);
} else {
const pendingState = this.pendingBoundaryStates.get(boundaryId);
if (pendingState !== null && pendingState !== undefined) {
this.pendingBoundaryStates.set(boundaryId, null);
}
}
});
this.batteryService?.updateCharacteristic(this.platform.Characteristic.BatteryLevel, this.robot.charge ?? 0);
this.batteryService?.updateCharacteristic(this.platform.Characteristic.ChargingState, !!this.robot.isCharging);
if (this.nextRoom != null && this.robot.isDocked) {
const boundaryId = this.nextRoom;
if (!this.boundaryLabels.has(boundaryId)) {
this.nextRoom = null;
return;
}
void this.clean({ boundaryId }).then(() => {
this.nextRoom = null;
const boundaryName = this.boundaryLabels.get(boundaryId) ?? boundaryId;
debug(`${this.name}: ## Starting cleaning of next room (${boundaryName})`);
}).catch(() => {
this.nextRoom = null;
});
}
}
private informationService(): Service {
return (
this.accessory.getService(this.platform.Service.AccessoryInformation)
|| this.accessory.addService(this.platform.Service.AccessoryInformation)
);
}
private boundaryServiceName(boundaryName: string): string {
const splitName = boundaryName.split(' ');
if (splitName.length >= 2 && /[']s$/g.test(splitName[splitName.length - 2])) {
return `${this.dict.clean} ${boundaryName}`;
}
return `${this.dict['clean the']} ${boundaryName}`;
}
private ensureUniqueBoundaryName(name: string, usedNames: Set<string>): string {
let candidate = name;
let counter = 2;
while (usedNames.has(candidate)) {
candidate = `${name} ${counter}`;
counter += 1;
}
usedNames.add(candidate);
return candidate;
}
private setupBoundaryServices(): void {
const maps = (Array.isArray(this.robot.maps) ? this.robot.maps : []) as Array<{ boundaries?: KoboldBoundary[] }>;
const usedNames = new Set<string>();
maps.forEach(map => {
if (!Array.isArray(map.boundaries)) {
return;
}
map.boundaries.forEach(boundary => {
if (boundary.type !== 'polygon') {
return;
}
if (this.boundaryServices.has(boundary.id)) {
return;
}
const displayName = this.ensureUniqueBoundaryName(boundary.name, usedNames);
this.boundaryLabels.set(boundary.id, displayName);
const serviceName = this.boundaryServiceName(displayName);
const service = this.createService(
this.platform.Service.Switch,
serviceName,
`cleanBoundary:${boundary.id}`,
);
service.getCharacteristic(this.platform.Characteristic.On)
.onSet(value => this.setClean(value, boundary.id))
.onGet(() => this.getClean(boundary.id));
this.boundaryServices.set(boundary.id, service);
this.pendingBoundaryStates.set(boundary.id, null);
});
});
}
private createService(
serviceType: HapServiceConstructor,
name: string,
subtype: string,
expose = true,
): Service {
const existing = this.accessory.getServiceById(serviceType, subtype);
if (expose) {
if (existing) {
this.applyConfiguredName(existing, name);
return existing;
}
const service = this.accessory.addService(serviceType, name, subtype);
this.applyConfiguredName(service, name);
return service;
}
if (existing) {
this.accessory.removeService(existing);
}
const service = new serviceType(name, subtype);
this.applyConfiguredName(service, name);
return service;
}
private getOrAddCharacteristic(service: Service, characteristic: HapCharacteristicConstructor) {
const ctor = characteristic as unknown as WithUUID<typeof Characteristic>;
if (service.testCharacteristic(ctor)) {
return service.getCharacteristic(characteristic);
}
return service.addCharacteristic(characteristic);
}
private applyConfiguredName(service: Service, name: string): void {
service.setCharacteristic(this.platform.Characteristic.Name, name);
service.addOptionalCharacteristic(this.platform.Characteristic.ConfiguredName);
service.setCharacteristic(this.platform.Characteristic.ConfiguredName, name);
}
private asBool(value: CharacteristicValue): boolean {
return value === true || value === 1;
}
private async getClean(boundaryId?: string): Promise<CharacteristicValue> {
if (boundaryId) {
const pending = this.pendingBoundaryStates.get(boundaryId);
if (pending !== null && pending !== undefined) {
return pending;
}
}
await this.platform.updateRobot(this.robot._serial);
const cleaning = boundaryId
? !!this.robot.canPause && this.robot.cleaningBoundaryId === boundaryId
: !!this.robot.canPause;
debug(`${this.name}: Cleaning ${boundaryId ?? 'house'} is ${cleaning ? 'ON'.brightGreen : 'OFF'.red}`);
return cleaning;
}
private async setClean(value: CharacteristicValue, boundaryId?: string): Promise<void> {
const on = this.asBool(value);
const boundaryName = boundaryId ? this.boundaryLabels.get(boundaryId) ?? boundaryId : 'home';
debug(`${this.name}: ${on ? 'Enabled '.brightGreen : 'Disabled'.red} Clean ${boundaryName}`);
if (boundaryId) {
this.pendingBoundaryStates.set(boundaryId, on);
}
await this.platform.updateRobot(this.robot._serial);
if (on) {
if (!boundaryId || this.robot.cleaningBoundaryId === boundaryId) {
if (this.robot.canResume) {
debug(`${this.name}: ## Resume cleaning`);
await this.runCommand(cb => this.robot.resumeCleaning(cb));
} else if (this.robot.canStart) {
debug(`${this.name}: ## Start cleaning`);
await this.clean({ boundaryId });
} else {
debug(`${this.name}: Cannot start, maybe already cleaning (expected)`);
}
} else if (this.robot.canPause || this.robot.canResume) {
debug(`${this.name}: ## Returning to dock to start cleaning of new room`);
await this.goToDockSequence();
this.nextRoom = boundaryId;
} else {
debug(`${this.name}: ## Start cleaning of new room`);
await this.clean({ boundaryId });
}
} else if (this.robot.canPause) {
debug(`${this.name}: ## Pause cleaning`);
await this.runCommand(cb => this.robot.pauseCleaning(cb));
} else {
debug(`${this.name}: Already paused`);
}
await this.platform.updateRobot(this.robot._serial);
if (boundaryId) {
this.pendingBoundaryStates.set(boundaryId, null);
}
}
private async clean(options?: { spot?: SpotSettings; boundaryId?: string }): Promise<void> {
if (this.refreshSetting === 'auto') {
setTimeout(() => {
this.platform.updateRobotTimer(this.robot._serial);
}, 60 * 1000);
}
const boundaryId = options?.boundaryId ?? null;
const spot = options?.spot;
const eco = !!this.robot.eco;
const extraCare = this.robot.navigationMode === 2;
const noGoLines = !!this.robot.noGoLines;
const room = boundaryId ? this.boundaryLabels.get(boundaryId) ?? '' : '';
const roomLabel = room !== '' ? `${room} ` : '';
const detailText = `eco: ${eco}, extraCare: ${extraCare}, nogoLines: ${noGoLines}, spot: ${JSON.stringify(spot)}`;
debug(`${this.name}: ## Start cleaning (${roomLabel}${detailText})`);
if (!boundaryId && !spot) {
await this.runCommand((cb) => this.robot.startCleaning(eco, extraCare ? 2 : 1, noGoLines, cb), (error, result) => {
this.platform.log.error(`Cannot start cleaning. ${error}: ${JSON.stringify(result)}`);
});
} else if (boundaryId) {
await this.runCommand((cb) => this.robot.startCleaningBoundary(eco, extraCare, boundaryId, cb), (error, result) => {
this.platform.log.error(`Cannot start room cleaning. ${error}: ${JSON.stringify(result)}`);
});
} else if (spot) {
const widthValue = spot.width ?? this.robot.spotWidth ?? this.spotWidthCharacteristic?.props.minValue ?? 100;
const heightValue = spot.height ?? this.robot.spotHeight ?? this.spotHeightCharacteristic?.props.minValue ?? 100;
await this.runCommand((cb) =>
this.robot.startSpotCleaning(
eco,
widthValue,
heightValue,
spot.repeat,
extraCare ? 2 : 1,
cb,
), (error, result) => {
this.platform.log.error(`Cannot start spot cleaning. ${error}: ${JSON.stringify(result)}`);
});
}
}
private async getGoToDock(): Promise<CharacteristicValue> {
return false;
}
private async setGoToDock(value: CharacteristicValue): Promise<void> {
const on = this.asBool(value);
if (!on) {
return;
}
await this.platform.updateRobot(this.robot._serial);
await this.goToDockSequence();
}
private async getEco(): Promise<CharacteristicValue> {
await this.platform.updateRobot(this.robot._serial);
const eco = !!this.robot.eco;
debug(`${this.name}: Eco Mode is ${eco ? 'ON'.brightGreen : 'OFF'.red}`);
return eco;
}
private async setEco(value: CharacteristicValue): Promise<void> {
const on = this.asBool(value);
this.robot.eco = on;
debug(`${this.name}: ${on ? 'Enabled '.red : 'Disabled'.red} Eco Mode`);
}
private async getNoGoLines(): Promise<CharacteristicValue> {
await this.platform.updateRobot(this.robot._serial);
const noGo = !!this.robot.noGoLines;
debug(`${this.name}: NoGoLine is ${noGo ? 'ON'.brightGreen : 'OFF'.red}`);
return noGo ? 1 : 0;
}
private async setNoGoLines(value: CharacteristicValue): Promise<void> {
const on = this.asBool(value);
this.robot.noGoLines = on;
debug(`${this.name}: ${on ? 'Enabled '.brightGreen : 'Disabled'.red} NoGoLine`);
}
private async getExtraCare(): Promise<CharacteristicValue> {
await this.platform.updateRobot(this.robot._serial);
debug(`${this.name}: Care Nav is ${this.robot.navigationMode === 2 ? 'ON'.brightGreen : 'OFF'.red}`);
return this.robot.navigationMode === 2 ? 1 : 0;
}
private async setExtraCare(value: CharacteristicValue): Promise<void> {
const on = this.asBool(value);
this.robot.navigationMode = on ? 2 : 1;
debug(`${this.name}: ${on ? 'Enabled '.brightGreen : 'Disabled'.red} Care Nav`);
}
private async getSchedule(): Promise<CharacteristicValue> {
await this.platform.updateRobot(this.robot._serial);
const enabled = !!this.robot.isScheduleEnabled;
debug(`${this.name}: Schedule is ${enabled ? 'ON'.brightGreen : 'OFF'.red}`);
return enabled;
}
private async setSchedule(value: CharacteristicValue): Promise<void> {
const on = this.asBool(value);
await this.platform.updateRobot(this.robot._serial);
if (on) {
debug(this.name + ': ' + 'Enabled'.brightGreen + ' Schedule');
await this.runCommand(cb => this.robot.enableSchedule(cb));
} else {
debug(this.name + ': ' + 'Disabled'.red + ' Schedule');
await this.runCommand(cb => this.robot.disableSchedule(cb));
}
}
private async getFindMe(): Promise<CharacteristicValue> {
return false;
}
private async setFindMe(value: CharacteristicValue): Promise<void> {
const on = this.asBool(value);
if (!on) {
return;
}
debug(`${this.name}: ## Find me`);
setTimeout(() => {
this.findMeService?.updateCharacteristic(this.platform.Characteristic.On, false);
}, 1000);
await this.runCommand(cb => this.robot.findMe(cb));
}
private async getSpotClean(): Promise<CharacteristicValue> {
const characteristic = this.spotCleanService?.getCharacteristic(this.platform.Characteristic.On);
const spotService = characteristic?.value as boolean | undefined;
return spotService ?? false;
}
private async setSpotClean(value: CharacteristicValue): Promise<void> {
const on = this.asBool(value);
const spot: SpotSettings = {
width: this.spotPlusFeatures && this.spotWidthCharacteristic
? ((this.spotWidthCharacteristic.value as number | undefined) ?? this.robot.spotWidth ?? this.spotWidthCharacteristic.props.minValue ?? 100)
: this.robot.spotWidth ?? null,
height: this.spotPlusFeatures && this.spotHeightCharacteristic
? ((this.spotHeightCharacteristic.value as number | undefined) ?? this.robot.spotHeight ?? this.spotHeightCharacteristic.props.minValue ?? 100)
: this.robot.spotHeight ?? null,
repeat: !!(this.spotRepeatCharacteristic?.value ?? false),
};
await this.platform.updateRobot(this.robot._serial);
if (on) {
if (this.robot.canResume) {
debug(`${this.name}: ## Resume (spot) cleaning`);
await this.runCommand(cb => this.robot.resumeCleaning(cb));
} else if (this.robot.canStart) {
await this.clean({ spot });
} else {
debug(`${this.name}: Cannot start spot cleaning, maybe already cleaning`);
}
} else if (this.robot.canPause) {
debug(`${this.name}: ## Pause cleaning`);
await this.runCommand(cb => this.robot.pauseCleaning(cb));
} else {
debug(`${this.name}: Already paused`);
}
}
private async getSpotWidth(): Promise<CharacteristicValue> {
await this.platform.updateRobot(this.robot._serial);
const width = this.robot.spotWidth ?? this.spotWidthCharacteristic?.props.minValue ?? 100;
debug(`${this.name}: Spot width is ${width}cm`);
return width;
}
private async setSpotWidth(value: CharacteristicValue): Promise<void> {
const numericValue = typeof value === 'number' ? value : Number(value);
this.robot.spotWidth = Number.isNaN(numericValue) ? this.robot.spotWidth ?? 100 : numericValue;
debug(`${this.name}: Set spot width to ${this.robot.spotWidth}cm`);
}
private async getSpotHeight(): Promise<CharacteristicValue> {
await this.platform.updateRobot(this.robot._serial);
const height = this.robot.spotHeight ?? this.spotHeightCharacteristic?.props.minValue ?? 100;
debug(`${this.name}: Spot height is ${height}cm`);
return height;
}
private async setSpotHeight(value: CharacteristicValue): Promise<void> {
const numericValue = typeof value === 'number' ? value : Number(value);
this.robot.spotHeight = Number.isNaN(numericValue) ? this.robot.spotHeight ?? 100 : numericValue;
debug(`${this.name}: Set spot height to ${this.robot.spotHeight}cm`);
}
private async getSpotRepeat(): Promise<CharacteristicValue> {
await this.platform.updateRobot(this.robot._serial);
const repeat = this.robot.spotRepeat ?? false;
debug(`${this.name}: Spot repeat is ${repeat ? 'ON'.brightGreen : 'OFF'.red}`);
return repeat;
}
private async setSpotRepeat(value: CharacteristicValue): Promise<void> {
const on = this.asBool(value);
this.robot.spotRepeat = on;
debug(`${this.name}: ${on ? 'Enabled '.brightGreen : 'Disabled'.red} Spot repeat`);
}
private async getDock(): Promise<CharacteristicValue> {
await this.platform.updateRobot(this.robot._serial);
const isDocked = !!this.robot.isDocked;
debug(`${this.name}: The Dock is ${isDocked ? 'OCCUPIED'.brightGreen : 'NOT OCCUPIED'.red}`);
return isDocked ? 1 : 0;
}
private async getBatteryLevel(): Promise<CharacteristicValue> {
await this.platform.updateRobot(this.robot._serial);
const charge = this.robot.charge ?? 0;
debug(`${this.name}: Battery is ${charge}%`);
return charge;
}
private async getBatteryChargingState(): Promise<CharacteristicValue> {
await this.platform.updateRobot(this.robot._serial);
const isCharging = !!this.robot.isCharging;
debug(`${this.name}: Battery is ${isCharging ? 'CHARGING'.brightGreen : 'NOT CHARGING'.red}`);
return isCharging;
}
private async goToDockSequence(): Promise<void> {
if (this.robot.canPause) {
debug(`${this.name}: ## Pause cleaning to go to dock`);
await this.runCommand(cb => this.robot.pauseCleaning(cb));
await this.delay(1000);
debug(`${this.name}: ## Go to dock`);
await this.runCommand(cb => this.robot.sendToBase(cb));
} else if (this.robot.canGoToBase) {
debug(`${this.name}: ## Go to dock`);
await this.runCommand(cb => this.robot.sendToBase(cb));
} else {
this.platform.log.warn(`${this.name}: Can't go to dock at the moment`);
}
}
private async runCommand<T = unknown>(
command: (callback: (error?: unknown, result?: T) => void) => void,
onError?: (error: unknown, result?: T) => void,
): Promise<T | undefined> {
return new Promise((resolve, reject) => {
command((error?: unknown, result?: T) => {
if (error) {
onError?.(error, result);
reject(error instanceof Error ? error : new Error(String(error)));
} else {
resolve(result);
}
});
});
}
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}