WIP dynamic platform

This commit is contained in:
Arne Blumentritt 2021-04-29 19:46:31 +02:00
parent 4a97891dfd
commit 0ed30314df
6 changed files with 3085 additions and 149 deletions

9
homebridge-neato.iml Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

2841
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -48,7 +48,7 @@
"dependencies": { "dependencies": {
"colors": "^1.4.0", "colors": "^1.4.0",
"debug": "^4.1.1", "debug": "^4.1.1",
"node-botvac": "^0.4.0", "node-botvac": "^0.4.1",
"uuid": "^3.3.2" "uuid": "^3.3.2"
}, },
"devDependencies": { "devDependencies": {

View File

@ -1,4 +1,4 @@
import {CharacteristicValue, PlatformAccessory, Service} from 'homebridge'; import {CharacteristicValue, Logger, PlatformAccessory, Service} from 'homebridge';
import {HomebridgeNeatoPlatform} from '../homebridgeNeatoPlatform'; import {HomebridgeNeatoPlatform} from '../homebridgeNeatoPlatform';
const debug = require('debug')('my-app:my-module'); const debug = require('debug')('my-app:my-module');
@ -10,14 +10,15 @@ const debug = require('debug')('my-app:my-module');
export class NeatoVacuumRobotAccessory export class NeatoVacuumRobotAccessory
{ {
private cleanService: Service; private cleanService: Service;
private findMeService: Service;
private robot: any; private robot: any;
private log: Logger;
// private goToDockService: Service; // private goToDockService: Service;
// private dockStateService: Service; // private dockStateService: Service;
// private ecoService: Service; // private ecoService: Service;
// private noGoLinesService: Service; // private noGoLinesService: Service;
// private extraCareService: Service; // private extraCareService: Service;
// private scheduleService: Service; // private scheduleService: Service;
// private findMeService: Service;
// private spotCleanService: Service; // private spotCleanService: Service;
/** /**
@ -31,7 +32,8 @@ export class NeatoVacuumRobotAccessory
private readonly isNew: Boolean) private readonly isNew: Boolean)
{ {
this.robot = accessory.context.robot; this.robot = accessory.context.robot;
this.log = platform.log;
// set accessory information // set accessory information
this.accessory.getService(this.platform.Service.AccessoryInformation)! this.accessory.getService(this.platform.Service.AccessoryInformation)!
.setCharacteristic(this.platform.Characteristic.Manufacturer, "Neato Robotics") .setCharacteristic(this.platform.Characteristic.Manufacturer, "Neato Robotics")
@ -40,13 +42,17 @@ export class NeatoVacuumRobotAccessory
.setCharacteristic(this.platform.Characteristic.FirmwareRevision, this.robot.meta.firmware) .setCharacteristic(this.platform.Characteristic.FirmwareRevision, this.robot.meta.firmware)
.setCharacteristic(this.platform.Characteristic.Name, this.robot.name); .setCharacteristic(this.platform.Characteristic.Name, this.robot.name);
let cleanServiceName = this.robot.name + " Clean";
let cleanServiceName = robot.name + " Clean";
this.cleanService = this.accessory.getService(cleanServiceName) || this.accessory.addService(this.platform.Service.Switch, cleanServiceName, "CLEAN"); this.cleanService = this.accessory.getService(cleanServiceName) || this.accessory.addService(this.platform.Service.Switch, cleanServiceName, "CLEAN");
let findMeServiceName = this.robot.name + " Find Me";
this.findMeService = this.accessory.getService(findMeServiceName) || this.accessory.addService(this.platform.Service.Switch, findMeServiceName, "FIND_ME");
this.cleanService.getCharacteristic(this.platform.Characteristic.On) this.cleanService.getCharacteristic(this.platform.Characteristic.On)
.onSet(this.setClean.bind(this)) .onSet(this.setClean.bind(this))
.onGet(this.getClean.bind(this)); .onGet(this.getClean.bind(this));
this.findMeService.getCharacteristic(this.platform.Characteristic.On)
.onSet(this.setFindMe.bind(this))
.onGet(this.getFindMe.bind(this));
// /** // /**
// * Updating characteristics values asynchronously. // * Updating characteristics values asynchronously.
@ -75,33 +81,33 @@ export class NeatoVacuumRobotAccessory
async setClean(on: CharacteristicValue) async setClean(on: CharacteristicValue)
{ {
// TODO debug(this.robot.name + ": " + (on ? "Enabled ".brightGreen : "Disabled".red) + " Clean " + (this.boundary ? JSON.stringify(this.boundary) : '')); // TODO debug(this.robot.name + ": " + (on ? "Enabled ".brightGreen : "Disabled".red) + " Clean " + (this.boundary ? JSON.stringify(this.boundary) : ''));
this.platform.updateRobot(this.robot._serial, (error, result) => try
{ {
await this.updateRobot();
// Start // Start
if (on) if (on)
{ {
// No room given or same room // No room given or same room
if (this.boundary == null || this.robot.cleaningBoundaryId === this.boundary.id) if (this.robot.boundary == null || this.robot.cleaningBoundaryId === this.robot.boundary.id)
{ {
// Resume cleaning // Resume cleaning
if (this.robot.canResume) if (this.robot.canResume)
{ {
debug(this.name + ": ## Resume cleaning"); debug(this.robot.name + ": ## Resume cleaning");
this.robot.resumeCleaning((error) => await this.robot.resumeCleaning();
{ return;
callback(error);
});
} }
// Start cleaning // Start cleaning
else if (this.robot.canStart) else if (this.robot.canStart)
{ {
this.clean(callback); // TODO this.clean(callback);
} }
// Cannot start // Cannot start
else else
{ {
debug(this.name + ": Cannot start, maybe already cleaning (expected)"); // TODO debug(this.name + ": Cannot start, maybe already cleaning (expected)");
callback(); return;
} }
} }
// Different room given // Different room given
@ -110,18 +116,18 @@ export class NeatoVacuumRobotAccessory
// Return to dock // Return to dock
if (this.robot.canPause || this.robot.canResume) if (this.robot.canPause || this.robot.canResume)
{ {
debug(this.name + ": ## Returning to dock to start cleaning of new room"); // debug(this.name + ": ## Returning to dock to start cleaning of new room");
this.setGoToDock(true, (error, result) => // this.setGoToDock(true, (error, result) =>
{ // {
this.nextRoom = this.boundary.id; // this.nextRoom = this.boundary.id;
callback(); // callback();
}); // });
} }
// Start new cleaning of new room // Start new cleaning of new room
else else
{ {
debug(this.name + ": ## Start cleaning of new room"); // debug(this.name + ": ## Start cleaning of new room");
this.clean(callback); // this.clean(callback);
} }
} }
} }
@ -130,32 +136,99 @@ export class NeatoVacuumRobotAccessory
{ {
if (this.robot.canPause) if (this.robot.canPause)
{ {
debug(this.name + ": ## Pause cleaning"); // debug(this.name + ": ## Pause cleaning");
this.robot.pauseCleaning((error) => // this.robot.pauseCleaning((error) => {
{ // callback(error);
callback(error); // });
});
} }
else else
{ {
debug(this.name + ": Already paused"); // debug(this.name + ": Already paused");
callback(); // callback();
} }
} }
}); }
catch (error)
{
this.log.warn("Cannot start cleaning: " + error);
throw new this.platform.api.hap.HapStatusError(this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE);
}
} }
async getClean(): Promise<CharacteristicValue> async getClean(): Promise<CharacteristicValue>
{ {
// implement your own code to check if the device is on try
const isOn = this.exampleStates.On; {
await this.updateRobot();
this.platform.log.debug('Get Characteristic On ->', isOn); let cleaning;
if (this.robot.boundary == null)
{
cleaning = this.robot.canPause;
}
else
{
cleaning = this.robot.canPause && (this.robot.cleaningBoundaryId === this.robot.boundary.id)
}
// if you need to return an error to show the device as "Not Responding" in the Home app: // TODO debug(this.robot.name + ": Cleaning is " + (cleaning ? 'ON'.brightGreen : 'OFF'.red));
// throw new this.platform.api.hap.HapStatusError(this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE); return cleaning;
}
catch (error)
{
this.log.warn("Cannot get cleaning status: " + error);
throw new this.platform.api.hap.HapStatusError(this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE);
}
}
return isOn;
getFindMe()
{
return false;
}
async setFindMe(on: CharacteristicValue)
{
if (on)
{
// TODO debug(this.name + ": ## Find me");
setTimeout(() => {
this.findMeService.updateCharacteristic(this.platform.Characteristic.On, false);
}, 1000);
try
{
await this.robot.findMe();
}
catch (error)
{
this.log.warn("Cannot start find me: " + error);
throw new this.platform.api.hap.HapStatusError(this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE);
}
}
}
async updateRobot()
{
// Data is up to date
if (typeof (this.robot.lastUpdate) !== 'undefined' && new Date().getTime() - this.robot.lastUpdate < 2000)
{
return;
}
else
{
debug(this.robot.name + ": ++ Updating robot state");
this.robot.lastUpdate = new Date().getTime();
try
{
await this.robot.getState();
}
catch (error)
{
this.log.error("Cannot update robot " + this.robot.name + ". Check if robot is online. " + error);
return false;
}
}
} }

View File

@ -49,135 +49,148 @@ export class HomebridgeNeatoPlatform implements DynamicPlatformPlugin
debug("Discovering new robots"); debug("Discovering new robots");
let client = new NeatoApi.Client(); let client = new NeatoApi.Client();
// Login try
client.authorize((this.config)['email'], (this.config)['password'], false, (error) => { {
if (error) // Login
{ client.authorize((this.config)['email'], (this.config)['password'], false, (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); if (error)
return; {
} throw new this.api.hap.HapStatusError(this.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE);
else }
{
// Get all robots // Get all robots from account
client.getRobots((error, robots) => { client.getRobots((error, robots) => {
if (error) if (error)
{ {
this.log.error("Successful login but can't connect to your neato robot: " + error); this.log.error("Successful login but can't connect to your neato robot: " + error);
return; throw new this.api.hap.HapStatusError(this.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE);
} }
else if (robots.length === 0) else if (robots.length === 0)
{ {
this.log.error("Successful login but no robots associated with your account."); this.log.error("Successful login but no robots associated with your account.");
return; throw new this.api.hap.HapStatusError(this.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE);
} }
else
{
debug("Found " + robots.length + " robots");
let loadedRobots = 0;
robots.forEach((robot) => { debug("Found " + robots.length + " robots");
// Get additional information for the robot let loadedRobots = 0;
robot.getState((error, state) => {
if (error) for (let robot of robots)
{
// Get additional information for the robot
robot.getState((error, state) => {
this.log.debug("Got state for robot: " + robot.name);
robot.meta = state.meta;
if (error)
{
this.log.error("Error getting robot meta information: " + error + ": " + state);
throw new this.api.hap.HapStatusError(this.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE);
}
try
{
const uuid = this.api.hap.uuid.generate(robot._serial);
const existingAccessory = this.robotAccessories.find(accessory => accessory.UUID === uuid);
if (existingAccessory)
{ {
this.log.error("Error getting robot meta information: " + error + ": " + state); // the accessory already exists
return; this.log.info('Restoring existing accessory from cache:', existingAccessory.displayName);
existingAccessory.context.robot = robot;
// TODO update maps
// if you need to update the accessory.context then you should run `api.updatePlatformAccessories`. eg.:
// existingAccessory.context.device = device;
// this.api.updatePlatformAccessories([existingAccessory]);
// create the accessory handler for the restored accessory
// this is imported from `platformAccessory.ts`
new NeatoVacuumRobotAccessory(this, existingAccessory, false);
// it is possible to remove platform accessories at any time using `api.unregisterPlatformAccessories`, eg.:
// remove platform accessories when no longer present
// this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [existingAccessory]);
// this.log.info('Removing existing accessory from cache:', existingAccessory.displayName);
} }
else else
{ {
const uuid = this.api.hap.uuid.generate(robot._serial); this.log.info('Adding new accessory: ', robot.name);
const existingAccessory = this.robotAccessories.find(accessory => accessory.UUID === uuid); const accessory = new this.api.platformAccessory(robot.name, uuid);
if (existingAccessory) accessory.context.robot = robot;
{ new NeatoVacuumRobotAccessory(this, accessory, true);
// the accessory already exists
this.log.info('Restoring existing accessory from cache:', existingAccessory.displayName);
// TODO update maps // link the accessory to your platform
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
// if you need to update the accessory.context then you should run `api.updatePlatformAccessories`. eg.: // TODO get maps
// existingAccessory.context.device = device;
// this.api.updatePlatformAccessories([existingAccessory]);
// create the accessory handler for the restored accessory
// this is imported from `platformAccessory.ts`
new NeatoVacuumRobotAccessory(this, existingAccessory, false);
// it is possible to remove platform accessories at any time using `api.unregisterPlatformAccessories`, eg.:
// remove platform accessories when no longer present
// this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [existingAccessory]);
// this.log.info('Removing existing accessory from cache:', existingAccessory.displayName);
}
else
{
this.log.info('Adding new accessory: ', robot.name);
const accessory = new this.api.platformAccessory(robot.name, uuid);
accessory.context.robot = robot;
new NeatoVacuumRobotAccessory(this, accessory, true);
// link the accessory to your platform
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
// TODO get maps
}
// // 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("Error creating robot accessory: " + robot.name);
this.log.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);
}
} }
} }

View File

@ -1,7 +1,7 @@
import { API } from 'homebridge'; import { API } from 'homebridge';
import { PLATFORM_NAME } from './settings'; import { PLATFORM_NAME } from './settings';
import { HomebridgeNeatoPlatform } from './platform'; import { HomebridgeNeatoPlatform } from './homebridgeNeatoPlatform';
/** /**
* This method registers the platform with Homebridge * This method registers the platform with Homebridge