3 Commits

Author SHA1 Message Date
Luis Riegger
0767779172 Add language support to README.md 2021-05-04 08:48:48 +02:00
Luis Riegger
bdb5807aeb Add language to plugin via dict object 2021-05-04 08:46:12 +02:00
Luis Riegger
010e177306 Add language selection to plugin schema 2021-05-04 08:32:42 +02:00
33 changed files with 1180 additions and 4593 deletions

View File

@@ -1,38 +0,0 @@
{
"parser": "@typescript-eslint/parser",
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended" // uses the recommended rules from the @typescript-eslint/eslint-plugin
],
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module"
},
"ignorePatterns": [
"dist",
"api.ts"
],
"rules": {
"quotes": ["warn", "double"],
"indent": ["warn", "tab"],
"semi": ["off"],
"comma-dangle": ["warn", "always-multiline"],
"dot-notation": "off",
"eqeqeq": "warn",
"curly": ["warn", "all"],
"prefer-arrow-callback": ["warn"],
"max-len": ["warn", 200],
"no-console": ["warn"], // use the provided Homebridge log method instead
"no-non-null-assertion": ["off"],
"comma-spacing": ["error"],
"no-multi-spaces": ["warn", { "ignoreEOLComments": true }],
"no-trailing-spaces": ["warn"],
"lines-between-class-members": ["warn", "always", {"exceptAfterSingleLine": true}],
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/semi": ["warn"],
"@typescript-eslint/member-delimiter-style": ["warn"]
}
}

View File

@@ -1,44 +0,0 @@
---
name: Bug Report
about: Create a report to help us improve
title: ''
labels: Bug
assignees: ''
---
<!-- You must use the issue template below when submitting a bug -->
**Describe The Bug:**
<!-- A clear and concise description of what the bug is. -->
**To Reproduce:**
<!-- Steps to reproduce the behavior. -->
**Expected behavior:**
<!-- A clear and concise description of what you expected to happen. -->
**Logs:**
```
Show the Homebridge logs here, remove any sensitive information.
```
**Plugin Config:**
```json
Show your Homebridge config.json here, remove any sensitive information.
```
**Screenshots:**
<!-- If applicable, add screenshots to help explain your problem. -->
**Environment:**
* **Plugin Version**:
* **Homebridge Version**: <!-- homebridge -V -->
* **Node.js Version**: <!-- node -v -->
* **NPM Version**: <!-- npm -v -->
* **Operating System**: <!-- Raspbian / Ubuntu / Debian / Windows / macOS / Docker / hb-service -->
<!-- Click the "Preview" tab before you submit to ensure the formatting is correct. -->

View File

@@ -1 +0,0 @@
blank_issues_enabled: false

View File

@@ -1,23 +0,0 @@
---
name: Feature Request
about: Suggest an idea for this project
title: ''
labels: 'New Feature'
assignees: ''
---
**Is your feature request related to a problem? Please describe:**
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
**Describe the solution you'd like:**
<!-- A clear and concise description of what you want to happen. -->
**Describe alternatives you've considered:**
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
**Additional context:**
<!-- Add any other context or screenshots about the feature request here. -->
<!-- Click the "Preview" tab before you submit to ensure the formatting is correct. -->

View File

@@ -1,38 +0,0 @@
---
name: Support Request
about: Need help?
title: ''
labels: Question
assignees: ''
---
<!-- You must use the issue template below when submitting a support request -->
**Describe Your Problem:**
<!-- A clear and concise description of what problem you are trying to solve. -->
**Logs:**
```
Show the Homebridge logs here, remove any sensitive information.
```
**Plugin Config:**
```json
Show your Homebridge config.json here, remove any sensitive information.
```
**Screenshots:**
<!-- If applicable, add screenshots to help explain your problem. -->
**Environment:**
* **Plugin Version**:
* **Homebridge Version**: <!-- homebridge -V -->
* **Node.js Version**: <!-- node -v -->
* **NPM Version**: <!-- npm -v -->
* **Operating System**: <!-- Raspbian / Ubuntu / Debian / Windows / macOS / Docker / hb-service -->
<!-- Click the "Preview" tab before you submit to ensure the formatting is correct. -->

119
.gitignore vendored
View File

@@ -1,120 +1,75 @@
# Ignore compiled code
dist
# ------------- Defaults ------------- #
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
node_modules
jspm_packages
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# See http://help.github.com/ignore-files/ for more about ignoring files.
# Output of 'npm pack'
*.tgz
# compiled output
/dist
/dist-server
/dist-e2e
/tmp
/out-tsc
# Yarn Integrity file
.yarn-integrity
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
*.iml
# dotenv environment variables file
.env
.env.test
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# misc
/.sass-cache
/connect.lock
/libpeerconnection.log
yarn-error.log
testem.log
/typings
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.pnp.*
# System Files
.DS_Store
Thumbs.db
package-lock.json

View File

@@ -1,135 +0,0 @@
# Ignore source code
src
# ------------- Defaults ------------- #
# gitHub actions
.github
# eslint
.eslintrc
# typescript
tsconfig.json
# vscode
.vscode
# nodemon
nodemon.json
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.pnp.*

View File

@@ -119,20 +119,4 @@
* Fixed homebridge crash with multiple robots per account
## 0.7.3
* Fixed warnings since homebridge 1.3.0
## 1.0.0-beta.4
* Added bin full sensor
* Added config-ui support for all options
* Added config parameter **prefix** to use robot name as prefix for service names
* Retrying mechanism if a robot is not available on homebridge launch
* Changed service names to not include robot name as prefix by default
* Changed background update to use better default intervals (1 minute while cleaning, 30 minutes while idle)
* Changed config parameter **refresh**. Renamed to **backgroundUpdate**, unit changed to minute and will only be used during idle
* Changed config parameter **hidden**. Renamed to **services**, now takes list of services that should be _visible_. Default are all available services.
* Fixed robots no longer disappear or change the room after connection issues with the Neato API
* Fixed plugin no longer crashes if non smart robot is assigned in neato account
* Fixed options for eco, nogo lines, extra care, spot repeat, spot size are now saved in homebridge and will no longer be overridden by Neato API
## TODO until 1.0.0 release
* Room cleaning
* Fixed warnings since homebridge 1.3.0

View File

@@ -31,6 +31,7 @@ Feel free to leave any feedback [here](https://github.com/naofireblade/homebridg
- Model and firmware version
- Automatic or periodic refresh of robot state
- Multiple robots
- Multiple language support (en, de, fr)
> <b name="d7">1</b> Only available on the Neato D7.
@@ -58,7 +59,8 @@ Add the following information to your config file. Change the values for email a
{
"platform": "NeatoVacuumRobot",
"email": "YourEmail",
"password": "YourPassword"
"password": "YourPassword",
"language": "de"
}
]
```
@@ -82,6 +84,7 @@ List of plugin features that you don't want to use in homekit (e.g. `dock`, `doc
"platform": "NeatoVacuumRobot",
"email": "YourEmail",
"password": "YourPassword",
"language": "de",
"refresh": "120",
"hidden": ["dock", "dockstate", "eco", "nogolines", "extracare", "schedule", "find", "spot"]
}

View File

@@ -0,0 +1,724 @@
const debug = require('debug')('homebridge-neato');
const colors = require('colors');
const CustomUUID = {
SpotCleanWidth: 'A7889A9A-2F27-4293-BEF8-3FE805B36F4E',
SpotCleanHeight: 'CA282DB2-62BF-4325-A1BE-F8BB5478781A',
SpotCleanRepeat: '1E79C603-63B8-4E6A-9CE1-D31D67981831'
};
let Service,
Characteristic,
SpotWidthCharacteristic,
SpotHeightCharacteristic,
SpotRepeatCharacteristic;
module.exports = function (_Service, _Characteristic)
{
Service = _Service;
Characteristic = _Characteristic;
SpotWidthCharacteristic = require('../characteristics/spotWidth')(Characteristic, CustomUUID);
SpotHeightCharacteristic = require('../characteristics/spotHeight')(Characteristic, CustomUUID);
SpotRepeatCharacteristic = require('../characteristics/spotRepeat')(Characteristic, CustomUUID);
return NeatoVacuumRobotAccessory;
};
function NeatoVacuumRobotAccessory(platform, robotObject)
{
this.platform = platform;
this.log = platform.log;
this.refresh = platform.refresh;
this.hiddenServices = platform.hiddenServices;
this.nextRoom = platform.nextRoom;
this.robotObject = robotObject;
this.robot = robotObject.device;
this.meta = robotObject.meta;
this.spotPlusFeatures = ((typeof robotObject.availableServices.spotCleaning !== 'undefined') && robotObject.availableServices.spotCleaning.includes("basic"));
this.boundary = (typeof robotObject.boundary === 'undefined') ? null : robotObject.boundary;
this.dict = {
'en': {
"clean": "Clean",
"clean the": "Clean the",
"goToDock": "Go to Dock",
"dockState": "Dock",
"eco": "Eco Mode",
"noGoLines": "NoGo Lines",
"extraCare": "Extra Care",
"schedule": "Schedule",
"findMe": "Find me",
"cleanSpot": "Clean Spot",
"battery": "Battery"
},
'de': {
"clean": "Sauge",
"clean the": "Sauge",
"goToDock": "Zur Basis",
"dockState": "In der Basis",
"eco": "Eco Modus",
"noGoLines": "NoGo Linien",
"extraCare": "Extra Care",
"schedule": "Zeitplan",
"findMe": "Finde mich",
"cleanSpot": "Spot Reinigung",
"battery": "Batterie"
},
'fr': {
"clean": "Aspirer",
"clean the": "Aspirer",
"goToDock": "Retour à la base",
"dockState": "Sur la base",
"eco": "Eco mode",
"noGoLines": "Lignes NoGo",
"extraCare": "Extra Care",
"schedule": "Planifier",
"findMe": "Me retrouver",
"cleanSpot": "Nettoyage local",
"battery": "Batterie"
}
}[this.platform.language]
if (this.boundary == null)
{
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.batteryService = new Service.BatteryService(this.name + " " + this.dict["battery"], "battery");
if (this.boundary == null)
{
this.cleanService = new Service.Switch(this.name + " " + this.dict["clean"], "clean");
this.goToDockService = new Service.Switch(this.name + " " + this.dict["goToDock"], "goToDock");
this.dockStateService = new Service.OccupancySensor(this.name + " " + this.dict["dockState"], "dockState");
this.ecoService = new Service.Switch(this.name + " " + this.dict["eco"], "eco");
this.noGoLinesService = new Service.Switch(this.name + " " + this.dict["noGoLines"], "noGoLines");
this.extraCareService = new Service.Switch(this.name + " " + this.dict["extraCare"], "extraCare");
this.scheduleService = new Service.Switch(this.name + " " + this.dict["schedule"], "schedule");
this.findMeService = new Service.Switch(this.name + " " + this.dict["findMe"], "findMe");
this.spotCleanService = new Service.Switch(this.name + " " + this.dict["cleanSpot"], "cleanSpot");
this.spotCleanService.addCharacteristic(SpotRepeatCharacteristic);
if (this.spotPlusFeatures)
{
this.spotCleanService.addCharacteristic(SpotWidthCharacteristic);
this.spotCleanService.addCharacteristic(SpotHeightCharacteristic);
}
}
else
{
const splitName = this.boundary.name.split(' ');
let serviceName = this.dict["clean the"] + " " + this.boundary.name;
if (splitName.length >= 2 && splitName[splitName.length - 2].match(/[']s$/g))
{
serviceName = this.dict["clean"] + " " + this.boundary.name;
}
this.cleanService = new Service.Switch(serviceName, "cleanBoundary:" + this.boundary.id);
}
this.log("Added cleaning device named: " + this.name);
}
NeatoVacuumRobotAccessory.prototype = {
identify: function (callback)
{
this.robot.getState((error, result) =>
{
if (error)
{
this.log.error("Error getting robot information: " + error + ": " + result);
}
else
{
this.log("### Robot information ###");
this.log(result);
}
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)
.setCharacteristic(Characteristic.Name, this.robot.name + (this.boundary == null ? '' : ' - ' + this.boundary.name));
this.cleanService.getCharacteristic(Characteristic.On).on('set', this.setClean.bind(this));
this.cleanService.getCharacteristic(Characteristic.On).on('get', this.getClean.bind(this));
this.services = [this.informationService, this.cleanService];
if (this.boundary == null)
{
this.batteryService.getCharacteristic(Characteristic.BatteryLevel).on('get', this.getBatteryLevel.bind(this));
this.batteryService.getCharacteristic(Characteristic.ChargingState).on('get', this.getBatteryChargingState.bind(this));
this.services.push(this.batteryService);
this.goToDockService.getCharacteristic(Characteristic.On).on('set', this.setGoToDock.bind(this));
this.goToDockService.getCharacteristic(Characteristic.On).on('get', this.getGoToDock.bind(this));
this.dockStateService.getCharacteristic(Characteristic.OccupancyDetected).on('get', this.getDock.bind(this));
this.ecoService.getCharacteristic(Characteristic.On).on('set', this.setEco.bind(this));
this.ecoService.getCharacteristic(Characteristic.On).on('get', this.getEco.bind(this));
this.noGoLinesService.getCharacteristic(Characteristic.On).on('set', this.setNoGoLines.bind(this));
this.noGoLinesService.getCharacteristic(Characteristic.On).on('get', this.getNoGoLines.bind(this));
this.extraCareService.getCharacteristic(Characteristic.On).on('set', this.setExtraCare.bind(this));
this.extraCareService.getCharacteristic(Characteristic.On).on('get', this.getExtraCare.bind(this));
this.scheduleService.getCharacteristic(Characteristic.On).on('set', this.setSchedule.bind(this));
this.scheduleService.getCharacteristic(Characteristic.On).on('get', this.getSchedule.bind(this));
this.findMeService.getCharacteristic(Characteristic.On).on('set', this.setFindMe.bind(this));
this.findMeService.getCharacteristic(Characteristic.On).on('get', this.getFindMe.bind(this));
this.spotCleanService.getCharacteristic(Characteristic.On).on('set', this.setSpotClean.bind(this));
this.spotCleanService.getCharacteristic(Characteristic.On).on('get', this.getSpotClean.bind(this));
this.spotCleanService.getCharacteristic(SpotRepeatCharacteristic).on('set', this.setSpotRepeat.bind(this));
this.spotCleanService.getCharacteristic(SpotRepeatCharacteristic).on('get', this.getSpotRepeat.bind(this));
if (this.spotPlusFeatures)
{
this.spotCleanService.getCharacteristic(SpotWidthCharacteristic).on('set', this.setSpotWidth.bind(this));
this.spotCleanService.getCharacteristic(SpotWidthCharacteristic).on('get', this.getSpotWidth.bind(this));
this.spotCleanService.getCharacteristic(SpotHeightCharacteristic).on('set', this.setSpotHeight.bind(this));
this.spotCleanService.getCharacteristic(SpotHeightCharacteristic).on('get', this.getSpotHeight.bind(this));
}
if (this.hiddenServices.indexOf('spot') === -1)
{
this.services.push(this.spotCleanService);
}
// Add optional services
if (this.hiddenServices.indexOf('dock') === -1)
this.services.push(this.goToDockService);
if (this.hiddenServices.indexOf('dockstate') === -1)
this.services.push(this.dockStateService);
if (this.hiddenServices.indexOf('eco') === -1)
this.services.push(this.ecoService);
if (this.hiddenServices.indexOf('nogolines') === -1)
this.services.push(this.noGoLinesService);
if (this.hiddenServices.indexOf('extracare') === -1)
this.services.push(this.extraCareService);
if (this.hiddenServices.indexOf('schedule') === -1)
this.services.push(this.scheduleService);
if (this.hiddenServices.indexOf('find') === -1)
this.services.push(this.findMeService);
}
return this.services;
},
getClean: function (callback)
{
this.platform.updateRobot(this.robot._serial, (error, result) =>
{
let cleaning;
if (this.boundary == null)
{
cleaning = this.robot.canPause;
}
else
{
cleaning = this.robot.canPause && (this.robot.cleaningBoundaryId === this.boundary.id)
}
debug(this.name + ": Cleaning is " + (cleaning ? 'ON'.brightGreen : 'OFF'.red));
callback(false, cleaning);
});
},
setClean: function (on, callback)
{
debug(this.name + ": " + (on ? "Enabled ".brightGreen : "Disabled".red) + " Clean " + (this.boundary ? JSON.stringify(this.boundary) : ''));
this.platform.updateRobot(this.robot._serial, (error, result) =>
{
// Start
if (on)
{
// No room given or same room
if (this.boundary == null || this.robot.cleaningBoundaryId === this.boundary.id)
{
// Resume cleaning
if (this.robot.canResume)
{
debug(this.name + ": ## Resume cleaning");
this.robot.resumeCleaning((error) =>
{
callback(error);
});
}
// Start cleaning
else if (this.robot.canStart)
{
this.clean(callback);
}
// Cannot start
else
{
debug(this.name + ": Cannot start, maybe already cleaning (expected)");
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 = this.boundary.id;
callback();
});
}
// Start new cleaning of new room
else
{
debug(this.name + ": ## Start cleaning of new room");
this.clean(callback);
}
}
}
// Stop
else
{
if (this.robot.canPause)
{
debug(this.name + ": ## Pause cleaning");
this.robot.pauseCleaning((error) =>
{
callback(error);
});
}
else
{
debug(this.name + ": Already paused");
callback();
}
}
});
},
clean: function (callback, spot)
{
// Start automatic update while cleaning
if (this.refresh === 'auto')
{
setTimeout(() =>
{
this.platform.updateRobotTimer(this.robot._serial);
}, 60 * 1000);
}
let eco = this.robotObject.mainAccessory.ecoService.getCharacteristic(Characteristic.On).value;
let extraCare = this.robotObject.mainAccessory.extraCareService.getCharacteristic(Characteristic.On).value;
let nogoLines = this.robotObject.mainAccessory.noGoLinesService.getCharacteristic(Characteristic.On).value;
let room = (this.boundary == null) ? '' : this.boundary.name;
debug(this.name + ": ## Start cleaning (" + (room !== '' ? room + " " : '') + "eco: " + eco + ", extraCare: " + extraCare + ", nogoLines: " + nogoLines + ", spot: " + JSON.stringify(spot) + ")");
// Normal cleaning
if (this.boundary == null && (typeof spot === 'undefined'))
{
this.robot.startCleaning(eco, extraCare ? 2 : 1, nogoLines, (error, result) =>
{
if (error)
{
this.log.error("Cannot start cleaning. " + error + ": " + JSON.stringify(result));
}
callback(error);
});
}
// Room cleaning
else if (room !== '')
{
this.robot.startCleaningBoundary(eco, extraCare, this.boundary.id, (error, result) =>
{
if (error)
{
this.log.error("Cannot start room cleaning. " + error + ": " + JSON.stringify(result));
}
callback(error);
});
}
// Spot cleaning
else
{
this.robot.startSpotCleaning(eco, spot.width, spot.height, spot.repeat, extraCare ? 2 : 1, (error, result) =>
{
if (error)
{
this.log.error("Cannot start spot cleaning. " + error + ": " + JSON.stringify(result));
}
callback(error);
});
}
},
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'.brightGreen : 'OFF'.red));
callback(false, this.robot.eco);
});
},
setEco: function (on, callback)
{
this.robot.eco = on;
debug(this.name + ": " + (on ? "Enabled ".red : "Disabled".red) + " Eco Mode ");
callback();
},
getNoGoLines: function (callback)
{
this.platform.updateRobot(this.robot._serial, () =>
{
debug(this.name + ": NoGoLine is " + (this.robot.eco ? 'ON'.brightGreen : 'OFF'.red));
callback(false, this.robot.noGoLines ? 1 : 0);
});
},
setNoGoLines: function (on, callback)
{
this.robot.noGoLines = on;
debug(this.name + ": " + (on ? "Enabled ".brightGreen : "Disabled".red) + " NoGoLine ");
callback();
},
getExtraCare: function (callback)
{
this.platform.updateRobot(this.robot._serial, () =>
{
debug(this.name + ": Care Nav is " + (this.robot.navigationMode === 2 ? 'ON'.brightGreen : 'OFF'.red));
callback(false, this.robot.navigationMode === 2 ? 1 : 0);
});
},
setExtraCare: function (on, callback)
{
this.robot.navigationMode = on ? 2 : 1;
debug(this.name + ": " + (on ? "Enabled ".brightGreen : "Disabled".red) + " Care Nav ");
callback();
},
getSchedule: function (callback)
{
this.platform.updateRobot(this.robot._serial, () =>
{
debug(this.name + ": Schedule is " + (this.robot.eco ? 'ON'.brightGreen : 'OFF'.red));
callback(false, this.robot.isScheduleEnabled);
});
},
setSchedule: function (on, callback)
{
this.platform.updateRobot(this.robot._serial, (error, result) =>
{
if (on)
{
debug(this.name + ": " + "Enabled".brightGreen + " Schedule");
this.robot.enableSchedule((error) =>
{
callback(error);
});
}
else
{
debug(this.name + ": " + "Disabled".red + " Schedule");
this.robot.disableSchedule((error) =>
{
callback(error);
});
}
});
},
getFindMe: function (callback)
{
callback(false, false);
},
setFindMe: function (on, callback)
{
if (on)
{
debug(this.name + ": ## Find me");
setTimeout(() =>
{
this.findMeService.setCharacteristic(Characteristic.On, false);
}, 1000);
this.robot.findMe((error) =>
{
callback(error);
});
}
},
getSpotClean: function (callback)
{
callback(false, this.spotCleanService.getCharacteristic(Characteristic.On).value);
},
setSpotClean: function (on, callback)
{
let spot = {
width: this.spotPlusFeatures ? this.spotCleanService.getCharacteristic(SpotWidthCharacteristic).value : null,
height: this.spotPlusFeatures ? this.spotCleanService.getCharacteristic(SpotHeightCharacteristic).value : null,
repeat: this.spotCleanService.getCharacteristic(SpotRepeatCharacteristic).value
};
this.platform.updateRobot(this.robot._serial, (error, result) =>
{
// Start
if (on)
{
// Resume cleaning
if (this.robot.canResume)
{
debug(this.name + ": ## Resume (spot) cleaning");
this.robot.resumeCleaning(callback);
}
// Start cleaning
else if (this.robot.canStart)
{
this.clean(callback, spot);
}
// Cannot start
else
{
debug(this.name + ": Cannot start spot cleaning, maybe already cleaning");
callback();
}
}
// Stop
else
{
if (this.robot.canPause)
{
debug(this.name + ": ## Pause cleaning");
this.robot.pauseCleaning((error) =>
{
callback(error);
});
}
else
{
debug(this.name + ": Already paused");
callback();
}
}
});
},
getSpotWidth: function (callback)
{
this.platform.updateRobot(this.robot._serial, () =>
{
debug(this.name + ": Spot width is " + this.robot.spotWidth + "cm");
callback(false, this.robot.spotWidth);
});
},
setSpotWidth: function (width, callback)
{
this.robot.spotWidth = width;
debug(this.name + ": Set spot width to " + width + "cm");
callback();
},
getSpotHeight: function (callback)
{
this.platform.updateRobot(this.robot._serial, () =>
{
debug(this.name + ": Spot height is " + this.robot.spotHeight + "cm");
callback(false, this.robot.spotHeight);
});
},
setSpotHeight: function (height, callback)
{
this.robot.spotHeight = height;
debug(this.name + ": Set spot height to " + height + "cm");
callback();
},
getSpotRepeat: function (callback)
{
this.platform.updateRobot(this.robot._serial, () =>
{
debug(this.name + ": Spot repeat is " + (this.robot.spotRepeat ? 'ON'.brightGreen : 'OFF'.red));
callback(false, this.robot.spotRepeat);
});
},
setSpotRepeat: function (on, callback)
{
this.robot.spotRepeat = on;
debug(this.name + ": " + (on ? "Enabled ".brightGreen : "Disabled".red) + " Spot repeat");
callback();
},
getDock: function (callback)
{
this.platform.updateRobot(this.robot._serial, () =>
{
debug(this.name + ": The Dock is " + (this.robot.isDocked ? "OCCUPIED".brightGreen : "NOT OCCUPIED".red));
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 ? "CHARGING".brightGreen : "NOT CHARGING".red));
callback(false, this.robot.isCharging);
});
},
updated: function ()
{
if (this.boundary == null)
{
// only update these values if the state is different from the current one, otherwise we might accidentally start an action
if (this.cleanService.getCharacteristic(Characteristic.On).value !== this.robot.canPause)
{
this.cleanService.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.goToDockService.getCharacteristic(Characteristic.On).value == true && this.robot.dockHasBeenSeen)
{
this.goToDockService.setCharacteristic(Characteristic.On, false);
}
if (this.scheduleService.getCharacteristic(Characteristic.On).value !== this.robot.isScheduleEnabled)
{
this.scheduleService.setCharacteristic(Characteristic.On, this.robot.isScheduleEnabled);
}
// no commands here, values can be updated without problems
this.dockStateService.setCharacteristic(Characteristic.OccupancyDetected, this.robot.isDocked ? 1 : 0);
this.ecoService.setCharacteristic(Characteristic.On, this.robot.eco);
this.noGoLinesService.setCharacteristic(Characteristic.On, this.robot.noGoLines);
this.extraCareService.setCharacteristic(Characteristic.On, this.robot.navigationMode == 2 ? true : false);
this.spotCleanService.setCharacteristic(SpotRepeatCharacteristic, this.robot.spotRepeat);
if (this.spotPlusFeatures)
{
let widthProps = this.spotCleanService.getCharacteristic(SpotWidthCharacteristic).props;
let heightProps = this.spotCleanService.getCharacteristic(SpotHeightCharacteristic).props;
this.spotCleanService.setCharacteristic(SpotWidthCharacteristic,
this.robot.spotWidth >= widthProps.minValue && this.robot.spotWidth <= widthProps.maxValue ? this.robot.spotWidth : widthProps.minValue);
this.spotCleanService.setCharacteristic(SpotHeightCharacteristic,
this.robot.spotHeight >= heightProps.minValue && this.robot.spotHeight <= heightProps.maxValue ? this.robot.spotHeight : heightProps.minValue);
}
}
this.batteryService.setCharacteristic(Characteristic.BatteryLevel, this.robot.charge);
this.batteryService.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");
});
}
}
};

View File

@@ -0,0 +1,21 @@
const inherits = require('util').inherits;
module.exports = function (Characteristic, CustomUUID)
{
let SpotHeight = function ()
{
Characteristic.call(this, 'Spot ↕', CustomUUID.SpotCleanHeight);
this.setProps({
format: Characteristic.Formats.INT,
unit: 'cm',
maxValue: 400,
minValue: 100,
minStep: 50,
perms: [Characteristic.Perms.READ, Characteristic.Perms.WRITE]
});
this.value = this.getDefaultValue();
};
inherits(SpotHeight, Characteristic);
return SpotHeight;
};

View File

@@ -0,0 +1,17 @@
const inherits = require('util').inherits;
module.exports = function (Characteristic, CustomUUID)
{
let SpotRepeat = function ()
{
Characteristic.call(this, 'Spot 2x', CustomUUID.SpotCleanRepeat);
this.setProps({
format: Characteristic.Formats.BOOL,
perms: [Characteristic.Perms.READ, Characteristic.Perms.WRITE]
});
this.value = this.getDefaultValue();
};
inherits(SpotRepeat, Characteristic);
return SpotRepeat;
};

View File

@@ -0,0 +1,21 @@
const inherits = require('util').inherits;
module.exports = function (Characteristic, CustomUUID)
{
let SpotWidth = function ()
{
Characteristic.call(this, 'Spot ↔', CustomUUID.SpotCleanWidth);
this.setProps({
format: Characteristic.Formats.INT,
unit: 'cm',
maxValue: 400,
minValue: 100,
minStep: 50,
perms: [Characteristic.Perms.READ, Characteristic.Perms.WRITE]
});
this.value = this.getDefaultValue();
};
inherits(SpotWidth, Characteristic);
return SpotWidth;
};

View File

@@ -1,148 +1,49 @@
{
"pluginAlias": "NeatoVacuumRobot",
"pluginType": "platform",
"singular": true,
"headerDisplay": "",
"schema": {
"type": "object",
"properties": {
"email": {
"title": "E-Mail",
"type": "string",
"required": true,
"format": "email"
},
"password": {
"title": "Password",
"type": "string",
"required": true
},
"language": {
"title": "Services Language",
"description": "The displayed language of the registered services (and associated Siri commands)",
"type": "string",
"default": "en",
"oneOf": [
{
"title": "English",
"enum": ["en"]
},
{
"title": "German",
"enum": ["de"]
},
{
"title": "French",
"enum": ["fr"]
}
],
"required": true
},
"prefix": {
"title": "Prefix with Robot Name",
"description": "Display the name of the robot in front of every service.",
"type": "boolean",
"default": false
},
"backgroundUpdate": {
"title": "Background Update Interval",
"description": "Interval for background updates while the robot is not cleaning (in minutes). During cleaning, the robot will automatically update at a faster rate.",
"type": "integer",
"minimum": 1,
"default": 30
},
"services": {
"type": "array",
"title": "Displayed Services",
"description": "The services to be made available for Homekit",
"uniqueItems": true,
"items": {
"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",
"enum": [
"clean",
"cleanZone",
"cleanSpot",
"goToDock",
"dockState",
"binFull",
"eco",
"noGoLines",
"extraCare",
"schedule",
"findMe",
"battery"
],
"enumNames": [
"Clean",
"Clean Zone",
"Clean Spot",
"Go to Dock",
"Docked State",
"Bin Full",
"Eco Mode",
"NoGo Lines",
"Extra Care",
"Schedule",
"Find me",
"Battery"
]
"required": true,
"format": "email",
"description": "Your Email Address"
},
"default": [
"clean",
"cleanZone",
"goToDock",
"dockState",
"binFull",
"eco",
"noGoLines",
"extraCare",
"schedule",
"findMe",
"cleanSpot",
"battery"
]
"password": {
"title": "password",
"type": "string",
"required": true,
"description": "Your Password"
},
"language": {
"title": "language",
"type": "string",
"default": "en",
"oneOf": [
{
"title": "English",
"enum": [
"en"
]
},
{
"title": "German",
"enum": [
"de"
]
},
{
"title": "French",
"enum": [
"fr"
]
}
],
"required": true
}
}
}
},
"layout": [
{
"type": "help",
"helpvalue": "<h4>Login</h4>"
},
{
"type": "help",
"helpvalue": "Enter the credentials of your Neato app. If you don't have a neato account yet, register <a target='_blank' href='https://www.neatorobotics.com/create-account/'>here</a>"
},
{
"type": "flex",
"flex-flow": "row wrap",
"items": [
"email",
{
"key": "password",
"type": "password"
}
]
},
{
"type": "help",
"helpvalue": "<h4>Options</h4>"
},
"backgroundUpdate",
"prefix",
"language",
{
"type": "fieldset",
"title": "Displayed Services",
"description": "<i>Services to be displayed in Homekit</i>",
"expandable": true,
"items": [
{
"key": "services",
"notitle": true
}
]
}
]
}
}

View File

@@ -1,9 +0,0 @@
<?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>

306
index.js Normal file
View File

@@ -0,0 +1,306 @@
"use strict";
let inherits = require('util').inherits,
debug = require('debug')('homebridge-neato'),
botvac = require('node-botvac'),
Service,
Characteristic,
NeatoVacuumRobotAccessory;
module.exports = function (homebridge)
{
Service = homebridge.hap.Service;
Characteristic = homebridge.hap.Characteristic;
NeatoVacuumRobotAccessory = require('./accessories/neatoVacuumRobot')(Service, Characteristic);
homebridge.registerPlatform("homebridge-neato", "NeatoVacuumRobot", NeatoVacuumRobotPlatform);
};
function NeatoVacuumRobotPlatform(log, config)
{
this.log = log;
this.serial = "1-3-3-7";
this.email = config['email'];
this.password = config['password'];
this.language = config['language'];
this.hiddenServices = '';
this.hiddenServices = ('disabled' in config ? config['disabled'] : this.hiddenServices);
this.hiddenServices = ('hidden' in config ? config['hidden'] : this.hiddenServices);
// Array of real robots and associated robot accessories (incl rooms)
this.robots = [];
this.nextRoom = null;
if ('refresh' in config && config['refresh'] !== 'auto')
{
// parse config parameter
this.refresh = parseInt(config['refresh']);
// must be integer and positive
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
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
else
{
this.refresh = 'auto';
}
this.log("Refresh is set to: " + this.refresh + (this.refresh !== 'auto' ? ' seconds' : ''));
}
NeatoVacuumRobotPlatform.prototype = {
accessories: function (callback)
{
debug("Get robots");
let accessories = [];
this.boundaryNames = [];
this.getRobots(() =>
{
// // MOCK MULTIPLE ROBOTS START
// let client = new botvac.Client();
// client.authorize(this.email, this.password, false, (error) =>
// {
// client.getRobots((error, robs) =>
// {
// let testRobot = robs[0];
// testRobot.getState((error, result) =>
// {
// testRobot.name = "Testrobot";
// this.robots.push({device: testRobot, meta: result.meta, availableServices: result.availableServices});
// // MOCK MULTIPLE ROBOTS END
this.robots.forEach((robot, i) =>
{
this.log("Found robot #" + (i + 1) + " named \"" + robot.device.name + "\" with serial \"" + robot.device._serial.substring(0, 9) + "XXXXXXXXXXXX\"");
let mainAccessory = new NeatoVacuumRobotAccessory(this, robot);
accessories.push(mainAccessory);
robot.mainAccessory = mainAccessory;
robot.roomAccessories = [];
// Start Update Intervall
this.updateRobotTimer(robot.device._serial);
// // MOCK ZONE CLEANING START
// robot.boundary = {name: "Testroom", id: "1"};
// let roomAccessory = new NeatoVacuumRobotAccessory(this, robot);
// accessories.push(roomAccessory);
// robot.roomAccessories.push(roomAccessory);
// // MOCK ZONE CLEANING END
if (robot.device.maps)
{
robot.device.maps.forEach((map) =>
{
if (map.boundaries)
{
map.boundaries.forEach((boundary) =>
{
if (boundary.type === "polygon")
{
robot.boundary = boundary;
let roomAccessory = new NeatoVacuumRobotAccessory(this, robot);
accessories.push(roomAccessory);
robot.roomAccessories.push(roomAccessory);
}
})
}
})
}
});
callback(accessories);
// // MOCK MULTIPLE ROBOTS START
// });
// });
// });
// // MOCK MULTIPLE ROBOTS END
});
},
getRobots: function (callback)
{
debug("Loading your robots");
let client = new botvac.Client();
// Login
client.authorize(this.email, this.password, false, (error) =>
{
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();
}
else
{
// Get all robots
client.getRobots((error, robots) =>
{
if (error)
{
this.log.error("Successful login but can't connect to your neato robot: " + error);
callback();
}
else if (robots.length === 0)
{
this.log.error("Successful login but no robots associated with your account.");
this.robots = [];
callback();
}
else
{
debug("Found " + robots.length + " robots");
let loadedRobots = 0;
robots.forEach((robot) =>
{
// Get additional information for the robot
robot.getState((error, state) =>
{
if (error)
{
this.log.error("Error getting robot meta information: " + error + ": " + state);
callback();
}
else
{
// 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.robots.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.robots.push({device: robot, meta: state.meta, availableServices: state.availableServices});
loadedRobots++;
if (loadedRobots === robots.length)
{
callback();
}
}
})
});
}
});
}
});
});
}
});
}
});
},
updateRobot: function (serial, callback)
{
let robot = this.getRobot(serial);
// Data is up to date
if (typeof (robot.lastUpdate) !== 'undefined' && new Date() - robot.lastUpdate < 2000)
{
callback();
}
else
{
debug(robot.device.name + ": ++ Updating robot state");
robot.lastUpdate = new Date();
robot.device.getState((error, result) =>
{
if (error)
{
this.log.error("Cannot update robot. Check if robot is online. " + error);
}
callback();
});
}
},
getRobot(serial)
{
let result;
this.robots.forEach(function (robot)
{
if (robot.device._serial === serial)
{
result = robot;
}
});
return result;
},
updateRobotTimer: function (serial)
{
this.updateRobot(serial, () =>
{
let robot = this.getRobot(serial);
// Clear any other overlapping timers for this robot
clearTimeout(robot.timer);
// Tell all accessories of this robot (mainAccessory and roomAccessories) that updated robot data is available
robot.mainAccessory.updated();
robot.roomAccessories.forEach(accessory =>
{
accessory.updated();
});
// Periodic refresh interval set in config
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
else if (this.refresh === 'auto' && robot.device.canPause)
{
debug(robot.device.name + ": ++ Next background update in 60 seconds while cleaning (auto mode)");
robot.timer = setTimeout(this.updateRobotTimer.bind(this), 60 * 1000, serial);
}
// No refresh
else
{
debug(robot.device.name + ": ++ Stopped background updates");
}
});
},
};

View File

@@ -1,12 +0,0 @@
{
"watch": [
"src"
],
"ext": "ts",
"ignore": [],
"exec": "tsc && homebridge -I -D",
"signal": "SIGTERM",
"env": {
"NODE_OPTIONS": "--trace-warnings"
}
}

2841
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,6 @@
{
"name": "homebridge-neato",
"displayName": "Homebridge Neato",
"version": "1.0.0-beta.5",
"version": "0.7.3",
"description": "A Neato vacuum robot plugin for homebridge.",
"license": "MIT",
"keywords": [
@@ -10,14 +9,8 @@
"botvac"
],
"engines": {
"node": ">=10.17.0",
"homebridge": ">=1.3.0"
},
"main": "dist/index.js",
"scripts": {
"lint": "eslint src/**.ts --max-warnings=0",
"watch": "npm run build && npm link && nodemon",
"build": "rimraf ./dist && tsc"
"node": ">=0.12.0",
"homebridge": ">=0.2.0"
},
"author": {
"name": "Arne Blumentritt",
@@ -41,34 +34,10 @@
"type": "git",
"url": "git://github.com/naofireblade/homebridge-neato.git"
},
"bugs": {
"url": "https://github.com/naofireblade/homebridge-neato/issues"
},
"dependencies": {
"colors": "^1.4.0",
"debug": "^4.1.1",
"node-botvac": "^0.4.2",
"node-botvac": ">=0.4.0",
"uuid": "^3.3.2"
},
"devDependencies": {
"@types/node": "^14.14.31",
"@typescript-eslint/eslint-plugin": "^4.16.1",
"@typescript-eslint/parser": "^4.16.1",
"eslint": "^7.21.0",
"homebridge": "^1.3.1",
"nodemon": "^2.0.7",
"rimraf": "^3.0.2",
"ts-node": "^9.1.1",
"typescript": "^4.2.2"
},
"funding": [
{
"type": "buymeacoffee",
"url": "https://buymeacoffee.com/naofireblade"
},
{
"type": "paypal",
"url": "https://paypal.me/ArneBlumentritt"
}
]
}
}

View File

@@ -1,707 +0,0 @@
import {CharacteristicValue, Logger, PlatformAccessory, PlatformAccessoryEvent, PlatformConfig, Service, WithUUID, CharacteristicGetHandler, CharacteristicSetHandler, Characteristic} from 'homebridge';
import {HomebridgeNeatoPlatform} from '../homebridgeNeatoPlatform';
import spotRepeat from '../characteristics/spotRepeat';
import spotWidth from '../characteristics/spotWidth';
import spotHeight from '../characteristics/spotHeight';
import {Options} from '../models/options';
import { RobotService, CleanType } from '../models/services';
import { ALL_SERVICES, BACKGROUND_INTERVAL, LOCALE, PREFIX } from '../defaults';
import { availableLocales, localize } from '../localization';
import { CharacteristicHandler } from '../characteristics/characteristicHandler';
/**
* 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 NeatoVacuumRobotAccessory
{
// Homebridge
private log: Logger;
private readonly batteryService?: Service;
private readonly cleanService?: Service;
private readonly findMeService?: Service;
private readonly goToDockService?: Service;
private readonly dockStateService?: Service;
private readonly binFullService?: Service;
private readonly ecoService?: Service;
private readonly noGoLinesService?: Service;
private readonly extraCareService?: Service;
private readonly scheduleService?: Service;
private readonly spotCleanService?: Service;
private spotPlusFeatures: boolean;
// Context
private robot: any;
private readonly options: Options;
// Config
private readonly backgroundUpdateInterval: number;
private readonly locale: availableLocales;
private readonly prefix: boolean;
private readonly availableServices: Set<RobotService>;
// 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: HomebridgeNeatoPlatform,
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.spotPlusFeatures = false;
this.backgroundUpdateInterval = NeatoVacuumRobotAccessory.parseBackgroundUpdateInterval(this.config['backgroundUpdate']);
this.prefix = this.config['prefix'] || PREFIX;
this.locale = this.config['language'] || LOCALE;
this.availableServices = new Set(this.config['services']) || ALL_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));
});
});
[
this.getClean,
this.setClean,
this.getSpotClean,
this.setSpotClean,
this.getGoToDock,
this.setGoToDock,
this.getDocked,
this.getBinFull,
this.getFindMe,
this.setFindMe,
this.getSchedule,
this.setSchedule,
this.getEco,
this.setEco,
this.getNoGoLines,
this.setNoGoLines,
this.getExtraCare,
this.setExtraCare
].forEach((f)=>f.bind(this))
// Services
this.cleanService = this.registerService(RobotService.CLEAN, this.platform.Service.Switch, [{
characteristic: this.platform.Characteristic.On,
getCharacteristicHandler: this.getClean,
setCharacteristicHandler: this.setClean
}]);
this.spotCleanService = this.registerService(RobotService.CLEAN_SPOT, this.platform.Service.Switch, [{
characteristic: this.platform.Characteristic.On,
getCharacteristicHandler: this.getSpotClean,
setCharacteristicHandler: this.setSpotClean
}]);
this.goToDockService = this.registerService(RobotService.GO_TO_DOCK, this.platform.Service.Switch, [{
characteristic: this.platform.Characteristic.On,
getCharacteristicHandler: this.getGoToDock,
setCharacteristicHandler: this.setGoToDock
}]);
this.dockStateService = this.registerService(RobotService.DOCKED, this.platform.Service.OccupancySensor, [{
characteristic: this.platform.Characteristic.OccupancyDetected.OccupancyDetected,
getCharacteristicHandler: this.getDocked,
}]);
this.binFullService = this.registerService(RobotService.BIN_FULL, this.platform.Service.OccupancySensor, [{
characteristic: this.platform.Characteristic.OccupancyDetected.OccupancyDetected,
getCharacteristicHandler: this.getBinFull,
}]);
this.findMeService = this.registerService(RobotService.FIND_ME, this.platform.Service.Switch, [{
characteristic: this.platform.Characteristic.On,
getCharacteristicHandler: this.getFindMe,
setCharacteristicHandler: this.setFindMe
}]);
this.scheduleService = this.registerService(RobotService.SCHEDULE, this.platform.Service.Switch, [{
characteristic: this.platform.Characteristic.On,
getCharacteristicHandler: this.getSchedule,
setCharacteristicHandler: this.setSchedule
}]);
this.ecoService = this.registerService(RobotService.ECO, this.platform.Service.Switch, [{
characteristic: this.platform.Characteristic.On,
getCharacteristicHandler: this.getEco,
setCharacteristicHandler: this.setEco
}]);
this.noGoLinesService = this.registerService(RobotService.NOGO_LINES, this.platform.Service.Switch, [{
characteristic: this.platform.Characteristic.On,
getCharacteristicHandler: this.getNoGoLines,
setCharacteristicHandler: this.setNoGoLines
}]);
this.extraCareService = this.registerService(RobotService.EXTRA_CARE, this.platform.Service.Switch, [{
characteristic: this.platform.Characteristic.On,
getCharacteristicHandler: this.getExtraCare,
setCharacteristicHandler: this.setExtraCare
}]);
this.batteryService = this.registerService(RobotService.BATTERY, this.platform.Service.Battery);
// This should be the main switch if the accessory is grouped in homekit
if (this.cleanService)
{
this.cleanService.setPrimaryService(true);
}
// Start background update
this.updateRobotPeriodically().then(() => {
// Add special characteristics to set spot cleaning options
this.spotPlusFeatures = ((typeof this.robot.availableServices.spotCleaning !== 'undefined') && this.robot.availableServices.spotCleaning.includes("basic"));
this.addSpotCleanCharacteristics();
// Save/Load options
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 addSpotCleanCharacteristics()
{
// Only add characteristics of service is available ond characteristics are not added yet
if (this.spotCleanService != null && !this.options.spotCharacteristics)
{
this.spotCleanService.addCharacteristic(spotRepeat(this.platform.Characteristic))
.onGet(this.getSpotRepeat.bind(this))
.onSet(this.setSpotRepeat.bind(this));
// Add these only if the robot supports them
if (this.spotPlusFeatures)
{
this.spotCleanService.addCharacteristic(spotWidth(this.platform.Characteristic))
.onGet(this.getSpotWidth.bind(this))
.onSet(this.setSpotWidth.bind(this));
this.spotCleanService.addCharacteristic(spotHeight(this.platform.Characteristic))
.onGet(this.getSpotHeight.bind(this))
.onSet(this.setSpotHeight.bind(this));
}
this.options.spotCharacteristics = true;
}
else if (this.spotCleanService == null)
{
this.options.spotCharacteristics = false;
}
}
private registerService(
serviceName: RobotService,
serviceType: WithUUID<typeof Service>,
characteristicHandlers: CharacteristicHandler[] = []
) : Service | undefined
{
const displayName = (this.prefix ? (this.robot.name + " ") : "") + localize(serviceName, this.locale);
// query existing service by type and subtype
const existingService = this.accessory.getServiceById(serviceType, serviceName)
if (this.availableServices.has(serviceName))
{
var service : Service
if (existingService && existingService.displayName === displayName) {
service = existingService
} else {
if (existingService) {this.accessory.removeService(existingService);} // delete to reset display name in case of locale or prefix change
service = this.accessory.addService(serviceType, displayName, serviceName);
}
characteristicHandlers.forEach(ch => {
var char = service.getCharacteristic(ch.characteristic)
if (ch.getCharacteristicHandler) {char.onGet(ch.getCharacteristicHandler)}
if (ch.setCharacteristicHandler) {char.onSet(ch.setCharacteristicHandler)}
});
return service
}
else
{
if (existingService)
{
this.accessory.removeService(existingService);
}
}
}
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 getClean(): 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 setClean(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;
}
getSpotRepeat()
{
return this.options.spotRepeat;
}
setSpotRepeat(on: CharacteristicValue)
{
this.debug(DebugType.STATUS, "Set SPOT REPEAT: " + on);
this.options.spotRepeat = <boolean>on;
}
getSpotWidth()
{
return this.options.spotWidth;
}
setSpotWidth(length: CharacteristicValue)
{
this.debug(DebugType.STATUS, "Set SPOT WIDTH: " + length + " cm");
this.options.spotWidth = <number>length;
}
getSpotHeight()
{
return this.options.spotHeight;
}
setSpotHeight(length: CharacteristicValue)
{
this.debug(DebugType.STATUS, "Set SPOT HEIGHT: " + length + " cm");
this.options.spotHeight = <number>length;
}
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();
}, 2 * 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.spotWidth, this.options.spotHeight, this.options.spotRepeat, 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.getClean());
}
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 DebugType
{
ACTION,
STATUS,
INFO
}

View File

@@ -1,37 +0,0 @@
// 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

@@ -1,7 +0,0 @@
import { Characteristic, CharacteristicGetHandler, CharacteristicSetHandler, WithUUID } from "homebridge";
export declare interface CharacteristicHandler{
characteristic: WithUUID<new () => Characteristic>
getCharacteristicHandler?: CharacteristicGetHandler,
setCharacteristicHandler?: CharacteristicSetHandler
}

View File

@@ -1,19 +0,0 @@
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

@@ -1,15 +0,0 @@
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

@@ -1,19 +0,0 @@
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

@@ -1,6 +0,0 @@
import { RobotService } from "./models/services";
export const BACKGROUND_INTERVAL = 30;
export const PREFIX = false;
export const ALL_SERVICES = new Set(Object.values(RobotService));
export const LOCALE = "en"

View File

@@ -1,219 +0,0 @@
import {API, Characteristic, DynamicPlatformPlugin, Logger, PlatformAccessory, PlatformConfig, Service} from "homebridge";
import NeatoApi from "node-botvac";
import {PLATFORM_NAME, PLUGIN_NAME} from "./settings";
import {NeatoVacuumRobotAccessory} from "./accessories/NeatoVacuumRobot";
/**
* 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 HomebridgeNeatoPlatform 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 NeatoApi.Client();
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;
robot.availableServices = state.availableServices;
// Update existing robot accessor
if (cachedRobot)
{
// TODO update maps
cachedRobot.context.robot = robot;
this.api.updatePlatformAccessories([cachedRobot]);
new NeatoVacuumRobotAccessory(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 NeatoVacuumRobotAccessory(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);
}
}
}

View File

@@ -1,12 +0,0 @@
import {API} from "homebridge";
import {PLATFORM_NAME} from "./settings";
import {HomebridgeNeatoPlatform} from "./homebridgeNeatoPlatform";
/**
* This method registers the platform with Homebridge
*/
export = (api: API) =>
{
api.registerPlatform(PLATFORM_NAME, HomebridgeNeatoPlatform);
};

View File

@@ -1,57 +0,0 @@
export enum availableLocales {
EN = "en",
DE = "de",
FR = "fr",
}
const localizationDicts = {
'en': {
"clean": "Clean",
"cleanZone": "Clean Zone",
"cleanThe": "Clean the",
"goToDock": "Go to Dock",
"dockState": "Docked",
"binFull": "Bin Full",
"eco": "Eco Mode",
"noGoLines": "NoGo Lines",
"extraCare": "Extra Care",
"schedule": "Schedule",
"findMe": "Find me",
"cleanSpot": "Clean Spot",
"battery": "Battery"
},
'de': {
"clean": "Sauge",
"cleanZone": "Sauge Zone",
"cleanThe": "Sauge",
"goToDock": "Zur Basis",
"dockState": "In der Basis",
"binFull": "Behälter voll",
"eco": "Eco Modus",
"noGoLines": "NoGo Linien",
"extraCare": "Extra Care",
"schedule": "Zeitplan",
"findMe": "Finde mich",
"cleanSpot": "Spot Reinigung",
"battery": "Batterie"
},
'fr': {
"clean": "Aspirer",
"cleanZone": "Aspirer Zone",
"cleanThe": "Aspirer",
"goToDock": "Retour à la base",
"dockState": "Sur la base",
"binFull": "Conteneur plein",
"eco": "Eco mode",
"noGoLines": "Lignes NoGo",
"extraCare": "Extra Care",
"schedule": "Planifier",
"findMe": "Me retrouver",
"cleanSpot": "Nettoyage local",
"battery": "Batterie"
}
}
export function localize(label: string, locale: availableLocales) : string {
return localizationDicts[locale][label] ?? label
}

View File

@@ -1,21 +0,0 @@
export class Options
{
public eco: boolean;
public extraCare: boolean;
public noGoLines: boolean;
public spotCharacteristics: boolean;
public spotRepeat: boolean;
public spotWidth: number;
public spotHeight: number;
constructor()
{
this.eco = false;
this.extraCare = false;
this.noGoLines = false;
this.spotCharacteristics = false;
this.spotRepeat = false;
this.spotWidth = 200;
this.spotHeight = 200;
}
}

View File

@@ -1,19 +0,0 @@
export enum CleanType {
ALL,
SPOT,
}
export enum RobotService {
CLEAN = "clean",
CLEAN_SPOT = "cleanSpot",
CLEAN_ZONE = "cleanZone",
GO_TO_DOCK = "goToDock",
DOCKED = "dockState",
BIN_FULL = "binFull",
FIND_ME = "findMe",
SCHEDULE = "schedule",
ECO = "eco",
NOGO_LINES = "noGoLines",
EXTRA_CARE = "extraCare",
BATTERY = "battery",
}

View File

@@ -1,9 +0,0 @@
/**
* This is the name of the platform that users will use to register the plugin in the Homebridge config.json
*/
export const PLATFORM_NAME = "NeatoVacuumRobot";
/**
* This must match the name of your plugin as defined the package.json
*/
export const PLUGIN_NAME = "homebridge-neato";

View File

@@ -1,26 +0,0 @@
{
"compilerOptions": {
"target": "ES2018", // ~node10
"module": "commonjs",
"lib": [
"es2015",
"es2016",
"es2017",
"es2018"
],
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"noImplicitAny": false
},
"include": [
"src/"
],
"exclude": [
"**/*.spec.ts"
]
}