homebridge-lovesac-stealthtech 1.1.0 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/accessory.js CHANGED
@@ -473,7 +473,7 @@ class LovesacAccessory {
473
473
  ? this.Characteristic.RotationSpeed : this.Characteristic.Brightness;
474
474
  this.volumeService.getCharacteristic(levelChar).updateValue(percent);
475
475
  this.volumeService.getCharacteristic(this.Characteristic.On)
476
- .updateValue(!this.device.state.mute && this.device.state.volume > 0);
476
+ .updateValue(this.device.state.power && !this.device.state.mute && this.device.state.volume > 0);
477
477
  }
478
478
  break;
479
479
  case constants_1.ResponseCode.Mute:
@@ -481,7 +481,7 @@ class LovesacAccessory {
481
481
  .updateValue(this.device.state.mute);
482
482
  if (this.volumeService) {
483
483
  this.volumeService.getCharacteristic(this.Characteristic.On)
484
- .updateValue(!this.device.state.mute && this.device.state.volume > 0);
484
+ .updateValue(this.device.state.power && !this.device.state.mute && this.device.state.volume > 0);
485
485
  }
486
486
  break;
487
487
  case constants_1.ResponseCode.Source:
@@ -489,10 +489,12 @@ class LovesacAccessory {
489
489
  .updateValue(this.device.state.source + 1);
490
490
  break;
491
491
  case constants_1.ResponseCode.Preset:
492
- this.updatePresetSwitches(this.device.state.preset);
492
+ if (this.device.state.power) {
493
+ this.updatePresetSwitches(this.device.state.preset);
494
+ }
493
495
  break;
494
496
  case constants_1.ResponseCode.QuietMode:
495
- if (this.quietModeService) {
497
+ if (this.quietModeService && this.device.state.power) {
496
498
  this.quietModeService.getCharacteristic(this.Characteristic.On)
497
499
  .updateValue(this.device.state.quietMode);
498
500
  }
@@ -19,6 +19,8 @@ export declare class BleClient implements IBleClient {
19
19
  constructor(log: Logger);
20
20
  get resolvedAddress(): string;
21
21
  connect(address?: string): Promise<void>;
22
+ private connectPeripheral;
23
+ private scanWithRetry;
22
24
  disconnect(): Promise<void>;
23
25
  isConnected(): boolean;
24
26
  write(characteristicUuid: string, data: Buffer): Promise<void>;
@@ -25,26 +25,27 @@ class BleClient {
25
25
  return;
26
26
  }
27
27
  const gen = ++this.connectGeneration;
28
- let peripheral;
29
- if (address) {
30
- this.log.debug('BLE: Starting scan for %s...', address);
31
- peripheral = await this.scanForDevice(address);
32
- if (!peripheral) {
33
- throw new Error(`Device ${address} not found`);
28
+ // Fast path: reuse cached peripheral from a prior connection (skip scan)
29
+ if (this.peripheral) {
30
+ this.log.debug('BLE: Reconnecting to cached peripheral %s...', this._resolvedAddress);
31
+ try {
32
+ await this.connectPeripheral(this.peripheral, gen);
33
+ return;
34
34
  }
35
- }
36
- else {
37
- this.log.debug('BLE: Starting auto-discovery scan...');
38
- peripheral = await this.scanForAnyDevice();
39
- if (!peripheral) {
40
- throw new Error('No Lovesac StealthTech device found');
35
+ catch (err) {
36
+ this.log.debug('BLE: Cached peripheral connect failed: %s — falling back to scan', err.message);
37
+ this.peripheral = null;
41
38
  }
42
39
  }
40
+ // Slow path: scan for the device (with one retry)
41
+ const peripheral = await this.scanWithRetry(address, gen);
43
42
  const resolvedId = peripheral.address !== '' && peripheral.address !== 'unknown'
44
43
  ? peripheral.address
45
44
  : peripheral.id ?? peripheral.uuid ?? 'unknown';
46
- this.log.debug('BLE: Connecting to %s...', resolvedId);
47
45
  this._resolvedAddress = resolvedId;
46
+ await this.connectPeripheral(peripheral, gen);
47
+ }
48
+ async connectPeripheral(peripheral, gen) {
48
49
  // Register disconnect handler BEFORE connecting to avoid race (P0-2).
49
50
  // Capture the generation so a stale handler from a timed-out attempt does
50
51
  // not clear state that belongs to a newer connection.
@@ -52,7 +53,6 @@ class BleClient {
52
53
  this.log.debug('BLE: Disconnected');
53
54
  if (gen === this.connectGeneration) {
54
55
  this._connected = false;
55
- this.peripheral = null;
56
56
  this.characteristics = {};
57
57
  }
58
58
  });
@@ -79,6 +79,33 @@ class BleClient {
79
79
  }
80
80
  this.log.debug('BLE: Found %d characteristics', Object.keys(this.characteristics).length);
81
81
  }
82
+ async scanWithRetry(address, gen) {
83
+ const label = address ?? 'auto-discovery';
84
+ const scanFn = address
85
+ ? () => this.scanForDevice(address)
86
+ : () => this.scanForAnyDevice();
87
+ this.log.debug('BLE: Starting scan for %s...', label);
88
+ let peripheral = await scanFn();
89
+ if (peripheral) {
90
+ return peripheral;
91
+ }
92
+ // First scan missed — retry once after a short delay
93
+ if (gen !== this.connectGeneration) {
94
+ throw new Error('Connection attempt superseded');
95
+ }
96
+ this.log.info('BLE: Scan found nothing, retrying in %ds...', settings_1.BLE_SCAN_RETRY_DELAY / 1000);
97
+ await new Promise(r => setTimeout(r, settings_1.BLE_SCAN_RETRY_DELAY));
98
+ if (gen !== this.connectGeneration) {
99
+ throw new Error('Connection attempt superseded');
100
+ }
101
+ peripheral = await scanFn();
102
+ if (peripheral) {
103
+ return peripheral;
104
+ }
105
+ throw new Error(address
106
+ ? `Device ${address} not found`
107
+ : 'No Lovesac StealthTech device found');
108
+ }
82
109
  async disconnect() {
83
110
  if (this.peripheral && this._connected) {
84
111
  try {
@@ -89,8 +116,8 @@ class BleClient {
89
116
  }
90
117
  }
91
118
  this._connected = false;
92
- this.peripheral = null;
93
119
  this.characteristics = {};
120
+ // Keep this.peripheral cached for fast reconnect
94
121
  }
95
122
  isConnected() {
96
123
  return this._connected;
@@ -30,6 +30,7 @@ export declare class LovesacDevice {
30
30
  private static readonly MAX_POLL_INTERVAL_MS;
31
31
  private onPollSuccess;
32
32
  private onPollFailure;
33
+ private backoffPollInterval;
33
34
  private markUnreachable;
34
35
  stopPolling(): void;
35
36
  getResolvedAddress(): string;
@@ -147,18 +147,25 @@ class LovesacDevice {
147
147
  onPollFailure(message) {
148
148
  this.consecutiveFailures++;
149
149
  this.log.warn('Background poll failed (%d/%d): %s', this.consecutiveFailures, settings_1.UNREACHABLE_THRESHOLD, message);
150
- if (this.consecutiveFailures >= settings_1.UNREACHABLE_THRESHOLD && this.reachable) {
151
- this.markUnreachable();
150
+ // Apply backoff at each threshold multiple (6, 12, 18, …) while unreachable.
151
+ // First crossing also marks the device unreachable and resets cached state.
152
+ if (this.consecutiveFailures >= settings_1.UNREACHABLE_THRESHOLD
153
+ && this.consecutiveFailures % settings_1.UNREACHABLE_THRESHOLD === 0) {
154
+ this.backoffPollInterval();
155
+ if (this.reachable) {
156
+ this.markUnreachable();
157
+ }
152
158
  }
153
159
  }
154
- markUnreachable() {
155
- this.reachable = false;
156
- // Exponential backoff: double the poll interval, capped at 10 minutes
160
+ backoffPollInterval() {
157
161
  const newInterval = Math.min(this.pollIntervalMs * 2, LovesacDevice.MAX_POLL_INTERVAL_MS);
158
162
  if (newInterval !== this.pollIntervalMs) {
159
163
  this.pollIntervalMs = newInterval;
160
164
  this.log.info('Poll interval backed off to %ds', this.pollIntervalMs / 1000);
161
165
  }
166
+ }
167
+ markUnreachable() {
168
+ this.reachable = false;
162
169
  this.log.warn('Device unreachable after %d consecutive poll failures — resetting cached state', this.consecutiveFailures);
163
170
  // Reset state to sentinels so next successful connection triggers full re-sync
164
171
  (0, responses_1.resetState)(this.state);
@@ -21,7 +21,8 @@ export declare const BLE_CONNECT_TIMEOUT = 30000;
21
21
  export declare const BLE_WRITE_TIMEOUT = 10000;
22
22
  export declare const BLE_DISCONNECT_TIMEOUT = 10000;
23
23
  export declare const BLE_DISCOVER_TIMEOUT = 15000;
24
- export declare const UNREACHABLE_THRESHOLD = 3;
24
+ export declare const BLE_SCAN_RETRY_DELAY = 2000;
25
+ export declare const UNREACHABLE_THRESHOLD = 6;
25
26
  /**
26
27
  * Race a promise against a timeout. Rejects with a descriptive error if
27
28
  * the timeout fires first. The underlying promise is NOT cancelled — the
package/dist/settings.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.UNREACHABLE_THRESHOLD = exports.BLE_DISCOVER_TIMEOUT = exports.BLE_DISCONNECT_TIMEOUT = exports.BLE_WRITE_TIMEOUT = exports.BLE_CONNECT_TIMEOUT = exports.BLE_SCAN_TIMEOUT = exports.MAX_VOLUME = exports.CharUUID = exports.SOFA_SERVICE_UUID_SHORT = exports.SOFA_SERVICE_UUID = exports.PLATFORM_NAME = exports.PLUGIN_NAME = void 0;
3
+ exports.UNREACHABLE_THRESHOLD = exports.BLE_SCAN_RETRY_DELAY = exports.BLE_DISCOVER_TIMEOUT = exports.BLE_DISCONNECT_TIMEOUT = exports.BLE_WRITE_TIMEOUT = exports.BLE_CONNECT_TIMEOUT = exports.BLE_SCAN_TIMEOUT = exports.MAX_VOLUME = exports.CharUUID = exports.SOFA_SERVICE_UUID_SHORT = exports.SOFA_SERVICE_UUID = exports.PLATFORM_NAME = exports.PLUGIN_NAME = void 0;
4
4
  exports.withTimeout = withTimeout;
5
5
  exports.errorMessage = errorMessage;
6
6
  exports.resolveDeviceConfig = resolveDeviceConfig;
@@ -29,8 +29,10 @@ exports.BLE_CONNECT_TIMEOUT = 30_000;
29
29
  exports.BLE_WRITE_TIMEOUT = 10_000;
30
30
  exports.BLE_DISCONNECT_TIMEOUT = 10_000;
31
31
  exports.BLE_DISCOVER_TIMEOUT = 15_000;
32
+ // Delay between scan retry attempts (milliseconds)
33
+ exports.BLE_SCAN_RETRY_DELAY = 2000;
32
34
  // Consecutive poll failures before marking device unreachable
33
- exports.UNREACHABLE_THRESHOLD = 3;
35
+ exports.UNREACHABLE_THRESHOLD = 6;
34
36
  /**
35
37
  * Race a promise against a timeout. Rejects with a descriptive error if
36
38
  * the timeout fires first. The underlying promise is NOT cancelled — the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homebridge-lovesac-stealthtech",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "Homebridge plugin for Lovesac StealthTech Sound + Charge BLE control",
5
5
  "main": "dist/index.js",
6
6
  "files": [