diff --git a/accessories/neatVacuumRobot.js b/accessories/neatoVacuumRobot.js similarity index 59% rename from accessories/neatVacuumRobot.js rename to accessories/neatoVacuumRobot.js index fbdaab2..ca7261d 100644 --- a/accessories/neatVacuumRobot.js +++ b/accessories/neatoVacuumRobot.js @@ -11,19 +11,19 @@ module.exports = function (_Service, _Characteristic) return NeatoVacuumRobotAccessory; }; -function NeatoVacuumRobotAccessory(robot, platform, boundary = undefined) +function NeatoVacuumRobotAccessory(robotObject, platform, boundary = undefined) { this.platform = platform; this.boundary = boundary; this.log = platform.log; this.refresh = platform.refresh; this.hiddenServices = platform.hiddenServices; - this.robot = robot; + this.robot = robotObject.device; this.nextRoom = null; if (typeof boundary === 'undefined') { - this.name = robot.name; + this.name = this.robot.name; } else { @@ -46,7 +46,6 @@ function NeatoVacuumRobotAccessory(robot, platform, boundary = undefined) platform.boundaryNames.push(this.boundary.name); this.name = this.robot.name + ' - ' + this.boundary.name; } - this.lastUpdate = null; this.vacuumRobotBatteryService = new Service.BatteryService("Battery", "battery"); @@ -72,24 +71,22 @@ function NeatoVacuumRobotAccessory(robot, platform, boundary = undefined) new Service.Switch(serviceName, "cleanBoundary:" + boundary.id); this.log("Adding zone cleaning for: " + boundary.name); } - - this.updateRobotTimer(); } NeatoVacuumRobotAccessory.prototype = { identify: function (callback) { - let that = this; - this.updateRobot(function () + this.platform.updateRobot(this.robot._serial, () => { // 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; + let _serial = this.robot._serial; + let _secret = this.robot._secret; + this.robot._serial = "*****"; + this.robot._secret = "*****"; + this.log(this.robot); + this.robot._serial = _serial; + this.robot._secret = _secret; + callback(); }); }, @@ -111,10 +108,7 @@ NeatoVacuumRobotAccessory.prototype = { .setCharacteristic(Characteristic.Name, this.robot.name + ' - ' + this.boundary.name) } - 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.vacuumRobotBatteryService]; + this.services = [this.informationService]; if (typeof this.boundary === "undefined") { @@ -144,7 +138,11 @@ NeatoVacuumRobotAccessory.prototype = { 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.push(this.vacuumRobotCleanService); + this.services.push(this.vacuumRobotBatteryService); if (this.hiddenServices.indexOf('dock') === -1) this.services.push(this.vacuumRobotGoToDockService); @@ -178,7 +176,7 @@ NeatoVacuumRobotAccessory.prototype = { getClean: function (callback, boundary) { - this.updateRobot((error, result) => + this.platform.updateRobot(this.robot._serial, (error, result) => { let cleaning; if (typeof boundary === 'undefined') @@ -197,7 +195,7 @@ NeatoVacuumRobotAccessory.prototype = { setClean: function (on, callback, boundary) { - this.updateRobot((error, result) => + this.platform.updateRobot(this.robot._serial, (error, result) => { // Start if (on) @@ -235,11 +233,6 @@ NeatoVacuumRobotAccessory.prototype = { this.setGoToDock(true, (error, result) => { this.nextRoom = boundary; - - setTimeout(() => - { - this.clean(callback, boundary); - }, 1000); }); } // Start new cleaning of new room @@ -273,8 +266,7 @@ NeatoVacuumRobotAccessory.prototype = { { setTimeout(() => { - clearTimeout(this.timer); - this.updateRobotTimer(); + this.platform.updateRobotTimer(this.robot._serial); }, 60 * 1000); } @@ -330,31 +322,30 @@ NeatoVacuumRobotAccessory.prototype = { setGoToDock: function (on, callback) { - let that = this; - this.updateRobot(function (error, result) + this.platform.updateRobot(this.robot._serial, (error, result) => { if (on) { - if (that.robot.canPause) + if (this.robot.canPause) { - debug(that.name + ": Pause cleaning to go to dock"); - that.robot.pauseCleaning(function (error, result) + debug(this.name + ": Pause cleaning to go to dock"); + this.robot.pauseCleaning((error, result) => { - setTimeout(function () + setTimeout(() => { - debug(that.name + ": Go to dock"); - that.robot.sendToBase(callback); + debug(this.name + ": Go to dock"); + this.robot.sendToBase(callback); }, 1000); }); } - else if (that.robot.canGoToBase) + else if (this.robot.canGoToBase) { - debug(that.name + ": Go to dock"); - that.robot.sendToBase(callback); + debug(this.name + ": Go to dock"); + this.robot.sendToBase(callback); } else { - that.log.warn(that.name + ": Can't go to dock at the moment"); + this.log.warn(this.name + ": Can't go to dock at the moment"); callback(); } } @@ -367,204 +358,152 @@ NeatoVacuumRobotAccessory.prototype = { getEco: function (callback) { - let that = this; - this.updateRobot(function () + this.platform.updateRobot(this.robot._serial, () => { - debug(that.name + ": Eco mode is " + (that.robot.eco ? 'ON' : 'OFF')); - callback(false, that.robot.eco); + debug(this.name + ": Eco Mode is " + (this.robot.eco ? 'ON' : 'OFF')); + callback(false, this.robot.eco); }); }, setEco: function (on, callback) { this.robot.eco = on; - debug(this.name + ": " + (on ? "Enabled" : "Disabled") + " Eco mode "); + debug(this.name + ": " + (on ? "Enabled " : "Disabled") + " Eco Mode "); callback(); }, getNoGoLines: function (callback) { - let that = this; - this.updateRobot(function () + this.platform.updateRobot(this.robot._serial, () => { - debug(that.name + ": Nogo Lines are " + (that.robot.eco ? 'ON' : 'OFF')); - callback(false, that.robot.noGoLines ? 1 : 0); + debug(this.name + ": NoGoLine is " + (this.robot.eco ? 'ON' : 'OFF')); + callback(false, this.robot.noGoLines ? 1 : 0); }); }, setNoGoLines: function (on, callback) { this.robot.noGoLines = on; - debug(this.name + ": " + (on ? "Enabled" : "Disabled") + " Nogo lines "); + debug(this.name + ": " + (on ? "Enabled " : "Disabled") + " NoGoLine "); callback(); }, getExtraCare: function (callback) { - let that = this; - this.updateRobot(function () + this.platform.updateRobot(this.robot._serial, () => { - debug(that.name + ": Extra Care Navigation is " + (that.robot.navigationMode == 2 ? 'ON' : 'OFF')); - callback(false, that.robot.navigationMode == 2 ? 1 : 0); + debug(this.name + ": Care Nav is " + (this.robot.navigationMode === 2 ? 'ON' : 'OFF')); + callback(false, this.robot.navigationMode === 2 ? 1 : 0); }); }, setExtraCare: function (on, callback) { this.robot.navigationMode = on ? 2 : 1; - debug(this.name + ": " + (on ? "Enabled" : "Disabled") + " Extra Care Navigation "); + debug(this.name + ": " + (on ? "Enabled " : "Disabled") + " Care Nav "); callback(); }, getSchedule: function (callback) { - let that = this; - this.updateRobot(function () + this.platform.updateRobot(this.robot._serial,() => { - debug(that.name + ": Schedule is " + (that.robot.eco ? 'ON' : 'OFF')); - callback(false, that.robot.isScheduleEnabled); + debug(this.name + ": Schedule is " + (this.robot.eco ? 'ON' : 'OFF')); + callback(false, this.robot.isScheduleEnabled ); }); }, setSchedule: function (on, callback) { - let that = this; - this.updateRobot(function (error, result) + this.platform.updateRobot(this.robot._serial, (error, result) => { if (on) { - debug(that.name + ": Enabled Schedule"); - that.robot.enableSchedule(callback); + debug(this.name + ": Enabled Schedule"); + this.robot.enableSchedule(callback); } else { - debug(that.name + ": Disabled Schedule"); - that.robot.disableSchedule(callback); + debug(this.name + ": Disabled Schedule"); + this.robot.disableSchedule(callback); } }); }, getDock: function (callback) { - let that = this; - this.updateRobot(function () + this.platform.updateRobot(this.robot._serial, () => { - debug(that.name + ": Is " + (that.robot.isDocked ? '' : 'not ') + "docked"); - callback(false, that.robot.isDocked ? 1 : 0); + debug(this.name + ": The Dock is " + (this.robot.isDocked ? '' : 'un ') + "occupied"); + callback(false, this.robot.isDocked ? 1 : 0); }); }, getBatteryLevel: function (callback) { - let that = this; - this.updateRobot(function () + this.platform.updateRobot(this.robot._serial, () => { - debug(that.name + ": Battery is at " + that.robot.charge + "%"); - callback(false, that.robot.charge); + debug(this.name + ": Battery is " + this.robot.charge + "%"); + callback(false, this.robot.charge); }); }, getBatteryChargingState: function (callback) { - let that = this; - this.updateRobot(function () + this.platform.updateRobot(this.robot._serial, () => { - debug(that.name + ": Is " + (that.robot.isCharging ? '' : 'not ') + "charging"); - callback(false, that.robot.isCharging); + debug(this.name + ": Battery is " + (this.robot.isCharging ? '' : 'not ') + "charging"); + callback(false, this.robot.isCharging); }); }, - updateRobot: function (callback) + updated: function () { - let that = this; - if (this.lastUpdate !== null && new Date() - this.lastUpdate < 2000) + if (!this.boundary) { - callback(); + // only update these values if the state is different from the current one, otherwise we might accidentally start an action + if (this.vacuumRobotCleanService.getCharacteristic(Characteristic.On).value !== this.robot.canPause) + { + this.vacuumRobotCleanService.setCharacteristic(Characteristic.On, this.robot.canPause); + } + + // dock switch is on (dock not seen before) and dock has just been seen -> turn switch off + if (this.vacuumRobotGoToDockService.getCharacteristic(Characteristic.On).value == true && this.robot.dockHasBeenSeen) + { + this.vacuumRobotGoToDockService.setCharacteristic(Characteristic.On, false); + } + + if (this.vacuumRobotScheduleService.getCharacteristic(Characteristic.On).value !== this.robot.isScheduleEnabled ) + { + this.vacuumRobotScheduleService.setCharacteristic(Characteristic.On, this.robot.isScheduleEnabled ); + } + + // no commands here, values can be updated without problems + this.vacuumRobotDockStateService.setCharacteristic(Characteristic.OccupancyDetected, this.robot.isDocked ? 1 : 0); + this.vacuumRobotEcoService.setCharacteristic(Characteristic.On, this.robot.eco); + this.vacuumRobotNoGoLinesService.setCharacteristic(Characteristic.On, this.robot.noGoLines); + this.vacuumRobotExtraCareService.setCharacteristic(Characteristic.On, this.robot.navigationMode == 2 ? true : false); + } else { - debug(this.name + ": Updating robot state"); - this.robot.getState(function (error, result) + if (this.vacuumRobotCleanBoundaryService.getCharacteristic(Characteristic.On).value !== this.robot.canPause) { - if (error) - { - that.log.error("Cannot update robot. Check if robot is online. " + error); - } - that.lastUpdate = new Date(); - callback(); - }); + this.vacuumRobotCleanBoundaryService.setCharacteristic(Characteristic.On, this.robot.canPause); + } } - }, - updateRobotTimer: function () - { - this.updateRobot((error, result) => + this.vacuumRobotBatteryService.setCharacteristic(Characteristic.BatteryLevel, this.robot.charge); + this.vacuumRobotBatteryService.setCharacteristic(Characteristic.ChargingState, this.robot.isCharging); + + // Robot has a next room to clean in queue + if (this.nextRoom !== null && this.robot.isDocked) { - - if (!this.boundary) + this.clean((error, result) => { - // only update these values if the state is different from the current one, otherwise we might accidentally start an action - if (this.vacuumRobotCleanService.getCharacteristic(Characteristic.On).value !== this.robot.canPause) - { - this.vacuumRobotCleanService.setCharacteristic(Characteristic.On, this.robot.canPause); - } - - // dock switch is on (dock not seen before) and dock has just been seen -> turn switch off - if (this.vacuumRobotGoToDockService.getCharacteristic(Characteristic.On).value == true && this.robot.dockHasBeenSeen) - { - this.vacuumRobotGoToDockService.setCharacteristic(Characteristic.On, false); - } - - if (this.vacuumRobotScheduleService.getCharacteristic(Characteristic.On).value !== this.robot.isScheduleEnabled) - { - this.vacuumRobotScheduleService.setCharacteristic(Characteristic.On, this.robot.isScheduleEnabled); - } - - // no commands here, values can be updated without problems - this.vacuumRobotDockStateService.setCharacteristic(Characteristic.OccupancyDetected, this.robot.isDocked ? 1 : 0); - this.vacuumRobotEcoService.setCharacteristic(Characteristic.On, this.robot.eco); - this.vacuumRobotNoGoLinesService.setCharacteristic(Characteristic.On, this.robot.noGoLines); - this.vacuumRobotExtraCareService.setCharacteristic(Characteristic.On, this.robot.navigationMode == 2 ? true : false); - - } - else - { - if (this.vacuumRobotCleanBoundaryService.getCharacteristic(Characteristic.On).value !== this.robot.canPause) - { - this.vacuumRobotCleanBoundaryService.setCharacteristic(Characteristic.On, this.robot.canPause); - } - } - - this.vacuumRobotBatteryService.setCharacteristic(Characteristic.BatteryLevel, this.robot.charge); - this.vacuumRobotBatteryService.setCharacteristic(Characteristic.ChargingState, this.robot.isCharging); - - // Robot has a next room to clean in queue - if (this.nextRoom !== null && this.robot.isDocked) - { - this.clean((error, result) => - { - this.nextRoom = null; - }, this.nextRoom); - } - - // robot is currently cleaning, refresh is set to auto or specific interval -> continue updating - if (this.robot.canPause && this.refresh !== 0) - { - let refreshTime = this.refresh === 'auto' ? 60 : this.refresh; - debug(this.name + ": Updating state in background every " + refreshTime + " seconds while cleaning"); - this.timer = setTimeout(this.updateRobotTimer.bind(this), refreshTime * 1000); - } - // robot is not cleaning, but a specific refresh interval is set -> continue updating - else if (this.refresh !== 'auto' && this.refresh !== 0) - { - debug(this.name + ": Updating state in background every " + this.refresh + " seconds (user setting)"); - this.timer = setTimeout(this.updateRobotTimer.bind(this), this.refresh * 1000); - } - // robot is not cleaning, no specific refresh interval is set -> stop updating - else - { - debug(this.name + ": Disabled Background Updates"); - } - }); - }, + this.nextRoom = null; + debug("Starting cleaning of next room"); + }, this.nextRoom); + } + } }; \ No newline at end of file diff --git a/index.js b/index.js index 2c47494..6168954 100644 --- a/index.js +++ b/index.js @@ -21,6 +21,9 @@ function NeatoVacuumRobotPlatform(log, config) this.password = config['password']; this.hiddenServices = ('disabled' in config ? config['disabled'] : ''); + // Array of real robots and associated robot accessories (incl rooms) + this.robots = []; + if ('refresh' in config && config['refresh'] !== 'auto') { // parse config parameter @@ -28,52 +31,62 @@ function NeatoVacuumRobotPlatform(log, config) // 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; + if (this.refresh > 0 && this.refresh < 60) + { + this.log.warn("Minimum refresh time is 60 seconds to not overload the neato servers"); + this.refresh = (this.refresh > 0 && this.refresh < 60) ? 60 : this.refresh; + } } // default auto else { this.refresh = 'auto'; } - debug("Refresh is set to: " + this.refresh); + this.log("Refresh is set to: " + this.refresh + (this.refresh !== 'auto' ? ' seconds' : '')); } NeatoVacuumRobotPlatform.prototype = { accessories: function (callback) { + debug("##############################################"); + debug("################# GET ROBOTS #################"); + debug("##############################################"); let accessories = []; - let platform = this; - platform.boundaryNames = []; - this.getRobots(function () + this.boundaryNames = []; + this.getRobots(() => { - if (platform.robots) + this.robots.forEach((robot, i) => { - platform.robots.forEach((robot, i) => + this.log("Found robot #" + (i + 1) + " named \"" + robot.device.name + "\" with serial \"" + robot.device._serial + "\""); + this.updateRobotTimer(robot.device._serial); + + let NeatoVacuumRobotAccessory = require('./accessories/neatoVacuumRobot')(Service, Characteristic); + let mainAccessory = new NeatoVacuumRobotAccessory(robot, this); + accessories.push(mainAccessory); + + robot.mainAccessory = mainAccessory; + robot.roomAccessories = []; + + if (robot.device.maps) { - platform.log("Found robot #" + (i + 1) + " named \"" + robot.name + "\" with serial \"" + robot._serial + "\""); - - let NeatoVacuumRobotAccessory = require('./accessories/neatVacuumRobot')(Service, Characteristic); - let robotAccessory = new NeatoVacuumRobotAccessory(robot, platform); - accessories.push(robotAccessory); - - if (robot.maps) + robot.device.maps.forEach((map) => { - robot.maps.forEach((map) => + if (map.boundaries) { - if (map.boundaries) + map.boundaries.forEach((boundary) => { - map.boundaries.forEach((boundary) => + if (boundary.type === "polygon") { - if (boundary.type === "polygon") - { - accessories.push(new NeatoVacuumRobotAccessory(robot, platform, boundary)) - } - }) - } - }) - } - }) - } + let roomAccessory = new NeatoVacuumRobotAccessory(robot, this, boundary); + accessories.push(roomAccessory); + + robot.roomAccessories.push(roomAccessory); + } + }) + } + }) + } + }); callback(accessories); }); }, @@ -82,83 +95,162 @@ NeatoVacuumRobotPlatform.prototype = { { debug("Loading your robots"); let client = new botvac.Client(); - let that = this; + + // Login client.authorize(this.email, this.password, false, (error) => { if (error) { - that.log(error); - that.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."); + this.log.error("Can't log on to neato cloud. Please check your internet connection and your credentials. Try again later if the neato servers have issues: " + error); callback(); } else { + // Get robots client.getRobots((error, robots) => { if (error) { - that.log(error); - that.log.error("Successful login but can't connect to your neato robot."); + this.log.error("Successful login but can't connect to your neato robot: " + error); + callback(); + } + else if (robots.length === 0) + { + this.log.error("Successful login but no robots associated with your account."); + this.robots = []; callback(); } else { - if (robots.length === 0) + debug("Found " + robots.length + " robots"); + let requestedRobot = 0; + + robots.forEach((robot) => { - that.log.error("Successful login but no robots associated with your account."); - that.robots = []; - callback(); - } - else - { - debug("Found " + robots.length + " robots"); - let updatedRobotCount = 0; - that.robots = robots; - that.robots.forEach((robot) => + // Get Maps for each robot + robot.getPersistentMaps((error, result) => { - robot.getPersistentMaps((error, result) => + if (error) + { + this.log.error("Error updating persistent maps: " + error + ": " + result); + callback(); + } + else if (result.length === 0) + { + robot.maps = []; + callback(); + } + else { - if (error) - { - that.log("Error updating persistent maps: " + error + ": " + result); - callback(); - return; - } robot.maps = result; - let processedMapCount = 0; - if (robot.maps.length === 0) - { - callback(); - } + let requestedMap = 0; robot.maps.forEach((map) => { + // Get Map Boundary Lines robot.getMapBoundaries(map.id, (error, result) => { if (error) { - this.log("error getting boundaries: " + error + ": " + result) + this.log.error("Error getting boundaries: " + error + ": " + result) } else { map.boundaries = result.boundaries; } - processedMapCount++; - if (processedMapCount === robot.maps.length) + requestedMap++; + + // Robot is completely requested if all maps are requested + if (requestedMap === robot.maps.length) { - updatedRobotCount++; - if (updatedRobotCount === that.robots.length) + this.robots.push({device: robot}); + requestedRobot++; + + // Initial request is complete if all robots are requested. + if (requestedRobot === robots.length) { callback(); } } }) - }) - }) - }) - } + }); + } + }); + }); } }); } }); - } + }, + + updateRobot: function (serial, callback) + { + let robot = this.getRobot(serial); + + // Data is up to date + if (typeof (robot.lastUpdate) !== 'undefined' && new Date() - robot.lastUpdate < 2000) + { + callback(); + } + else + { + debug(robot.device.name + ": ++ Updating robot state"); + robot.device.getState(function (error, result) + { + if (error) + { + this.log.error("Cannot update robot. Check if robot is online. " + error); + } + robot.lastUpdate = new Date(); + callback(); + }); + } + }, + + getRobot(serial) + { + let result; + this.robots.forEach(function (robot) + { + if (robot.device._serial === serial) + { + result = robot; + } + }); + return result; + }, + + updateRobotTimer: function (serial) + { + this.updateRobot(serial, () => + { + let robot = this.getRobot(serial); + // Clear any other overlapping timers for this robot + clearTimeout(robot.timer); + + // Tell all accessories of this robot (mainAccessory and roomAccessories) that updated robot data is available + robot.mainAccessory.updated(); + robot.roomAccessories.forEach(accessory => + { + accessory.updated(); + }); + + // Periodic refresh interval set in config + if (this.refresh !== 'auto' && this.refresh !== 0) + { + debug(robot.device.name + ": ++ Next background update in " + this.refresh + " seconds"); + robot.timer = setTimeout(this.updateRobotTimer.bind(this), this.refresh * 1000, serial); + } + // Auto refresh set in config + else if (this.refresh === 'auto' && robot.device.canPause) + { + debug(robot.device.name + ": ++ Next background update in 60 seconds while cleaning (auto mode)"); + robot.timer = setTimeout(this.updateRobotTimer.bind(this), 60 * 1000, serial); + } + // No refresh + else + { + debug(robot.device.name + ": ++ Stopped background updates"); + } + }); + }, }; \ No newline at end of file diff --git a/package.json b/package.json index 8dc485c..0d97a98 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "homebridge-neato", - "version": "0.7.0-beta.0", + "version": "0.7.0-beta.1", "description": "A Neato vacuum robot plugin for homebridge.", "license": "MIT", "keywords": [