initial merge attempt
This commit is contained in:
parent
750b25066b
commit
369a6f6412
@ -4,7 +4,7 @@
|
||||
],
|
||||
"ext": "ts",
|
||||
"ignore": [],
|
||||
"exec": "tsc && homebridge -I -D",
|
||||
"exec": "tsc",
|
||||
"signal": "SIGTERM",
|
||||
"env": {
|
||||
"NODE_OPTIONS": "--trace-warnings"
|
||||
|
@ -15,8 +15,8 @@
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"lint": "eslint src/**.ts --max-warnings=0",
|
||||
"watch": "npm run build && npm link && nodemon",
|
||||
"build": "rimraf ./dist && tsc"
|
||||
"build": "rimraf ./dist && tsc",
|
||||
"watch": "npm run build && npm link && nodemon"
|
||||
},
|
||||
"author": {
|
||||
"name": "Luis R.",
|
||||
|
648
src/accessories/koboldVacuumRobot.ts
Normal file
648
src/accessories/koboldVacuumRobot.ts
Normal file
@ -0,0 +1,648 @@
|
||||
import {CharacteristicValue, Logger, PlatformAccessory, PlatformAccessoryEvent, PlatformConfig, Service} from 'homebridge';
|
||||
import {HomebridgeKoboldPlatform} from '../homebridgeKoboldPlatform';
|
||||
import {Options} from '../models/options';
|
||||
|
||||
/**
|
||||
* Platform Accessory
|
||||
* An instance of this class is created for each accessory your platform registers
|
||||
* Each accessory may expose multiple services of different service types.
|
||||
*/
|
||||
export class KoboldVacuumRobotAccessory
|
||||
{
|
||||
// Homebridge
|
||||
private log: Logger;
|
||||
private batteryService: Service;
|
||||
private readonly cleanService: Service | null;
|
||||
private readonly findMeService: Service | null;
|
||||
private readonly goToDockService: Service | null;
|
||||
private readonly dockStateService: Service | null;
|
||||
private readonly binFullService: Service | null;
|
||||
private readonly ecoService: Service | null;
|
||||
private readonly noGoLinesService: Service | null;
|
||||
private readonly extraCareService: Service | null;
|
||||
private readonly scheduleService: Service | null;
|
||||
private readonly spotCleanService: Service | null;
|
||||
|
||||
// Context
|
||||
private robot: any;
|
||||
private readonly options: Options;
|
||||
|
||||
// Config
|
||||
private readonly backgroundUpdateInterval: number;
|
||||
private readonly prefix: boolean;
|
||||
private readonly availableServices: string[];
|
||||
|
||||
// Transient
|
||||
private isSpotCleaning: boolean;
|
||||
private timer: any;
|
||||
|
||||
/**
|
||||
* These are just used to create a working example
|
||||
* You should implement your own code to track the state of your accessory
|
||||
*/
|
||||
|
||||
constructor(
|
||||
private readonly platform: HomebridgeKoboldPlatform,
|
||||
private readonly accessory: PlatformAccessory,
|
||||
private readonly config: PlatformConfig)
|
||||
{
|
||||
this.log = platform.log;
|
||||
|
||||
this.robot = accessory.context.robot;
|
||||
this.options = accessory.context.options || new Options();
|
||||
|
||||
this.backgroundUpdateInterval = KoboldVacuumRobotAccessory.parseBackgroundUpdateInterval(this.config['backgroundUpdate']);
|
||||
this.prefix = this.config['prefix'] || PREFIX;
|
||||
this.availableServices = this.config['services'] || SERVICES;
|
||||
|
||||
this.isSpotCleaning = false;
|
||||
|
||||
// Information
|
||||
this.accessory.getService(this.platform.Service.AccessoryInformation)!
|
||||
.setCharacteristic(this.platform.Characteristic.Manufacturer, "Neato Robotics")
|
||||
.setCharacteristic(this.platform.Characteristic.Model, this.robot.meta.modelName)
|
||||
.setCharacteristic(this.platform.Characteristic.SerialNumber, this.robot._serial)
|
||||
.setCharacteristic(this.platform.Characteristic.FirmwareRevision, this.robot.meta.firmware)
|
||||
.setCharacteristic(this.platform.Characteristic.Name, this.robot.name);
|
||||
|
||||
// Identify
|
||||
this.accessory.on(PlatformAccessoryEvent.IDENTIFY, () => {
|
||||
this.robot.findMe();
|
||||
|
||||
this.robot.getState((error, result) => {
|
||||
this.log.info("[" + this.robot.name + "] Identified");
|
||||
if (error)
|
||||
{
|
||||
this.debug(DebugType.INFO, JSON.stringify("Error: " + error));
|
||||
}
|
||||
this.debug(DebugType.INFO, "Status: " + JSON.stringify(result));
|
||||
this.debug(DebugType.INFO,
|
||||
"Config: Background Update Interval: " + this.backgroundUpdateInterval + ", Prefix: " + this.prefix + ", Enabled services: " + JSON.stringify(this.availableServices));
|
||||
});
|
||||
});
|
||||
|
||||
// Services
|
||||
this.cleanService = this.getSwitchService(RobotService.CLEAN_HOUSE);
|
||||
this.spotCleanService = this.getSwitchService(RobotService.CLEAN_SPOT);
|
||||
this.goToDockService = this.getSwitchService(RobotService.GO_TO_DOCK);
|
||||
this.dockStateService = this.getOccupancyService(RobotService.DOCKED)
|
||||
this.binFullService = this.getOccupancyService(RobotService.BIN_FULL)
|
||||
this.findMeService = this.getSwitchService(RobotService.FIND_ME);
|
||||
this.scheduleService = this.getSwitchService(RobotService.SCHEDULE);
|
||||
this.ecoService = this.getSwitchService(RobotService.ECO);
|
||||
this.noGoLinesService = this.getSwitchService(RobotService.NOGO_LINES);
|
||||
this.extraCareService = this.getSwitchService(RobotService.EXTRA_CARE);
|
||||
this.batteryService = this.accessory.getService(this.platform.Service.Battery) || this.accessory.addService(this.platform.Service.Battery)
|
||||
|
||||
if (this.cleanService)
|
||||
{
|
||||
this.cleanService.getCharacteristic(this.platform.Characteristic.On)
|
||||
.onSet(this.setCleanHouse.bind(this))
|
||||
.onGet(this.getCleanHouse.bind(this));
|
||||
}
|
||||
if (this.spotCleanService)
|
||||
{
|
||||
this.spotCleanService.getCharacteristic(this.platform.Characteristic.On)
|
||||
.onSet(this.setSpotClean.bind(this))
|
||||
.onGet(this.getSpotClean.bind(this));
|
||||
}
|
||||
if (this.goToDockService)
|
||||
{
|
||||
this.goToDockService.getCharacteristic(this.platform.Characteristic.On)
|
||||
.onSet(this.setGoToDock.bind(this))
|
||||
.onGet(this.getGoToDock.bind(this));
|
||||
}
|
||||
if (this.dockStateService)
|
||||
{
|
||||
this.dockStateService.getCharacteristic(this.platform.Characteristic.OccupancyDetected)
|
||||
.onGet(this.getDocked.bind(this));
|
||||
}
|
||||
if (this.binFullService)
|
||||
{
|
||||
this.binFullService.getCharacteristic(this.platform.Characteristic.OccupancyDetected)
|
||||
.onGet(this.getBinFull.bind(this));
|
||||
}
|
||||
if (this.findMeService)
|
||||
{
|
||||
this.findMeService.getCharacteristic(this.platform.Characteristic.On)
|
||||
.onSet(this.setFindMe.bind(this))
|
||||
.onGet(this.getFindMe.bind(this));
|
||||
}
|
||||
if (this.scheduleService)
|
||||
{
|
||||
this.scheduleService.getCharacteristic(this.platform.Characteristic.On)
|
||||
.onSet(this.setSchedule.bind(this))
|
||||
.onGet(this.getSchedule.bind(this));
|
||||
}
|
||||
if (this.ecoService)
|
||||
{
|
||||
this.ecoService.getCharacteristic(this.platform.Characteristic.On)
|
||||
.onSet(this.setEco.bind(this))
|
||||
.onGet(this.getEco.bind(this));
|
||||
}
|
||||
if (this.noGoLinesService)
|
||||
{
|
||||
this.noGoLinesService.getCharacteristic(this.platform.Characteristic.On)
|
||||
.onSet(this.setNoGoLines.bind(this))
|
||||
.onGet(this.getNoGoLines.bind(this));
|
||||
}
|
||||
if (this.extraCareService)
|
||||
{
|
||||
this.extraCareService.getCharacteristic(this.platform.Characteristic.On)
|
||||
.onSet(this.setExtraCare.bind(this))
|
||||
.onGet(this.getExtraCare.bind(this));
|
||||
}
|
||||
|
||||
// Start background update
|
||||
this.updateRobotPeriodically().then(() => {
|
||||
if (!accessory.context.options)
|
||||
{
|
||||
this.options.eco = this.robot.eco;
|
||||
this.options.noGoLines = this.robot.noGoLines;
|
||||
this.options.extraCare = this.robot.navigationMode == 2;
|
||||
this.debug(DebugType.INFO, "Options initially set to eco: " + this.options.eco + ", noGoLines: " + this.options.noGoLines + ", extraCare: " + this.options.extraCare);
|
||||
accessory.context.options = this.options;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.debug(DebugType.INFO, "Options loaded from cache eco: " + this.options.eco + ", noGoLines: " + this.options.noGoLines + ", extraCare: " + this.options.extraCare);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private getSwitchService(serviceName: string)
|
||||
{
|
||||
let displayName = this.prefix ? this.robot.name + serviceName : serviceName;
|
||||
|
||||
if (this.availableServices.includes(serviceName))
|
||||
{
|
||||
return this.accessory.getService(displayName) || this.accessory.addService(this.platform.Service.Switch, displayName, serviceName);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (this.accessory.getService(displayName))
|
||||
{
|
||||
this.accessory.removeService(<Service>this.accessory.getService(displayName));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private getOccupancyService(serviceName: string)
|
||||
{
|
||||
let displayName = this.prefix ? this.robot.name + serviceName : serviceName;
|
||||
|
||||
if (this.availableServices.includes(serviceName))
|
||||
{
|
||||
return this.accessory.getService(displayName) || this.accessory.addService(this.platform.Service.OccupancySensor, displayName, serviceName);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (this.accessory.getService(displayName))
|
||||
{
|
||||
this.accessory.removeService(<Service>this.accessory.getService(displayName));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static parseBackgroundUpdateInterval(configValue: any)
|
||||
{
|
||||
// Parse as number
|
||||
let backgroundUpdateInterval = parseInt(configValue) || BACKGROUND_INTERVAL;
|
||||
|
||||
// must be integer and positive
|
||||
backgroundUpdateInterval = ((backgroundUpdateInterval % 1) !== 0 || backgroundUpdateInterval < 0) ? BACKGROUND_INTERVAL : backgroundUpdateInterval;
|
||||
|
||||
return backgroundUpdateInterval;
|
||||
}
|
||||
|
||||
async getCleanHouse(): Promise<CharacteristicValue>
|
||||
{
|
||||
try
|
||||
{
|
||||
await this.updateRobot();
|
||||
return this.robot.canPause && !this.isSpotCleaning;
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
this.log.error("Cannot get cleaning status: " + error);
|
||||
throw new this.platform.api.hap.HapStatusError(this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
async setCleanHouse(on: CharacteristicValue)
|
||||
{
|
||||
this.debug(DebugType.STATUS, "Set CLEAN HOUSE: " + on);
|
||||
try
|
||||
{
|
||||
await this.updateRobot();
|
||||
|
||||
// Start
|
||||
if (on)
|
||||
{
|
||||
// Resume cleaning
|
||||
if (this.robot.canResume)
|
||||
{
|
||||
this.debug(DebugType.ACTION, "Resume cleaning");
|
||||
await this.robot.resumeCleaning();
|
||||
}
|
||||
// Start cleaning
|
||||
else if (this.robot.canStart)
|
||||
{
|
||||
await this.clean(CleanType.ALL)
|
||||
}
|
||||
// Cannot start
|
||||
else
|
||||
{
|
||||
this.debug(DebugType.INFO, "Cannot start, maybe already cleaning (expected)");
|
||||
}
|
||||
}
|
||||
// Stop
|
||||
else
|
||||
{
|
||||
if (this.robot.canPause)
|
||||
{
|
||||
this.debug(DebugType.ACTION, "Pause cleaning");
|
||||
await this.robot.pauseCleaning();
|
||||
}
|
||||
else
|
||||
{
|
||||
this.debug(DebugType.INFO, "Already paused");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
this.log.error("Error setting cleaning to: " + on + ". " + error);
|
||||
throw new this.platform.api.hap.HapStatusError(this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
async getSpotClean(): Promise<CharacteristicValue>
|
||||
{
|
||||
try
|
||||
{
|
||||
await this.updateRobot();
|
||||
return this.robot.canPause && this.isSpotCleaning;
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
this.log.error("Cannot get spot cleaning status: " + error);
|
||||
throw new this.platform.api.hap.HapStatusError(this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
async setSpotClean(on: CharacteristicValue)
|
||||
{
|
||||
this.debug(DebugType.STATUS, "Set SPOT CLEAN: " + on);
|
||||
try
|
||||
{
|
||||
if (on)
|
||||
{
|
||||
await this.clean(CleanType.SPOT)
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO stop/pause
|
||||
}
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
this.log.error("Error setting spot cleaning to: " + on + ". " + error);
|
||||
throw new this.platform.api.hap.HapStatusError(this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
getGoToDock()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
async setGoToDock(on: CharacteristicValue)
|
||||
{
|
||||
this.debug(DebugType.STATUS, "Set GO TO DOCK: " + on);
|
||||
if (on)
|
||||
{
|
||||
await this.updateRobot();
|
||||
|
||||
setTimeout(() => {
|
||||
if (this.goToDockService)
|
||||
{
|
||||
this.goToDockService.updateCharacteristic(this.platform.Characteristic.On, false);
|
||||
}
|
||||
}, 10000);
|
||||
|
||||
try
|
||||
{
|
||||
if (this.robot.canPause)
|
||||
{
|
||||
this.debug(DebugType.ACTION, "Pause cleaning to go to dock");
|
||||
await this.robot.pauseCleaning();
|
||||
setTimeout(async () => {
|
||||
await this.robot.sendToBase();
|
||||
}, 1000);
|
||||
}
|
||||
else if (this.robot.canGoToBase)
|
||||
{
|
||||
this.debug(DebugType.ACTION, "Going to dock");
|
||||
await this.robot.sendToBase();
|
||||
}
|
||||
else
|
||||
{
|
||||
this.log.warn("[" + this.robot.name + "] Can't go to dock at the moment");
|
||||
}
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
this.log.error("Error setting go to dock to: " + on + ". " + error);
|
||||
throw new this.platform.api.hap.HapStatusError(this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getDocked(): Promise<CharacteristicValue>
|
||||
{
|
||||
try
|
||||
{
|
||||
await this.updateRobot();
|
||||
return this.robot.isDocked;
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
this.log.error("Cannot get docked status: " + error);
|
||||
throw new this.platform.api.hap.HapStatusError(this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
async getBinFull(): Promise<CharacteristicValue>
|
||||
{
|
||||
try
|
||||
{
|
||||
await this.updateRobot();
|
||||
return this.robot.isBinFull;
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
this.log.error("Cannot get bin full status: " + error);
|
||||
throw new this.platform.api.hap.HapStatusError(this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
async getSchedule(): Promise<CharacteristicValue>
|
||||
{
|
||||
try
|
||||
{
|
||||
await this.updateRobot();
|
||||
return this.robot.isScheduleEnabled;
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
this.log.error("Cannot get schedule status: " + error);
|
||||
throw new this.platform.api.hap.HapStatusError(this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
async setSchedule(on: CharacteristicValue)
|
||||
{
|
||||
this.debug(DebugType.STATUS, "Set SCHEDULE: " + on);
|
||||
try
|
||||
{
|
||||
if (on)
|
||||
{
|
||||
await this.robot.enableSchedule();
|
||||
}
|
||||
else
|
||||
{
|
||||
await this.robot.disableSchedule();
|
||||
}
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
this.log.error("Error setting schedule to: " + on + ". " + error);
|
||||
throw new this.platform.api.hap.HapStatusError(this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
getEco()
|
||||
{
|
||||
return this.options.eco;
|
||||
}
|
||||
|
||||
setEco(on: CharacteristicValue)
|
||||
{
|
||||
this.debug(DebugType.STATUS, "Set ECO: " + on);
|
||||
this.options.eco = <boolean>on;
|
||||
}
|
||||
|
||||
getExtraCare()
|
||||
{
|
||||
return this.options.extraCare;
|
||||
}
|
||||
|
||||
setExtraCare(on: CharacteristicValue)
|
||||
{
|
||||
this.debug(DebugType.STATUS, "Set EXTRA CARE: " + on);
|
||||
this.options.extraCare = <boolean>on;
|
||||
}
|
||||
|
||||
getNoGoLines()
|
||||
{
|
||||
return this.options.noGoLines;
|
||||
}
|
||||
|
||||
setNoGoLines(on: CharacteristicValue)
|
||||
{
|
||||
this.debug(DebugType.STATUS, "Set NOGO LINES: " + on);
|
||||
this.options.noGoLines = <boolean>on;
|
||||
}
|
||||
|
||||
getFindMe()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
async setFindMe(on: CharacteristicValue)
|
||||
{
|
||||
this.debug(DebugType.STATUS, "Set FIND ME: " + on);
|
||||
if (on)
|
||||
{
|
||||
this.debug(DebugType.ACTION, "Find me");
|
||||
setTimeout(() => {
|
||||
if (this.findMeService)
|
||||
{
|
||||
this.findMeService.updateCharacteristic(this.platform.Characteristic.On, false);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
try
|
||||
{
|
||||
await this.robot.findMe();
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
this.log.error(this.robot.name + " ## Cannot start find me. " + error);
|
||||
throw new this.platform.api.hap.HapStatusError(this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async clean(cleanType: CleanType)
|
||||
{
|
||||
// Enable shorter background update while cleaning
|
||||
setTimeout(() => {
|
||||
this.updateRobotPeriodically();
|
||||
}, 60 * 1000);
|
||||
|
||||
this.log.info(
|
||||
"[" + this.robot.name + "] > Start cleaning with options type: " + CleanType[cleanType] + ", eco: " + this.options.eco + ", noGoLines: " + this.options.noGoLines + ", extraCare: "
|
||||
+ this.options.extraCare);
|
||||
|
||||
try
|
||||
{
|
||||
switch (cleanType)
|
||||
{
|
||||
case CleanType.ALL:
|
||||
await this.robot.startCleaning(this.options.eco, this.options.extraCare ? 2 : 1, this.options.noGoLines);
|
||||
break;
|
||||
case CleanType.SPOT:
|
||||
await this.robot.startSpotCleaning(this.options.eco, this.options.spot.width, this.options.spot.height, this.options.spot.repeat, this.options.extraCare ? 2 : 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
this.log.error("Cannot start cleaning. " + error);
|
||||
}
|
||||
}
|
||||
|
||||
async updateRobot()
|
||||
{
|
||||
// Data is outdated
|
||||
if (typeof (this.robot.lastUpdate) === 'undefined' || new Date().getTime() - this.robot.lastUpdate > 2000)
|
||||
{
|
||||
this.robot.lastUpdate = new Date().getTime();
|
||||
try
|
||||
{
|
||||
this.robot.getState((error, result) => {
|
||||
this.isSpotCleaning = result != null && result.action == 2;
|
||||
|
||||
// Battery
|
||||
this.batteryService.updateCharacteristic(this.platform.Characteristic.BatteryLevel, this.robot.charge);
|
||||
this.batteryService.updateCharacteristic(this.platform.Characteristic.ChargingState, this.robot.isCharging);
|
||||
});
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
this.log.error("Cannot update robot " + this.robot.name + ". Check if robot is online. " + error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async updateRobotPeriodically()
|
||||
{
|
||||
this.debug(DebugType.INFO, "Performing background update")
|
||||
|
||||
await this.updateRobot()
|
||||
await this.updateCharacteristics();
|
||||
|
||||
// Clear any other overlapping timers for this robot
|
||||
clearTimeout(this.timer);
|
||||
|
||||
// Tell all accessories of this robot (mainAccessory and roomAccessories) that updated robot data is available
|
||||
// this.robot.mainAccessory.updated();
|
||||
// this.robot.roomAccessories.forEach(accessory => {
|
||||
// accessory.updated();
|
||||
// });
|
||||
|
||||
// Periodic refresh interval set in config
|
||||
let interval;
|
||||
if (this.robot.canPause)
|
||||
{
|
||||
interval = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
interval = this.backgroundUpdateInterval;
|
||||
}
|
||||
|
||||
this.debug(DebugType.INFO, "Background update done. Next update in " + interval + " minute" + (interval == 1 ? "" : "s") + ((this.robot.canPause) ? ", robot is currently cleaning." : "."));
|
||||
this.timer = setTimeout(this.updateRobotPeriodically.bind(this), interval * 60 * 1000);
|
||||
}
|
||||
|
||||
async updateCharacteristics()
|
||||
{
|
||||
if (this.cleanService)
|
||||
{
|
||||
this.cleanService.updateCharacteristic(this.platform.Characteristic.On, await this.getCleanHouse());
|
||||
}
|
||||
if (this.spotCleanService)
|
||||
{
|
||||
this.spotCleanService.updateCharacteristic(this.platform.Characteristic.On, await this.getSpotClean());
|
||||
}
|
||||
if (this.goToDockService)
|
||||
{
|
||||
this.goToDockService.updateCharacteristic(this.platform.Characteristic.On, await this.getGoToDock());
|
||||
}
|
||||
if (this.dockStateService)
|
||||
{
|
||||
this.dockStateService.updateCharacteristic(this.platform.Characteristic.OccupancyDetected, await this.getDocked());
|
||||
}
|
||||
if (this.binFullService)
|
||||
{
|
||||
this.binFullService.updateCharacteristic(this.platform.Characteristic.OccupancyDetected, await this.getBinFull());
|
||||
}
|
||||
if (this.scheduleService)
|
||||
{
|
||||
this.scheduleService.updateCharacteristic(this.platform.Characteristic.On, await this.getSchedule());
|
||||
}
|
||||
}
|
||||
|
||||
private debug(debugType: DebugType, message: String)
|
||||
{
|
||||
switch (debugType)
|
||||
{
|
||||
case DebugType.ACTION:
|
||||
this.log.debug("[" + this.robot.name + "] > " + message);
|
||||
break;
|
||||
case DebugType.STATUS:
|
||||
this.log.debug("[" + this.robot.name + "] " + message);
|
||||
break;
|
||||
case DebugType.INFO:
|
||||
this.log.debug("[" + this.robot.name + "] " + message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum CleanType
|
||||
{
|
||||
ALL,
|
||||
SPOT
|
||||
}
|
||||
|
||||
enum DebugType
|
||||
{
|
||||
ACTION,
|
||||
STATUS,
|
||||
INFO
|
||||
}
|
||||
|
||||
enum RobotService
|
||||
{
|
||||
CLEAN_HOUSE = "Clean house",
|
||||
CLEAN_SPOT = "Clean spot",
|
||||
GO_TO_DOCK = "Go to dock",
|
||||
DOCKED = "Docked sensor",
|
||||
BIN_FULL = "Bin full sensor",
|
||||
FIND_ME = "Find me",
|
||||
SCHEDULE = "Schedule",
|
||||
ECO = "Eco",
|
||||
NOGO_LINES = "Nogo lines",
|
||||
EXTRA_CARE = "Extra care"
|
||||
}
|
||||
|
||||
const BACKGROUND_INTERVAL = 30;
|
||||
const PREFIX = false;
|
||||
const SERVICES = Object.values(RobotService);
|
37
src/accessories/room.ts
Normal file
37
src/accessories/room.ts
Normal file
@ -0,0 +1,37 @@
|
||||
// import {CharacteristicValue, Logger, PlatformAccessory, PlatformConfig, Service} from 'homebridge';
|
||||
// import {HomebridgeNeatoPlatform} from '../homebridgeNeatoPlatform';
|
||||
//
|
||||
// const debug = require('debug')('my-app:my-module');
|
||||
//
|
||||
// /**
|
||||
// * Platform Accessory
|
||||
// * An instance of this class is created for each accessory your platform registers
|
||||
// * Each accessory may expose multiple services of different service types.
|
||||
// */
|
||||
// export class Room
|
||||
// {
|
||||
//
|
||||
// private robot: any;
|
||||
// private log: Logger;
|
||||
// private readonly refresh: any;
|
||||
//
|
||||
//
|
||||
// /**
|
||||
// * These are just used to create a working example
|
||||
// * You should implement your own code to track the state of your accessory
|
||||
// */
|
||||
//
|
||||
// constructor(
|
||||
// private readonly platform: HomebridgeNeatoPlatform,
|
||||
// private readonly accessory: PlatformAccessory,
|
||||
// private readonly isNew: Boolean,
|
||||
// private readonly config: PlatformConfig)
|
||||
// {
|
||||
//
|
||||
// }
|
||||
//
|
||||
// async setCleanRoom()
|
||||
// {
|
||||
//
|
||||
// }
|
||||
// }
|
19
src/characteristics/spotHeight.ts
Normal file
19
src/characteristics/spotHeight.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import type { Characteristic, WithUUID } from 'homebridge';
|
||||
import { Formats, Perms } from 'homebridge';
|
||||
|
||||
export default function spotHeight(CustomCharacteristic: typeof Characteristic): WithUUID<new () => Characteristic> {
|
||||
return class SpotHeight extends CustomCharacteristic {
|
||||
static readonly UUID = 'CA282DB2-62BF-4325-A1BE-F8BB5478781A';
|
||||
|
||||
constructor() {
|
||||
super('Spot ↕', SpotHeight.UUID, {
|
||||
format: Formats.INT,
|
||||
unit: 'cm',
|
||||
maxValue: 400,
|
||||
minValue: 100,
|
||||
minStep: 50,
|
||||
perms: [Perms.PAIRED_READ, Perms.PAIRED_WRITE]
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
15
src/characteristics/spotRepeat.ts
Normal file
15
src/characteristics/spotRepeat.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import type { Characteristic, WithUUID } from 'homebridge';
|
||||
import { Formats, Perms } from 'homebridge';
|
||||
|
||||
export default function spotRepeat(CustomCharacteristic: typeof Characteristic): WithUUID<new () => Characteristic> {
|
||||
return class SpotRepeat extends CustomCharacteristic {
|
||||
static readonly UUID = '1E79C603-63B8-4E6A-9CE1-D31D67981831';
|
||||
|
||||
constructor() {
|
||||
super('Spot 2x', SpotRepeat.UUID, {
|
||||
format: Formats.BOOL,
|
||||
perms: [Perms.PAIRED_READ, Perms.PAIRED_WRITE]
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
19
src/characteristics/spotWidth.ts
Normal file
19
src/characteristics/spotWidth.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import type { Characteristic, WithUUID } from 'homebridge';
|
||||
import { Formats, Perms } from 'homebridge';
|
||||
|
||||
export default function spotWidth(CustomCharacteristic: typeof Characteristic): WithUUID<new () => Characteristic> {
|
||||
return class SpotWidth extends CustomCharacteristic {
|
||||
static readonly UUID = 'A7889A9A-2F27-4293-BEF8-3FE805B36F4E';
|
||||
|
||||
constructor() {
|
||||
super('Spot ↔', SpotWidth.UUID, {
|
||||
format: Formats.INT,
|
||||
unit: 'cm',
|
||||
maxValue: 400,
|
||||
minValue: 100,
|
||||
minStep: 50,
|
||||
perms: [Perms.PAIRED_READ, Perms.PAIRED_WRITE]
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
219
src/homebridgeKoboldPlatform.ts
Normal file
219
src/homebridgeKoboldPlatform.ts
Normal file
@ -0,0 +1,219 @@
|
||||
import {API, Characteristic, DynamicPlatformPlugin, Logger, PlatformAccessory, PlatformConfig, Service} from "homebridge";
|
||||
import KoboldApi from "node-kobold-control";
|
||||
import {PLATFORM_NAME, PLUGIN_NAME} from "./settings";
|
||||
import {KoboldVacuumRobotAccessory} from "./accessories/koboldVacuumRobot";
|
||||
|
||||
/**
|
||||
* HomebridgePlatform
|
||||
* This class is the main constructor for your plugin, this is where you should
|
||||
* parse the user config and discover/register accessories with Homebridge.
|
||||
*/
|
||||
export class HomebridgeKoboldPlatform implements DynamicPlatformPlugin
|
||||
{
|
||||
public readonly Service: typeof Service = this.api.hap.Service;
|
||||
public readonly Characteristic: typeof Characteristic = this.api.hap.Characteristic;
|
||||
|
||||
// this is used to track restored cached accessories
|
||||
public readonly cachedRobotAccessories: PlatformAccessory[] = [];
|
||||
|
||||
constructor(
|
||||
public readonly log: Logger,
|
||||
public readonly config: PlatformConfig,
|
||||
public readonly api: API)
|
||||
{
|
||||
this.api.on("didFinishLaunching", () => {
|
||||
this.discoverRobots();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is invoked when homebridge restores cached accessories from disk at startup.
|
||||
* It should be used to setup event handlers for characteristics and update respective values.
|
||||
*/
|
||||
configureAccessory(accessory: PlatformAccessory)
|
||||
{
|
||||
// add the restored accessory to the accessories cache so we can track if it has already been registered
|
||||
this.cachedRobotAccessories.push(accessory);
|
||||
}
|
||||
|
||||
|
||||
discoverRobots()
|
||||
{
|
||||
const client = new KoboldApi.Client();
|
||||
this.log.debug("blubb");
|
||||
|
||||
try
|
||||
{
|
||||
// Login
|
||||
client.authorize((this.config)["email"], (this.config)["password"], false, (error) => {
|
||||
if (error)
|
||||
{
|
||||
this.log.error("Cannot connect to neato server. No new robots will be found and existing robots will be unresponsive. Retrying in 5 minutes.");
|
||||
this.log.error("Error: " + error);
|
||||
|
||||
setTimeout(() => {
|
||||
this.discoverRobots();
|
||||
}, 5 * 60 * 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all robots from account
|
||||
client.getRobots((error, robots) => {
|
||||
if (error)
|
||||
{
|
||||
this.log.error("Successful login but can't list the robots in your neato robots. Retrying in 5 minutes.");
|
||||
this.log.error("Error: " + error);
|
||||
|
||||
setTimeout(() => {
|
||||
this.discoverRobots();
|
||||
}, 5 * 60 * 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
// Neato robots in account
|
||||
if (robots.length === 0)
|
||||
{
|
||||
this.log.error("Neato account has no robots. Did you add your robot here: https://neatorobotics.com/my-neato/ ?");
|
||||
}
|
||||
else
|
||||
{
|
||||
this.log.info("Neato account has " + robots.length + " robot" + (robots.length === 1 ? "" : "s"));
|
||||
}
|
||||
|
||||
// Neato robots in cache
|
||||
this.log.debug("Plugin Cache has " + this.cachedRobotAccessories.length + " robot" + (this.cachedRobotAccessories.length === 1 ? "" : "s"));
|
||||
for (let cachedRobot of this.cachedRobotAccessories)
|
||||
{
|
||||
let accountRobot = robots.find(robot => this.api.hap.uuid.generate(robot._serial) === cachedRobot.UUID);
|
||||
if (accountRobot)
|
||||
{
|
||||
this.log.debug("[" + cachedRobot.displayName + "] Cached robot found in Neato account.");
|
||||
}
|
||||
else
|
||||
{
|
||||
this.log.error("[" + cachedRobot.displayName + "] Cached robot not found in Neato account. Robot will now be removed from homebridge.");
|
||||
this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [cachedRobot]);
|
||||
}
|
||||
}
|
||||
|
||||
// Add / Update homebridge accessories with robot information from neato. This must be done for new and existing robots to reflect changes in the name, firmware, pluginconfig etc.
|
||||
for (let robot of robots)
|
||||
{
|
||||
// Check if robot already exists as an accessory
|
||||
const uuid = this.api.hap.uuid.generate(robot._serial);
|
||||
const cachedRobot = this.cachedRobotAccessories.find(accessory => accessory.UUID === uuid);
|
||||
|
||||
if (cachedRobot)
|
||||
{
|
||||
this.log.debug("[" + robot.name + "] Connecting to cached robot and updating information.");
|
||||
}
|
||||
else
|
||||
{
|
||||
this.log.debug("[" + robot.name + "] Connecting to new robot and updating information.");
|
||||
}
|
||||
|
||||
robot.getState((error, state) => {
|
||||
if (error)
|
||||
{
|
||||
this.log.error("[" + robot.name + "] Cannot connect to robot. Is the robot connected to the internet? Retrying in 5 minutes.");
|
||||
this.log.error("Error: " + error);
|
||||
setTimeout(() => {
|
||||
this.discoverRobots();
|
||||
}, 5 * 60 * 1000);
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
robot.meta = state.meta;
|
||||
|
||||
// Update existing robot accessor
|
||||
if (cachedRobot)
|
||||
{
|
||||
// TODO update maps
|
||||
|
||||
cachedRobot.context.robot = robot;
|
||||
this.api.updatePlatformAccessories([cachedRobot]);
|
||||
new KoboldVacuumRobotAccessory(this, cachedRobot, this.config);
|
||||
this.log.info("[" + robot.name + "] Successfully loaded robot from cache");
|
||||
}
|
||||
// Create new robot accessory
|
||||
else
|
||||
{
|
||||
// TODO get maps
|
||||
|
||||
const newRobot = new this.api.platformAccessory(robot.name, uuid);
|
||||
newRobot.context.robot = robot;
|
||||
new KoboldVacuumRobotAccessory(this, newRobot, this.config);
|
||||
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [newRobot]);
|
||||
this.log.info("[" + robot.name + "] Successfully created as new robot");
|
||||
}
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
this.log.error("[" + robot.name + "] Creating accessory failed. Error: " + error);
|
||||
throw new this.api.hap.HapStatusError(this.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
// // Get all maps for each robot
|
||||
// robot.getPersistentMaps((error, maps) => {
|
||||
// if (error)
|
||||
// {
|
||||
// this.log.error("Error updating persistent maps: " + error + ": " + maps);
|
||||
// callback();
|
||||
// }
|
||||
// // Robot has no maps
|
||||
// else if (maps.length === 0)
|
||||
// {
|
||||
// robot.maps = [];
|
||||
// this.robotAccessories.push({device: robot, meta: state.meta, availableServices: state.availableServices});
|
||||
// loadedRobots++;
|
||||
// if (loadedRobots === robots.length)
|
||||
// {
|
||||
// callback();
|
||||
// }
|
||||
// }
|
||||
// // Robot has maps
|
||||
// else
|
||||
// {
|
||||
// robot.maps = maps;
|
||||
// let loadedMaps = 0;
|
||||
// robot.maps.forEach((map) => {
|
||||
// // Save zones in each map
|
||||
// robot.getMapBoundaries(map.id, (error, result) => {
|
||||
// if (error)
|
||||
// {
|
||||
// this.log.error("Error getting boundaries: " + error + ": " + result)
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// map.boundaries = result.boundaries;
|
||||
// }
|
||||
// loadedMaps++;
|
||||
//
|
||||
// // Robot is completely requested if zones for all maps are loaded
|
||||
// if (loadedMaps === robot.maps.length)
|
||||
// {
|
||||
// this.robotAccessories.push({device: robot, meta: state.meta, availableServices: state.availableServices});
|
||||
// loadedRobots++;
|
||||
// if (loadedRobots === robots.length)
|
||||
// {
|
||||
// callback();
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
this.log.error("Can't log on to neato cloud. Please check your internet connection and your credentials. Try again later if the neato servers have issues. Error: " + error);
|
||||
}
|
||||
}
|
||||
}
|
12
src/index.ts
Normal file
12
src/index.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import {API} from "homebridge";
|
||||
|
||||
import {PLATFORM_NAME} from "./settings";
|
||||
import {HomebridgeKoboldPlatform} from "./homebridgeKoboldPlatform";
|
||||
|
||||
/**
|
||||
* This method registers the platform with Homebridge
|
||||
*/
|
||||
export = (api: API) =>
|
||||
{
|
||||
api.registerPlatform(PLATFORM_NAME, HomebridgeKoboldPlatform);
|
||||
};
|
15
src/models/options.ts
Normal file
15
src/models/options.ts
Normal file
@ -0,0 +1,15 @@
|
||||
export class Options
|
||||
{
|
||||
public eco: boolean;
|
||||
public extraCare: boolean;
|
||||
public noGoLines: boolean;
|
||||
public spot: any;
|
||||
|
||||
constructor()
|
||||
{
|
||||
this.eco = false;
|
||||
this.extraCare = false;
|
||||
this.noGoLines = false;
|
||||
this.spot = {};
|
||||
}
|
||||
}
|
9
src/settings.ts
Normal file
9
src/settings.ts
Normal file
@ -0,0 +1,9 @@
|
||||
/**
|
||||
* This is the name of the platform that users will use to register the plugin in the Homebridge config.json
|
||||
*/
|
||||
export const PLATFORM_NAME = "KoboldVacuumRobot";
|
||||
|
||||
/**
|
||||
* This must match the name of your plugin as defined the package.json
|
||||
*/
|
||||
export const PLUGIN_NAME = "homebridge-kobold";
|
Loading…
Reference in New Issue
Block a user