Skip to content

Commit ae578c0

Browse files
committed
Refactor Roomba connection handling
Resolves #66
1 parent 1346008 commit ae578c0

File tree

2 files changed

+101
-28
lines changed

2 files changed

+101
-28
lines changed

.changeset/breezy-files-agree.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"homebridge-roomba2": minor
3+
---
4+
5+
Refactor Roomba connection handling to improve reporting of issues connecting to Roomba and to reuse
6+
existing Roomba connections to avoid conflicts [#66]

src/accessory.ts

Lines changed: 95 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ import { AccessoryConfig, AccessoryPlugin, NodeCallback, API, Logging, Service,
66
*/
77
const STATUS_COALLESCE_WINDOW_MILLIS = 5_000;
88

9+
/**
10+
* How long to wait to connect to Roomba.
11+
*/
12+
const CONNECT_TIMEOUT = 15_000;
13+
914
/**
1015
* Whether to output debug logging at info level. Useful during debugging to be able to
1116
* see debug logs from this plugin.
@@ -66,6 +71,16 @@ export default class RoombaAccessory implements AccessoryPlugin {
6671
*/
6772
private pendingStatusRequests: NodeCallback<Status>[]
6873

74+
/**
75+
* The currently connected Roomba instance _only_ used in the connect() method.
76+
*/
77+
private _currentlyConnectedRoomba?: Roomba;
78+
79+
/**
80+
* How many requests are currently using the connected Roomba instance.
81+
*/
82+
private _currentlyConnectedRoombaRequests = 0;
83+
6984
public constructor(log: Logging, config: AccessoryConfig, api: API) {
7085
this.api = api;
7186
this.log = !DEBUG
@@ -178,24 +193,79 @@ export default class RoombaAccessory implements AccessoryPlugin {
178193
return services;
179194
}
180195

181-
private getRoomba() {
182-
return new dorita980.Local(this.blid, this.robotpwd, this.ipaddress);
183-
}
196+
private async connect(callback: (error: Error | null, roomba?: Roomba) => Promise<void>) {
197+
const getRoomba = () => {
198+
if (this._currentlyConnectedRoomba) {
199+
this._currentlyConnectedRoombaRequests++;
200+
return this._currentlyConnectedRoomba;
201+
}
202+
203+
const roomba = new dorita980.Local(this.blid, this.robotpwd, this.ipaddress);
204+
this._currentlyConnectedRoomba = roomba;
205+
this._currentlyConnectedRoombaRequests = 1;
206+
return roomba;
207+
};
208+
const stopUsingRoomba = async(roomba: Roomba) => {
209+
if (roomba !== this._currentlyConnectedRoomba) {
210+
this.log.warn("Releasing an unexpected Roomba instance");
211+
await roomba.end();
212+
return;
213+
}
214+
215+
this._currentlyConnectedRoombaRequests--;
216+
if (this._currentlyConnectedRoombaRequests === 0) {
217+
this.log.debug("Releasing Roomba instance");
218+
await roomba.end();
219+
this._currentlyConnectedRoomba = undefined;
220+
} else {
221+
this.log.debug("Leaving Roomba instance with %i ongoing requests", this._currentlyConnectedRoombaRequests);
222+
}
223+
};
224+
225+
const roomba = getRoomba();
226+
if (roomba.connected) {
227+
this.log.debug("Reusing connected Roomba");
228+
229+
await callback(null, roomba);
230+
await stopUsingRoomba(roomba);
231+
return;
232+
}
233+
234+
let timedOut = false;
235+
236+
const timeout = setTimeout(async() => {
237+
timedOut = true;
238+
239+
this.log.warn("Timed out trying to connect to Roomba");
240+
241+
await callback(new Error("Connect timed out"));
242+
await stopUsingRoomba(roomba);
243+
}, CONNECT_TIMEOUT);
244+
245+
roomba.on("connect", async() => {
246+
if (timedOut) {
247+
this.log.debug("Connection established to Roomba after timeout");
248+
return;
249+
}
250+
251+
clearTimeout(timeout);
184252

185-
private onConnected(roomba: Roomba, callback: () => void) {
186-
roomba.on("connect", () => {
187253
this.log.debug("Connected to Roomba");
188-
callback();
254+
await callback(null, roomba);
255+
await stopUsingRoomba(roomba);
189256
});
190257
}
191258

192259
private setRunningState(powerOn: CharacteristicValue, callback: CharacteristicSetCallback) {
193-
const roomba = this.getRoomba();
194-
195260
if (powerOn) {
196261
this.log("Starting Roomba");
197262

198-
this.onConnected(roomba, async() => {
263+
this.connect(async(error, roomba) => {
264+
if (error || !roomba) {
265+
callback(error || new Error("Unknown error"));
266+
return;
267+
}
268+
199269
try {
200270
/* To start Roomba we signal both a clean and a resume, as if Roomba is paused in a clean cycle,
201271
we need to instruct it to resume instead.
@@ -216,14 +286,17 @@ export default class RoombaAccessory implements AccessoryPlugin {
216286
this.log("Roomba failed: %s", (error as Error).message);
217287

218288
callback(error as Error);
219-
} finally {
220-
this.endRoombaIfNeeded(roomba);
221289
}
222290
});
223291
} else {
224292
this.log("Roomba pause and dock");
225293

226-
this.onConnected(roomba, async() => {
294+
this.connect(async(error, roomba) => {
295+
if (error || !roomba) {
296+
callback(error || new Error("Unknown error"));
297+
return;
298+
}
299+
227300
try {
228301
this.log("Roomba is pausing");
229302

@@ -239,22 +312,16 @@ export default class RoombaAccessory implements AccessoryPlugin {
239312

240313
this.log("Roomba paused, returning to Dock");
241314

242-
this.dockWhenStopped(roomba, 3000);
315+
await this.dockWhenStopped(roomba, 3000);
243316
} catch (error) {
244317
this.log("Roomba failed: %s", (error as Error).message);
245318

246-
this.endRoombaIfNeeded(roomba);
247-
248319
callback(error as Error);
249320
}
250321
});
251322
}
252323
}
253324

254-
private endRoombaIfNeeded(roomba: Roomba) {
255-
roomba.end();
256-
}
257-
258325
private async dockWhenStopped(roomba: Roomba, pollingInterval: number) {
259326
try {
260327
const state = await roomba.getRobotState(["cleanMissionStatus"]);
@@ -264,7 +331,6 @@ export default class RoombaAccessory implements AccessoryPlugin {
264331
this.log("Roomba has stopped, issuing dock request");
265332

266333
await roomba.dock();
267-
this.endRoombaIfNeeded(roomba);
268334

269335
this.log("Roomba docking");
270336

@@ -279,19 +345,16 @@ export default class RoombaAccessory implements AccessoryPlugin {
279345
this.log("Roomba is still running. Will check again in %is", pollingInterval / 1000);
280346

281347
await setTimeout(() => this.log.debug("Trying to dock again..."), pollingInterval);
282-
this.dockWhenStopped(roomba, pollingInterval);
348+
await this.dockWhenStopped(roomba, pollingInterval);
283349

284350
break;
285351
default:
286-
this.endRoombaIfNeeded(roomba);
287-
288352
this.log("Roomba is not running");
289353

290354
break;
291355
}
292356
} catch (error) {
293357
this.log.warn("Roomba failed to dock: %s", (error as Error).message);
294-
this.endRoombaIfNeeded(roomba);
295358
}
296359
}
297360

@@ -342,9 +405,15 @@ export default class RoombaAccessory implements AccessoryPlugin {
342405
}
343406
this.currentGetStatusTimestamp = now;
344407

345-
const roomba = this.getRoomba();
408+
this.log.debug("getStatus connecting to Roomba...");
409+
410+
this.connect(async(error, roomba) => {
411+
if (error || !roomba) {
412+
callback(error || new Error("Unknown error"));
413+
this.setCachedStatus({ error: error as Error });
414+
return;
415+
}
346416

347-
this.onConnected(roomba, async() => {
348417
this.log.debug("getStatus connected to Roomba in %ims", Date.now() - now);
349418

350419
try {
@@ -368,8 +437,6 @@ export default class RoombaAccessory implements AccessoryPlugin {
368437
} finally {
369438
this.currentGetStatusTimestamp = undefined;
370439
this.pendingStatusRequests = [];
371-
372-
this.endRoombaIfNeeded(roomba);
373440
}
374441
});
375442
}

0 commit comments

Comments
 (0)