Merge branch 'master' into patch-1

This commit is contained in:
Arne 2019-09-23 09:12:25 +02:00 committed by GitHub
commit 0e92f09079
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 922 additions and 503 deletions

75
.gitignore vendored Normal file
View File

@ -0,0 +1,75 @@
# Logs
logs
*.log
npm-debug.log*
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules
jspm_packages
# Optional npm cache directory
.npm
# Optional REPL history
.node_repl_history
# See http://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
/dist
/dist-server
/dist-e2e
/tmp
/out-tsc
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
*.iml
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# misc
/.sass-cache
/connect.lock
/libpeerconnection.log
yarn-error.log
testem.log
/typings
# System Files
.DS_Store
Thumbs.db
package-lock.json

View File

@ -78,4 +78,34 @@
## 0.5.1 ## 0.5.1
* Updated node-botvac dependency to 0.1.7 * Updated node-botvac dependency to 0.1.7
## 0.5.2
* Added schema file for use with homebridge-config-ui-x
## 0.6.0
* Added support for zone cleaning
## 0.6.1
* Fixed homebridge startup failed when robot does not support zone cleaning
## 0.6.2
* Fixed homebridge startup failed when robot does not support mapping
## 0.6.3
* Fixed homebridge crash when robot has a map without zones
* Fixed homebridge crash when homebridge has no internet connection or the neato servers are offline
* Fixed homebridge crash when 2 zones have the same name
## 0.7.0
* Fixed room switches not taking eco and extraCare mode into account
* Fixed room switches to support pause/resume of cleaning
* Added feature that enabling another room switch, returns to robot to dock and starts cleaning the new room automatically
* Improved requests for multiple rooms (TODO)
* Added model and firmware information to homekit

View File

@ -5,23 +5,32 @@
This is a plugin for [homebridge](https://github.com/nfarina/homebridge) to control your [Neato](https://www.neatorobotics.com/) vacuum robot. You can download it via [npm](https://www.npmjs.com/package/homebridge-neato). This is a plugin for [homebridge](https://github.com/nfarina/homebridge) to control your [Neato](https://www.neatorobotics.com/) vacuum robot. You can download it via [npm](https://www.npmjs.com/package/homebridge-neato).
If you like this plugin, I would be very grateful for your support:
<a href="https://www.buymeacoffee.com/2D1nUuK36" target="_blank"><img width="140" src="https://bmc-cdn.nyc3.digitaloceanspaces.com/BMC-button-images/custom_images/orange_img.png" alt="Buy Me A Coffee"></a>
Feel free to leave any feedback [here](https://github.com/naofireblade/homebridge-neato/issues). Feel free to leave any feedback [here](https://github.com/naofireblade/homebridge-neato/issues).
## Features ## Features
- Start and pause cleaning - Start and pause house cleaning
- Start and pause spot cleaning - Eco mode
- Extra care navigation
- Nogo lines
- Zones
- Spot cleaning
- 2x2 or 4x4
- repeat
- Return to dock - Return to dock
- Toggle schedule - Find the robot
- Toggle eco mode - Enable/Disable the schedule
- Toggle extra care navigation - Robot information
- Toggle nogo lines - battery level
- Toggle 4x4 mode (spot) - charging state
- Toggle repeat mode (spot) - dock occupancy
- Get battery info - model and firmware version
- Get dock info - Automatic and periodic refresh for notifications
- Periodic refresh of robot state - Multiple robots
- Support for multiple robots
## Installation ## Installation
@ -50,9 +59,9 @@ Add the following information to your config file. Change the values for email a
The following config contains advanced optional settings. The following config contains advanced optional settings.
The parameter **refresh** sets an interval in seconds that is used to update the robot state in the background. This is only required for automations based on the robot state. The default value is `auto` which means that the update is automatically enabled while cleaning and disabled while not cleaning. You can set a value in seconds e.g. `120` to enable background updates even when the robot is not cleaning. You can also disable background updates completely by setting the value `0`. This might be required if you experience timeouts in the app because you have other home automation apps that are connected to your robot. The parameter **refresh** is default set to auto and updates the robot state when the cleaning was started via homekit so that you can activate automations after the cleaning is done. If you want to get robot state updates after starting the cleaning from the neato app or a schedule, you have to set refresh to a static value in seconds e.g. `120`. You can disable background updates completely by setting this to `0`.
The parameter **disabled** accepts a list of switches/sensors that can be disabled in the neato homekit plugin (e.g. dock, dockstate, eco, schedule, spot). The parameter **disabled** accepts a list of switches/sensors that can be disabled in the neato homekit plugin (e.g. `dock`, `dockstate`, `eco`, `schedule`, `findme`, `spot`, `spotrepeat`, `spot4x4`).
```json ```json
"platforms": [ "platforms": [
@ -68,13 +77,16 @@ The parameter **disabled** accepts a list of switches/sensors that can be disabl
## Tested robots ## Tested robots
- BotVac Connected (Firmware 2.2.0) - BotVac Connected
- BotVac D3 Connected - BotVac D3 Connected
- BotVac D5 Connected (Firmware 4.0.0, 4.3.0) - BotVac D4 Connected
- BotVac D5 Connected
- BotVac D6 Connected
- BotVac D7 Connected - BotVac D7 Connected
The plugin should work with D4 and D6 as well. If you have connected neato robot, please [tell me](https://github.com/naofireblade/homebridge-neato/issues) about your experience with this plugin.
## Contributors ## Contributors
Many thanks go to Many thanks go to
- [ghulands](https://github.com/ghulands) for finding and fixing a bug when no robot is associated with the neato account - [ghulands](https://github.com/ghulands) for finding and fixing a bug when no robot is associated with the neato account
- [Berkay](https://github.com/btutal) for adding the schema file to use the plugin with homebridge-config-ui-x
- [Antoine de Maleprade](https://github.com/az0uz) for adding the zone cleaning feature
- [DJay](https://github.com/DJay-X) for testing out new beta versions

View File

@ -0,0 +1,536 @@
const debug = require('debug')('homebridge-neato');
let Service,
Characteristic;
module.exports = function (_Service, _Characteristic)
{
Service = _Service;
Characteristic = _Characteristic;
return NeatoVacuumRobotAccessory;
};
function NeatoVacuumRobotAccessory(platform, robotObject, boundary = undefined)
{
this.platform = platform;
this.log = platform.log;
this.refresh = platform.refresh;
this.hiddenServices = platform.hiddenServices;
this.robotObject = robotObject;
this.robot = robotObject.device;
this.meta = robotObject.meta;
this.boundary = boundary;
this.nextRoom = null;
if (typeof boundary === 'undefined')
{
this.name = this.robot.name;
}
else
{
// if boundary name already exists
if (platform.boundaryNames.includes(this.boundary.name))
{
let lastChar = this.boundary.name.slice(-1);
// boundary name already contains a count number
if (!isNaN(lastChar))
{
// Increment existing count number
this.boundary.name = this.boundary.name.slice(0, -1) + (parseInt(lastChar) + 1);
}
else
{
// Add a new count number
this.boundary.name = this.boundary.name + " 2";
}
}
platform.boundaryNames.push(this.boundary.name);
this.name = this.robot.name + ' - ' + this.boundary.name;
}
this.vacuumRobotBatteryService = new Service.BatteryService("Battery", "battery");
if (typeof boundary === 'undefined')
{
this.vacuumRobotCleanService = new Service.Switch(this.name + " Clean", "clean");
this.vacuumRobotGoToDockService = new Service.Switch(this.name + " Go to Dock", "goToDock");
this.vacuumRobotDockStateService = new Service.OccupancySensor(this.name + " Dock", "dockState");
this.vacuumRobotEcoService = new Service.Switch(this.name + " Eco Mode", "eco");
this.vacuumRobotNoGoLinesService = new Service.Switch(this.name + " NoGo Lines", "noGoLines");
this.vacuumRobotExtraCareService = new Service.Switch(this.name + " Extra Care", "extraCare");
this.vacuumRobotScheduleService = new Service.Switch(this.name + " Schedule", "schedule");
this.vacuumRobotFindMeService = new Service.Switch(this.name + " Find Me", "findMe");
}
else
{
const splitName = boundary.name.split(' ');
let serviceName = "Clean the " + boundary.name;
if (splitName.length >= 2 && splitName[splitName.length - 2].match(/[']s$/g))
{
serviceName = "Clean " + boundary.name;
}
this.vacuumRobotCleanBoundaryService =
new Service.Switch(serviceName, "cleanBoundary:" + boundary.id);
this.log("Adding zone cleaning for: " + boundary.name);
}
}
NeatoVacuumRobotAccessory.prototype = {
identify: function (callback)
{
this.platform.updateRobot(this.robot._serial, () =>
{
// hide serial and secret in log
let _serial = this.robot._serial;
let _secret = this.robot._secret;
this.robot._serial = "*****";
this.robot._secret = "*****";
this.log(this.robot);
this.robot._serial = _serial;
this.robot._secret = _secret;
callback();
});
},
getServices: function ()
{
this.informationService = new Service.AccessoryInformation();
this.informationService
.setCharacteristic(Characteristic.Manufacturer, "Neato Robotics")
.setCharacteristic(Characteristic.Model, this.meta.modelName)
.setCharacteristic(Characteristic.SerialNumber, this.robot._serial)
.setCharacteristic(Characteristic.FirmwareRevision, this.meta.firmware);
if (typeof this.boundary === "undefined")
{
this.informationService
.setCharacteristic(Characteristic.Name, this.robot.name)
}
else
{
this.informationService
.setCharacteristic(Characteristic.Name, this.robot.name + ' - ' + this.boundary.name)
}
this.services = [this.informationService];
if (typeof this.boundary === "undefined")
{
this.vacuumRobotCleanService.getCharacteristic(Characteristic.On).on('set', (on, serviceCallback) =>
{
this.setClean(on, serviceCallback, this.boundary)
});
this.vacuumRobotCleanService.getCharacteristic(Characteristic.On).on('get', (serviceCallback) =>
{
this.getClean(serviceCallback, this.boundary);
});
this.vacuumRobotGoToDockService.getCharacteristic(Characteristic.On).on('set', this.setGoToDock.bind(this));
this.vacuumRobotGoToDockService.getCharacteristic(Characteristic.On).on('get', this.getGoToDock.bind(this));
this.vacuumRobotDockStateService.getCharacteristic(Characteristic.OccupancyDetected).on('get', this.getDock.bind(this));
this.vacuumRobotEcoService.getCharacteristic(Characteristic.On).on('set', this.setEco.bind(this));
this.vacuumRobotEcoService.getCharacteristic(Characteristic.On).on('get', this.getEco.bind(this));
this.vacuumRobotNoGoLinesService.getCharacteristic(Characteristic.On).on('set', this.setNoGoLines.bind(this));
this.vacuumRobotNoGoLinesService.getCharacteristic(Characteristic.On).on('get', this.getNoGoLines.bind(this));
this.vacuumRobotExtraCareService.getCharacteristic(Characteristic.On).on('set', this.setExtraCare.bind(this));
this.vacuumRobotExtraCareService.getCharacteristic(Characteristic.On).on('get', this.getExtraCare.bind(this));
this.vacuumRobotScheduleService.getCharacteristic(Characteristic.On).on('set', this.setSchedule.bind(this));
this.vacuumRobotScheduleService.getCharacteristic(Characteristic.On).on('get', this.getSchedule.bind(this));
this.vacuumRobotFindMeService.getCharacteristic(Characteristic.On).on('set', this.setFindMe.bind(this));
this.vacuumRobotFindMeService.getCharacteristic(Characteristic.On).on('get', this.getFindMe.bind(this));
this.vacuumRobotBatteryService.getCharacteristic(Characteristic.BatteryLevel).on('get', this.getBatteryLevel.bind(this));
this.vacuumRobotBatteryService.getCharacteristic(Characteristic.ChargingState).on('get', this.getBatteryChargingState.bind(this));
this.services.push(this.vacuumRobotCleanService);
this.services.push(this.vacuumRobotBatteryService);
if (this.hiddenServices.indexOf('dock') === -1)
this.services.push(this.vacuumRobotGoToDockService);
if (this.hiddenServices.indexOf('dockstate') === -1)
this.services.push(this.vacuumRobotDockStateService);
if (this.hiddenServices.indexOf('eco') === -1)
this.services.push(this.vacuumRobotEcoService);
if (this.hiddenServices.indexOf('nogolines') === -1)
this.services.push(this.vacuumRobotNoGoLinesService);
if (this.hiddenServices.indexOf('extracare') === -1)
this.services.push(this.vacuumRobotExtraCareService);
if (this.hiddenServices.indexOf('schedule') === -1)
this.services.push(this.vacuumRobotScheduleService);
// if (this.hiddenServices.indexOf('find') === -1)
// this.services.push(this.vacuumRobotFindMeService);
}
else
{
this.vacuumRobotCleanBoundaryService.getCharacteristic(Characteristic.On).on('set', (on, serviceCallback) =>
{
this.setClean(on, serviceCallback, this.boundary)
});
this.vacuumRobotCleanBoundaryService.getCharacteristic(Characteristic.On).on('get', (serviceCallback) =>
{
this.getClean(serviceCallback, this.boundary);
});
this.services.push(this.vacuumRobotCleanBoundaryService);
}
return this.services;
},
getClean: function (callback, boundary)
{
this.platform.updateRobot(this.robot._serial, (error, result) =>
{
let cleaning;
if (typeof boundary === 'undefined')
{
cleaning = this.robot.canPause;
}
else
{
cleaning = this.robot.canPause && (this.robot.cleaningBoundaryId === boundary.id)
}
debug(this.name + ": Cleaning is " + (cleaning ? 'ON' : 'OFF'));
callback(false, cleaning);
});
},
setClean: function (on, callback, boundary)
{
this.platform.updateRobot(this.robot._serial, (error, result) =>
{
// Start
if (on)
{
// No room given or same room
if (typeof boundary === 'undefined' || this.robot.cleaningBoundaryId === boundary.id)
{
// Resume cleaning
if (this.robot.canResume)
{
debug(this.name + ": ## Resume cleaning");
this.robot.resumeCleaning(callback);
}
// Start cleaning
else if (this.robot.canStart)
{
this.clean(callback, boundary);
}
// Cannot start
else
{
debug(this.name + ": Cannot start, maybe already cleaning");
callback();
}
}
// Different room given
else
{
// Return to dock
if (this.robot.canPause || this.robot.canResume)
{
debug(this.name + ": ## Returning to dock to start cleaning of new room");
this.setGoToDock(true, (error, result) =>
{
this.nextRoom = boundary;
});
}
// Start new cleaning of new room
else
{
this.clean(callback, boundary);
}
}
}
// Stop
else
{
if (this.robot.canPause)
{
debug(this.name + ": ## Pause cleaning");
this.robot.pauseCleaning(callback);
}
else
{
debug(this.name + ": Already paused");
callback();
}
}
});
},
clean: function (callback, boundary)
{
// Start automatic update while cleaning
if (this.refresh === 'auto')
{
setTimeout(() =>
{
this.platform.updateRobotTimer(this.robot._serial);
}, 60 * 1000);
}
let eco = this.robotObject.mainAccessory.vacuumRobotEcoService.getCharacteristic(Characteristic.On).value;
let extraCare = this.robotObject.mainAccessory.vacuumRobotExtraCareService.getCharacteristic(Characteristic.On).value;
let nogoLines = this.robotObject.mainAccessory.vacuumRobotNoGoLinesService.getCharacteristic(Characteristic.On).value;
let room = (typeof boundary === 'undefined') ? '' : boundary.name;
debug(this.name + ": ## Start cleaning (" + room + " eco: " + eco + ", extraCare: " + extraCare + ", nogoLines: " + nogoLines + ")");
// Normal cleaning
if (typeof boundary === 'undefined')
{
this.robot.startCleaning(
eco,
extraCare ? 2 : 1,
nogoLines,
(error, result) =>
{
if (error)
{
this.log.error("Cannot start cleaning. " + error + ": " + JSON.stringify(result));
callback(true);
}
else
{
callback();
}
});
}
// Room cleaning
else
{
this.robot.startCleaningBoundary(eco, extraCare, boundary.id, (error, result) =>
{
if (error)
{
this.log.error("Cannot start room cleaning. " + error + ": " + JSON.stringify(result));
callback(true);
}
else
{
callback();
}
});
}
},
getGoToDock: function (callback)
{
callback(false, false);
},
setGoToDock: function (on, callback)
{
this.platform.updateRobot(this.robot._serial, (error, result) =>
{
if (on)
{
if (this.robot.canPause)
{
debug(this.name + ": ## Pause cleaning to go to dock");
this.robot.pauseCleaning((error, result) =>
{
setTimeout(() =>
{
debug(this.name + ": ## Go to dock");
this.robot.sendToBase(callback);
}, 1000);
});
}
else if (this.robot.canGoToBase)
{
debug(this.name + ": ## Go to dock");
this.robot.sendToBase(callback);
}
else
{
this.log.warn(this.name + ": Can't go to dock at the moment");
callback();
}
}
else
{
callback();
}
});
},
getEco: function (callback)
{
this.platform.updateRobot(this.robot._serial, () =>
{
debug(this.name + ": Eco Mode is " + (this.robot.eco ? 'ON' : 'OFF'));
callback(false, this.robot.eco);
});
},
setEco: function (on, callback)
{
this.robot.eco = on;
debug(this.name + ": " + (on ? "Enabled " : "Disabled") + " Eco Mode ");
callback();
},
getNoGoLines: function (callback)
{
this.platform.updateRobot(this.robot._serial, () =>
{
debug(this.name + ": NoGoLine is " + (this.robot.eco ? 'ON' : 'OFF'));
callback(false, this.robot.noGoLines ? 1 : 0);
});
},
setNoGoLines: function (on, callback)
{
this.robot.noGoLines = on;
debug(this.name + ": " + (on ? "Enabled " : "Disabled") + " NoGoLine ");
callback();
},
getExtraCare: function (callback)
{
this.platform.updateRobot(this.robot._serial, () =>
{
debug(this.name + ": Care Nav is " + (this.robot.navigationMode === 2 ? 'ON' : 'OFF'));
callback(false, this.robot.navigationMode === 2 ? 1 : 0);
});
},
setExtraCare: function (on, callback)
{
this.robot.navigationMode = on ? 2 : 1;
debug(this.name + ": " + (on ? "Enabled " : "Disabled") + " Care Nav ");
callback();
},
getSchedule: function (callback)
{
this.platform.updateRobot(this.robot._serial, () =>
{
debug(this.name + ": Schedule is " + (this.robot.eco ? 'ON' : 'OFF'));
callback(false, this.robot.isScheduleEnabled);
});
},
setSchedule: function (on, callback)
{
this.platform.updateRobot(this.robot._serial, (error, result) =>
{
if (on)
{
debug(this.name + ": Enabled Schedule");
this.robot.enableSchedule(callback);
}
else
{
debug(this.name + ": Disabled Schedule");
this.robot.disableSchedule(callback);
}
});
},
getFindMe: function (callback)
{
callback(false, false);
},
setFindMe: function (on, callback)
{
if (on)
{
debug(this.name + ": ## Find me");
setTimeout(() =>
{
this.vacuumRobotFindMeService.setCharacteristic(Characteristic.On, false);
}, 1000);
this.robot.findMe(callback);
}
},
getDock: function (callback)
{
this.platform.updateRobot(this.robot._serial, () =>
{
debug(this.name + ": The Dock is " + (this.robot.isDocked ? '' : 'un ') + "occupied");
callback(false, this.robot.isDocked ? 1 : 0);
});
},
getBatteryLevel: function (callback)
{
this.platform.updateRobot(this.robot._serial, () =>
{
debug(this.name + ": Battery is " + this.robot.charge + "%");
callback(false, this.robot.charge);
});
},
getBatteryChargingState: function (callback)
{
this.platform.updateRobot(this.robot._serial, () =>
{
debug(this.name + ": Battery is " + (this.robot.isCharging ? '' : 'not ') + "charging");
callback(false, this.robot.isCharging);
});
},
updated: function ()
{
if (!this.boundary)
{
// only update these values if the state is different from the current one, otherwise we might accidentally start an action
if (this.vacuumRobotCleanService.getCharacteristic(Characteristic.On).value !== this.robot.canPause)
{
this.vacuumRobotCleanService.setCharacteristic(Characteristic.On, this.robot.canPause);
}
// dock switch is on (dock not seen before) and dock has just been seen -> turn switch off
if (this.vacuumRobotGoToDockService.getCharacteristic(Characteristic.On).value == true && this.robot.dockHasBeenSeen)
{
this.vacuumRobotGoToDockService.setCharacteristic(Characteristic.On, false);
}
if (this.vacuumRobotScheduleService.getCharacteristic(Characteristic.On).value !== this.robot.isScheduleEnabled)
{
this.vacuumRobotScheduleService.setCharacteristic(Characteristic.On, this.robot.isScheduleEnabled);
}
// no commands here, values can be updated without problems
this.vacuumRobotDockStateService.setCharacteristic(Characteristic.OccupancyDetected, this.robot.isDocked ? 1 : 0);
this.vacuumRobotEcoService.setCharacteristic(Characteristic.On, this.robot.eco);
this.vacuumRobotNoGoLinesService.setCharacteristic(Characteristic.On, this.robot.noGoLines);
this.vacuumRobotExtraCareService.setCharacteristic(Characteristic.On, this.robot.navigationMode == 2 ? true : false);
}
else
{
if (this.vacuumRobotCleanBoundaryService.getCharacteristic(Characteristic.On).value !== this.robot.canPause)
{
this.vacuumRobotCleanBoundaryService.setCharacteristic(Characteristic.On, this.robot.canPause);
}
}
this.vacuumRobotBatteryService.setCharacteristic(Characteristic.BatteryLevel, this.robot.charge);
this.vacuumRobotBatteryService.setCharacteristic(Characteristic.ChargingState, this.robot.isCharging);
// Robot has a next room to clean in queue
if (this.nextRoom !== null && this.robot.isDocked)
{
this.clean((error, result) =>
{
this.nextRoom = null;
debug("## Starting cleaning of next room");
}, this.nextRoom);
}
}
};

23
config.schema.json Normal file
View File

@ -0,0 +1,23 @@
{
"pluginAlias": "NeatoVacuumRobot",
"pluginType": "platform",
"headerDisplay": "For Advanced settings like Refresh time interval or Disabled switches/sensors. [Check Here](https://github.com/naofireblade/homebridge-neato#readme)",
"schema": {
"type": "object",
"properties": {
"email": {
"title": "email",
"type": "string",
"required": true,
"format": "email",
"description": "Your Email Address"
},
"password": {
"title": "password",
"type": "string",
"required": true,
"description": "Your Password"
}
}
}
}

687
index.js
View File

@ -1,541 +1,268 @@
"use strict"; "use strict";
var inherits = require('util').inherits, let inherits = require('util').inherits,
debug = require('debug')('homebridge-neato'), debug = require('debug')('homebridge-neato'),
botvac = require('node-botvac'), botvac = require('node-botvac'),
Service, Service,
Characteristic Characteristic;
module.exports = function (homebridge) { module.exports = function (homebridge)
{
Service = homebridge.hap.Service; Service = homebridge.hap.Service;
Characteristic = homebridge.hap.Characteristic; Characteristic = homebridge.hap.Characteristic;
homebridge.registerPlatform("homebridge-neato", "NeatoVacuumRobot", NeatoVacuumRobotPlatform); homebridge.registerPlatform("homebridge-neato", "NeatoVacuumRobot", NeatoVacuumRobotPlatform);
} };
function NeatoVacuumRobotPlatform(log, config) { function NeatoVacuumRobotPlatform(log, config)
{
this.log = log; this.log = log;
this.serial = "1-3-3-7"; this.serial = "1-3-3-7";
this.email = config['email']; this.email = config['email'];
this.password = config['password']; this.password = config['password'];
this.hiddenServices = ('disabled' in config ? config['disabled'] : ''); this.hiddenServices = ('disabled' in config ? config['disabled'] : '');
if ('refresh' in config && config['refresh'] !== 'auto') { // Array of real robots and associated robot accessories (incl rooms)
this.robots = [];
if ('refresh' in config && config['refresh'] !== 'auto')
{
// parse config parameter // parse config parameter
this.refresh = parseInt(config['refresh']); this.refresh = parseInt(config['refresh']);
// must be integer and positive // must be integer and positive
this.refresh = (typeof this.refresh !== 'number' || (this.refresh % 1) !== 0 || this.refresh < 0) ? 60 : this.refresh; this.refresh = (typeof this.refresh !== 'number' || (this.refresh % 1) !== 0 || this.refresh < 0) ? 60 : this.refresh;
// minimum 60s to save some load on the neato servers // minimum 60s to save some load on the neato servers
this.refresh = (this.refresh > 0 && this.refresh < 60) ? 60 : this.refresh; if (this.refresh > 0 && this.refresh < 60)
{
this.log.warn("Minimum refresh time is 60 seconds to not overload the neato servers");
this.refresh = (this.refresh > 0 && this.refresh < 60) ? 60 : this.refresh;
}
} }
// default auto // default auto
else { else
{
this.refresh = 'auto'; this.refresh = 'auto';
} }
debug("Refresh is set to: " + this.refresh); this.log("Refresh is set to: " + this.refresh + (this.refresh !== 'auto' ? ' seconds' : ''));
} }
NeatoVacuumRobotPlatform.prototype = { NeatoVacuumRobotPlatform.prototype = {
accessories: function (callback) { accessories: function (callback)
this.accessories = []; {
debug("Get robots");
let accessories = [];
this.boundaryNames = [];
this.getRobots(() =>
{
this.robots.forEach((robot, i) =>
{
this.log("Found robot #" + (i + 1) + " named \"" + robot.device.name + "\" with serial \"" + robot.device._serial + "\"");
let that = this; // Start Update Intervall
this.robots = this.getRobots(function () { this.updateRobotTimer(robot.device._serial);
for (var i = 0; i < that.robots.length; i++) {
that.log("Found robot #" + (i + 1) + " named \"" + that.robots[i].name + "\" with serial \"" + that.robots[i]._serial + "\""); let NeatoVacuumRobotAccessory = require('./accessories/neatoVacuumRobot')(Service, Characteristic);
var robotAccessory = new NeatoVacuumRobotAccessory(that.robots[i], that); let mainAccessory = new NeatoVacuumRobotAccessory(this, robot);
that.accessories.push(robotAccessory); accessories.push(mainAccessory);
}
callback(that.accessories); robot.mainAccessory = mainAccessory;
robot.roomAccessories = [];
if (robot.device.maps)
{
robot.device.maps.forEach((map) =>
{
if (map.boundaries)
{
map.boundaries.forEach((boundary) =>
{
if (boundary.type === "polygon")
{
let roomAccessory = new NeatoVacuumRobotAccessory(this, robot, boundary);
accessories.push(roomAccessory);
robot.roomAccessories.push(roomAccessory);
}
})
}
})
}
});
callback(accessories);
}); });
}, },
getRobots: function (callback) { getRobots: function (callback)
{
debug("Loading your robots"); debug("Loading your robots");
let client = new botvac.Client(); let client = new botvac.Client();
let that = this;
client.authorize(this.email, this.password, false, function (error) { // Login
if (error) { client.authorize(this.email, this.password, false, (error) =>
that.log(error); {
that.log.error("Can't log on to neato cloud. Please check your credentials."); if (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);
callback(); callback();
} }
else { else
client.getRobots(function (error, robots) { {
if (error) { // Get robots
that.log(error); client.getRobots((error, robots) =>
that.log.error("Successful login but can't connect to your neato robot."); {
if (error)
{
this.log.error("Successful login but can't connect to your neato robot: " + error);
callback(); callback();
} }
else { else if (robots.length === 0)
if (robots.length === 0) { {
that.log.error("Successful login but no robots associated with your account."); this.log.error("Successful login but no robots associated with your account.");
that.robots = []; this.robots = [];
callback(); callback();
} }
else { else
debug("Found " + robots.length + " robots"); {
that.robots = robots; debug("Found " + robots.length + " robots");
callback(); let requestedRobot = 0;
}
robots.forEach((robot) =>
{
// Get Maps for each robot
robot.getPersistentMaps((error, result) =>
{
if (error)
{
this.log.error("Error updating persistent maps: " + error + ": " + result);
callback();
}
else if (result.length === 0)
{
robot.maps = [];
callback();
}
else
{
robot.maps = result;
let requestedMap = 0;
robot.maps.forEach((map) =>
{
// Get Map Boundary Lines
robot.getMapBoundaries(map.id, (error, result) =>
{
if (error)
{
this.log.error("Error getting boundaries: " + error + ": " + result)
}
else
{
map.boundaries = result.boundaries;
}
requestedMap++;
// Robot is completely requested if all maps are requested
if (requestedMap === robot.maps.length)
{
// Get additional information
robot.getState((error, result) =>
{
if (error)
{
this.log.error("Error getting robot meta information: " + error + ": " + result);
callback();
}
else
{
this.robots.push({device: robot, meta: result.meta});
requestedRobot++;
// Initial request is complete if all robots are requested.
if (requestedRobot === robots.length)
{
callback();
}
}
});
}
})
});
}
});
});
} }
}); });
} }
}); });
}
}
function NeatoVacuumRobotAccessory(robot, platform) {
this.platform = platform;
this.log = platform.log;
this.refresh = platform.refresh;
this.hiddenServices = platform.hiddenServices;
this.robot = robot;
this.name = robot.name;
this.lastUpdate = null;
this.tempSpot4x4 = false;
this.tempSpotRepeat = false;
this.vacuumRobotCleanService = new Service.Switch(this.name + " Clean", "clean");
this.vacuumRobotGoToDockService = new Service.Switch(this.name + " Go to Dock", "goToDock");
this.vacuumRobotDockStateService = new Service.OccupancySensor(this.name + " Dock", "dockState");
this.vacuumRobotEcoService = new Service.Switch(this.name + " Eco Mode", "eco");
this.vacuumRobotNoGoLinesService = new Service.Switch(this.name + " NoGo Lines", "noGoLines");
this.vacuumRobotExtraCareService = new Service.Switch(this.name + " Extra Care", "extraCare");
this.vacuumRobotScheduleService = new Service.Switch(this.name + " Schedule", "schedule");
this.vacuumRobotBatteryService = new Service.BatteryService("Battery", "battery");
this.vacuumRobotSpotCleanService = new Service.Switch(this.name + " Spot Clean", "spotClean");
this.vacuumRobotSpotRepeatService = new Service.Switch(this.name + " Spot Repeat", "spotRepeat");
this.vacuumRobotSpot4x4Service = new Service.Switch(this.name + " Spot Clean 4x4", "spotClean4x4");
this.updateRobotTimer();
}
NeatoVacuumRobotAccessory.prototype = {
identify: function (callback) {
let that = this;
this.updateRobot(function () {
// hide serial and secret in log
let _serial = that.robot._serial;
let _secret = that.robot._secret;
that.robot._serial = "*****";
that.robot._secret = "*****";
that.log(that.robot);
that.robot._serial = _serial;
that.robot._secret = _secret;
callback();
});
}, },
getServices: function () { updateRobot: function (serial, callback)
this.informationService = new Service.AccessoryInformation(); {
this.informationService let robot = this.getRobot(serial);
.setCharacteristic(Characteristic.Name, this.robot.name)
.setCharacteristic(Characteristic.Manufacturer, "Neato Robotics")
.setCharacteristic(Characteristic.Model, "Coming soon")
.setCharacteristic(Characteristic.SerialNumber, this.robot._serial);
this.vacuumRobotCleanService.getCharacteristic(Characteristic.On).on('set', this.setClean.bind(this)); // Data is up to date
this.vacuumRobotCleanService.getCharacteristic(Characteristic.On).on('get', this.getClean.bind(this)); if (typeof (robot.lastUpdate) !== 'undefined' && new Date() - robot.lastUpdate < 2000)
{
this.vacuumRobotGoToDockService.getCharacteristic(Characteristic.On).on('set', this.setGoToDock.bind(this));
this.vacuumRobotGoToDockService.getCharacteristic(Characteristic.On).on('get', this.getGoToDock.bind(this));
this.vacuumRobotDockStateService.getCharacteristic(Characteristic.OccupancyDetected).on('get', this.getDock.bind(this));
this.vacuumRobotEcoService.getCharacteristic(Characteristic.On).on('set', this.setEco.bind(this));
this.vacuumRobotEcoService.getCharacteristic(Characteristic.On).on('get', this.getEco.bind(this));
this.vacuumRobotNoGoLinesService.getCharacteristic(Characteristic.On).on('set', this.setNoGoLines.bind(this));
this.vacuumRobotNoGoLinesService.getCharacteristic(Characteristic.On).on('get', this.getNoGoLines.bind(this));
this.vacuumRobotExtraCareService.getCharacteristic(Characteristic.On).on('set', this.setExtraCare.bind(this));
this.vacuumRobotExtraCareService.getCharacteristic(Characteristic.On).on('get', this.getExtraCare.bind(this));
this.vacuumRobotScheduleService.getCharacteristic(Characteristic.On).on('set', this.setSchedule.bind(this));
this.vacuumRobotScheduleService.getCharacteristic(Characteristic.On).on('get', this.getSchedule.bind(this));
this.vacuumRobotBatteryService.getCharacteristic(Characteristic.BatteryLevel).on('get', this.getBatteryLevel.bind(this));
this.vacuumRobotBatteryService.getCharacteristic(Characteristic.ChargingState).on('get', this.getBatteryChargingState.bind(this));
this.vacuumRobotSpotCleanService.getCharacteristic(Characteristic.On).on('set', this.setSpotClean.bind(this));
this.vacuumRobotSpotCleanService.getCharacteristic(Characteristic.On).on('get', this.getSpotClean.bind(this));
this.vacuumRobotSpotCleanService.getCharacteristic(Characteristic.On).on('set', this.setSpotClean.bind(this));
this.vacuumRobotSpotCleanService.getCharacteristic(Characteristic.On).on('get', this.getSpotClean.bind(this));
this.vacuumRobotSpot4x4Service.getCharacteristic(Characteristic.On).on('set', this.setSpot4x4.bind(this));
this.vacuumRobotSpot4x4Service.getCharacteristic(Characteristic.On).on('get', this.getSpot4x4.bind(this));
this.vacuumRobotSpotRepeatService.getCharacteristic(Characteristic.On).on('set', this.setSpotRepeat.bind(this));
this.vacuumRobotSpotRepeatService.getCharacteristic(Characteristic.On).on('get', this.getSpotRepeat.bind(this));
this.services = [this.informationService, this.vacuumRobotCleanService, this.vacuumRobotBatteryService];
if (this.hiddenServices.indexOf('dock') === -1)
this.services.push(this.vacuumRobotGoToDockService);
if (this.hiddenServices.indexOf('dockstate') === -1)
this.services.push(this.vacuumRobotDockStateService);
if (this.hiddenServices.indexOf('eco') === -1)
this.services.push(this.vacuumRobotEcoService);
if (this.hiddenServices.indexOf('nogolines') === -1)
this.services.push(this.vacuumRobotNoGoLinesService);
if (this.hiddenServices.indexOf('extracare') === -1)
this.services.push(this.vacuumRobotExtraCareService);
if (this.hiddenServices.indexOf('schedule') === -1)
this.services.push(this.vacuumRobotScheduleService);
if (this.hiddenServices.indexOf('spot') === -1) {
this.services.push(this.vacuumRobotSpotCleanService);
this.services.push(this.vacuumRobotSpot4x4Service);
this.services.push(this.vacuumRobotSpotRepeatService);
};
return this.services;
},
setClean: function (on, callback) {
let that = this;
this.updateRobot(function (error, result) {
if (on) {
if (that.robot.canResume || that.robot.canStart) {
// start extra update robot timer if refresh is set to "auto"
if (that.refresh === 'auto') {
setTimeout(function () {
clearTimeout(that.timer);
that.updateRobotTimer();
}, 60 * 1000);
}
if (that.robot.canResume) {
debug(that.name + ": Resume cleaning");
that.robot.resumeCleaning(callback);
}
else {
let eco = that.vacuumRobotEcoService.getCharacteristic(Characteristic.On).value;
let extraCare = that.vacuumRobotExtraCareService.getCharacteristic(Characteristic.On).value;
let nogoLines = that.vacuumRobotNoGoLinesService.getCharacteristic(Characteristic.On).value;
debug(that.name + ": Start cleaning (eco: " + eco + ", extraCare: " + extraCare + ", nogoLines: " + nogoLines + ")");
that.robot.startCleaning(
eco,
extraCare ? 2 : 1,
nogoLines,
function (error, result) {
if (error) {
that.log.error(error + ": " + result);
callback(true);
}
else {
callback();
}
});
}
}
else {
debug(that.name + ": Cant start, maybe already cleaning");
callback();
}
}
else {
if (that.robot.canPause) {
debug(that.name + ": Pause cleaning");
that.robot.pauseCleaning(callback);
}
else {
debug(that.name + ": Already stopped");
callback();
}
}
});
},
setSpotClean: function (on, callback) {
let that = this;
this.updateRobot(function (error, result) {
if (on) {
if (that.robot.canResume || that.robot.canStart) {
// start extra update robot timer if refresh is set to "auto"
if (that.refresh === 'auto') {
setTimeout(function () {
clearTimeout(that.timer);
that.updateRobotTimer();
}, 60 * 1000);
}
if (that.robot.canResume) {
debug(that.name + ": Resume spot cleaning");
that.robot.resumeCleaning(callback);
}
else {
let eco = that.vacuumRobotEcoService.getCharacteristic(Characteristic.On).value;
if (!that.vacuumRobotSpot4x4Service.getCharacteristic(Characteristic.On).value) {
var width = 200;
var height = 200;
} else {
var width = 400;
var height = 400;
}
let repeat = that.vacuumRobotSpotRepeatService.getCharacteristic(Characteristic.On).value;
let extraCare = false;
debug(that.name + ": Start spot cleaning (eco: " + eco + ", width: " + width + ", height: " + height + ", repeat: " + repeat + ")");
that.robot.startSpotCleaning(
eco,
width,
height,
repeat ? 2 : 1,
extraCare ? 2 : 1,
function (error, result) {
if (error) {
that.log.error(error + ": " + result);
callback(true);
}
else {
callback();
}
});
}
}
else {
debug(that.name + ": Cant start, maybe already cleaning");
callback();
}
}
else {
if (that.robot.canPause) {
debug(that.name + ": Pause spot cleaning");
that.robot.pauseCleaning(callback);
}
else {
debug(that.name + ": Already stopped");
callback();
}
}
});
},
setGoToDock: function (on, callback) {
let that = this;
this.updateRobot(function (error, result) {
if (on) {
if (that.robot.canPause) {
debug(that.name + ": Pause cleaning to go to dock");
that.robot.pauseCleaning(function (error, result) {
setTimeout(function () {
debug("Go to dock");
that.robot.sendToBase(callback);
}, 1000);
});
}
else if (that.robot.canGoToBase) {
debug(that.name + ": Go to dock");
that.robot.sendToBase(callback);
}
else {
that.log.warn(that.name + ": Can't go to dock at the moment");
callback();
}
} else {
callback();
}
});
},
setEco: function (on, callback) {
debug(this.name + ": " + (on ? "Enable eco mode" : "Disable eco mode"));
this.robot.eco = on;
callback();
},
setNoGoLines: function (on, callback) {
debug(this.name + ": " + (on ? "Enable nogo lines" : "Disable nogo lines"));
this.robot.noGoLines = on;
callback();
},
setExtraCare: function (on, callback) {
debug(this.name + ": " + (on ? "Enable extra care navigation" : "Disable extra care navigation"));
this.robot.navigationMode = on ? 2 : 1;
callback();
},
setSchedule: function (on, callback) {
let that = this;
this.updateRobot(function (error, result) {
if (on) {
debug(that.name + ": Enable schedule");
that.robot.enableSchedule(callback);
}
else {
debug(that.name + ": Disable schedule");
that.robot.disableSchedule(callback);
}
});
},
setSpotRepeat: function (on, callback) {
debug(this.name + ": " + (on ? "Enable spot cleaning repeat mode (2x)" : "Disable spot cleaning repeat mode (2x)"));
if (on) {
this.tempSpotRepeat = true;
}
else {
this.tempSpotRepeat = false;
}
callback();
},
setSpot4x4: function (on, callback) {
debug(this.name + ": " + (on ? "Enable spot cleaning 4x4 mode" : "Disable spot cleaning 4x4 mode"));
if (on) {
this.tempSpot4x4 = true;
}
else {
this.tempSpot4x4 = false;
}
callback();
},
getClean: function (callback) {
let that = this;
this.updateRobot(function (error, result) {
debug(that.name + ": Is cleaning: " + that.robot.canPause);
callback(false, that.robot.canPause);
});
},
getGoToDock: function (callback) {
callback(false, false);
},
getDock: function (callback) {
let that = this;
this.updateRobot(function () {
debug(that.name + ": Is docked: " + that.robot.isDocked);
callback(false, that.robot.isDocked ? 1 : 0);
});
},
getEco: function (callback) {
let that = this;
this.updateRobot(function () {
debug(that.name + ": Is eco: " + that.robot.eco);
callback(false, that.robot.eco);
});
},
getNoGoLines: function (callback) {
let that = this;
this.updateRobot(function () {
debug(that.name + ": Is nogo lines: " + that.robot.noGoLines);
callback(false, that.robot.noGoLines ? 1 : 0);
});
},
getExtraCare: function (callback) {
let that = this;
this.updateRobot(function () {
debug(that.name + ": Is extra care navigation: " + (that.robot.navigationMode == 2 ? true : false));
callback(false, that.robot.navigationMode == 2 ? 1 : 0);
});
},
getSchedule: function (callback) {
let that = this;
this.updateRobot(function () {
debug(that.name + ": Is schedule: " + that.robot.isScheduleEnabled);
callback(false, that.robot.isScheduleEnabled);
});
},
getSpotRepeat: function (callback) {
let that = this;
debug(that.name + ": Is spot cleaning repeat: " + that.tempSpotRepeat);
callback(false, that.tempSpotRepeat);
},
getSpot4x4: function (callback) {
let that = this;
debug(that.name + ": Is spot cleaning 4x4: " + that.tempSpot4x4);
callback(false, that.tempSpot4x4);
},
getBatteryLevel: function (callback) {
let that = this;
this.updateRobot(function () {
debug(that.name + ": Battery: " + that.robot.charge + "%");
callback(false, that.robot.charge);
});
},
getBatteryChargingState: function (callback) {
let that = this;
this.updateRobot(function () {
debug(that.name + ": Is charging: " + that.robot.isCharging);
callback(false, that.robot.isCharging);
});
},
updateRobot: function (callback) {
let that = this;
if (this.lastUpdate !== null && new Date() - this.lastUpdate < 2000) {
callback(); callback();
} }
else { else
debug(this.name + ": Updating robot state"); {
this.robot.getState(function (error, result) { debug(robot.device.name + ": ++ Updating robot state");
if (error) { robot.device.getState((error, result) =>
that.log.error(error + ": " + result); {
if (error)
{
this.log.error("Cannot update robot. Check if robot is online. " + error);
} }
that.lastUpdate = new Date(); robot.lastUpdate = new Date();
callback(); callback();
}); });
} }
}, },
updateRobotTimer: function () { getRobot(serial)
let that = this; {
this.updateRobot(function (error, result) { let result;
this.robots.forEach(function (robot)
{
if (robot.device._serial === serial)
{
result = robot;
}
});
return result;
},
// only update these values if the state is different from the current one, otherwise we might accidentally start an action updateRobotTimer: function (serial)
if (that.vacuumRobotCleanService.getCharacteristic(Characteristic.On).value !== that.robot.canPause) { {
that.vacuumRobotCleanService.setCharacteristic(Characteristic.On, that.robot.canPause); this.updateRobot(serial, () =>
} {
if (that.vacuumRobotSpotCleanService.getCharacteristic(Characteristic.On).value !== that.robot.canPause) { let robot = this.getRobot(serial);
that.vacuumRobotSpotCleanService.setCharacteristic(Characteristic.On, that.robot.canPause); // Clear any other overlapping timers for this robot
} clearTimeout(robot.timer);
// dock switch is on (dock not seen before) and dock has just been seen -> turn switch off // Tell all accessories of this robot (mainAccessory and roomAccessories) that updated robot data is available
if (that.vacuumRobotGoToDockService.getCharacteristic(Characteristic.On).value == true && that.robot.dockHasBeenSeen) { robot.mainAccessory.updated();
that.vacuumRobotGoToDockService.setCharacteristic(Characteristic.On, false); robot.roomAccessories.forEach(accessory =>
} {
accessory.updated();
});
if (that.vacuumRobotScheduleService.getCharacteristic(Characteristic.On).value !== that.robot.isScheduleEnabled) { // Periodic refresh interval set in config
that.vacuumRobotScheduleService.setCharacteristic(Characteristic.On, that.robot.isScheduleEnabled); if (this.refresh !== 'auto' && this.refresh !== 0)
{
debug(robot.device.name + ": ++ Next background update in " + this.refresh + " seconds");
robot.timer = setTimeout(this.updateRobotTimer.bind(this), this.refresh * 1000, serial);
} }
// Auto refresh set in config
// no commands here, values can be updated without problems else if (this.refresh === 'auto' && robot.device.canPause)
that.vacuumRobotDockStateService.setCharacteristic(Characteristic.OccupancyDetected, that.robot.isDocked ? 1 : 0); {
that.vacuumRobotEcoService.setCharacteristic(Characteristic.On, that.robot.eco); debug(robot.device.name + ": ++ Next background update in 60 seconds while cleaning (auto mode)");
that.vacuumRobotNoGoLinesService.setCharacteristic(Characteristic.On, that.robot.noGoLines); robot.timer = setTimeout(this.updateRobotTimer.bind(this), 60 * 1000, serial);
that.vacuumRobotExtraCareService.setCharacteristic(Characteristic.On, that.robot.navigationMode == 2 ? true : false);
that.vacuumRobotBatteryService.setCharacteristic(Characteristic.BatteryLevel, that.robot.charge);
that.vacuumRobotBatteryService.setCharacteristic(Characteristic.ChargingState, that.robot.isCharging);
// robot is currently cleaning, update if refresh is set to auto or a specific interval
if (that.robot.canPause && that.refresh !== 0) {
let refreshTime = that.refresh === 'auto' ? 60 : that.refresh
debug("Updating state in background every " + refreshTime + " seconds while cleaning");
that.timer = setTimeout(that.updateRobotTimer.bind(that), refreshTime * 1000);
} }
// robot is not cleaning, but a specific refresh interval is set // No refresh
else if (that.refresh !== 'auto' && that.refresh !== 0) { else
debug("Updating state in background every " + that.refresh + " seconds (user setting)"); {
that.timer = setTimeout(that.updateRobotTimer.bind(that), that.refresh * 1000); debug(robot.device.name + ": ++ Stopped background updates");
}
else {
debug("Updating state in background disabled");
} }
}); });
}, },
} };

View File

@ -1,6 +1,6 @@
{ {
"name": "homebridge-neato", "name": "homebridge-neato",
"version": "0.5.1", "version": "0.7.0-beta.5",
"description": "A Neato vacuum robot plugin for homebridge.", "description": "A Neato vacuum robot plugin for homebridge.",
"license": "MIT", "license": "MIT",
"keywords": [ "keywords": [
@ -13,14 +13,30 @@
"homebridge": ">=0.2.0" "homebridge": ">=0.2.0"
}, },
"author": { "author": {
"name": "Arne Blumentritt" "name": "Arne Blumentritt",
"url2": "https://github.com/naofireblade"
}, },
"contributors": [
{
"name": "ghulands",
"url": "https://github.com/ghulands"
},
{
"name": "Berkay",
"url": "https://github.com/btutal"
},
{
"name": "Antoine de Maleprade",
"url": "https://github.com/az0uz"
}
],
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git://github.com/naofireblade/homebridge-neato.git" "url": "git://github.com/naofireblade/homebridge-neato.git"
}, },
"dependencies": { "dependencies": {
"node-botvac": ">=0.1.7", "debug": "^4.1.1",
"debug": "^2.2.0" "node-botvac": ">=0.3.0",
"uuid": "^3.3.2"
} }
} }