Moved update logic to platform to reduce server requests for multiple rooms

This commit is contained in:
Arne 2019-09-21 23:43:44 +02:00
parent 7594843bb3
commit 114ff70d60
3 changed files with 257 additions and 226 deletions

View File

@ -11,19 +11,19 @@ module.exports = function (_Service, _Characteristic)
return NeatoVacuumRobotAccessory; return NeatoVacuumRobotAccessory;
}; };
function NeatoVacuumRobotAccessory(robot, platform, boundary = undefined) function NeatoVacuumRobotAccessory(robotObject, platform, boundary = undefined)
{ {
this.platform = platform; this.platform = platform;
this.boundary = boundary; this.boundary = boundary;
this.log = platform.log; this.log = platform.log;
this.refresh = platform.refresh; this.refresh = platform.refresh;
this.hiddenServices = platform.hiddenServices; this.hiddenServices = platform.hiddenServices;
this.robot = robot; this.robot = robotObject.device;
this.nextRoom = null; this.nextRoom = null;
if (typeof boundary === 'undefined') if (typeof boundary === 'undefined')
{ {
this.name = robot.name; this.name = this.robot.name;
} }
else else
{ {
@ -46,7 +46,6 @@ function NeatoVacuumRobotAccessory(robot, platform, boundary = undefined)
platform.boundaryNames.push(this.boundary.name); platform.boundaryNames.push(this.boundary.name);
this.name = this.robot.name + ' - ' + this.boundary.name; this.name = this.robot.name + ' - ' + this.boundary.name;
} }
this.lastUpdate = null;
this.vacuumRobotBatteryService = new Service.BatteryService("Battery", "battery"); this.vacuumRobotBatteryService = new Service.BatteryService("Battery", "battery");
@ -72,24 +71,22 @@ function NeatoVacuumRobotAccessory(robot, platform, boundary = undefined)
new Service.Switch(serviceName, "cleanBoundary:" + boundary.id); new Service.Switch(serviceName, "cleanBoundary:" + boundary.id);
this.log("Adding zone cleaning for: " + boundary.name); this.log("Adding zone cleaning for: " + boundary.name);
} }
this.updateRobotTimer();
} }
NeatoVacuumRobotAccessory.prototype = { NeatoVacuumRobotAccessory.prototype = {
identify: function (callback) identify: function (callback)
{ {
let that = this; this.platform.updateRobot(this.robot._serial, () =>
this.updateRobot(function ()
{ {
// hide serial and secret in log // hide serial and secret in log
let _serial = that.robot._serial; let _serial = this.robot._serial;
let _secret = that.robot._secret; let _secret = this.robot._secret;
that.robot._serial = "*****"; this.robot._serial = "*****";
that.robot._secret = "*****"; this.robot._secret = "*****";
that.log(that.robot); this.log(this.robot);
that.robot._serial = _serial; this.robot._serial = _serial;
that.robot._secret = _secret; this.robot._secret = _secret;
callback();
}); });
}, },
@ -111,10 +108,7 @@ NeatoVacuumRobotAccessory.prototype = {
.setCharacteristic(Characteristic.Name, this.robot.name + ' - ' + this.boundary.name) .setCharacteristic(Characteristic.Name, this.robot.name + ' - ' + this.boundary.name)
} }
this.vacuumRobotBatteryService.getCharacteristic(Characteristic.BatteryLevel).on('get', this.getBatteryLevel.bind(this)); this.services = [this.informationService];
this.vacuumRobotBatteryService.getCharacteristic(Characteristic.ChargingState).on('get', this.getBatteryChargingState.bind(this));
this.services = [this.informationService, this.vacuumRobotBatteryService];
if (typeof this.boundary === "undefined") 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('set', this.setSchedule.bind(this));
this.vacuumRobotScheduleService.getCharacteristic(Characteristic.On).on('get', this.getSchedule.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.vacuumRobotCleanService);
this.services.push(this.vacuumRobotBatteryService);
if (this.hiddenServices.indexOf('dock') === -1) if (this.hiddenServices.indexOf('dock') === -1)
this.services.push(this.vacuumRobotGoToDockService); this.services.push(this.vacuumRobotGoToDockService);
@ -178,7 +176,7 @@ NeatoVacuumRobotAccessory.prototype = {
getClean: function (callback, boundary) getClean: function (callback, boundary)
{ {
this.updateRobot((error, result) => this.platform.updateRobot(this.robot._serial, (error, result) =>
{ {
let cleaning; let cleaning;
if (typeof boundary === 'undefined') if (typeof boundary === 'undefined')
@ -197,7 +195,7 @@ NeatoVacuumRobotAccessory.prototype = {
setClean: function (on, callback, boundary) setClean: function (on, callback, boundary)
{ {
this.updateRobot((error, result) => this.platform.updateRobot(this.robot._serial, (error, result) =>
{ {
// Start // Start
if (on) if (on)
@ -235,11 +233,6 @@ NeatoVacuumRobotAccessory.prototype = {
this.setGoToDock(true, (error, result) => this.setGoToDock(true, (error, result) =>
{ {
this.nextRoom = boundary; this.nextRoom = boundary;
setTimeout(() =>
{
this.clean(callback, boundary);
}, 1000);
}); });
} }
// Start new cleaning of new room // Start new cleaning of new room
@ -273,8 +266,7 @@ NeatoVacuumRobotAccessory.prototype = {
{ {
setTimeout(() => setTimeout(() =>
{ {
clearTimeout(this.timer); this.platform.updateRobotTimer(this.robot._serial);
this.updateRobotTimer();
}, 60 * 1000); }, 60 * 1000);
} }
@ -330,31 +322,30 @@ NeatoVacuumRobotAccessory.prototype = {
setGoToDock: function (on, callback) setGoToDock: function (on, callback)
{ {
let that = this; this.platform.updateRobot(this.robot._serial, (error, result) =>
this.updateRobot(function (error, result)
{ {
if (on) if (on)
{ {
if (that.robot.canPause) if (this.robot.canPause)
{ {
debug(that.name + ": Pause cleaning to go to dock"); debug(this.name + ": Pause cleaning to go to dock");
that.robot.pauseCleaning(function (error, result) this.robot.pauseCleaning((error, result) =>
{ {
setTimeout(function () setTimeout(() =>
{ {
debug(that.name + ": Go to dock"); debug(this.name + ": Go to dock");
that.robot.sendToBase(callback); this.robot.sendToBase(callback);
}, 1000); }, 1000);
}); });
} }
else if (that.robot.canGoToBase) else if (this.robot.canGoToBase)
{ {
debug(that.name + ": Go to dock"); debug(this.name + ": Go to dock");
that.robot.sendToBase(callback); this.robot.sendToBase(callback);
} }
else 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(); callback();
} }
} }
@ -367,140 +358,107 @@ NeatoVacuumRobotAccessory.prototype = {
getEco: function (callback) getEco: function (callback)
{ {
let that = this; this.platform.updateRobot(this.robot._serial, () =>
this.updateRobot(function ()
{ {
debug(that.name + ": Eco mode is " + (that.robot.eco ? 'ON' : 'OFF')); debug(this.name + ": Eco Mode is " + (this.robot.eco ? 'ON' : 'OFF'));
callback(false, that.robot.eco); callback(false, this.robot.eco);
}); });
}, },
setEco: function (on, callback) setEco: function (on, callback)
{ {
this.robot.eco = on; this.robot.eco = on;
debug(this.name + ": " + (on ? "Enabled" : "Disabled") + " Eco mode "); debug(this.name + ": " + (on ? "Enabled " : "Disabled") + " Eco Mode ");
callback(); callback();
}, },
getNoGoLines: function (callback) getNoGoLines: function (callback)
{ {
let that = this; this.platform.updateRobot(this.robot._serial, () =>
this.updateRobot(function ()
{ {
debug(that.name + ": Nogo Lines are " + (that.robot.eco ? 'ON' : 'OFF')); debug(this.name + ": NoGoLine is " + (this.robot.eco ? 'ON' : 'OFF'));
callback(false, that.robot.noGoLines ? 1 : 0); callback(false, this.robot.noGoLines ? 1 : 0);
}); });
}, },
setNoGoLines: function (on, callback) setNoGoLines: function (on, callback)
{ {
this.robot.noGoLines = on; this.robot.noGoLines = on;
debug(this.name + ": " + (on ? "Enabled" : "Disabled") + " Nogo lines "); debug(this.name + ": " + (on ? "Enabled " : "Disabled") + " NoGoLine ");
callback(); callback();
}, },
getExtraCare: function (callback) getExtraCare: function (callback)
{ {
let that = this; this.platform.updateRobot(this.robot._serial, () =>
this.updateRobot(function ()
{ {
debug(that.name + ": Extra Care Navigation is " + (that.robot.navigationMode == 2 ? 'ON' : 'OFF')); debug(this.name + ": Care Nav is " + (this.robot.navigationMode === 2 ? 'ON' : 'OFF'));
callback(false, that.robot.navigationMode == 2 ? 1 : 0); callback(false, this.robot.navigationMode === 2 ? 1 : 0);
}); });
}, },
setExtraCare: function (on, callback) setExtraCare: function (on, callback)
{ {
this.robot.navigationMode = on ? 2 : 1; this.robot.navigationMode = on ? 2 : 1;
debug(this.name + ": " + (on ? "Enabled" : "Disabled") + " Extra Care Navigation "); debug(this.name + ": " + (on ? "Enabled " : "Disabled") + " Care Nav ");
callback(); callback();
}, },
getSchedule: function (callback) getSchedule: function (callback)
{ {
let that = this; this.platform.updateRobot(this.robot._serial,() =>
this.updateRobot(function ()
{ {
debug(that.name + ": Schedule is " + (that.robot.eco ? 'ON' : 'OFF')); debug(this.name + ": Schedule is " + (this.robot.eco ? 'ON' : 'OFF'));
callback(false, that.robot.isScheduleEnabled); callback(false, this.robot.isScheduleEnabled );
}); });
}, },
setSchedule: function (on, callback) setSchedule: function (on, callback)
{ {
let that = this; this.platform.updateRobot(this.robot._serial, (error, result) =>
this.updateRobot(function (error, result)
{ {
if (on) if (on)
{ {
debug(that.name + ": Enabled Schedule"); debug(this.name + ": Enabled Schedule");
that.robot.enableSchedule(callback); this.robot.enableSchedule(callback);
} }
else else
{ {
debug(that.name + ": Disabled Schedule"); debug(this.name + ": Disabled Schedule");
that.robot.disableSchedule(callback); this.robot.disableSchedule(callback);
} }
}); });
}, },
getDock: function (callback) getDock: function (callback)
{ {
let that = this; this.platform.updateRobot(this.robot._serial, () =>
this.updateRobot(function ()
{ {
debug(that.name + ": Is " + (that.robot.isDocked ? '' : 'not ') + "docked"); debug(this.name + ": The Dock is " + (this.robot.isDocked ? '' : 'un ') + "occupied");
callback(false, that.robot.isDocked ? 1 : 0); callback(false, this.robot.isDocked ? 1 : 0);
}); });
}, },
getBatteryLevel: function (callback) getBatteryLevel: function (callback)
{ {
let that = this; this.platform.updateRobot(this.robot._serial, () =>
this.updateRobot(function ()
{ {
debug(that.name + ": Battery is at " + that.robot.charge + "%"); debug(this.name + ": Battery is " + this.robot.charge + "%");
callback(false, that.robot.charge); callback(false, this.robot.charge);
}); });
}, },
getBatteryChargingState: function (callback) getBatteryChargingState: function (callback)
{ {
let that = this; this.platform.updateRobot(this.robot._serial, () =>
this.updateRobot(function ()
{ {
debug(that.name + ": Is " + (that.robot.isCharging ? '' : 'not ') + "charging"); debug(this.name + ": Battery is " + (this.robot.isCharging ? '' : 'not ') + "charging");
callback(false, that.robot.isCharging); callback(false, this.robot.isCharging);
}); });
}, },
updateRobot: function (callback) updated: function ()
{ {
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("Cannot update robot. Check if robot is online. " + error);
}
that.lastUpdate = new Date();
callback();
});
}
},
updateRobotTimer: function ()
{
this.updateRobot((error, result) =>
{
if (!this.boundary) if (!this.boundary)
{ {
// only update these values if the state is different from the current one, otherwise we might accidentally start an action // only update these values if the state is different from the current one, otherwise we might accidentally start an action
@ -515,9 +473,9 @@ NeatoVacuumRobotAccessory.prototype = {
this.vacuumRobotGoToDockService.setCharacteristic(Characteristic.On, false); this.vacuumRobotGoToDockService.setCharacteristic(Characteristic.On, false);
} }
if (this.vacuumRobotScheduleService.getCharacteristic(Characteristic.On).value !== this.robot.isScheduleEnabled) if (this.vacuumRobotScheduleService.getCharacteristic(Characteristic.On).value !== this.robot.isScheduleEnabled )
{ {
this.vacuumRobotScheduleService.setCharacteristic(Characteristic.On, this.robot.isScheduleEnabled); this.vacuumRobotScheduleService.setCharacteristic(Characteristic.On, this.robot.isScheduleEnabled );
} }
// no commands here, values can be updated without problems // no commands here, values can be updated without problems
@ -544,27 +502,8 @@ NeatoVacuumRobotAccessory.prototype = {
this.clean((error, result) => this.clean((error, result) =>
{ {
this.nextRoom = null; this.nextRoom = null;
debug("Starting cleaning of next room");
}, this.nextRoom); }, 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");
}
});
},
}; };

178
index.js
View File

@ -21,6 +21,9 @@ function NeatoVacuumRobotPlatform(log, config)
this.password = config['password']; this.password = config['password'];
this.hiddenServices = ('disabled' in config ? config['disabled'] : ''); 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') if ('refresh' in config && config['refresh'] !== 'auto')
{ {
// parse config parameter // parse config parameter
@ -28,37 +31,45 @@ function NeatoVacuumRobotPlatform(log, config)
// must be integer and positive // must be integer and positive
this.refresh = (typeof this.refresh !== 'number' || (this.refresh % 1) !== 0 || this.refresh < 0) ? 60 : this.refresh; 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 // minimum 60s to save some load on the neato servers
if (this.refresh > 0 && this.refresh < 60)
{
this.log.warn("Minimum refresh time is 60 seconds to not overload the neato servers");
this.refresh = (this.refresh > 0 && this.refresh < 60) ? 60 : this.refresh; this.refresh = (this.refresh > 0 && this.refresh < 60) ? 60 : this.refresh;
} }
}
// default auto // default auto
else else
{ {
this.refresh = 'auto'; this.refresh = 'auto';
} }
debug("Refresh is set to: " + this.refresh); this.log("Refresh is set to: " + this.refresh + (this.refresh !== 'auto' ? ' seconds' : ''));
} }
NeatoVacuumRobotPlatform.prototype = { NeatoVacuumRobotPlatform.prototype = {
accessories: function (callback) accessories: function (callback)
{ {
debug("##############################################");
debug("################# GET ROBOTS #################");
debug("##############################################");
let accessories = []; let accessories = [];
let platform = this; this.boundaryNames = [];
platform.boundaryNames = []; this.getRobots(() =>
this.getRobots(function ()
{ {
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);
platform.log("Found robot #" + (i + 1) + " named \"" + robot.name + "\" with serial \"" + robot._serial + "\"");
let NeatoVacuumRobotAccessory = require('./accessories/neatVacuumRobot')(Service, Characteristic); let NeatoVacuumRobotAccessory = require('./accessories/neatoVacuumRobot')(Service, Characteristic);
let robotAccessory = new NeatoVacuumRobotAccessory(robot, platform); let mainAccessory = new NeatoVacuumRobotAccessory(robot, this);
accessories.push(robotAccessory); accessories.push(mainAccessory);
if (robot.maps) robot.mainAccessory = mainAccessory;
robot.roomAccessories = [];
if (robot.device.maps)
{ {
robot.maps.forEach((map) => robot.device.maps.forEach((map) =>
{ {
if (map.boundaries) if (map.boundaries)
{ {
@ -66,14 +77,16 @@ NeatoVacuumRobotPlatform.prototype = {
{ {
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); callback(accessories);
}); });
}, },
@ -82,83 +95,162 @@ NeatoVacuumRobotPlatform.prototype = {
{ {
debug("Loading your robots"); debug("Loading your robots");
let client = new botvac.Client(); let client = new botvac.Client();
let that = this;
// Login
client.authorize(this.email, this.password, false, (error) => client.authorize(this.email, this.password, false, (error) =>
{ {
if (error) if (error)
{ {
that.log(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);
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.");
callback(); callback();
} }
else else
{ {
// Get robots
client.getRobots((error, robots) => client.getRobots((error, robots) =>
{ {
if (error) if (error)
{ {
that.log(error); this.log.error("Successful login but can't connect to your neato robot: " + error);
that.log.error("Successful login but can't connect to your neato robot.");
callback(); callback();
} }
else else if (robots.length === 0)
{ {
if (robots.length === 0) this.log.error("Successful login but no robots associated with your account.");
{ this.robots = [];
that.log.error("Successful login but no robots associated with your account.");
that.robots = [];
callback(); callback();
} }
else else
{ {
debug("Found " + robots.length + " robots"); debug("Found " + robots.length + " robots");
let updatedRobotCount = 0; let requestedRobot = 0;
that.robots = robots;
that.robots.forEach((robot) => robots.forEach((robot) =>
{ {
// Get Maps for each robot
robot.getPersistentMaps((error, result) => robot.getPersistentMaps((error, result) =>
{ {
if (error) if (error)
{ {
that.log("Error updating persistent maps: " + error + ": " + result); this.log.error("Error updating persistent maps: " + error + ": " + result);
callback(); callback();
return;
} }
robot.maps = result; else if (result.length === 0)
let processedMapCount = 0;
if (robot.maps.length === 0)
{ {
robot.maps = [];
callback(); callback();
} }
else
{
robot.maps = result;
let requestedMap = 0;
robot.maps.forEach((map) => robot.maps.forEach((map) =>
{ {
// Get Map Boundary Lines
robot.getMapBoundaries(map.id, (error, result) => robot.getMapBoundaries(map.id, (error, result) =>
{ {
if (error) if (error)
{ {
this.log("error getting boundaries: " + error + ": " + result) this.log.error("Error getting boundaries: " + error + ": " + result)
} }
else else
{ {
map.boundaries = result.boundaries; map.boundaries = result.boundaries;
} }
processedMapCount++; requestedMap++;
if (processedMapCount === robot.maps.length)
// Robot is completely requested if all maps are requested
if (requestedMap === robot.maps.length)
{ {
updatedRobotCount++; this.robots.push({device: robot});
if (updatedRobotCount === that.robots.length) requestedRobot++;
// Initial request is complete if all robots are requested.
if (requestedRobot === robots.length)
{ {
callback(); 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");
}
});
},
}; };

View File

@ -1,6 +1,6 @@
{ {
"name": "homebridge-neato", "name": "homebridge-neato",
"version": "0.7.0-beta.0", "version": "0.7.0-beta.1",
"description": "A Neato vacuum robot plugin for homebridge.", "description": "A Neato vacuum robot plugin for homebridge.",
"license": "MIT", "license": "MIT",
"keywords": [ "keywords": [