initial merge attempt

This commit is contained in:
Luis Riegger 2021-05-08 19:33:37 +02:00
parent 750b25066b
commit 369a6f6412
16 changed files with 996 additions and 3 deletions

View File

@ -4,7 +4,7 @@
],
"ext": "ts",
"ignore": [],
"exec": "tsc && homebridge -I -D",
"exec": "tsc",
"signal": "SIGTERM",
"env": {
"NODE_OPTIONS": "--trace-warnings"

View File

@ -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.",

View 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
View 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()
// {
//
// }
// }

View 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]
});
}
};
}

View 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]
});
}
};
}

View 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]
});
}
};
}

View 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
View 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
View 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
View 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";