diff --git a/package.json b/package.json index 9d67205..7069368 100644 --- a/package.json +++ b/package.json @@ -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": { @@ -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" }, diff --git a/src/OccupancySensorRouter.test.ts b/src/OccupancySensorRouter.test.ts index a141c6b..b7dc679 100644 --- a/src/OccupancySensorRouter.test.ts +++ b/src/OccupancySensorRouter.test.ts @@ -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 () => { diff --git a/src/OccupancySensorRouter.ts b/src/OccupancySensorRouter.ts index b35d1b5..6d5772c 100644 --- a/src/OccupancySensorRouter.ts +++ b/src/OccupancySensorRouter.ts @@ -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. + }); }), ); } @@ -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. diff --git a/src/PicoRemote.ts b/src/PicoRemote.ts index bd7b925..cae15f5 100644 --- a/src/PicoRemote.ts +++ b/src/PicoRemote.ts @@ -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)); diff --git a/src/platform.ts b/src/platform.ts index 9b16373..44ab844 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -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 {