94 Commits

Author SHA1 Message Date
Luis Riegger
4752c39887 merge config 2021-05-10 10:12:43 +02:00
Luis Riegger
2d35e8b814 restructure robot for localization 2021-05-10 10:07:30 +02:00
Luis Riegger
109ebbf162 modularize and localization 2021-05-10 09:57:12 +02:00
Arne Blumentritt
44f550fe92 Fixed to spot cleaning options 2021-05-10 09:44:23 +02:00
Arne Blumentritt
d9b5c7f572 Added spot cleaning 2021-05-09 20:34:23 +02:00
Arne Blumentritt
7a0758f883 Added retrying mechanism 2021-05-06 10:15:53 +02:00
Arne Blumentritt
94b3cc143f Removed beta debug information 2021-05-05 12:06:54 +02:00
Arne Blumentritt
a62972d022 Removed lint check for beta 2021-05-05 10:35:31 +02:00
Arne Blumentritt
dea0d157eb Merge remote-tracking branch 'origin/dynamic-platform' into dynamic-platform 2021-05-05 10:30:04 +02:00
Arne Blumentritt
bc527a60ba Improved handling of differences between robots in account and cache
Added debug output for all robots in account
2021-05-05 10:29:55 +02:00
Arne
653d685148 Update support-request.md 2021-05-04 17:04:20 +02:00
Arne
7ffc2a1a31 Update feature-request.md 2021-05-04 17:03:59 +02:00
Arne
96b2e7f3e6 Update bug-report.md 2021-05-04 17:03:10 +02:00
Arne
073fa915cf Update config.yml 2021-05-04 17:01:39 +02:00
Arne Blumentritt
e53bb3d777 Fixed issue with non smart robots 2021-05-04 14:29:36 +02:00
Arne Blumentritt
552e360f6f Linting 2021-05-04 14:08:54 +02:00
Arne Blumentritt
7173b2ec9e Updated node-botvac 2021-05-04 14:01:32 +02:00
Arne Blumentritt
f7f70ac478 Added option to enable services 2021-05-04 09:58:35 +02:00
Arne Blumentritt
296a81010a WIP dynamic platform 2021-05-04 09:11:50 +02:00
Arne Blumentritt
b13885bea7 WIP dynamic platform 2021-05-03 15:39:10 +02:00
Arne Blumentritt
e0bd97ee5d WIP dynamic platform 2021-05-01 20:48:12 +02:00
Arne Blumentritt
0530ba6c39 WIP dynamic platform 2021-05-01 02:59:57 +02:00
Arne Blumentritt
eda198ed09 WIP dynamic platform 2021-04-30 19:51:04 +02:00
Arne Blumentritt
0ed30314df WIP dynamic platform 2021-04-29 19:46:31 +02:00
Arne Blumentritt
4a97891dfd WIP dynamic platform 2021-04-28 20:07:39 +02:00
Arne Blumentritt
6f1f078ba4 WIP dynamic platform 2021-04-27 19:35:45 +02:00
Arne Blumentritt
313c532f87 WIP dynamic platform 2021-04-27 19:35:12 +02:00
Arne Blumentritt
3af668399b Finalised v0.7.3 2021-04-25 21:41:23 +02:00
Arne Blumentritt
877c3d7d26 Fixed homebridge 1.3 warning because of additional parameter in setter callback 2021-04-25 20:05:46 +02:00
Arne Blumentritt
77945f8420 Fixed #61 Warnings in homebridge 1.3 because of illegal characteristic values 2021-04-25 19:35:34 +02:00
Arne
5ff3951668 Update README.md 2020-10-08 12:10:43 +02:00
Arne
b83d30cfad Update README.md 2020-03-14 00:33:45 +01:00
Arne
b53ef4ff2f Update README.md 2020-03-14 00:32:48 +01:00
Arne
d8ca308338 Updated changelog 2019-10-19 17:33:58 +02:00
Arne
75180b27cb Bumped version to v0.7.2 2019-10-19 17:31:38 +02:00
Arne
b8971b64a9 Updated readme 2019-10-18 10:02:55 +02:00
Arne
32e47d0a8f Fixed launch error with multiple robots 2019-10-16 22:32:53 +02:00
Arne
f0dd89353e Bumped version to v0.7.1 2019-10-15 09:33:32 +02:00
Arne
d809be5b03 Fixed robot without floorplan cannot start cleaning 2019-10-14 22:33:08 +02:00
Arne
6e5b0d522c Fixed robot not shown before setting up a floor plan 2019-10-14 12:47:52 +02:00
Arne
9af5fe8a26 Bumped version to v0.7.0 2019-10-12 11:29:10 +02:00
Arne
61b66444ac Fixed missing callback when sending to another room 2019-10-12 11:28:09 +02:00
Arne
7197d78f33 Fixed a bug in spot cleaning 2019-10-11 12:17:24 +02:00
Arne
fa25b0d6a5 Bumped version to v0.7.0-beta.10 2019-09-26 09:08:35 +02:00
Arne
b7c2f82173 Bumped version to v0.7.8-beta.9 2019-09-26 08:59:35 +02:00
Arne
09e19d38cf Refactored zone cleaning and improved debugging 2019-09-26 08:59:17 +02:00
Arne
500ae64e0f Renamed hidden config option (backwards compatible) 2019-09-25 09:57:23 +02:00
Arne
12dd6d4676 Renamed spot repeat button 2019-09-24 20:32:09 +02:00
Arne
5402123340 Bumped version to v0.7.0-beta.8 and enabled find me 2019-09-24 20:09:16 +02:00
Arne
e75686e665 Bumped version to v0.7.0-beta.7 2019-09-24 20:06:20 +02:00
Arne
bef37d88c8 Fixed a bug that crashed the plugin on devices with zone cleaning
Fixed a bug in the update cache timing
2019-09-24 20:06:04 +02:00
Arne
af54046927 Fixed a bug that caused the cleaning of all rooms at once 2019-09-23 12:12:18 +02:00
Arne
1a4308ac40 Bumped version to v0.7.0-beta.6 2019-09-23 11:48:50 +02:00
Arne
46ba5e5f30 Added spot cleaning function with individual spot size (D7) 2019-09-23 11:48:35 +02:00
Arne
b8fa1db8ae Merge pull request #23 from nicoh88/patch-1
Added support for spot cleaning, added repeat and 4x4 mode for spot cleaning
2019-09-23 09:31:33 +02:00
Arne
0e92f09079 Merge branch 'master' into patch-1 2019-09-23 09:12:25 +02:00
Arne
73bc399d64 Bumped version to v0.7.0-beta.5 2019-09-22 22:30:29 +02:00
Arne
e49b4af85a Bumped version to v0.7.0-beta.4 2019-09-22 22:16:58 +02:00
Arne
139f415a42 Prepared find me function and fixed room cleaning exception 2019-09-22 22:16:43 +02:00
Arne
c789598019 Bumped version to v0.7.0-beta.3 2019-09-22 16:08:02 +02:00
Arne
c6def8c0fc Fixed some issues and added more device info 2019-09-22 16:07:35 +02:00
Arne
114ff70d60 Moved update logic to platform to reduce server requests for multiple rooms 2019-09-21 23:43:44 +02:00
Arne
7594843bb3 Fixed boundary issues 2019-09-21 14:47:41 +02:00
Arne
0ac882414a Added feature to start cleaning of a new room. 2019-09-21 14:28:31 +02:00
Arne
6a57eaa57e Bumped version to 0.7.0-beta.0 2019-09-21 14:25:49 +02:00
Arne
8822670f9b Fixed boundary cleaning 2019-09-19 12:10:17 +02:00
Arne
150b8973ee Changed coffee button 2019-09-19 11:26:42 +02:00
Arne
ca2af3968c Updated debug lib 2019-09-19 11:26:27 +02:00
Arne
6f19a189b2 Reworked room cleaning (WIP) 2019-09-19 11:25:36 +02:00
Arne
5139929bce Formatting only 2019-09-19 09:37:16 +02:00
Arne
a3b64b7c53 Added option to buy me a coffee 2019-09-17 22:27:06 +02:00
Arne
0338580c0a Version 0.6.3 2019-08-14 23:45:19 +02:00
Arne
54f303394e Fixed homebridge crash when 2 zones have the same name 2019-08-14 23:43:03 +02:00
Arne
c43a666378 Fixed homebridge crash when homebridge has no internet connection or the neato servers are offline 2019-08-14 23:06:03 +02:00
Arne
20c54a02e8 Fixed homebridge crash when robot has a map without zones 2019-08-14 22:48:23 +02:00
Arne
1436e4b342 Fixed homebridge startup failed when robot does not support mapping 2019-07-31 10:05:59 +02:00
Arne
db8305bbee Fixed homebridge startup failed when robot does not support zone cleaning 2019-07-20 22:37:27 +02:00
Arne
f795781d2a Code cleanup 2019-07-20 22:27:16 +02:00
Arne
3cfde323a8 Added gitignore 2019-07-20 22:20:31 +02:00
Arne
4a97487400 Added contributor and bumped version to v0.6.0 2019-07-14 23:35:09 +02:00
Arne
5ea9dde49c Merge pull request #28 from az0uz/feature/zone_cleaning
Support for zone cleaning
2019-07-14 23:32:43 +02:00
Arne
8ef24c9d40 Update package.json 2019-07-10 10:27:11 +02:00
Antoine de Maleprade
979dc40ccf fixed uuid reset when restarting homebridge, removed logs 2019-05-06 20:11:04 -07:00
Antoine de Maleprade
7d824ee0b9 Working zone cleaning, implemented multiple accesories to have a room per switch in homekit 2019-04-28 17:35:49 -07:00
Antoine de Maleprade
b87e49e12f fix: zone services discovery 2019-04-27 22:52:49 -07:00
Antoine de Maleprade
afe7d690ef Added zone cleaning 2019-04-27 22:06:07 -07:00
Arne
442b91c347 Versionbump to v0.5.2 2019-04-09 12:46:08 +02:00
Arne
55020c005b Merge pull request #26 from btutal/master
Add Schema for Config
2019-04-09 12:43:31 +02:00
Berkay
7c72aabb25 Add Schema for Config
Add schema file for homebridge-config-ui-x and similar plugins
2019-04-08 22:26:30 +02:00
Nico Hartung
769712405a Update README.md for new spot cleaning 2019-01-03 23:25:38 +01:00
Nico Hartung
7b70d8f076 Added support for spot cleaning, added repeat and 4x4 mode for spot cleaning
Repeat and 4x4 mode are not persistent - node-botvac api not supported. After a reboot of homebridge set it to off/false - use it for spot cleaning in compination with homekit scenes or automations.
2019-01-03 23:23:14 +01:00
Arne
e1754e02ed Fixed console log in node-botvac 2018-11-17 13:27:33 +01:00
Arne
0b864eb229 Updated readme 2018-11-14 22:42:38 +01:00
Arne
1d1fa121a4 Updated readme 2018-11-14 18:24:52 +01:00
29 changed files with 4746 additions and 450 deletions

38
.eslintrc Normal file
View File

@@ -0,0 +1,38 @@
{
"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"]
}
}

44
.github/ISSUE_TEMPLATE/bug-report.md vendored Normal file
View File

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

1
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

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

View File

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

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

120
.gitignore vendored Normal file
View File

@@ -0,0 +1,120 @@
# 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
# 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.*

135
.npmignore Normal file
View File

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

@@ -74,4 +74,65 @@
* Changed error handling
* Changed debug messages
* Updated node-botvac dependency to 0.1.6
* Removed extra care navigation option parameter (is now a button)
* Removed extra care navigation option parameter (is now a button)
## 0.5.1
* 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
* Added find me function
* Added spot cleaning function with individual spot size and repeat option
* Added model and firmware information to homekit
* Added logic to be able to change the currently cleaned room
* Improved number of requests when having multiple rooms
* Fixed room switches not taking eco and extraCare mode into account
* Fixed room switches not supporting pause/resume
## 0.7.1
* Fixed robot not shown before setting up a floor plan
## 0.7.2
* 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

View File

@@ -1,32 +1,50 @@
# homebridge-neato
[![npm](https://img.shields.io/npm/v/homebridge-neato.svg?style=flat-square)](https://www.npmjs.com/package/homebridge-neato)
[![npm](https://img.shields.io/npm/dt/homebridge-neato.svg?style=flat-square)](https://www.npmjs.com/package/homebridge-neato)
[![GitHub last commit](https://img.shields.io/github/last-commit/naofireblade/homebridge-neato.svg?style=flat-square)](https://github.com/naofireblade/homebridge-neato)
[![npm](https://img.shields.io/npm/v/homebridge-neato)](https://www.npmjs.com/package/homebridge-neato)
[![npm](https://img.shields.io/npm/dt/homebridge-neato)](https://www.npmjs.com/package/homebridge-neato?activeTab=versions)
[![GitHub last commit](https://img.shields.io/github/last-commit/naofireblade/homebridge-neato)](https://github.com/naofireblade/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).
Feel free to leave any feedback [here](https://github.com/naofireblade/homebridge-neato/issues).
If you like this plugin and find it useful, I would be forever grateful for your support:
If you update from a previous version 0.3.x you have to adapt your config (plugin is now a platform).
<a href="https://www.buymeacoffee.com/naofireblade" 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).
## Features
- Start and pause cleaning
- House Cleaning
- Eco mode
- Extra care navigation
- Nogo lines
- Zone cleaning <sup>[1](#d7)</sup><sup>, </sup><sup>[2](#change-room)</sup>
- Spot cleaning
- Individual spot size <sup>[1](#d7)</sup><sup>, </sup><sup>[3](#eve)</sup>
- Clean twice <sup>[3](#eve)</sup>
- Return to dock
- Toggle schedule
- Toggle eco mode
- Toggle extra care navigation
- toggle nogo lines
- Get battery info
- Get dock info
- Periodic refresh of robot state
- Support for multiple robots
- Find the robot
- Schedule (de)activation
- Robot information
- Battery level
- Charging state
- Dock occupancy
- Model and firmware version
- Automatic or periodic refresh of robot state
- Multiple robots
> <b name="d7">1</b> Only available on the Neato D7.
> <b name="change-room">2</b> You can send the robot from one room to another as well. He will return to the base, wait there some seconds and then starts cleaning the next room.
> <b name="eve">3</b> You need a third party app like eve to access these features.
## Installation
1. Install homebridge using: `npm install -g homebridge`
2. Install this plugin using: `npm install -g homebridge-neato`
3. If you don't have a Neato account yet create one [here](https://www.neatorobotics.com/create-account/).
3. If you don't have a Neato account yet, create one [here](https://www.neatorobotics.com/create-account/).
4. Update your configuration file. See the sample below.
## Configuration
@@ -47,11 +65,16 @@ Add the following information to your config file. Change the values for email a
### Advanced
The following config contains advanced optional settings.
Below are explanations for advanced parameters to adjust the plugin to your needs. All parameters are *optional*.
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.
**refresh**
Timer for periodic refresh of robot state. The default is `auto`. The options are:
`auto` Updates the robot state when a cleaning was started via homekit so that you can activate automations based on a successful cleaning.
`120` Or any other time in seconds (minimum `60`) is required if you want to receive robot state updates after starting the cleaning from outside of homekit (e.g. neato app or schedule).
`0` Disables background updates completely.
The parameter **disabled** accepts a list of switches/sensors that can be disabled in the neato homekit plugin (e.g. dock, dockstate, eco, schedule).
**hidden**
List of plugin features that you don't want to use in homekit (e.g. `dock`, `dockstate`, `eco`, `nogolines`, `extracare`, `schedule`, `find`, `spot`).
```json
"platforms": [
@@ -60,20 +83,18 @@ The parameter **disabled** accepts a list of switches/sensors that can be disabl
"email": "YourEmail",
"password": "YourPassword",
"refresh": "120",
"disabled": ["dock", "dockstate", "eco", "nogolines", "extracare", "schedule"]
"hidden": ["dock", "dockstate", "eco", "nogolines", "extracare", "schedule", "find", "spot"]
}
]
```
## Tested robots
- BotVac Connected (Firmware 2.2.0)
- BotVac D3 Connected
- BotVac D5 Connected (Firmware 4.0.0, Firmware 4.3.0)
- 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.
The plugin is successfully tested with all Neato Connected Robots.
## Contributors
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 tons of new beta versions

148
config.schema.json Normal file
View File

@@ -0,0 +1,148 @@
{
"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": {
"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"
]
},
"default": [
"clean",
"cleanZone",
"goToDock",
"dockState",
"binFull",
"eco",
"noGoLines",
"extraCare",
"schedule",
"findMe",
"cleanSpot",
"battery"
]
}
}
},
"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
}
]
}
]
}

9
homebridge-neato.iml Normal file
View File

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

416
index.js
View File

@@ -1,416 +0,0 @@
"use strict";
var inherits = require('util').inherits,
debug = require('debug')('homebridge-neato'),
botvac = require('node-botvac'),
Service,
Characteristic
module.exports = function (homebridge) {
Service = homebridge.hap.Service;
Characteristic = homebridge.hap.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.hiddenServices = ('disabled' in config ? config['disabled'] : '');
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
this.refresh = (this.refresh > 0 && this.refresh < 60) ? 60 : this.refresh;
}
// default auto
else {
this.refresh = 'auto';
}
debug("Refresh is set to: " + this.refresh);
}
NeatoVacuumRobotPlatform.prototype = {
accessories: function (callback) {
this.accessories = [];
let that = this;
this.robots = this.getRobots(function () {
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 + "\"");
var robotAccessory = new NeatoVacuumRobotAccessory(that.robots[i], that);
that.accessories.push(robotAccessory);
}
callback(that.accessories);
});
},
getRobots: function (callback) {
debug("Loading your robots");
let client = new botvac.Client();
let that = this;
client.authorize(this.email, this.password, false, function (error) {
if (error) {
that.log(error);
that.log.error("Can't log on to neato cloud. Please check your credentials.");
callback();
}
else {
client.getRobots(function (error, robots) {
if (error) {
that.log(error);
that.log.error("Successful login but can't connect to your neato robot.");
callback();
}
else {
if (robots.length === 0) {
that.log.error("Successful login but no robots associated with your account.");
that.robots = [];
callback();
}
else {
debug("Found " + robots.length + " robots");
that.robots = robots;
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.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.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 () {
this.informationService = new Service.AccessoryInformation();
this.informationService
.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));
this.vacuumRobotCleanService.getCharacteristic(Characteristic.On).on('get', this.getClean.bind(this));
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.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);
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();
}
}
});
},
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);
}
});
},
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);
});
},
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();
}
else {
debug(this.name + ": Updating robot state");
this.robot.getState(function (error, result) {
if (error) {
that.log.error(error + ": " + result);
}
that.lastUpdate = new Date();
callback();
});
}
},
updateRobotTimer: function () {
let that = this;
this.updateRobot(function (error, result) {
// only update these values if the state is different from the current one, otherwise we might accidentally start an action
if (that.vacuumRobotCleanService.getCharacteristic(Characteristic.On).value !== that.robot.canPause) {
that.vacuumRobotCleanService.setCharacteristic(Characteristic.On, that.robot.canPause);
}
// dock switch is on (dock not seen before) and dock has just been seen -> turn switch off
if (that.vacuumRobotGoToDockService.getCharacteristic(Characteristic.On).value == true && that.robot.dockHasBeenSeen) {
that.vacuumRobotGoToDockService.setCharacteristic(Characteristic.On, false);
}
if (that.vacuumRobotScheduleService.getCharacteristic(Characteristic.On).value !== that.robot.isScheduleEnabled) {
that.vacuumRobotScheduleService.setCharacteristic(Characteristic.On, that.robot.isScheduleEnabled);
}
// no commands here, values can be updated without problems
that.vacuumRobotDockStateService.setCharacteristic(Characteristic.OccupancyDetected, that.robot.isDocked ? 1 : 0);
that.vacuumRobotEcoService.setCharacteristic(Characteristic.On, that.robot.eco);
that.vacuumRobotNoGoLinesService.setCharacteristic(Characteristic.On, that.robot.noGoLines);
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
else if (that.refresh !== 'auto' && that.refresh !== 0) {
debug("Updating state in background every " + that.refresh + " seconds (user setting)");
that.timer = setTimeout(that.updateRobotTimer.bind(that), that.refresh * 1000);
}
else {
debug("Updating state in background disabled");
}
});
},
}

12
nodemon.json Normal file
View File

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

2841
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
{
"name": "homebridge-neato",
"version": "0.5.0",
"displayName": "Homebridge Neato",
"version": "1.0.0-beta.5",
"description": "A Neato vacuum robot plugin for homebridge.",
"license": "MIT",
"keywords": [
@@ -9,18 +10,65 @@
"botvac"
],
"engines": {
"node": ">=0.12.0",
"homebridge": ">=0.2.0"
"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"
},
"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": {
"type": "git",
"url": "git://github.com/naofireblade/homebridge-neato.git"
},
"bugs": {
"url": "https://github.com/naofireblade/homebridge-neato/issues"
},
"dependencies": {
"node-botvac": ">=0.1.6",
"debug": "^2.2.0"
}
"colors": "^1.4.0",
"debug": "^4.1.1",
"node-botvac": "^0.4.2",
"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

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

37
src/accessories/room.ts Normal file
View File

@@ -0,0 +1,37 @@
// import {CharacteristicValue, Logger, PlatformAccessory, PlatformConfig, Service} from 'homebridge';
// import {HomebridgeNeatoPlatform} from '../homebridgeNeatoPlatform';
//
// const debug = require('debug')('my-app:my-module');
//
// /**
// * Platform Accessory
// * An instance of this class is created for each accessory your platform registers
// * Each accessory may expose multiple services of different service types.
// */
// export class Room
// {
//
// private robot: any;
// private log: Logger;
// private readonly refresh: any;
//
//
// /**
// * These are just used to create a working example
// * You should implement your own code to track the state of your accessory
// */
//
// constructor(
// private readonly platform: HomebridgeNeatoPlatform,
// private readonly accessory: PlatformAccessory,
// private readonly isNew: Boolean,
// private readonly config: PlatformConfig)
// {
//
// }
//
// async setCleanRoom()
// {
//
// }
// }

View File

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

View File

@@ -0,0 +1,19 @@
import type { Characteristic, WithUUID } from 'homebridge';
import { Formats, Perms } from 'homebridge';
export default function spotHeight(CustomCharacteristic: typeof Characteristic): WithUUID<new () => Characteristic> {
return class SpotHeight extends CustomCharacteristic {
static readonly UUID = 'CA282DB2-62BF-4325-A1BE-F8BB5478781A';
constructor() {
super('Spot ↕', SpotHeight.UUID, {
format: Formats.INT,
unit: 'cm',
maxValue: 400,
minValue: 100,
minStep: 50,
perms: [Perms.PAIRED_READ, Perms.PAIRED_WRITE]
});
}
};
}

View File

@@ -0,0 +1,15 @@
import type { Characteristic, WithUUID } from 'homebridge';
import { Formats, Perms } from 'homebridge';
export default function spotRepeat(CustomCharacteristic: typeof Characteristic): WithUUID<new () => Characteristic> {
return class SpotRepeat extends CustomCharacteristic {
static readonly UUID = '1E79C603-63B8-4E6A-9CE1-D31D67981831';
constructor() {
super('Spot 2x', SpotRepeat.UUID, {
format: Formats.BOOL,
perms: [Perms.PAIRED_READ, Perms.PAIRED_WRITE]
});
}
};
}

View File

@@ -0,0 +1,19 @@
import type { Characteristic, WithUUID } from 'homebridge';
import { Formats, Perms } from 'homebridge';
export default function spotWidth(CustomCharacteristic: typeof Characteristic): WithUUID<new () => Characteristic> {
return class SpotWidth extends CustomCharacteristic {
static readonly UUID = 'A7889A9A-2F27-4293-BEF8-3FE805B36F4E';
constructor() {
super('Spot ↔', SpotWidth.UUID, {
format: Formats.INT,
unit: 'cm',
maxValue: 400,
minValue: 100,
minStep: 50,
perms: [Perms.PAIRED_READ, Perms.PAIRED_WRITE]
});
}
};
}

6
src/defaults.ts Normal file
View File

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

@@ -0,0 +1,219 @@
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);
}
}
}

12
src/index.ts Normal file
View File

@@ -0,0 +1,12 @@
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);
};

57
src/localization.ts Normal file
View File

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

21
src/models/options.ts Normal file
View File

@@ -0,0 +1,21 @@
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;
}
}

19
src/models/services.ts Normal file
View File

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

9
src/settings.ts Normal file
View File

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

26
tsconfig.json Normal file
View File

@@ -0,0 +1,26 @@
{
"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"
]
}