@homebridge-plugins/homebridge-roomba
Version:
homebridge-plugin for Roomba devices
255 lines • 9.39 kB
JavaScript
import { Buffer } from 'node:buffer';
import * as dgram from 'node:dgram';
import * as https from 'node:https';
export async function getRoombas(email, password, log, config) {
let robots = [];
if (config.disableDiscovery) {
log.info('Using manual discovery as per config');
robots = config.roombas || [];
}
else {
log.info('Logging into iRobot...');
try {
const credentials = await getCredentials(email, password);
robots = await iRobotLogin(credentials);
log.debug('robots:', JSON.stringify(robots));
}
catch (e) {
log.error('Failed to login to iRobot, see below for details');
log.error(e.message ?? e);
}
}
// Extract the key from the JSON object and set it as the blid if not provided or if blid is 0
for (const key in robots) {
if (Object.prototype.hasOwnProperty.call(robots, key)) {
const robot = robots[key];
if (!robot.blid || robot.blid === '0') {
robot.blid = key;
log.debug(`Set blid for robot ${robot.name} to ${robot.blid}`);
}
}
}
// Ensure robots is an array
if (!Array.isArray(robots)) {
log.debug('Converting robots object to array');
robots = Object.values(robots);
}
log.debug('Processed robots:', JSON.stringify(robots));
const goodRoombas = [];
const badRoombas = [];
for (const robot of robots) {
if (!config.disableDiscovery) {
log.debug('roomba name:', robot.name, 'blid:', robot.blid, 'password:', robot.password);
if (!robot.name || !robot.blid || !robot.password) {
log.error('Skipping configuration for roomba:', robot.name, 'due to missing name, blid or password');
continue;
}
log.info('Configuring roomba:', robot.name);
try {
const robotIP = await getIP(robot.blid);
robot.ip = robotIP.ip;
robot.model = getModel(robotIP.sku);
robot.multiRoom = getMultiRoom(robot.model);
robot.info = robotIP;
goodRoombas.push(robot);
}
catch (e) {
log.error('Failed to connect roomba:', robot.name, 'with error:', e.message ?? e);
log.error('This usually happens if the Roomba is not on the same network as Homebridge, or the Roomba is not reachable from the network');
badRoombas.push(robot);
}
}
else {
log.info('Skipping configuration for roomba:', robot.name, 'due to config');
}
}
for (const roomba of badRoombas) {
log.warn('Not creating an accessory for unreachable Roomba:', roomba.name);
}
return goodRoombas;
}
function getModel(sku) {
switch (sku.charAt(0)) {
case 'j':
case 'i':
case 's':
return sku.substring(0, 2);
case 'R':
return sku.substring(1, 4);
default:
return sku;
}
}
function getMultiRoom(model) {
switch (model.charAt(0)) {
case 's':
case 'j':
return Number.parseInt(model.charAt(1)) > 4;
case 'i':
return Number.parseInt(model.charAt(1)) > 2;
case 'm':
return Number.parseInt(model.charAt(1)) === 6;
default:
return false;
}
}
async function getIP(blid, attempt = 1) {
return new Promise((resolve, reject) => {
if (attempt > 5) {
reject(new Error(`No Roomba Found With Blid: ${blid}`));
return;
}
const server = dgram.createSocket('udp4');
server.on('error', (err) => {
reject(err);
});
server.on('message', (msg) => {
try {
const parsedMsg = JSON.parse(msg.toString());
const [prefix, id] = parsedMsg.hostname.split('-');
if ((prefix === 'Roomba' || prefix === 'iRobot') && id === blid) {
server.close();
resolve(parsedMsg);
}
}
catch (e) { }
});
server.on('listening', () => {
setTimeout(() => {
getIP(blid, attempt + 1).then(resolve).catch(reject);
}, 5000);
});
server.bind(() => {
const message = Buffer.from('irobotmcs');
server.setBroadcast(true);
server.send(message, 0, message.length, 5678, '255.255.255.255');
});
});
}
async function getCredentials(email, password) {
return new Promise((resolve, reject) => {
const apiKey = '3_rWtvxmUKwgOzu3AUPTMLnM46lj-LxURGflmu5PcE_sGptTbD-wMeshVbLvYpq01K';
const gigyaURL = new URL('https://accounts.us1.gigya.com/accounts.login');
gigyaURL.search = new URLSearchParams({
apiKey,
targetenv: 'mobile',
loginID: email,
password,
format: 'json',
targetEnv: 'mobile',
}).toString();
const gigyaLoginOptions = {
hostname: gigyaURL.hostname,
path: gigyaURL.pathname + gigyaURL.search,
method: 'POST',
headers: {
Connection: 'close',
},
};
const req = https.request(gigyaLoginOptions, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
gigyaLoginResponse(null, res, JSON.parse(data), resolve, reject);
});
});
req.on('error', (error) => {
gigyaLoginResponse(error, undefined, undefined, resolve, reject);
});
req.end();
});
}
function gigyaLoginResponse(error, response, body, resolve, reject) {
if (error) {
reject?.(new Error(`Fatal error logging into Gigya API. Please check your credentials or Gigya API Key. ${error.message}`));
return;
}
if (response?.statusCode !== undefined && [401, 403].includes(response.statusCode)) {
reject?.(new Error(`Authentication error. Check your credentials. ${response.statusCode}`));
}
else if (response && response.statusCode === 400) {
reject?.(new Error(`Error logging into Gigya API. ${response.statusCode}`));
}
else if (response && response.statusCode === 200) {
gigyaSuccess(body, resolve, reject);
}
else {
reject?.(new Error('Unexpected response. Checking again...'));
}
}
function gigyaSuccess(body, resolve, reject) {
if (body.statusCode === 403) {
reject?.(new Error(`Authentication error. Please check your credentials. ${body.statusCode}`));
return;
}
if (body.statusCode === 400) {
reject?.(new Error(`Error logging into Gigya API. ${body.statusCode}`));
return;
}
if (body.statusCode === 200 && body.errorCode === 0 && body.UID && body.UIDSignature && body.signatureTimestamp && body.sessionInfo && body.sessionInfo.sessionToken) {
resolve?.(body);
}
else {
reject?.(new Error(`Error logging into iRobot account. Missing fields in login response. ${body.statusCode}`));
}
}
async function iRobotLogin(body, server = 1) {
return new Promise((resolve, reject) => {
const iRobotLoginOptions = {
hostname: `unauth${server}.prod.iot.irobotapi.com`,
path: '/v2/login',
method: 'POST',
headers: {
'Connection': 'close',
'Content-Type': 'application/json',
},
};
const req = https.request(iRobotLoginOptions, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
try {
iRobotLoginResponse(null, res, JSON.parse(data), resolve, reject);
}
catch (e) {
if (server === 1) {
iRobotLogin(body, 2).then(resolve).catch(reject);
}
else {
iRobotLoginResponse(e.message ?? e, undefined, undefined, resolve, reject);
}
}
});
});
req.on('error', (error) => {
iRobotLoginResponse(error, undefined, undefined, resolve, reject);
});
req.write(JSON.stringify({
app_id: 'ANDROID-C7FB240E-DF34-42D7-AE4E-A8C17079A294',
assume_robot_ownership: 0,
gigya: {
signature: body.UIDSignature,
timestamp: body.signatureTimestamp,
uid: body.UID,
},
}));
req.end();
});
}
function iRobotLoginResponse(error, _response, body, resolve, reject) {
if (error) {
reject?.(new Error(`Fatal error logging into iRobot account. Please check your credentials or API Key. ${error.message}`));
return;
}
if (body && body.robots) {
resolve?.(body.robots);
}
else {
reject?.(new Error(`Fatal error logging into iRobot account. Please check your credentials or API Key. ${body?.statusCode}`));
}
}
//# sourceMappingURL=roomba.js.map