diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..b0c0c8b --- /dev/null +++ b/.eslintrc @@ -0,0 +1,3 @@ +{ + "extends": "airbnb" +} diff --git a/README.md b/README.md index f0bc16e..0575f4f 100755 --- a/README.md +++ b/README.md @@ -1,40 +1,75 @@ # homebridge-roomba -homebridge-plugin for Roomba980 -[![homebridge-plugin for Roomba980](http://img.youtube.com/vi/BbHbArJ95g0/0.jpg)](https://www.youtube.com/watch?v=BbHbArJ95g0) +[![NPM version][npm-image]][npm-url] +[npm-image]: https://img.shields.io/npm/v/homebridge-roomba.svg +[npm-url]: https://www.npmjs.com/package/homebridge-roomba + +homebridge-plugin for Roomba980(Roomba 900 Software Version 2.x). + +[![homebridge-plugin for Roomba980](https://cloud.githubusercontent.com/assets/3190760/25561826/ac495bda-2dae-11e7-96be-e7d8409e2901.gif)](https://www.youtube.com/watch?v=BbHbArJ95g0) # Installation -1. Install homebridge using: npm install -g homebridge -2. Install this plugin using: npm install -g homebridge-roomba -3. Update your configuration file. See sample-config.json in this repository for a sample. +## 1. Install homebridge and homebridge plugin. +1-1. Install homebridge: `npm install -g homebridge` +1-2. Install homebridge-roomba: `npm install -g homebridge-roomba` -# Configuration -Configuration sample: +## 2. Confirm the IP address to which Roomba is connected with the official application. +2-1. Open the `iRobot HOME App`. +2-2. Select More ➔ Settings ➔ Wi-Fi Settings ➔ Details of robot's Wi-Fi +2-3. Check IP Address items. (exsample: 192.16.xx.xx) +## 3. Get robotpwd and blid. +3-1. Move to the directory where you installed `homebridge-roomba`. + (exsample path `/Users/xxxxxx/.nodebrew/node/v7.7.1/lib/node_modules/homebridge-roomba/`) +3-2. `npm run getrobotpwd 192.16.xx.xx` +3-3. Follow the displayed message. ``` -"accessories": [ - "accessories": [ - { - "accessory": "Roomba", - "name": "Roomba", - "blid":"0123456789abcdef", - "robotpwd":"abcdefghijklmnop" - } -] +Make sure your robot is on the Home Base and powered on (green lights on). +Then press and hold the HOME button on your robot until it plays a series of tones (about 2 seconds). +Release the button and your robot will flash WIFI light. +Then press any key here... ``` +This process often fails. +Please check the following points and try several times. + +- Is the environment installing Rumba and homebridge connected to the same wifi? +- Is Rumba in the Dock and in a charged state? +- Please try running "npm run getrobotpwd 192.16.xx.xx" after the sound has sounded after pressing the home button of the room for 2 seconds +- Please check the version of Node.js. I confirmed that it works with "v 7.7.1". -# How do You find the BLID and robotpwd? +If successful, the following message will be displayed. +Please check "blid" and "Password" of displayed message. -1. [install Charles](https://www.charlesproxy.com/) -2. Start the Charles. -3. [Set the packet capture of the iPhone.](http://qiita.com/HIkaruSato/items/1f66c1a189bf9c19f838) -4. Start the iRobot app. -5. Check the contents of communication 「https://irobot.axeda.com」. -6. Check the request parameters of the top of the communication. ``` -example: -blid=0123456789abcdef&robotpwd=abcdefghijklmnop&method=getStatus +Robot Data: +{ ver: '2', + hostname: 'Roomba-xxxxxxxxxxxxxxxx', + robotname: 'Your Roomba’s Name', + ip: '192.168.xx.xx', + mac: 'xx:xx:xx:xx:xx:xx', + sw: 'vx.x.x-x', + sku: 'R98----', + nc: 0, + proto: 'mqtt', + blid: '0123456789abcdef' } +Password=> :1:2345678910:ABCDEFGHIJKLMNOP <= Yes, all this string. +``` + +blid is `0123456789abcdef`. +robotpwd is `:1:2345678910:ABCDEFGHIJKLMNOP`. + +## 4. Update homebridge configuration file. +``` +"accessories": [ + { + "accessory": "Roomba", + "name": "Roomba", + "blid":"0123456789abcdef", + "robotpwd":":1:2345678910:ABCDEFGHIJKLMNOP", + "ipaddress": "192.168.xx.xx" + } +] ``` diff --git a/index.js b/index.js index b784af9..5670a26 100755 --- a/index.js +++ b/index.js @@ -1,136 +1,185 @@ -var Service, Characteristic; -var request = require('request'); - -module.exports = function (homebridge) { - Service = homebridge.hap.Service; - Characteristic = homebridge.hap.Characteristic; - homebridge.registerAccessory("homebridge-roomba", "Roomba", RoombaAccessory); -} - - -function RoombaAccessory(log, config) { - this.log = log; - - // url info - this.blid = config["blid"]; - this.robotpwd = config["robotpwd"]; - this.irobotapi = 'https://irobot.axeda.com/services/v1/rest/Scripto/execute/AspenApiRequest'; - this.name = config["name"]; -} - -RoombaAccessory.prototype = { - - httpRequest: function (type,callback) { - request({ - url: this.irobotapi, - method: 'POST', - headers: {'Content-Type': 'application/x-www-form-urlencoded'}, - json:true, - form: { - 'blid':this.blid, - 'robotpwd':this.robotpwd, - 'method':'multipleFieldSet', - 'value':'{"remoteCommand":"'+type+'"}' - } - }, - function (error, response, body) { - callback(error, response, body) - }) - }, - - setPowerState: function(powerOn, callback) { - if (powerOn) { - this.log("Roomba Start!"); - this.httpRequest('start',function(error, response, responseBody) { - if (error) { - this.log('Roomba Failed: %s', error.message); - this.log(response); - callback(error); - } else { - this.log('Roomba is Ruuuuuuuuuuning!'); - callback(); - } - }.bind(this)); - } else { - this.log("Roomba Pause & Dock!"); - this.httpRequest('pause',function(error, response, responseBody) { - if (error) { - this.log('Roomba Failed: %s', error.message); - this.log(response); - callback(error); - } else { - this.log('Roomba is Pause!'); - - var checkStatus = function(time){ - setTimeout( - function() { - this.log('Roomba Checking the Status!'); - request({ - url: this.irobotapi, - method: 'POST', - headers: {'Content-Type': 'application/x-www-form-urlencoded'}, - json:true, - form: { - 'blid':this.blid, - 'robotpwd':this.robotpwd, - 'method':'getStatus' - } - }, - function (error, response, body) { - this.log('Roomba Get Status!'); - var status = JSON.parse(body.robot_status); - switch (status.phase){ - case "stop": - this.httpRequest('dock',function(error, response, responseBody) { - if (error) { - this.log('Roomba Failed: %s', error.message); - this.log(response); - callback(error); - } else { - this.log('Roomba Back Home! Goodbye!'); - callback(); - } - }.bind(this)); - break; - case "run": - this.log('Roomba is still Running... Wait a 3sec.'); - checkStatus(3000); - break; - default: - this.log('Roomba is not Running...You Please Help.'); - callback(); - break; - } - }.bind(this)); - - }.bind(this),time - ); - }.bind(this); - checkStatus(3000); +/** + * homebridge-roomba + */ - } - }.bind(this)); - } - }, +let Service; +let Characteristic; + +// dorita980をrequire +const dorita980 = require('dorita980'); +// 遅延時間を設定 +const delayTime = 5000; + + +/** + * Homebridgeアクセサリの初期化処理 + * + * @param {Object} log ログ + * @param {Object} config config.jsonのaccessoriesに指定した設定値 + * @param {String} config.name アクセサリ名 + * @param {String} config.blid Roombaのblid + * @param {String} config.robotpwd Roombaのパスワード + * @param {String} config.ipaddress Roombaの接続しているIP Address + * @method roombaAccessory + */ +const roombaAccessory = function (log, config) { + this.log = log; + this.name = config.name; + this.blid = config.blid; + this.robotpwd = config.robotpwd; + this.ipaddress = config.ipaddress; +}; + + +/** + * Homebridgeがキャッシュされたアクセサリを復元しようとした時に呼び出され処理 + * ここでアクセサリの各挙動を設定できます + */ +roombaAccessory.prototype = { + /** + * ユーザーがiOSアプリで「デバイスを識別する」をクリックしたときに呼び出される関数 + * + * @param {Function} callback + * @method identify + */ + identify(callback) { + this.log(callback); + this.log('Identify requested!'); + callback(); + }, + + /** + * setSwitchState + * + * @param {Number} powerOn 0 or 1 + * @param {Function} callback + * @method setSwitchState + */ + setSwitchState(powerOn, callback) { + const that = this; + + // dorita980 Local + const roombaViaLocal = new dorita980.Local(this.blid, this.robotpwd, this.ipaddress); + if (powerOn) { + // 掃除をして + that.log('Roomba Start!'); + // Roombaに接続する + roombaViaLocal.on('connect', () => { + that.log('Roomba Connect!'); - identify: function (callback) { - this.log("Identify requested!"); - callback(); // success - }, + // Roombaに掃除を開始させる + roombaViaLocal.start().then(() => { + that.log('Roomba is Ruuuuuuuuuunning!'); - getServices: function () { - var informationService = new Service.AccessoryInformation(); - informationService - .setCharacteristic(Characteristic.Manufacturer, "Roomba Manufacturer") - .setCharacteristic(Characteristic.Model, "Roomba Model") - .setCharacteristic(Characteristic.SerialNumber, "Roomba Serial Number"); + // 実行後、公式アプリのチャンネルを解放するためにローカル接続を切断する + roombaViaLocal.end(); + callback(); + }).catch((error) => { + // エラー + that.log('Roomba Failed: %s', error.message); + that.log(error); + callback(error); + }); + }); + } else { + // 掃除をやめて + that.log('Roomba Pause & Dock!'); + // Roombaに接続する + roombaViaLocal.on('connect', () => { + that.log('Roomba Connect!'); - var switchService = new Service.Switch(this.name); - switchService - .getCharacteristic(Characteristic.On) - .on('set', this.setPowerState.bind(this)); + // Roombaの掃除を一時停止させる + roombaViaLocal.pause().then(() => { + that.log('Roomba is Pauuuuuuuuuuse!'); + callback(); - return [switchService]; + // Roombaの状態を取得する関数 + const checkStatus = (time) => { + setTimeout(() => { + that.log('Checking the Status of Roomba!'); + + // Roombaの現在の状態を取得 + roombaViaLocal.getMission().then((response) => { + that.log('Get Status of Roomba!'); + that.log(response); + // response.cleanMissionStatus.phaseの値を見てRoombaの状況毎に処理を分岐 + switch (response.cleanMissionStatus.phase) { + case 'stop': + // RoombaをDockに戻す + roombaViaLocal.dock().then((() => { + // 実行後、公式アプリのチャンネルを解放するためにローカル接続を切断する + roombaViaLocal.end(); + that.log('Roomba is Back Home! Goodbye!'); + })).catch((error) => { + that.log('Roomba Failed: %s', error.message); + that.log(error); + }); + break; + case 'run': + // Roombaが走行中の場合、遅延時間待ってから再度ステータスをチェックする + that.log(`Roomba is still Running... Waiting ${time}ms.`); + checkStatus(time); + break; + default: + // 実行後、公式アプリのチャンネルを解放するためにローカル接続を切断する + roombaViaLocal.end(); + that.log('Roomba is not Running....You Please Help.'); + break; + } + }).catch((error) => { + that.error(error); + }); + }, time); + }; + // Roombaの状態を取得する + checkStatus(delayTime); + }).catch((error) => { + that.log('Roomba Failed: %s', error.message); + callback(error); + }); + }); } -}; \ No newline at end of file + }, + + + /** + * 一連のサービスを返す関数 + * + * @return {Array} サービスの配列 + * @method getServices + */ + getServices() { + // Homekitで定義されているサービス(機能)のうちSwitchを利用する + // Homekitで定義されているサービスの一覧は下記URLを参照 + // - http://qiita.com/tamaki/items/cf6a09729534eae8f24b#%E3%81%A9%E3%82%93%E3%81%AAservice%E3%81%8C%E3%81%82%E3%82%8B%E3%81%8B%E3%82%92%E8%AA%BF%E3%81%B9%E3%82%8B + const switchService = new Service.Switch(this.name); + switchService + // getCharacteristicは、既存のサービスと一致する名前またはテンプレートを検索し、それをオブジェクトとして返却する + .getCharacteristic(Characteristic.On) + // イベントにイベントリスナーを追加。 + // setイベントは、iOSが値を設定した場合や、 + // setValueというメソッドがhomebridgeで呼び出された場合に呼び出されます。 + .on('set', this.setSwitchState.bind(this)); + + return [switchService]; + }, +}; + + +/** + * export + */ +module.exports = (homebridge) => { + // Homebridgeは内部的にHap-NodeJSというパッケージをrequireしています。 + // HAP-NodeJSは、HomeKitアクセサリサーバのNode.js実装で、 + // 独自のHomeKitアクセサリを作成するためのAPIを提供しています。 + + // homebridge.hap.Serviceでは、HomeKitで定義している「とあるデバイス」の「とある機能」を提供するための雛形です。 + // homebridge.hap.Characteristicは、Serviceに割り当てられる特定の状態を定義するための雛形です。 + + Service = homebridge.hap.Service; + Characteristic = homebridge.hap.Characteristic; + // homebridge.registerAccessory(プラグイン名, プラットフォーム名, コンストラクタ名); + homebridge.registerAccessory('homebridge-roomba', 'Roomba', roombaAccessory); +}; diff --git a/package.json b/package.json index 83d9daf..b7301c4 100755 --- a/package.json +++ b/package.json @@ -1,23 +1,37 @@ { "name": "homebridge-roomba", - "version": "0.0.2", "description": "homebridge-plugin for Roomba980", + "version": "1.0.0", "license": "ISC", - "keywords": [ - "homebridge-plugin" - ], - "engines": { - "node": ">=0.12.0", - "homebridge": ">=0.2.0" - }, "author": { "name": "umesan" }, + "main": "", + "keywords": [ + "homebridge-plugin", + "unofficial", + "iRobot", + "Roomba", + "980", + "IoT" + ], "repository": { "type": "git", "url": "git://github.com/umesan/homebridge-roomba" }, + "scripts": { + "getrobotpwd": "cd node_modules/dorita980 && npm install && node ./bin/getpassword.js", + "lint": "eslint index.js" + }, "dependencies": { - "request": "^2.65.0" + "dorita980": "^3.0.11" + }, + "devDependencies": { + "eslint": "^3.19.0", + "eslint-config-airbnb": "^11.1.0", + "eslint-plugin-import": "^1.15.0", + "eslint-plugin-jsx-a11y": "^2.2.2", + "eslint-plugin-react": "^6.3.0", + "eslint-loader": "^1.6.1" } } \ No newline at end of file diff --git a/sample-config.json b/sample-config.json index 844cd9d..b3b5a1d 100755 --- a/sample-config.json +++ b/sample-config.json @@ -1,21 +1,19 @@ { - "bridge": { - "name": "Homebridge", - "username": "CD:22:3D:E3:CE:30", - "port": 51826, - "pin": "031-45-156" - }, - - "description": "for Roomba980", - - "platforms": [], - - "accessories": [ - { - "accessory": "Roomba", - "name": "Roomba", - "blid":"123456789abcdefg", - "robotpwd":"abcdefghijklmnop" - } - ] + "bridge": { + "name": "Homebridge", + "username": "CD:22:3D:E3:CE:30", + "port": 51826, + "pin": "031-45-156" + }, + "description": "for Roomba980", + "platforms": [], + "accessories": [ + { + "accessory": "Roomba", + "name": "Roomba", + "blid":"123456789abcdefg", + "robotpwd": ":1:2345678901:ABCDEFGHIJKLMNOP", + "ipaddress": "192.168.x.xx" + } + ] } \ No newline at end of file