Skip to content

Commit

Permalink
Make the plugin re-subscribe on disconnection
Browse files Browse the repository at this point in the history
When the client connection is lost, the bridge removes subscriptions to
events, which makes sense. The plugin only subscribes to events when it
wires up the accessories, and never again.

This change listens for the disconnected event, and makes the plugin
re-subscribe when that happens. This should keep devices requiring
subscription, namely Picos and Occupancy Sensors, working after client
connection is re-established.
  • Loading branch information
thenewwazoo committed Jan 16, 2023
1 parent 6a2b735 commit 86745cd
Show file tree
Hide file tree
Showing 5 changed files with 42 additions and 24 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"displayName": "Lutron Caseta LEAP",
"name": "homebridge-lutron-caseta-leap",
"version": "2.4.4-beta0",
"version": "2.4.5-beta0",
"description": "Support for the Lutron Caseta Smart Bridge 2",
"license": "Apache-2.0",
"repository": {
Expand Down Expand Up @@ -34,7 +34,7 @@
"dependencies": {
"@homebridge/plugin-ui-utils": "^0.0.19",
"homebridge-dummy": "^0.8.0",
"lutron-leap": "^3.2.0-beta0",
"lutron-leap": "^3.3.0",
"node-forge": "^1.2.1",
"typed-emitter": "^2.1.0"
},
Expand Down
2 changes: 2 additions & 0 deletions src/OccupancySensorRouter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,8 @@ describe('router sets initial state', () => {
const mockBridge = {
bridgeID: 'fakebridge',
subscribeToOccupancy: mockSub,
/* eslint-disable @typescript-eslint/no-empty-function */
on: (event: string): void => {},
};

test('initial and update', async () => {
Expand Down
50 changes: 28 additions & 22 deletions src/OccupancySensorRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,30 +60,36 @@ export class OccupancySensorRouter {
this.subMap.set(
bridge.bridgeID,
new Promise((resolve, reject) => {
// update state and call all registered callbacks when we get an update
const _handleOccupancyUpdate = (r: Response) => {
logDebug('subscription cb called');
this.updateState(bridge.bridgeID, r.Body! as MultipleOccupancyGroupStatus);
this.callRegistered(bridge.bridgeID, r.Body! as MultipleOccupancyGroupStatus);
};

// we get a complete listing of occupancy groups and their
// statuses when we subscribe, so update our internal state
// while we've got the info handy
const _handleGlobalUpdate = (initial: MultipleOccupancyGroupStatus) => {
logDebug(`response from subscription call recd: ${util.inspect(initial, { depth: null })}`);
this.updateState(bridge.bridgeID, initial);
};

// subscribe to occupancy updates for this bridge, and...
bridge
.subscribeToOccupancy(
((r: Response) => {
logDebug('subscription cb called');
// ...update state and call all registered callbacks when we get an update
this.updateState(bridge.bridgeID, r.Body! as MultipleOccupancyGroupStatus);
this.callRegistered(bridge.bridgeID, r.Body! as MultipleOccupancyGroupStatus);
}).bind(this),
)
.then(
((initial: MultipleOccupancyGroupStatus) => {
// we get a complete listing of occupancy groups and their
// statuses when we subscribe, so update our internal state
// while we've got the info handy

logDebug(`response from subscription call recd: ${util.inspect(initial, { depth: null })}`);
this.updateState(bridge.bridgeID, initial);

// resolve the promise that we'll subscribe to the bridge
resolve();
}).bind(this),
)
.subscribeToOccupancy(_handleOccupancyUpdate.bind(this))
.then(_handleGlobalUpdate.bind(this))
// resolve the promise that we'll subscribe to the bridge
.then(() => resolve())
.catch((e) => reject(e));

// when the bridge is disconnected, subscriptions are lost. re-establish them.
bridge.on('disconnected', () => {
bridge.subscribeToOccupancy(_handleOccupancyUpdate.bind(this)).then(_handleGlobalUpdate.bind(this));
// WARNING: uncaught throw here will crash the program, but
// there's nothing to be done if re-subscribing fails.
// better to just blow it all up.
});
}),
);
}
Expand All @@ -93,7 +99,7 @@ export class OccupancySensorRouter {
// when there's an update. This function will subscribe to the bridge if
// it hasn't already been done.

// The key into the three state maps
// Create the key used for looking into the three state maps we're about to use
const key = this.makeKey(bridge.bridgeID, occupancyGroup);

// If we're not already subscribed to this bridge's updates, let's do that.
Expand Down
6 changes: 6 additions & 0 deletions src/PicoRemote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,12 @@ export class PicoRemote {

this.platform.log.debug(`subscribing to ${button.href} events`);
this.bridge.subscribeToButton(button, this.handleEvent.bind(this));

// when the connection is lost, so are subscriptions.
this.bridge.on('disconnected', () => {
this.platform.log.debug(`re-subscribing to ${button.href} events after connection loss`);
this.bridge.subscribeToButton(button, this.handleEvent.bind(this));
});
}

this.platform.on('unsolicited', this.handleUnsolicited.bind(this));
Expand Down
4 changes: 4 additions & 0 deletions src/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,10 @@ export class LutronCasetaLeap
}
const client = new LeapClient(bridgeInfo.ipAddr, LEAP_PORT, these.ca, these.key, these.cert, logfile);
const bridge = new SmartBridge(bridgeInfo.bridgeid.toLowerCase(), client);
// every pico and occupancy sensor needs to subscribe to
// 'disconnected', and that may be a lot of devices, so set the max
// according to Caseta's 75-device limit
bridge.setMaxListeners(75);
this.bridgeMgr.addBridge(bridge);
this.processAllDevices(bridge);
} else {
Expand Down

0 comments on commit 86745cd

Please sign in to comment.