homebridge-tsvesync 1.0.13 → 1.0.14

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.
Files changed (34) hide show
  1. package/dist/accessories/air-purifier.accessory.d.ts +32 -2
  2. package/dist/accessories/air-purifier.accessory.d.ts.map +1 -1
  3. package/dist/accessories/air-purifier.accessory.js +110 -51
  4. package/dist/accessories/air-purifier.accessory.js.map +1 -1
  5. package/dist/accessories/base.accessory.d.ts +12 -3
  6. package/dist/accessories/base.accessory.d.ts.map +1 -1
  7. package/dist/accessories/base.accessory.js +53 -24
  8. package/dist/accessories/base.accessory.js.map +1 -1
  9. package/dist/accessories/fan.accessory.d.ts +6 -2
  10. package/dist/accessories/fan.accessory.d.ts.map +1 -1
  11. package/dist/accessories/fan.accessory.js +27 -33
  12. package/dist/accessories/fan.accessory.js.map +1 -1
  13. package/dist/accessories/humidifier.accessory.d.ts +6 -2
  14. package/dist/accessories/humidifier.accessory.d.ts.map +1 -1
  15. package/dist/accessories/humidifier.accessory.js +38 -37
  16. package/dist/accessories/humidifier.accessory.js.map +1 -1
  17. package/dist/accessories/light.accessory.d.ts +5 -1
  18. package/dist/accessories/light.accessory.d.ts.map +1 -1
  19. package/dist/accessories/light.accessory.js +25 -19
  20. package/dist/accessories/light.accessory.js.map +1 -1
  21. package/dist/accessories/outlet.accessory.d.ts +7 -1
  22. package/dist/accessories/outlet.accessory.d.ts.map +1 -1
  23. package/dist/accessories/outlet.accessory.js +25 -14
  24. package/dist/accessories/outlet.accessory.js.map +1 -1
  25. package/dist/types/device.types.d.ts +1 -1
  26. package/dist/types/device.types.d.ts.map +1 -1
  27. package/package.json +2 -2
  28. package/src/accessories/air-purifier.accessory.ts +117 -84
  29. package/src/accessories/base.accessory.ts +62 -35
  30. package/src/accessories/fan.accessory.ts +40 -44
  31. package/src/accessories/humidifier.accessory.ts +61 -48
  32. package/src/accessories/light.accessory.ts +32 -18
  33. package/src/accessories/outlet.accessory.ts +33 -14
  34. package/src/types/device.types.ts +1 -1
@@ -4,17 +4,22 @@ import { TSVESyncPlatform } from '../platform';
4
4
  import { DeviceCapabilities, VeSyncAirPurifier } from '../types/device.types';
5
5
 
6
6
  export class AirPurifierAccessory extends BaseAccessory {
7
+ private readonly capabilities: DeviceCapabilities;
7
8
  protected readonly device: VeSyncAirPurifier;
8
9
 
9
10
  constructor(
10
11
  platform: TSVESyncPlatform,
11
12
  accessory: PlatformAccessory,
12
- device: VeSyncAirPurifier,
13
+ device: VeSyncAirPurifier
13
14
  ) {
14
15
  super(platform, accessory, device);
15
16
  this.device = device;
17
+ this.capabilities = this.getDeviceCapabilities();
16
18
  }
17
19
 
20
+ /**
21
+ * Set up the air purifier service
22
+ */
18
23
  protected setupService(): void {
19
24
  // Get or create the air purifier service
20
25
  this.service = this.accessory.getService(this.platform.Service.AirPurifier) ||
@@ -68,7 +73,7 @@ export class AirPurifierAccessory extends BaseAccessory {
68
73
  this.setupCharacteristic(
69
74
  this.platform.Characteristic.RotationSpeed,
70
75
  this.getRotationSpeed.bind(this),
71
- this.setRotationSpeed.bind(this)
76
+ this.handleSetRotationSpeed.bind(this)
72
77
  );
73
78
  }
74
79
 
@@ -98,88 +103,114 @@ export class AirPurifierAccessory extends BaseAccessory {
98
103
  );
99
104
  }
100
105
 
101
- protected updateDeviceSpecificStates(details: any): void {
102
- const capabilities = this.getDeviceCapabilities();
106
+ /**
107
+ * Get device capabilities
108
+ */
109
+ protected getDeviceCapabilities(): DeviceCapabilities {
110
+ return {
111
+ hasSpeed: true,
112
+ hasAirQuality: true,
113
+ hasChildLock: true,
114
+ hasBrightness: false,
115
+ hasColorTemp: false,
116
+ hasColor: false,
117
+ hasHumidity: false,
118
+ hasWaterLevel: false,
119
+ hasSwingMode: false,
120
+ };
121
+ }
103
122
 
104
- if (capabilities.hasAirQuality) {
105
- const airQualityService = this.accessory.getService(this.platform.Service.AirQualitySensor);
106
- if (airQualityService) {
107
- // Ensure we have a valid number for air quality
108
- const airQuality = typeof details.airQuality === 'number' && !isNaN(details.airQuality)
109
- ? details.airQuality
110
- : 0;
111
-
112
- airQualityService.updateCharacteristic(
113
- this.platform.Characteristic.AirQuality,
114
- this.convertAirQualityToHomeKit(airQuality)
115
- );
116
-
117
- // Ensure PM2.5 value is a valid number
118
- const pm25Value = typeof details.pm25 === 'number' && !isNaN(details.pm25)
119
- ? details.pm25
120
- : typeof airQuality === 'number' && !isNaN(airQuality)
121
- ? airQuality
122
- : 0;
123
-
124
- airQualityService.updateCharacteristic(
125
- this.platform.Characteristic.PM2_5Density,
126
- pm25Value
127
- );
128
- }
123
+ /**
124
+ * Update device states based on the latest details
125
+ */
126
+ protected async updateDeviceSpecificStates(details: any): Promise<void> {
127
+ // Update power state
128
+ const isOn = details.enabled;
129
+ this.service.updateCharacteristic(this.platform.Characteristic.Active, isOn ? 1 : 0);
130
+ this.service.updateCharacteristic(this.platform.Characteristic.CurrentAirPurifierState, isOn ? 2 : 0);
131
+
132
+ // Update rotation speed
133
+ const speed = details.speed;
134
+ const rotationSpeed = Math.round((speed / this.device.maxSpeed) * 100);
135
+ this.service.updateCharacteristic(this.platform.Characteristic.RotationSpeed, rotationSpeed);
136
+
137
+ // Update air quality if available
138
+ if (this.capabilities.hasAirQuality) {
139
+ const pm25 = details.airQuality?.pm25 ?? 0;
140
+ const airQuality = this.convertAirQualityToHomeKit(pm25);
141
+ this.service.updateCharacteristic(this.platform.Characteristic.AirQuality, airQuality);
142
+ this.service.updateCharacteristic(this.platform.Characteristic.PM2_5Density, pm25);
129
143
  }
130
144
 
131
- if (capabilities.hasSpeed && details.speed !== undefined && details.speed !== null) {
132
- const normalizedSpeed = Math.min(Math.max((details.speed / (this.device.maxSpeed || 1)) * 100, 0), 100);
133
- this.updateCharacteristicValue(
134
- this.platform.Characteristic.RotationSpeed,
135
- normalizedSpeed
145
+ // Update filter life if available
146
+ if (details.filterLife !== undefined) {
147
+ const filterLife = details.filterLife;
148
+ this.service.updateCharacteristic(this.platform.Characteristic.FilterLifeLevel, filterLife);
149
+ this.service.updateCharacteristic(
150
+ this.platform.Characteristic.FilterChangeIndication,
151
+ filterLife <= 20 ? 1 : 0
136
152
  );
137
153
  }
138
154
 
139
- if (capabilities.hasChildLock && details.childLock !== undefined) {
140
- this.updateCharacteristicValue(
141
- this.platform.Characteristic.LockPhysicalControls,
142
- details.childLock ? 1 : 0
143
- );
144
- }
155
+ // Update device state based on activity
156
+ this.updatePollingState(isOn);
157
+ }
145
158
 
146
- if (details.filterLife !== undefined && details.filterLife !== null) {
147
- this.updateCharacteristicValue(
148
- this.platform.Characteristic.FilterChangeIndication,
149
- details.filterLife <= 20 ? 1 : 0
150
- );
151
-
152
- this.updateCharacteristicValue(
153
- this.platform.Characteristic.FilterLifeLevel,
154
- details.filterLife
155
- );
159
+ /**
160
+ * Handle set active state
161
+ */
162
+ private async handleSetActive(value: CharacteristicValue): Promise<void> {
163
+ try {
164
+ const newState = value as number;
165
+ if (newState === 1) {
166
+ await this.device.turnOn();
167
+ } else {
168
+ await this.device.turnOff();
169
+ }
170
+ } catch (err) {
171
+ await this.handleDeviceError('set active state', err);
172
+ throw err;
156
173
  }
174
+ }
157
175
 
158
- // Update air purifier state
159
- const isActive = details.deviceStatus === 'on';
160
- this.updateCharacteristicValue(
161
- this.platform.Characteristic.Active,
162
- isActive ? 1 : 0
163
- );
176
+ /**
177
+ * Handle set target state
178
+ */
179
+ private async handleSetTargetState(value: CharacteristicValue): Promise<void> {
180
+ try {
181
+ const newState = value as number;
182
+ if (this.device.setMode) {
183
+ await this.device.setMode(newState === 1 ? 'auto' : 'manual');
184
+ }
185
+ } catch (err) {
186
+ await this.handleDeviceError('set target state', err);
187
+ throw err;
188
+ }
189
+ }
164
190
 
165
- this.updateCharacteristicValue(
166
- this.platform.Characteristic.CurrentAirPurifierState,
167
- isActive ? 2 : 0 // 0 = INACTIVE, 2 = PURIFYING_AIR
168
- );
191
+ /**
192
+ * Handle set rotation speed
193
+ */
194
+ private async handleSetRotationSpeed(value: CharacteristicValue): Promise<void> {
195
+ const speed = Math.round((value as number / 100) * this.device.maxSpeed);
196
+ await this.withRetry(async () => {
197
+ await this.device.changeFanSpeed(speed);
198
+ }, 'set rotation speed');
169
199
  }
170
200
 
171
- protected getDeviceCapabilities(): DeviceCapabilities {
172
- return {
173
- hasBrightness: false,
174
- hasColorTemp: false,
175
- hasColor: false,
176
- hasSpeed: true,
177
- hasHumidity: false,
178
- hasAirQuality: true,
179
- hasWaterLevel: false,
180
- hasChildLock: true,
181
- hasSwingMode: false,
182
- };
201
+ /**
202
+ * Handle set child lock
203
+ */
204
+ private async handleSetChildLock(value: CharacteristicValue): Promise<void> {
205
+ try {
206
+ const enabled = value as number === 1;
207
+ if (this.device.setChildLock) {
208
+ await this.device.setChildLock(enabled);
209
+ }
210
+ } catch (err) {
211
+ await this.handleDeviceError('set child lock', err);
212
+ throw err;
213
+ }
183
214
  }
184
215
 
185
216
  private async getActive(): Promise<CharacteristicValue> {
@@ -252,19 +283,15 @@ export class AirPurifierAccessory extends BaseAccessory {
252
283
  return Math.min(Math.max((this.device.speed / this.device.maxSpeed) * 100, 0), 100);
253
284
  }
254
285
 
255
- private async setRotationSpeed(value: CharacteristicValue): Promise<void> {
256
- try {
257
- const speed = Math.round((value as number / 100) * this.device.maxSpeed);
258
- const success = await this.device.setSpeed(speed);
259
-
260
- if (!success) {
261
- throw new Error(`Failed to set speed to ${speed}`);
262
- }
263
-
264
- await this.persistDeviceState('speed', speed);
265
- } catch (error) {
266
- this.handleDeviceError('set rotation speed', error);
286
+ /**
287
+ * Convert HomeKit rotation speed (0-100) to fan speed (1-maxSpeed)
288
+ */
289
+ private convertRotationSpeedToFanSpeed(value: number): number {
290
+ if (value === 0) {
291
+ return 0; // Off
267
292
  }
293
+ // Convert from 0-100 to 1-maxSpeed
294
+ return Math.max(1, Math.round((value / 100) * this.device.maxSpeed));
268
295
  }
269
296
 
270
297
  private async getLockPhysicalControls(): Promise<CharacteristicValue> {
@@ -297,4 +324,10 @@ export class AirPurifierAccessory extends BaseAccessory {
297
324
  private async getFilterLifeLevel(): Promise<CharacteristicValue> {
298
325
  return this.device.filterLife;
299
326
  }
327
+
328
+ protected async updateRotationSpeed(speed: number): Promise<void> {
329
+ await this.withRetry(async () => {
330
+ await this.device.changeFanSpeed(speed);
331
+ }, 'update rotation speed');
332
+ }
300
333
  }
@@ -13,6 +13,7 @@ export abstract class BaseAccessory {
13
13
  private readonly pollingManager: PollingManager;
14
14
  private readonly retryManager: RetryManager;
15
15
  private readonly logger: PluginLogger;
16
+ private needsRetry = false;
16
17
 
17
18
  constructor(
18
19
  platform: TSVESyncPlatform,
@@ -83,6 +84,10 @@ export abstract class BaseAccessory {
83
84
  return result;
84
85
  } catch (error) {
85
86
  this.logger.operationEnd(context, error as Error);
87
+ await this.handleDeviceError(
88
+ `Failed to ${operationName}`,
89
+ error instanceof Error ? error : new Error(String(error))
90
+ );
86
91
  throw error;
87
92
  }
88
93
  }
@@ -95,7 +100,7 @@ export abstract class BaseAccessory {
95
100
  /**
96
101
  * Update device-specific states
97
102
  */
98
- protected abstract updateDeviceSpecificStates(details: any): void;
103
+ protected abstract updateDeviceSpecificStates(details: any): Promise<void>;
99
104
 
100
105
  /**
101
106
  * Get device capabilities
@@ -168,36 +173,21 @@ export abstract class BaseAccessory {
168
173
  * Sync the device state with VeSync
169
174
  */
170
175
  protected async syncDeviceState(): Promise<void> {
171
- const context = this.getLogContext('sync device state');
172
-
173
176
  try {
174
- await this.withRetry(async () => {
175
- await this.device.getDetails();
176
- const details = {
177
- deviceStatus: this.device.deviceStatus,
178
- // Add any other properties needed from the device
179
- };
180
-
181
- // Update polling state based on device status
182
- const isActive = details.deviceStatus === 'on';
183
- this.updatePollingState(isActive);
184
-
185
- this.logger.pollingEvent({
186
- ...context,
187
- interval: this.pollingManager.getCurrentInterval(),
188
- active: isActive,
189
- });
190
-
191
- // Update device-specific states
192
- this.updateDeviceSpecificStates(details);
193
- }, 'sync device state');
177
+ if (this.needsRetry) {
178
+ this.platform.log.debug(`[${this.accessory.displayName}] Retrying previous failed operation`);
179
+ }
180
+
181
+ const details = await this.device.getDetails();
182
+ this.needsRetry = false; // Clear retry flag on success
183
+
184
+ // Update the device state
185
+ await this.updateDeviceSpecificStates(details);
194
186
  } catch (error) {
195
- this.logger.error(
187
+ await this.handleDeviceError(
196
188
  'Failed to sync device state',
197
- context,
198
- error as Error
189
+ error instanceof Error ? error : new Error(String(error))
199
190
  );
200
- throw error; // Let polling manager handle the error
201
191
  }
202
192
  }
203
193
 
@@ -318,15 +308,52 @@ export abstract class BaseAccessory {
318
308
  }
319
309
 
320
310
  /**
321
- * Handle device errors consistently
311
+ * Handle device errors with appropriate recovery actions
322
312
  */
323
- protected handleDeviceError(operation: string, error: unknown): void {
313
+ protected async handleDeviceError(operation: string, error: unknown): Promise<void> {
324
314
  const errorObj = error instanceof Error ? error : new Error(String(error));
325
- this.logger.error(
326
- `Failed to ${operation}`,
327
- this.getLogContext(operation),
328
- errorObj
329
- );
330
- throw errorObj;
315
+ const errorDetails = errorObj as any;
316
+ const errorCode = errorDetails?.error?.code || errorDetails?.code;
317
+ const errorMsg = errorDetails?.error?.msg || errorDetails?.msg;
318
+
319
+ // Log the error with context
320
+ this.platform.log.error(`[${this.accessory.displayName}] ${operation}: ${JSON.stringify(errorDetails)}`);
321
+
322
+ // Handle specific error codes
323
+ switch (errorCode) {
324
+ case 4041008: // Device not found
325
+ this.platform.log.warn(`[${this.accessory.displayName}] Device not found, will attempt to rediscover`);
326
+ await this.attemptReconnection();
327
+ break;
328
+ case -11102086: // Internal error
329
+ this.platform.log.warn(`[${this.accessory.displayName}] VeSync API internal error, will retry later`);
330
+ // Mark device for retry on next update cycle
331
+ this.markForRetry();
332
+ break;
333
+ default:
334
+ if (errorMsg?.includes('not found')) {
335
+ await this.attemptReconnection();
336
+ }
337
+ }
338
+ }
339
+
340
+ /**
341
+ * Attempt to reconnect to the device by triggering device discovery
342
+ */
343
+ private async attemptReconnection(): Promise<void> {
344
+ try {
345
+ // Attempt to refresh the device list
346
+ await this.platform.discoverDevices();
347
+ this.platform.log.info(`[${this.accessory.displayName}] Successfully refreshed device list`);
348
+ } catch (error) {
349
+ this.platform.log.error(`[${this.accessory.displayName}] Failed to reconnect: ${error}`);
350
+ }
351
+ }
352
+
353
+ /**
354
+ * Mark the device for retry on next update cycle
355
+ */
356
+ private markForRetry(): void {
357
+ this.needsRetry = true;
331
358
  }
332
359
  }
@@ -9,14 +9,16 @@ const COUNTER_CLOCKWISE = 1;
9
9
 
10
10
  export class FanAccessory extends BaseAccessory {
11
11
  protected readonly device: VeSyncFan;
12
+ private readonly capabilities: DeviceCapabilities;
12
13
 
13
14
  constructor(
14
15
  platform: TSVESyncPlatform,
15
16
  accessory: PlatformAccessory,
16
- device: VeSyncFan,
17
+ device: VeSyncFan
17
18
  ) {
18
19
  super(platform, accessory, device);
19
20
  this.device = device;
21
+ this.capabilities = this.getDeviceCapabilities();
20
22
  }
21
23
 
22
24
  protected setupService(): void {
@@ -38,7 +40,7 @@ export class FanAccessory extends BaseAccessory {
38
40
  this.setupCharacteristic(
39
41
  this.platform.Characteristic.RotationSpeed,
40
42
  this.getRotationSpeed.bind(this),
41
- this.setRotationSpeed.bind(this)
43
+ this.handleSetRotationSpeed.bind(this)
42
44
  );
43
45
  }
44
46
 
@@ -74,48 +76,50 @@ export class FanAccessory extends BaseAccessory {
74
76
  );
75
77
  }
76
78
 
77
- protected updateDeviceSpecificStates(details: any): void {
78
- const capabilities = this.getDeviceCapabilities();
79
+ /**
80
+ * Update device states based on the latest details
81
+ */
82
+ protected async updateDeviceSpecificStates(details: any): Promise<void> {
83
+ const fanDetails = details as {
84
+ deviceStatus: string;
85
+ speed: number;
86
+ childLock: boolean;
87
+ swingMode: boolean;
88
+ };
89
+
90
+ // Update active state
91
+ const isActive = fanDetails.deviceStatus === 'on';
92
+ this.updateCharacteristicValue(
93
+ this.platform.Characteristic.Active,
94
+ isActive ? 1 : 0
95
+ );
79
96
 
80
- // Update speed if available
81
- if (capabilities.hasSpeed && details.speed !== undefined) {
82
- const normalizedSpeed = (details.speed / this.device.maxSpeed) * 100;
97
+ // Update rotation speed if supported
98
+ if (this.capabilities.hasSpeed) {
83
99
  this.updateCharacteristicValue(
84
100
  this.platform.Characteristic.RotationSpeed,
85
- normalizedSpeed
101
+ fanDetails.speed
86
102
  );
87
103
  }
88
104
 
89
- // Update rotation direction if available
90
- if ('rotationDirection' in details) {
105
+ // Update child lock if supported
106
+ if (this.capabilities.hasChildLock) {
91
107
  this.updateCharacteristicValue(
92
- this.platform.Characteristic.RotationDirection,
93
- details.rotationDirection === 'clockwise' ? CLOCKWISE : COUNTER_CLOCKWISE
108
+ this.platform.Characteristic.LockPhysicalControls,
109
+ fanDetails.childLock ? 1 : 0
94
110
  );
95
111
  }
96
112
 
97
- // Update swing mode if available
98
- if (capabilities.hasSwingMode && details.swingMode !== undefined) {
113
+ // Update swing mode if supported
114
+ if (this.capabilities.hasSwingMode) {
99
115
  this.updateCharacteristicValue(
100
116
  this.platform.Characteristic.SwingMode,
101
- details.swingMode ? 1 : 0
102
- );
103
- }
104
-
105
- // Update child lock if available
106
- if (capabilities.hasChildLock && details.childLock !== undefined) {
107
- this.updateCharacteristicValue(
108
- this.platform.Characteristic.LockPhysicalControls,
109
- details.childLock ? 1 : 0
117
+ fanDetails.swingMode ? 1 : 0
110
118
  );
111
119
  }
112
120
 
113
- // Update active state
114
- const isActive = details.deviceStatus === 'on';
115
- this.updateCharacteristicValue(
116
- this.platform.Characteristic.Active,
117
- isActive ? 1 : 0
118
- );
121
+ // Update polling state based on active status
122
+ this.updatePollingState(isActive);
119
123
  }
120
124
 
121
125
  protected getDeviceCapabilities(): DeviceCapabilities {
@@ -151,23 +155,15 @@ export class FanAccessory extends BaseAccessory {
151
155
  }
152
156
  }
153
157
 
154
- private async getRotationSpeed(): Promise<CharacteristicValue> {
155
- return (this.device.speed / this.device.maxSpeed) * 100;
158
+ private async handleSetRotationSpeed(value: CharacteristicValue): Promise<void> {
159
+ const speed = Math.round((value as number / 100) * this.device.maxSpeed);
160
+ await this.withRetry(async () => {
161
+ await this.device.changeFanSpeed(speed);
162
+ }, 'set rotation speed');
156
163
  }
157
164
 
158
- private async setRotationSpeed(value: CharacteristicValue): Promise<void> {
159
- try {
160
- const speed = Math.round((value as number / 100) * this.device.maxSpeed);
161
- const success = await this.device.setSpeed(speed);
162
-
163
- if (!success) {
164
- throw new Error(`Failed to set speed to ${speed}`);
165
- }
166
-
167
- await this.persistDeviceState('speed', speed);
168
- } catch (error) {
169
- this.handleDeviceError('set rotation speed', error);
170
- }
165
+ private async getRotationSpeed(): Promise<CharacteristicValue> {
166
+ return (this.device.speed / this.device.maxSpeed) * 100;
171
167
  }
172
168
 
173
169
  private async getRotationDirection(): Promise<CharacteristicValue> {