├── package.json ├── LICENSE ├── README.md └── index.js /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "homebridge-dyson360eye", 3 | "version": "0.1.3", 4 | "description": "A Dyson 360 Eye robot vacuum plugin for homebridge.", 5 | "license": "MIT", 6 | "keywords": [ 7 | "homebridge-plugin", 8 | "dyson", 9 | "mqtt" 10 | ], 11 | "engines": { 12 | "node": ">=5.12.0", 13 | "homebridge": ">=0.4.20" 14 | }, 15 | "author": { 16 | "name": "Peter Stevenson" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git://github.com/peteakalad/homebridge-dyson360eye.git" 21 | }, 22 | "dependencies": { 23 | "mqtt": "^2.8.1", 24 | "debug": "^2.6.1" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 peteakalad 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # homebridge-dyson360eye 2 | Homebridge plugin for the Dyson 360 Eye Robot Vacuum 3 | 4 | Requires Homebridge installed first, refer to https://github.com/nfarina/homebridge 5 | 6 | The config requires a username and password, which you obtain from sticker in your manual or on the device label behind the bin. 7 | The password has to be SHA-512 encrypted and then base64 encoded before inserting into the config. This can be done via a tool at https://caligatio.github.io/jsSHA/ (client side javascript only). Put password into 'Input text', input type 'TEXT', SHA Variant 'SHA-512', Number of Rounds "1", Output Type "Base64". The output hash should be copied and placed into the config. The example below is 'password' encoded correctly. 8 | 9 | Ensure you set your 360 eye robot up to have a static IP in line with the config, this can be done through your router DHCP settings. 10 | 11 | Configuration Accessories Section Sample for Homebridge config.json: 12 | 13 | ``` 14 | "accessories": [ 15 | { 16 | "accessory": "Dyson360EyeRobotVacuum", 17 | "name": "Robot", 18 | "host": "192.168.1.111", 19 | "port": 1883, 20 | "username": "JJ5-UK-HKA9999Z", 21 | "password": "sQnzu7wkTrgkQZF+0G1hi5AI3Qmzvv0bXgc5THBqi7mAsdd4Xll27ASbRt9fEyavWi6m0QP9B8lThf+rDKy8hg==", 22 | "refresh": 0 23 | } 24 | ]​ 25 | ``` 26 | 27 | Once set up you can do commands like; 28 | 29 | Hey siri .. 30 | * ..set NAME max on = set robot to max power 31 | * ..set NAME quiet on = set robot to quiet power 32 | * ..set NAME clean on = start cleaning 33 | * ..set NAME clean off = pause 34 | * ..set NAME go to dock on = stop cleaning, return to dock 35 | 36 | Where NAME is the configured robot name in the configuration (above it is 'Robot') 37 | 38 | I have scenes set up in Home app, which set the switches appropriately, so the following siri commands/scenes work: 39 | * "Start cleaning" (turn on clean switch) 40 | * "Pause cleaning" (turn off clean switch) 41 | * "Stop Cleaning" (turn on go to dock switch) 42 | * "Resume Cleaning" (turn on clean switch) 43 | 44 | This can be considered alpha but still may be of use. 45 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var 3 | Service, 4 | Characteristic, 5 | vacuumRobotCleanService, 6 | vacuumRobotGoToDockService, 7 | vacuumRobotDockStateService, 8 | vacuumRobotMaxPowerService, 9 | vacuumRobotBatteryService, 10 | refresh, 11 | timer; 12 | const mqtt = require('mqtt'); 13 | const EventEmitter = require('events'); 14 | 15 | module.exports = function (homebridge) { 16 | Service = homebridge.hap.Service; 17 | Characteristic = homebridge.hap.Characteristic; 18 | homebridge.registerAccessory("homebridge-dyson360eye", "Dyson360EyeRobotVacuum", Dyson360EyeRobotVacuum); 19 | } 20 | 21 | function Dyson360EyeRobotVacuum(log, config) { 22 | this.log = log; 23 | this.name = config['name']; 24 | this.serial = "1-9-2-8"; 25 | this.host = config['host']; 26 | this.port = config['port']; 27 | this.username = config['username']; 28 | this.password = config['password']; 29 | this.refresh = config['refresh']; 30 | 31 | this.vacuumRobotCleanService = new Service.Switch(this.name + " Clean", "clean"); 32 | this.vacuumRobotGoToDockService = new Service.Switch(this.name + " Go to Dock", "goToDock"); 33 | this.vacuumRobotDockStateService = new Service.OccupancySensor(this.name + " Dock", "dockState"); 34 | this.vacuumRobotMaxPowerService = new Service.Switch(this.name + " Max", "maxPower"); 35 | this.vacuumRobotQuietPowerService = new Service.Switch(this.name + " Quiet", "quietPower"); 36 | this.vacuumRobotBatteryService = new Service.BatteryService("Battery", "battery"); 37 | 38 | this.gettingState = false; 39 | this.state = null; 40 | this.initConnection(); 41 | } 42 | 43 | Dyson360EyeRobotVacuum.prototype = { 44 | initConnection: function() { 45 | this.log('initConnection'); 46 | this.url = 'mqtt://' + this.host + ':' + this.port; 47 | this.log(this.url); 48 | this.options = { 49 | keepalive: 10, 50 | clientId: 'homebridge-dyson_' + Math.random().toString(16), 51 | protocolId: 'MQIsdp', 52 | protocolVersion: 3, 53 | clean: true, 54 | reconnectPeriod: 1000, 55 | connectTimeout: 30 * 1000, 56 | username: this.username, 57 | password: this.password, 58 | rejectUnauthorized: false 59 | }; 60 | this.json_emitter = new EventEmitter(); 61 | var that = this; 62 | this.mqtt_client = mqtt.connect(this.url, this.options); 63 | this.mqtt_client.on('connect', function() { 64 | that.log('connected'); 65 | that.mqtt_client.subscribe("N223/" + that.username + "/status"); 66 | that.log('subscribed to ' + "N223/" + that.username + "/status"); 67 | that.getState(); 68 | }) 69 | this.mqtt_client.on('message', function(topic, message) { 70 | var json = JSON.parse(message); 71 | that.log(JSON.stringify(json)); 72 | if (json !== null) { 73 | if (json.msg === "CURRENT-STATE") { 74 | that.gettingState = false; 75 | that.state = json; 76 | that.json_emitter.emit('state', that.state.state); 77 | that.log('state is ' + that.state.state); 78 | } else 79 | { 80 | that.log('NOT PROCESSED: ' + json); 81 | } 82 | } 83 | }); 84 | }, 85 | 86 | identify: function (callback) { 87 | this.log("Identify requested"); 88 | callback(); 89 | }, 90 | 91 | getServices: function () { 92 | this.informationService = new Service.AccessoryInformation(); 93 | this.informationService 94 | .setCharacteristic(Characteristic.Manufacturer, "Dyson") 95 | .setCharacteristic(Characteristic.Model, this.name) 96 | .setCharacteristic(Characteristic.SerialNumber, this.serial); 97 | 98 | this.vacuumRobotCleanService.getCharacteristic(Characteristic.On).on('set', this.setClean.bind(this)); 99 | this.vacuumRobotCleanService.getCharacteristic(Characteristic.On).on('get', this.getClean.bind(this)); 100 | 101 | this.vacuumRobotGoToDockService.getCharacteristic(Characteristic.On).on('set', this.setGoToDock.bind(this)); 102 | this.vacuumRobotGoToDockService.getCharacteristic(Characteristic.On).on('get', this.getGoToDock.bind(this)); 103 | 104 | this.vacuumRobotDockStateService.getCharacteristic(Characteristic.OccupancyDetected).on('get', this.getDock.bind(this)); 105 | 106 | this.vacuumRobotMaxPowerService.getCharacteristic(Characteristic.On).on('set', this.setMaxPower.bind(this)); 107 | this.vacuumRobotMaxPowerService.getCharacteristic(Characteristic.On).on('get', this.getMaxPower.bind(this)); 108 | 109 | this.vacuumRobotQuietPowerService.getCharacteristic(Characteristic.On).on('set', this.setQuietPower.bind(this)); 110 | this.vacuumRobotQuietPowerService.getCharacteristic(Characteristic.On).on('get', this.getQuietPower.bind(this)); 111 | 112 | this.vacuumRobotBatteryService.getCharacteristic(Characteristic.BatteryLevel).on('get', this.getBatteryLevel.bind(this)); 113 | this.vacuumRobotBatteryService.getCharacteristic(Characteristic.ChargingState).on('get', this.getBatteryChargingState.bind(this)); 114 | 115 | return [ 116 | this.informationService, 117 | this.vacuumRobotCleanService, 118 | this.vacuumRobotGoToDockService, 119 | this.vacuumRobotDockStateService, 120 | this.vacuumRobotMaxPowerService, 121 | this.vacuumRobotQuietPowerService, 122 | this.vacuumRobotBatteryService 123 | ]; 124 | }, 125 | 126 | setClean: function (on, callback) { 127 | var that = this; 128 | this.log('setClean'); 129 | this.json_emitter.once('state', (json) => { 130 | var now = new Date(); 131 | that.log('on ' + on); 132 | that.log('state ' + that.state.state); 133 | if (on && (that.state.state === 'INACTIVE_CHARGING' || that.state.state === 'INACTIVE_CHARGED')) 134 | { 135 | var message = '{"msg":"START","time":"' + now.toISOString() + '", "fullCleanType":"immediate"}'; 136 | } else if (!on && that.state.state === 'FULL_CLEAN_RUNNING') { 137 | var message = '{"msg":"PAUSE","time":"' + now.toISOString() + '"}'; 138 | } else if (on && that.state.state === 'FULL_CLEAN_PAUSED') { 139 | var message = '{"msg":"RESUME","time":"' + now.toISOString() + '"}'; 140 | } 141 | that.log(message); 142 | that.mqtt_client.publish( 143 | "N223/" + that.username + "/command", 144 | message 145 | ); 146 | that.vacuumRobotCleanService.getCharacteristic(Characteristic.On).updateValue(false); 147 | that.vacuumRobotGoToDockService.getCharacteristic(Characteristic.On).updateValue(false); 148 | that.vacuumRobotDockStateService.getCharacteristic(Characteristic.OccupancyDetected).updateValue(false); 149 | that.getClean(callback); 150 | }); 151 | this.getState(); 152 | }, 153 | 154 | setGoToDock: function (on, callback) { 155 | let that = this; 156 | this.log('setGoToDock'); 157 | var now = new Date(); 158 | if (on) 159 | { 160 | var message = '{"msg":"ABORT","time":"' + now.toISOString() + '", "fullCleanType":"immediate"}'; 161 | this.mqtt_client.publish( 162 | "N223/" + that.username + "/command", 163 | message 164 | ); 165 | } 166 | this.vacuumRobotGoToDockService.getCharacteristic(Characteristic.On).updateValue(false); 167 | this.vacuumRobotCleanService.getCharacteristic(Characteristic.On).updateValue(false); 168 | this.vacuumRobotDockStateService.getCharacteristic(Characteristic.OccupancyDetected).updateValue(false); 169 | this.getGoToDock(callback); 170 | }, 171 | 172 | setMaxPower: function (on, callback, fromQuiet) { 173 | this.log('setMaxPower to ' + on); 174 | var that = this; 175 | var now = new Date(); 176 | if (on) 177 | { 178 | var powerMode = 'fullPower'; 179 | } else { 180 | var powerMode = 'halfPower'; 181 | } 182 | var message = '{"msg":"STATE-SET","time":"' + now.toISOString() + '","data":{"currentVacuumPowerMode":"' + powerMode + '","defaultVacuumPowerMode":"' + powerMode + '"}}'; 183 | this.mqtt_client.publish( 184 | "N223/" + that.username + "/command", 185 | message 186 | ); 187 | this.json_emitter.once('state', (json) => { 188 | that.log('once in setMaxPower ' + (that.state.currentVacuumPowerMode === 'fullPower')); 189 | if (fromQuiet === true) 190 | { 191 | that.log('*** IN FROM QUIET'); 192 | callback(null, that.state.currentVacuumPowerMode === 'halfPower'); 193 | } else 194 | { 195 | callback(null, that.state.currentVacuumPowerMode === 'fullPower'); 196 | } 197 | }); 198 | this.mqtt_client.publish( 199 | "N223/" + that.username + "/command", 200 | message 201 | ); 202 | //this.getState(); 203 | //this.vacuumRobotMaxPowerService.getCharacteristic(Characteristic.On).updateValue(false); 204 | //this.vacuumRobotQuietPowerService.getCharacteristic(Characteristic.On).updateValue(false); 205 | //this.getMaxPower(callback); 206 | }, 207 | 208 | setQuietPower: function (on, callback) { 209 | this.log('setQuietPower: ' + on); 210 | this.setMaxPower(!on, callback,true); 211 | }, 212 | 213 | getClean: function(callback) { 214 | this.log('getClean'); 215 | callback(null, this.state.state === 'FULL_CLEAN_RUNNING'); 216 | }, 217 | 218 | getGoToDock: function(callback) { 219 | callback(null, this.state.state === 'FULL_CLEAN_ABORTED'); 220 | }, 221 | 222 | getDock: function(callback) { 223 | callback(null, this.state.state === 'INACTIVE_CHARGING' || this.state.state === 'INACTIVE_CHARGED'); 224 | }, 225 | 226 | getMaxPower: function(callback) { 227 | this.log('getMaxPower ' + this.state.currentVacuumPowerMode === 'fullPower'); 228 | callback(null, this.state.currentVacuumPowerMode === 'fullPower'); 229 | }, 230 | 231 | getQuietPower: function(callback) { 232 | this.log('getQuietPower '+ this.state.currentVacuumPowerMode === 'halfPower'); 233 | callback(null, this.state.currentVacuumPowerMode === 'halfPower'); 234 | }, 235 | 236 | getBatteryLevel: function(callback) { 237 | callback(false, this.state.batteryChargeLevel); 238 | }, 239 | 240 | getBatteryChargingState: function(callback) { 241 | callback(false, this.state.state === 'INACTIVE_CHARGING'); 242 | }, 243 | 244 | getState: function(callback) { 245 | if (this.gettingState == true) 246 | { 247 | this.log('already getting state'); 248 | return; 249 | } 250 | this.gettingState = true; 251 | this.log("Get state (new)"); 252 | let that = this; 253 | this.json_emitter.once('state', (json) => { 254 | that.log('in the once state for get state'); 255 | that.vacuumRobotMaxPowerService.getCharacteristic(Characteristic.On).updateValue(that.state.currentVacuumPowerMode === 'fullPower',null); 256 | that.vacuumRobotQuietPowerService.getCharacteristic(Characteristic.On).updateValue(that.state.currentVacuumPowerMode === 'halfPower',null); 257 | that.log('>>>>>>>>>>>>>> max is ' + (that.state.currentVacuumPowerMode === 'fullPower') + ' and quiet ' + (that.state.currentVacuumPowerMode === 'halfPower')); 258 | }); 259 | this.mqtt_client.publish( 260 | 'N223/' + that.username + '/command', 261 | '{"msg":"REQUEST-CURRENT-STATE"}' 262 | ); 263 | } 264 | } 265 | 266 | --------------------------------------------------------------------------------