@homebridge-plugins/homebridge-roomba
Version:
homebridge-plugin for Roomba devices
849 lines • 36.7 kB
JavaScript
import dorita980 from 'dorita980';
/**
* How long to wait to connect to Roomba.
*/
const CONNECT_TIMEOUT_MILLIS = 60_000;
/**
* How long after HomeKit has asked for the plugin's status should we continue frequently monitoring and reporting Roomba's status?
*/
const USER_INTERESTED_MILLIS = 60_000;
/**
* How long after Roomba has been active should we continue frequently monitoring and reporting Roomba's status?
*/
const AFTER_ACTIVE_MILLIS = 120_000;
/**
* How long will we wait for the Roomba to send status before giving up?
*/
const STATUS_TIMEOUT_MILLIS = 60_000;
/**
* Coalesce multiple refreshState requests into one when they're less than this many millis apart.
*/
const REFRESH_STATE_COALESCE_MILLIS = 10_000;
const ROBOT_CIPHERS = ['AES128-SHA256', 'TLS_AES_256_GCM_SHA384'];
const EMPTY_STATUS = {
timestamp: 0,
};
const NO_VALUE = new Error('No value');
async function delay(duration) {
return new Promise((resolve) => {
setTimeout(resolve, duration);
});
}
export default class RoombaAccessory {
platform;
api;
log;
name;
model;
serialnum;
blid;
robotpwd;
ipaddress;
cleanBehaviour;
mission;
stopBehaviour;
debug;
idlePollIntervalMillis;
deviceInfo;
version;
accessoryInfo;
filterMaintenance;
switchService;
batteryService;
dockService;
runningService;
binService;
dockingService;
homeService;
tankService;
/**
* The last known state from Roomba, if any.
*/
cachedStatus = EMPTY_STATUS;
lastUpdatedStatus = EMPTY_STATUS;
lastRefreshState = 0;
/**
* The current promise that returns a Roomba instance (_only_ used in the connect() method).
*/
_currentRoombaPromise;
/**
* Whether the plugin is actively polling Roomba's state and updating HomeKit
*/
currentPollTimeout;
/**
* When we think a user / HomeKit was last interested in Roomba's state.
*/
userLastInterestedTimestamp;
/**
* When we last saw the Roomba active.
*/
roombaLastActiveTimestamp;
/**
* The duration of the last poll interval used.
*/
lastPollInterval;
/**
* An index into `ROBOT_CIPHERS` indicating the current cipher configuration used to communicate with Roomba.
*/
currentCipherIndex = 0;
constructor(platform, accessory, log, device, config, api) {
this.platform = platform;
this.api = api;
this.log = log;
this.debug = !!config.debug;
if (!this.debug) {
this.log = log;
}
else {
this.log = Object.assign(log, {
debug: (message, ...parameters) => {
log.info(`DEBUG: ${message}`, ...parameters);
},
});
}
this.name = device.name;
this.model = device.model;
const { serialNumber, deviceInfo } = this.serialNum(device);
this.deviceInfo = deviceInfo;
this.serialnum = serialNumber;
this.blid = device.blid;
this.robotpwd = device.password;
this.ipaddress = device.ipaddress ?? device.ip;
this.version = device.softwareVer ?? this.platform.version ?? '0.0.0';
this.cleanBehaviour = device.cleanBehaviour !== undefined ? device.cleanBehaviour : 'everywhere';
this.mission = device.mission || { pmap_id: 'local' };
this.stopBehaviour = device.stopBehaviour !== undefined ? device.stopBehaviour : 'home';
this.idlePollIntervalMillis = device.idleWatchInterval ? (device.idleWatchInterval * 60_000) : config.idleWatchInterval ? (config.idleWatchInterval * 60_000) : 900_000;
const showDockAsContactSensor = device.dockContactSensor === undefined ? true : device.dockContactSensor;
const showRunningAsContactSensor = device.runningContactSensor;
const showBinStatusAsContactSensor = device.binContactSensor;
const showDockingAsContactSensor = device.dockingContactSensor;
const showHomeSwitch = device.homeSwitch;
const showTankAsFilterMaintenance = device.tankContactSensor;
const Service = api.hap.Service;
const Characteristic = this.api.hap.Characteristic;
function removeServiceIfPresent(uuid, subType) {
const service = accessory.getServiceById(uuid, subType);
if (service) {
accessory.removeService(service);
}
}
this.accessoryInfo = accessory.getService(Service.AccessoryInformation) || accessory.addService(Service.AccessoryInformation);
this.filterMaintenance = accessory.getService(Service.FilterMaintenance) || accessory.addService(Service.FilterMaintenance);
this.switchService = accessory.getService(this.name) || accessory.addService(Service.Switch, this.name);
this.switchService.setPrimaryService(true);
this.batteryService = accessory.getService(Service.Battery) || accessory.addService(Service.Battery);
const DOCK_SERVICE_NAME = `${this.name} Dock`;
const RUNNING_SERVICE_NAME = `${this.name} Running`;
const BIN_SERVICE_NAME = `${this.name} Bin Full`;
const DOCKING_SERVICE_NAME = `${this.name} Docking`;
const TANK_SERVICE_NAME = `${this.name} Water Tank Empty`;
const HOME_SERVICE_NAME = `${this.name} Home`;
if (showDockAsContactSensor) {
this.dockService = accessory.getServiceById(Service.ContactSensor, 'docked') || accessory.addService(Service.ContactSensor, DOCK_SERVICE_NAME, 'docked');
}
else {
removeServiceIfPresent(Service.ContactSensor, 'docked');
}
if (showRunningAsContactSensor) {
this.runningService = accessory.getServiceById(Service.ContactSensor, 'running') || accessory.addService(Service.ContactSensor, RUNNING_SERVICE_NAME, 'running');
}
else {
removeServiceIfPresent(Service.ContactSensor, 'running');
}
if (showBinStatusAsContactSensor) {
this.binService = accessory.getServiceById(Service.ContactSensor, 'Full') || accessory.addService(Service.ContactSensor, BIN_SERVICE_NAME, 'Full');
}
else {
removeServiceIfPresent(Service.ContactSensor, 'Full');
}
if (showDockingAsContactSensor) {
this.dockingService = accessory.getServiceById(Service.ContactSensor, 'docking') || accessory.addService(Service.ContactSensor, DOCKING_SERVICE_NAME, 'docking');
}
else {
removeServiceIfPresent(Service.ContactSensor, 'docking');
}
if (showTankAsFilterMaintenance) {
this.tankService = accessory.getServiceById(Service.FilterMaintenance, 'Empty') || accessory.addService(Service.FilterMaintenance, TANK_SERVICE_NAME, 'Empty');
}
else {
removeServiceIfPresent(Service.FilterMaintenance, 'Empty');
}
if (showHomeSwitch) {
this.homeService = accessory.getServiceById(Service.Switch, 'returning') || accessory.addService(Service.Switch, HOME_SERVICE_NAME, 'returning');
}
else {
removeServiceIfPresent(Service.Switch, 'returning');
}
// Set accessory information
this.accessoryInfo
.setCharacteristic(Characteristic.Manufacturer, 'iRobot')
.setCharacteristic(Characteristic.AppMatchingIdentifier, 'id1012014442')
.setCharacteristic(Characteristic.SerialNumber, this.serialnum)
.setCharacteristic(Characteristic.Identify, true)
.setCharacteristic(Characteristic.Name, this.name)
.setCharacteristic(Characteristic.Model, this.model)
.setCharacteristic(Characteristic.ProductData, this.blid)
.setCharacteristic(Characteristic.FirmwareRevision, this.version);
this.switchService
.setCharacteristic(Characteristic.Name, this.name)
.getCharacteristic(Characteristic.On)
.on('set', this.setRunningState.bind(this))
.on('get', this.createCharacteristicGetter('Running status', this.runningStatus));
this.batteryService
.getCharacteristic(Characteristic.BatteryLevel)
.on('get', this.createCharacteristicGetter('Battery level', this.batteryLevelStatus));
this.batteryService
.getCharacteristic(Characteristic.ChargingState)
.on('get', this.createCharacteristicGetter('Charging status', this.chargingStatus));
this.batteryService
.getCharacteristic(Characteristic.StatusLowBattery)
.on('get', this.createCharacteristicGetter('Low Battery status', this.batteryStatus));
this.filterMaintenance
.getCharacteristic(Characteristic.FilterChangeIndication)
.on('get', this.createCharacteristicGetter('Bin status', this.binStatus));
if (this.dockService) {
this.dockService
.setCharacteristic(Characteristic.Name, DOCK_SERVICE_NAME)
.getCharacteristic(Characteristic.ContactSensorState)
.on('get', this.createCharacteristicGetter('Dock status', this.dockedStatus));
}
if (this.runningService) {
this.runningService
.setCharacteristic(Characteristic.Name, RUNNING_SERVICE_NAME)
.getCharacteristic(Characteristic.ContactSensorState)
.on('get', this.createCharacteristicGetter('Running status', this.runningStatus));
}
if (this.binService) {
this.binService
.setCharacteristic(Characteristic.Name, BIN_SERVICE_NAME)
.getCharacteristic(Characteristic.ContactSensorState)
.on('get', this.createCharacteristicGetter('Bin status', this.binStatus));
}
if (this.dockingService) {
this.dockingService
.setCharacteristic(Characteristic.Name, DOCKING_SERVICE_NAME)
.getCharacteristic(Characteristic.ContactSensorState)
.on('get', this.createCharacteristicGetter('Docking status', this.dockingStatus));
}
if (this.homeService) {
this.homeService
.setCharacteristic(Characteristic.Name, HOME_SERVICE_NAME)
.getCharacteristic(Characteristic.On)
.on('set', this.setDockingState.bind(this))
.on('get', this.createCharacteristicGetter('Returning Home', this.dockingStatus));
}
if (this.tankService) {
this.tankService
.setCharacteristic(Characteristic.Name, TANK_SERVICE_NAME)
.getCharacteristic(Characteristic.FilterChangeIndication)
.on('get', this.createCharacteristicGetter('Tank status', this.tankStatus));
this.tankService
.getCharacteristic(Characteristic.FilterLifeLevel)
.on('get', this.createCharacteristicGetter('Tank level', this.tankLevelStatus));
}
this.startPolling();
}
serialNum(device) {
let deviceInfo;
let serialNumber;
const serialNum = device.ipaddress ?? device.ip;
if (device.info) {
deviceInfo = device.info;
if (device.info.serialNum) {
serialNumber = device.info.serialNum;
return { serialNumber, deviceInfo };
}
else {
serialNumber = serialNum;
return { serialNumber, deviceInfo };
}
}
else {
deviceInfo = undefined;
serialNumber = serialNum;
return { serialNumber, deviceInfo };
}
}
identify() {
this.log.info('Identify requested');
this.connect(async (error, roomba) => {
if (error || !roomba) {
return;
}
try {
await roomba.find();
}
catch (error) {
this.log.warn('Roomba failed to locate: %s', error.message);
}
});
}
getServices() {
const services = [
this.accessoryInfo,
this.switchService,
this.batteryService,
this.filterMaintenance,
];
if (this.dockService) {
services.push(this.dockService);
}
if (this.runningService) {
services.push(this.runningService);
}
if (this.binService) {
services.push(this.binService);
}
if (this.dockingService) {
services.push(this.dockingService);
}
if (this.homeService) {
services.push(this.homeService);
}
if (this.tankService) {
services.push(this.tankService);
}
return services;
}
/**
* Refresh our knowledge of Roomba's state by connecting to Roomba and getting its status.
* @param callback a function to call when the state refresh has completed.
*/
refreshState(callback) {
const now = Date.now();
this.connect(async (error, roomba) => {
if (error || !roomba) {
this.log.warn('Failed to refresh Roomba\'s state: %s', error ? error.message : 'Unknown');
callback(false);
return;
}
const startedWaitingForStatus = Date.now();
/* Wait until we've received a state with all of the information we desire */
return new Promise((resolve) => {
let receivedState;
const timeout = setTimeout(() => {
this.log.debug('Timeout waiting for full state from Roomba ({}ms). Last state received was: %s', Date.now() - startedWaitingForStatus, receivedState ? JSON.stringify(receivedState) : '<none>');
resolve();
callback(false);
}, STATUS_TIMEOUT_MILLIS);
const updateState = (state) => {
receivedState = state;
if (this.receivedRobotStateIsComplete(state)) {
clearTimeout(timeout);
/* NB: the actual state is received and updated in the listener in connect() */
this.log.debug('Refreshed Roomba\'s state in %ims: %s', Date.now() - now, JSON.stringify(state));
roomba.off('state', updateState);
resolve();
callback(true);
}
};
roomba.on('state', updateState);
});
});
}
receivedRobotStateIsComplete(state) {
return (state.batPct !== undefined && state.bin !== undefined && state.cleanMissionStatus !== undefined);
}
receiveRobotState(state) {
const parsed = this.parseState(state);
this.mergeCachedStatus(parsed);
return true;
}
/**
* Returns a Promise that, when resolved, provides access to a connected Roomba instance.
* In order to reuse connected Roomba instances, this function returns the same Promise across
* multiple calls, until that Roomba instance is disconnected.
* <p>
* If the Promise fails it means there was a failure connecting to the Roomba instance.
* @returns a RoombaHolder containing a connected Roomba instance
*/
async connectedRoomba(attempts = 0) {
return new Promise((resolve, reject) => {
let connected = false;
let failed = false;
const roomba = new dorita980.Local(this.blid, this.robotpwd, this.ipaddress, 2, {
ciphers: ROBOT_CIPHERS[this.currentCipherIndex],
});
const startConnecting = Date.now();
const timeout = setTimeout(() => {
failed = true;
this.log.debug('Timed out after %ims trying to connect to Roomba', Date.now() - startConnecting);
roomba.end();
reject(new Error('Connect timed out'));
}, CONNECT_TIMEOUT_MILLIS);
roomba.on('state', (state) => {
this.receiveRobotState(state);
});
const onError = (error) => {
this.log.debug('Connection received error: %s', error.message);
roomba.off('error', onError);
roomba.end();
clearTimeout(timeout);
if (!connected) {
failed = true;
/* Check for recoverable errors */
if (error instanceof Error && shouldTryDifferentCipher(error) && attempts < ROBOT_CIPHERS.length) {
/* Perhaps a cipher error, so we retry using the next cipher */
this.currentCipherIndex = (this.currentCipherIndex + 1) % ROBOT_CIPHERS.length;
this.log.debug('Retrying connection to Roomba with cipher %s', ROBOT_CIPHERS[this.currentCipherIndex]);
this.connectedRoomba(attempts + 1).then(resolve).catch(reject);
}
else {
reject(error);
}
}
};
roomba.on('error', onError);
this.log.debug('Connecting to Roomba...');
const onConnect = () => {
roomba.off('connect', onConnect);
clearTimeout(timeout);
if (failed) {
this.log.debug('Connection established to Roomba after failure');
return;
}
connected = true;
this.log.debug('Connected to Roomba in %ims', Date.now() - startConnecting);
resolve({
roomba,
useCount: 0,
});
};
roomba.on('connect', onConnect);
});
}
connect(callback) {
/* Use the current Promise, if possible, so we share the connected Roomba instance, whether
it is already connected, or when it becomes connected.
*/
this.log.debug('currentRoombaPromise: %s', this._currentRoombaPromise ? 'yes' : 'no');
const promise = this._currentRoombaPromise || this.connectedRoomba();
this._currentRoombaPromise = promise;
promise.then((holder) => {
holder.useCount++;
callback(null, holder.roomba).finally(() => {
holder.useCount--;
if (holder.useCount <= 0) {
this._currentRoombaPromise = undefined;
holder.roomba.end();
}
else {
this.log.debug('Leaving Roomba instance with %i ongoing requests', holder.useCount);
}
});
}).catch((error) => {
/* Failed to connect to Roomba */
this._currentRoombaPromise = undefined;
callback(error);
});
}
setRunningState(powerOn, callback) {
if (powerOn) {
this.log.info('Starting Roomba');
this.connect(async (error, roomba) => {
if (error || !roomba) {
callback(error || new Error('Unknown error'));
return;
}
try {
/* If Roomba is paused in a clean cycle we need to instruct it to resume instead, otherwise we just start a clean. */
if (this.cachedStatus.paused) {
await roomba.resume();
}
else {
if (this.cleanBehaviour === 'rooms') {
await roomba.cleanRoom(this.mission);
this.log.debug('Roomba is cleaning your rooms');
}
else {
await roomba.clean();
this.log.debug('Roomba is running');
}
}
callback();
/* After sending an action to Roomba, we start polling to ensure HomeKit has up to date status */
this.refreshStatusForUser();
}
catch (error) {
this.log.warn('Roomba failed: %s', error.message);
callback(error);
}
});
}
else {
this.log.info('Stopping Roomba');
this.connect(async (error, roomba) => {
if (error || !roomba) {
callback(error || new Error('Unknown error'));
return;
}
try {
const response = await roomba.getRobotState(['cleanMissionStatus']);
const state = this.parseState(response);
if (state.running) {
this.log.debug('Roomba is pausing');
await roomba.pause();
callback();
if (this.stopBehaviour === 'home') {
this.log.debug('Roomba paused, returning to Dock');
await this.dockWhenStopped(roomba, 3000);
}
else {
this.log.debug('Roomba is paused');
}
}
else if (state.docking) {
this.log.debug('Roomba is docking');
await roomba.pause();
callback();
this.log.debug('Roomba paused');
}
else if (state.charging) {
this.log.debug('Roomba is already docked');
callback();
}
else {
this.log.debug('Roomba is not running');
callback();
}
this.refreshStatusForUser();
}
catch (error) {
this.log.warn('Roomba failed: %s', error.message);
callback(error);
}
});
}
}
setDockingState(docking, callback) {
this.log.debug('Setting docking state to %s', JSON.stringify(docking));
this.connect(async (error, roomba) => {
if (error || !roomba) {
callback(error || new Error('Unknown error'));
return;
}
try {
if (docking) {
await roomba.dock();
this.log.debug('Roomba is docking');
}
else {
await roomba.pause();
this.log.debug('Roomba is paused');
}
callback();
/* After sending an action to Roomba, we start polling to ensure HomeKit has up to date status */
this.refreshStatusForUser();
}
catch (error) {
this.log.warn('Roomba failed: %s', error.message);
callback(error);
}
});
}
async dockWhenStopped(roomba, pollingInterval) {
try {
const state = await roomba.getRobotState(['cleanMissionStatus']);
switch (state.cleanMissionStatus.phase) {
case 'stop':
this.log.debug('Roomba has stopped, issuing dock request');
await roomba.dock();
this.log.debug('Roomba docking');
this.refreshStatusForUser();
break;
case 'run':
this.log.debug('Roomba is still running. Will check again in %is', pollingInterval / 1000);
await delay(pollingInterval);
this.log.debug('Trying to dock again...');
await this.dockWhenStopped(roomba, pollingInterval);
break;
default:
this.log.debug('Roomba is not running');
break;
}
}
catch (error) {
this.log.warn('Roomba failed to dock: %s', error.message);
}
}
/**
* Creates as a Characteristic getter function that derives the CharacteristicValue from Roomba's status.
*/
createCharacteristicGetter(name, extractValue) {
return (callback) => {
/* Calculate the max age of cached information based on how often we're refreshing Roomba's status */
const maxCacheAge = (this.lastPollInterval || 0) + STATUS_TIMEOUT_MILLIS * 2;
const returnCachedStatus = (status) => {
const value = extractValue(status);
if (value === undefined) {
this.log.debug('%s: Returning no value (%s old, max %s)', name, millisToString(Date.now() - status.timestamp), millisToString(maxCacheAge));
callback(NO_VALUE);
}
else {
this.log.debug('%s: Returning %s (%s old, max %s)', name, String(value), millisToString(Date.now() - status.timestamp), millisToString(maxCacheAge));
callback(null, value);
}
};
this.refreshStatusForUser();
if (Date.now() - this.cachedStatus.timestamp < maxCacheAge) {
returnCachedStatus(this.cachedStatus);
}
else {
/* Wait a short period of time (not too long for Homebridge) for a value to be received by a status check so we can report it */
setTimeout(() => {
if (Date.now() - this.cachedStatus.timestamp < maxCacheAge) {
returnCachedStatus(this.cachedStatus);
}
else {
this.log.debug('%s: Returning no value due to timeout', name);
callback(NO_VALUE);
}
}, 500);
}
};
}
/**
* Merge in changes to the cached status, and update our characteristics so the plugin
* preemptively reports state back to Homebridge.
*/
mergeCachedStatus(status) {
this.setCachedStatus({
...this.cachedStatus,
timestamp: Date.now(),
...status,
});
if (Object.keys(status).length > 1) {
this.log.debug('Merged updated state %s => %s', JSON.stringify(status), JSON.stringify(this.cachedStatus));
}
if (this.isActive()) {
this.roombaLastActiveTimestamp = Date.now();
}
}
/**
* Update the cached status and update our characteristics so the plugin preemptively
* reports state back to Homebridge.
*/
setCachedStatus(status) {
this.cachedStatus = status;
this.updateCharacteristics(status);
}
parseState(state) {
const status = {
timestamp: Date.now(),
};
if (state.batPct !== undefined) {
status.batteryLevel = state.batPct;
}
if (state.bin !== undefined) {
status.binFull = state.bin.full;
}
if (state.tankLvl !== undefined) {
status.tankLevel = state.tankLvl;
}
if (state.cleanMissionStatus !== undefined) {
/* See https://www.openhab.org/addons/bindings/irobot/ for a list of phases */
switch (state.cleanMissionStatus.phase) {
case 'run':
status.running = true;
status.charging = false;
status.docking = false;
break;
case 'charge':
case 'recharge':
status.running = false;
status.charging = true;
status.docking = false;
break;
case 'hmUsrDock':
case 'hmMidMsn':
case 'hmPostMsn':
status.running = false;
status.charging = false;
status.docking = true;
break;
case 'stop':
case 'stuck':
case 'evac':
status.running = false;
status.charging = false;
status.docking = false;
break;
default:
this.log.warn('Unsupported phase: %s', state.cleanMissionStatus.phase);
status.running = false;
status.charging = false;
status.docking = false;
break;
}
status.paused = !status.running && state.cleanMissionStatus.cycle === 'clean';
}
return status;
}
updateCharacteristics(status) {
// this.log.debug("Updating characteristics for status: %s", JSON.stringify(status));
const updateCharacteristic = (service, characteristicId, extractValue) => {
const value = extractValue(status);
if (value !== undefined) {
const previousValue = extractValue(this.lastUpdatedStatus);
if (value !== previousValue) {
this.log.debug('Updating %s %s from %s to %s', service.displayName, service.getCharacteristic(characteristicId).displayName, String(previousValue), String(value));
service.updateCharacteristic(characteristicId, value);
}
}
};
const Characteristic = this.api.hap.Characteristic;
updateCharacteristic(this.switchService, Characteristic.On, this.runningStatus);
updateCharacteristic(this.batteryService, Characteristic.ChargingState, this.chargingStatus);
updateCharacteristic(this.batteryService, Characteristic.BatteryLevel, this.batteryLevelStatus);
updateCharacteristic(this.batteryService, Characteristic.StatusLowBattery, this.batteryStatus);
updateCharacteristic(this.filterMaintenance, Characteristic.FilterChangeIndication, this.binStatus);
if (this.dockService) {
updateCharacteristic(this.dockService, Characteristic.ContactSensorState, this.dockedStatus);
}
if (this.runningService) {
updateCharacteristic(this.runningService, Characteristic.ContactSensorState, this.runningStatus);
}
if (this.binService) {
updateCharacteristic(this.binService, Characteristic.ContactSensorState, this.binStatus);
}
if (this.dockingService) {
updateCharacteristic(this.dockingService, Characteristic.ContactSensorState, this.dockingStatus);
}
if (this.homeService) {
updateCharacteristic(this.homeService, Characteristic.On, this.dockingStatus);
}
if (this.tankService) {
updateCharacteristic(this.tankService, Characteristic.FilterChangeIndication, this.tankStatus);
updateCharacteristic(this.tankService, Characteristic.FilterLifeLevel, this.tankLevelStatus);
}
this.lastUpdatedStatus = {
...this.lastUpdatedStatus,
...status,
};
}
/**
* Trigger a refresh of Roomba's status for a user.
*/
refreshStatusForUser() {
this.userLastInterestedTimestamp = Date.now();
this.startPolling(true);
}
/**
* Start polling Roomba's status and reporting updates to HomeKit.
* We start polling whenever an event occurs, so we update HomeKit promptly
* when the status changes.
*/
startPolling(adhoc) {
const checkStatus = (adhoc) => {
const now = Date.now();
if (!adhoc || now - this.lastRefreshState > REFRESH_STATE_COALESCE_MILLIS) {
this.lastRefreshState = now;
if (adhoc) {
this.log.debug('Refreshing Roomba\'s status');
}
else {
this.log.debug('Automatically refreshing Roomba\'s status');
}
/* Cancel any existing timeout */
if (this.currentPollTimeout) {
clearTimeout(this.currentPollTimeout);
this.currentPollTimeout = undefined;
}
this.refreshState(() => {
const interval = this.currentPollInterval();
this.lastPollInterval = interval;
this.log.debug('Will refresh Roomba\'s status again automatically in %s', millisToString(interval));
if (this.currentPollTimeout) {
clearTimeout(this.currentPollTimeout);
this.currentPollTimeout = undefined;
}
this.currentPollTimeout = setTimeout(() => checkStatus(false), interval);
});
}
};
checkStatus(adhoc || false);
}
currentPollInterval = () => {
/* Check if the user is still interested */
const timeSinceUserLastInterested = Date.now() - (this.userLastInterestedTimestamp || 0);
if (timeSinceUserLastInterested < USER_INTERESTED_MILLIS) {
/* HomeKit is actively querying Roomba's status so a user may be interested */
return 5_000;
}
const timeSinceLastActive = Date.now() - (this.roombaLastActiveTimestamp || 0);
if (this.isActive() || timeSinceLastActive < AFTER_ACTIVE_MILLIS) {
/* Roomba is actively doing things */
return 10_000;
}
/* Roomba is idle */
return this.idlePollIntervalMillis;
};
isActive() {
return this.cachedStatus.running || this.cachedStatus.docking || false;
}
runningStatus = (status) => status.running === undefined
? undefined
: status.running
? 1
: 0;
chargingStatus = (status) => status.charging === undefined
? undefined
: status.charging
? this.api.hap.Characteristic.ChargingState.CHARGING
: this.api.hap.Characteristic.ChargingState.NOT_CHARGING;
dockingStatus = (status) => status.docking === undefined
? undefined
: status.docking
? this.api.hap.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED
: this.api.hap.Characteristic.ContactSensorState.CONTACT_DETECTED;
dockedStatus = (status) => status.charging === undefined
? undefined
: status.charging
? this.api.hap.Characteristic.ContactSensorState.CONTACT_DETECTED
: this.api.hap.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED;
batteryLevelStatus = (status) => status.batteryLevel === undefined
? undefined
: status.batteryLevel;
binStatus = (status) => status.binFull === undefined
? undefined
: status.binFull
? this.api.hap.Characteristic.FilterChangeIndication.CHANGE_FILTER
: this.api.hap.Characteristic.FilterChangeIndication.FILTER_OK;
batteryStatus = (status) => status.batteryLevel === undefined
? undefined
: status.batteryLevel <= 20
? this.api.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW
: this.api.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL;
tankStatus = (status) => status.tankLevel === undefined
? undefined
: status.tankLevel
? this.api.hap.Characteristic.FilterChangeIndication.FILTER_OK
: this.api.hap.Characteristic.FilterChangeIndication.CHANGE_FILTER;
tankLevelStatus = (status) => status.tankLevel === undefined
? undefined
: status.tankLevel;
}
function millisToString(millis) {
if (millis < 1_000) {
return `${millis}ms`;
}
else if (millis < 60_000) {
return `${Math.round((millis / 1000) * 10) / 10}s`;
}
else {
return `${Math.round((millis / 60_000) * 10) / 10}m`;
}
}
function shouldTryDifferentCipher(error) {
/* Explicit TLS errors definitely suggest a different cipher should be used */
if (error.message.includes('TLS')) {
return true;
}
/* We have seen this error connecting to an i1+ https://github.com/homebridge-plugins/homebridge-roomba/issues/129#issuecomment-1520733025 */
if (error.message.toLowerCase().includes('identifier rejected')) {
return true;
}
return false;
}
//# sourceMappingURL=accessory.js.map