homebridge-tsvesync 1.3.3 → 1.3.7
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/CHANGELOG.md +32 -0
- package/dist/accessories/light.accessory.d.ts +26 -4
- package/dist/accessories/light.accessory.d.ts.map +1 -1
- package/dist/accessories/light.accessory.js +292 -15
- package/dist/accessories/light.accessory.js.map +1 -1
- package/dist/platform.d.ts.map +1 -1
- package/dist/platform.js +19 -1
- package/dist/platform.js.map +1 -1
- package/dist/types/device.types.d.ts +16 -0
- package/dist/types/device.types.d.ts.map +1 -1
- package/dist/utils/device-factory.d.ts +1 -0
- package/dist/utils/device-factory.d.ts.map +1 -1
- package/dist/utils/device-factory.js +10 -2
- package/dist/utils/device-factory.js.map +1 -1
- package/package.json +2 -2
- package/src/accessories/light.accessory.ts +417 -36
- package/src/platform.ts +21 -1
- package/src/types/device.types.ts +18 -0
- package/src/utils/device-factory.ts +16 -4
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { CharacteristicValue, PlatformAccessory } from 'homebridge';
|
|
1
|
+
import { CharacteristicValue, PlatformAccessory, Service } from 'homebridge';
|
|
2
2
|
import { BaseAccessory } from './base.accessory';
|
|
3
3
|
import { TSVESyncPlatform } from '../platform';
|
|
4
|
-
import { DeviceCapabilities, VeSyncBulb } from '../types/device.types';
|
|
4
|
+
import { DeviceCapabilities, VeSyncBulb, VeSyncDimmerSwitch, VeSyncLightDevice } from '../types/device.types';
|
|
5
5
|
|
|
6
6
|
// Constants for color temperature
|
|
7
7
|
const MIN_COLOR_TEMP = 140; // 7143K (cool)
|
|
@@ -9,17 +9,22 @@ const MAX_COLOR_TEMP = 500; // 2000K (warm)
|
|
|
9
9
|
const DEFAULT_COLOR_TEMP = MIN_COLOR_TEMP;
|
|
10
10
|
|
|
11
11
|
export class LightAccessory extends BaseAccessory {
|
|
12
|
-
protected readonly device:
|
|
13
|
-
private
|
|
12
|
+
protected readonly device: VeSyncLightDevice;
|
|
13
|
+
private capabilities!: DeviceCapabilities;
|
|
14
|
+
private isDimmerDevice = false;
|
|
15
|
+
private indicatorService?: Service;
|
|
16
|
+
private indicatorColorState = { hue: 0, saturation: 0 };
|
|
17
|
+
private indicatorColorUpdateTimeout?: NodeJS.Timeout;
|
|
18
|
+
private lastDimmerRefresh = 0;
|
|
19
|
+
private static readonly DIMMER_REFRESH_DEBOUNCE_MS = 2000;
|
|
14
20
|
|
|
15
21
|
constructor(
|
|
16
22
|
platform: TSVESyncPlatform,
|
|
17
23
|
accessory: PlatformAccessory,
|
|
18
|
-
device:
|
|
24
|
+
device: VeSyncLightDevice
|
|
19
25
|
) {
|
|
20
26
|
super(platform, accessory, device);
|
|
21
27
|
this.device = device;
|
|
22
|
-
this.capabilities = this.getDeviceCapabilities();
|
|
23
28
|
}
|
|
24
29
|
|
|
25
30
|
protected setupService(): void {
|
|
@@ -35,7 +40,9 @@ export class LightAccessory extends BaseAccessory {
|
|
|
35
40
|
);
|
|
36
41
|
|
|
37
42
|
// Set up optional characteristics based on device capabilities
|
|
38
|
-
|
|
43
|
+
this.isDimmerDevice = this.detectDimmer(this.device);
|
|
44
|
+
this.capabilities = this.getDeviceCapabilities();
|
|
45
|
+
const capabilities = this.capabilities;
|
|
39
46
|
|
|
40
47
|
if (capabilities.hasBrightness) {
|
|
41
48
|
this.setupCharacteristic(
|
|
@@ -58,6 +65,10 @@ export class LightAccessory extends BaseAccessory {
|
|
|
58
65
|
this.platform.Characteristic.Name,
|
|
59
66
|
async () => this.device.deviceName
|
|
60
67
|
);
|
|
68
|
+
|
|
69
|
+
if (this.isDimmerDevice) {
|
|
70
|
+
this.setupIndicatorService();
|
|
71
|
+
}
|
|
61
72
|
}
|
|
62
73
|
|
|
63
74
|
private setupColorTemperature(): void {
|
|
@@ -92,9 +103,7 @@ export class LightAccessory extends BaseAccessory {
|
|
|
92
103
|
* Update device states based on the latest details
|
|
93
104
|
*/
|
|
94
105
|
protected async updateDeviceSpecificStates(details: any): Promise<void> {
|
|
95
|
-
const lightDetails = details as {
|
|
96
|
-
deviceStatus: string;
|
|
97
|
-
brightness?: number;
|
|
106
|
+
const lightDetails = details as VeSyncLightDevice & {
|
|
98
107
|
colorTemp?: number;
|
|
99
108
|
hue?: number;
|
|
100
109
|
saturation?: number;
|
|
@@ -138,10 +147,41 @@ export class LightAccessory extends BaseAccessory {
|
|
|
138
147
|
);
|
|
139
148
|
}
|
|
140
149
|
}
|
|
150
|
+
|
|
151
|
+
if (this.isDimmerDevice && this.indicatorService) {
|
|
152
|
+
const dimmer = lightDetails as VeSyncDimmerSwitch;
|
|
153
|
+
const isRgbOn = dimmer.rgbLightStatus === 'on';
|
|
154
|
+
this.updateIndicatorCharacteristic(
|
|
155
|
+
this.platform.Characteristic.On,
|
|
156
|
+
isRgbOn
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
const { hue, saturation } = this.rgbToHsv(
|
|
160
|
+
dimmer.rgbLightValue.red,
|
|
161
|
+
dimmer.rgbLightValue.green,
|
|
162
|
+
dimmer.rgbLightValue.blue
|
|
163
|
+
);
|
|
164
|
+
this.indicatorColorState = { hue, saturation };
|
|
165
|
+
this.updateIndicatorCharacteristic(this.platform.Characteristic.Hue, hue);
|
|
166
|
+
this.updateIndicatorCharacteristic(this.platform.Characteristic.Saturation, saturation);
|
|
167
|
+
}
|
|
141
168
|
}
|
|
142
169
|
|
|
143
170
|
protected getDeviceCapabilities(): DeviceCapabilities {
|
|
144
|
-
|
|
171
|
+
if (this.isDimmerDevice) {
|
|
172
|
+
return {
|
|
173
|
+
hasBrightness: true,
|
|
174
|
+
hasColorTemp: false,
|
|
175
|
+
hasColor: false,
|
|
176
|
+
hasSpeed: false,
|
|
177
|
+
hasHumidity: false,
|
|
178
|
+
hasAirQuality: false,
|
|
179
|
+
hasWaterLevel: false,
|
|
180
|
+
hasChildLock: false,
|
|
181
|
+
hasSwingMode: false,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
145
185
|
const model = this.device.deviceType.toUpperCase();
|
|
146
186
|
return {
|
|
147
187
|
hasBrightness: true, // All VeSync bulbs support brightness
|
|
@@ -170,6 +210,10 @@ export class LightAccessory extends BaseAccessory {
|
|
|
170
210
|
}
|
|
171
211
|
|
|
172
212
|
await this.persistDeviceState('deviceStatus', isOn ? 'on' : 'off');
|
|
213
|
+
|
|
214
|
+
if (this.isDimmerDevice) {
|
|
215
|
+
await this.refreshDimmerDetails();
|
|
216
|
+
}
|
|
173
217
|
} catch (error) {
|
|
174
218
|
this.handleDeviceError('set on state', error);
|
|
175
219
|
}
|
|
@@ -181,34 +225,67 @@ export class LightAccessory extends BaseAccessory {
|
|
|
181
225
|
|
|
182
226
|
private async setBrightness(value: CharacteristicValue): Promise<void> {
|
|
183
227
|
try {
|
|
184
|
-
const
|
|
185
|
-
|
|
228
|
+
const target = Math.round(Number(value));
|
|
229
|
+
|
|
230
|
+
if (this.isDimmerDevice && target <= 0) {
|
|
231
|
+
const success = await this.device.turnOff();
|
|
232
|
+
if (!success) {
|
|
233
|
+
throw new Error('Failed to turn off device when setting brightness to 0');
|
|
234
|
+
}
|
|
235
|
+
await this.persistDeviceState('deviceStatus', 'off');
|
|
236
|
+
await this.persistDeviceState('brightness', 0);
|
|
237
|
+
await this.refreshDimmerDetails();
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const brightness = Math.min(100, Math.max(this.isDimmerDevice ? 1 : 0, target));
|
|
242
|
+
|
|
243
|
+
const success = await this.device.setBrightness(brightness);
|
|
244
|
+
|
|
186
245
|
if (!success) {
|
|
187
|
-
throw new Error(`Failed to set brightness to ${
|
|
246
|
+
throw new Error(`Failed to set brightness to ${brightness}`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
await this.persistDeviceState('brightness', brightness);
|
|
250
|
+
if (this.isDimmerDevice && brightness > 0) {
|
|
251
|
+
await this.persistDeviceState('deviceStatus', 'on');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (this.isDimmerDevice) {
|
|
255
|
+
await this.refreshDimmerDetails();
|
|
188
256
|
}
|
|
189
|
-
|
|
190
|
-
await this.persistDeviceState('brightness', value);
|
|
191
257
|
} catch (error) {
|
|
192
258
|
this.handleDeviceError('set brightness', error);
|
|
193
259
|
}
|
|
194
260
|
}
|
|
195
261
|
|
|
196
262
|
private async getColorTemperature(): Promise<CharacteristicValue> {
|
|
197
|
-
|
|
263
|
+
if (this.isDimmerDevice) {
|
|
264
|
+
return DEFAULT_COLOR_TEMP;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const bulb = this.device as VeSyncBulb;
|
|
268
|
+
return bulb.colorTemp || DEFAULT_COLOR_TEMP;
|
|
198
269
|
}
|
|
199
270
|
|
|
200
271
|
private async setColorTemperature(value: CharacteristicValue): Promise<void> {
|
|
201
272
|
try {
|
|
202
|
-
if (
|
|
273
|
+
if (this.isDimmerDevice) {
|
|
203
274
|
throw new Error('Device does not support color temperature');
|
|
204
275
|
}
|
|
205
|
-
|
|
206
|
-
const
|
|
207
|
-
|
|
276
|
+
|
|
277
|
+
const bulb = this.device as VeSyncBulb;
|
|
278
|
+
|
|
279
|
+
if (!bulb.setColorTemperature) {
|
|
280
|
+
throw new Error('Device does not support color temperature');
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const success = await bulb.setColorTemperature(value as number);
|
|
284
|
+
|
|
208
285
|
if (!success) {
|
|
209
286
|
throw new Error(`Failed to set color temperature to ${value}`);
|
|
210
287
|
}
|
|
211
|
-
|
|
288
|
+
|
|
212
289
|
await this.persistDeviceState('colorTemp', value);
|
|
213
290
|
} catch (error) {
|
|
214
291
|
this.handleDeviceError('set color temperature', error);
|
|
@@ -216,24 +293,40 @@ export class LightAccessory extends BaseAccessory {
|
|
|
216
293
|
}
|
|
217
294
|
|
|
218
295
|
private async getHue(): Promise<CharacteristicValue> {
|
|
219
|
-
|
|
296
|
+
if (this.isDimmerDevice) {
|
|
297
|
+
return 0;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const bulb = this.device as VeSyncBulb;
|
|
301
|
+
return bulb.hue || 0;
|
|
220
302
|
}
|
|
221
303
|
|
|
222
304
|
private async getSaturation(): Promise<CharacteristicValue> {
|
|
223
|
-
|
|
305
|
+
if (this.isDimmerDevice) {
|
|
306
|
+
return 0;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const bulb = this.device as VeSyncBulb;
|
|
310
|
+
return bulb.saturation || 0;
|
|
224
311
|
}
|
|
225
312
|
|
|
226
313
|
private async setHue(value: CharacteristicValue): Promise<void> {
|
|
227
314
|
try {
|
|
228
|
-
if (
|
|
315
|
+
if (this.isDimmerDevice) {
|
|
229
316
|
throw new Error('Device does not support color');
|
|
230
317
|
}
|
|
231
|
-
|
|
232
|
-
const
|
|
318
|
+
|
|
319
|
+
const bulb = this.device as VeSyncBulb;
|
|
320
|
+
|
|
321
|
+
if (!bulb.setColor) {
|
|
322
|
+
throw new Error('Device does not support color');
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const success = await bulb.setColor(
|
|
233
326
|
value as number,
|
|
234
|
-
|
|
327
|
+
bulb.saturation || 0
|
|
235
328
|
);
|
|
236
|
-
|
|
329
|
+
|
|
237
330
|
if (!success) {
|
|
238
331
|
throw new Error(`Failed to set hue to ${value}`);
|
|
239
332
|
}
|
|
@@ -246,22 +339,310 @@ export class LightAccessory extends BaseAccessory {
|
|
|
246
339
|
|
|
247
340
|
private async setSaturation(value: CharacteristicValue): Promise<void> {
|
|
248
341
|
try {
|
|
249
|
-
if (
|
|
342
|
+
if (this.isDimmerDevice) {
|
|
250
343
|
throw new Error('Device does not support color');
|
|
251
344
|
}
|
|
252
|
-
|
|
253
|
-
const
|
|
254
|
-
|
|
345
|
+
|
|
346
|
+
const bulb = this.device as VeSyncBulb;
|
|
347
|
+
|
|
348
|
+
if (!bulb.setColor) {
|
|
349
|
+
throw new Error('Device does not support color');
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const success = await bulb.setColor(
|
|
353
|
+
bulb.hue || 0,
|
|
255
354
|
value as number
|
|
256
355
|
);
|
|
257
|
-
|
|
356
|
+
|
|
258
357
|
if (!success) {
|
|
259
358
|
throw new Error(`Failed to set saturation to ${value}`);
|
|
260
359
|
}
|
|
261
|
-
|
|
360
|
+
|
|
262
361
|
await this.persistDeviceState('saturation', value);
|
|
263
362
|
} catch (error) {
|
|
264
363
|
this.handleDeviceError('set saturation', error);
|
|
265
364
|
}
|
|
266
365
|
}
|
|
267
|
-
|
|
366
|
+
|
|
367
|
+
private detectDimmer(device: VeSyncLightDevice): device is VeSyncDimmerSwitch {
|
|
368
|
+
return typeof (device as VeSyncDimmerSwitch).rgbColorSet === 'function';
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
private setupIndicatorService(): void {
|
|
372
|
+
const serviceName = `${this.device.deviceName} Indicator`;
|
|
373
|
+
const existing = this.accessory.getService(serviceName);
|
|
374
|
+
this.indicatorService = existing || this.accessory.addService(
|
|
375
|
+
this.platform.Service.Lightbulb,
|
|
376
|
+
serviceName,
|
|
377
|
+
'indicator'
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
const indicatorService = this.indicatorService;
|
|
381
|
+
if (!indicatorService) {
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
this.setupCharacteristicForService(
|
|
386
|
+
indicatorService,
|
|
387
|
+
this.platform.Characteristic.On,
|
|
388
|
+
this.getIndicatorOn.bind(this),
|
|
389
|
+
this.setIndicatorOn.bind(this)
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
this.setupCharacteristicForService(
|
|
393
|
+
indicatorService,
|
|
394
|
+
this.platform.Characteristic.Hue,
|
|
395
|
+
this.getIndicatorHue.bind(this),
|
|
396
|
+
this.setIndicatorHue.bind(this)
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
this.setupCharacteristicForService(
|
|
400
|
+
indicatorService,
|
|
401
|
+
this.platform.Characteristic.Saturation,
|
|
402
|
+
this.getIndicatorSaturation.bind(this),
|
|
403
|
+
this.setIndicatorSaturation.bind(this)
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
private setupCharacteristicForService(
|
|
408
|
+
service: Service,
|
|
409
|
+
characteristic: any,
|
|
410
|
+
onGet?: () => Promise<CharacteristicValue>,
|
|
411
|
+
onSet?: (value: CharacteristicValue) => Promise<void>
|
|
412
|
+
): void {
|
|
413
|
+
this.withService(service, () => {
|
|
414
|
+
this.setupCharacteristic(characteristic, onGet, onSet);
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
private updateIndicatorCharacteristic(
|
|
419
|
+
characteristic: any,
|
|
420
|
+
value: CharacteristicValue
|
|
421
|
+
): void {
|
|
422
|
+
if (!this.indicatorService) {
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
this.withService(this.indicatorService, () => {
|
|
427
|
+
this.updateCharacteristicValue(characteristic, value);
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
private withService<T>(service: Service, fn: () => T): T {
|
|
432
|
+
const currentService = this.service;
|
|
433
|
+
this.service = service;
|
|
434
|
+
try {
|
|
435
|
+
return fn();
|
|
436
|
+
} finally {
|
|
437
|
+
this.service = currentService;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
private async getIndicatorOn(): Promise<CharacteristicValue> {
|
|
442
|
+
if (!this.isDimmerDevice) {
|
|
443
|
+
return false;
|
|
444
|
+
}
|
|
445
|
+
const dimmer = this.device as VeSyncDimmerSwitch;
|
|
446
|
+
return dimmer.rgbLightStatus === 'on';
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
private async setIndicatorOn(value: CharacteristicValue): Promise<void> {
|
|
450
|
+
if (!this.isDimmerDevice) {
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const isOn = Boolean(value);
|
|
455
|
+
const dimmer = this.device as VeSyncDimmerSwitch;
|
|
456
|
+
const success = isOn ? await dimmer.rgbColorOn() : await dimmer.rgbColorOff();
|
|
457
|
+
|
|
458
|
+
if (!success) {
|
|
459
|
+
throw new Error(`Failed to turn ${isOn ? 'on' : 'off'} indicator light`);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
await this.refreshDimmerDetails();
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
private async getIndicatorHue(): Promise<CharacteristicValue> {
|
|
466
|
+
if (!this.isDimmerDevice) {
|
|
467
|
+
return 0;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const { hue } = this.rgbToHsv(
|
|
471
|
+
(this.device as VeSyncDimmerSwitch).rgbLightValue.red,
|
|
472
|
+
(this.device as VeSyncDimmerSwitch).rgbLightValue.green,
|
|
473
|
+
(this.device as VeSyncDimmerSwitch).rgbLightValue.blue
|
|
474
|
+
);
|
|
475
|
+
this.indicatorColorState.hue = hue;
|
|
476
|
+
return hue;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
private async setIndicatorHue(value: CharacteristicValue): Promise<void> {
|
|
480
|
+
if (!this.isDimmerDevice) {
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
this.indicatorColorState.hue = Number(value);
|
|
485
|
+
this.scheduleIndicatorColorUpdate();
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
private async getIndicatorSaturation(): Promise<CharacteristicValue> {
|
|
489
|
+
if (!this.isDimmerDevice) {
|
|
490
|
+
return 0;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const { saturation } = this.rgbToHsv(
|
|
494
|
+
(this.device as VeSyncDimmerSwitch).rgbLightValue.red,
|
|
495
|
+
(this.device as VeSyncDimmerSwitch).rgbLightValue.green,
|
|
496
|
+
(this.device as VeSyncDimmerSwitch).rgbLightValue.blue
|
|
497
|
+
);
|
|
498
|
+
this.indicatorColorState.saturation = saturation;
|
|
499
|
+
return saturation;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
private async setIndicatorSaturation(value: CharacteristicValue): Promise<void> {
|
|
503
|
+
if (!this.isDimmerDevice) {
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
this.indicatorColorState.saturation = Number(value);
|
|
508
|
+
this.scheduleIndicatorColorUpdate();
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
private scheduleIndicatorColorUpdate(): void {
|
|
512
|
+
if (this.indicatorColorUpdateTimeout) {
|
|
513
|
+
clearTimeout(this.indicatorColorUpdateTimeout);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
this.indicatorColorUpdateTimeout = setTimeout(() => {
|
|
517
|
+
void this.pushIndicatorColor().catch(error => {
|
|
518
|
+
void this.handleDeviceError('set indicator color', error);
|
|
519
|
+
});
|
|
520
|
+
}, 250);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
private async pushIndicatorColor(): Promise<void> {
|
|
524
|
+
if (!this.isDimmerDevice) {
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const [red, green, blue] = this.hsvToRgb(
|
|
529
|
+
this.indicatorColorState.hue,
|
|
530
|
+
this.indicatorColorState.saturation,
|
|
531
|
+
100
|
|
532
|
+
);
|
|
533
|
+
|
|
534
|
+
const dimmer = this.device as VeSyncDimmerSwitch;
|
|
535
|
+
const success = await dimmer.rgbColorSet(red, green, blue);
|
|
536
|
+
|
|
537
|
+
if (!success) {
|
|
538
|
+
throw new Error('Failed to update indicator color');
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
this.indicatorColorState = {
|
|
542
|
+
hue: this.indicatorColorState.hue,
|
|
543
|
+
saturation: this.indicatorColorState.saturation,
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
await this.refreshDimmerDetails();
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
private hsvToRgb(h: number, s: number, v: number): [number, number, number] {
|
|
550
|
+
let hue = h % 360;
|
|
551
|
+
if (hue < 0) {
|
|
552
|
+
hue += 360;
|
|
553
|
+
}
|
|
554
|
+
const saturation = Math.max(0, Math.min(100, s)) / 100;
|
|
555
|
+
const value = Math.max(0, Math.min(100, v)) / 100;
|
|
556
|
+
|
|
557
|
+
const c = value * saturation;
|
|
558
|
+
const x = c * (1 - Math.abs(((hue / 60) % 2) - 1));
|
|
559
|
+
const m = value - c;
|
|
560
|
+
|
|
561
|
+
let rPrime = 0;
|
|
562
|
+
let gPrime = 0;
|
|
563
|
+
let bPrime = 0;
|
|
564
|
+
|
|
565
|
+
if (hue < 60) {
|
|
566
|
+
rPrime = c;
|
|
567
|
+
gPrime = x;
|
|
568
|
+
} else if (hue < 120) {
|
|
569
|
+
rPrime = x;
|
|
570
|
+
gPrime = c;
|
|
571
|
+
} else if (hue < 180) {
|
|
572
|
+
gPrime = c;
|
|
573
|
+
bPrime = x;
|
|
574
|
+
} else if (hue < 240) {
|
|
575
|
+
gPrime = x;
|
|
576
|
+
bPrime = c;
|
|
577
|
+
} else if (hue < 300) {
|
|
578
|
+
rPrime = x;
|
|
579
|
+
bPrime = c;
|
|
580
|
+
} else {
|
|
581
|
+
rPrime = c;
|
|
582
|
+
bPrime = x;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const red = Math.round((rPrime + m) * 255);
|
|
586
|
+
const green = Math.round((gPrime + m) * 255);
|
|
587
|
+
const blue = Math.round((bPrime + m) * 255);
|
|
588
|
+
|
|
589
|
+
return [red, green, blue];
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
private rgbToHsv(r: number, g: number, b: number): { hue: number; saturation: number; value: number } {
|
|
593
|
+
const red = r / 255;
|
|
594
|
+
const green = g / 255;
|
|
595
|
+
const blue = b / 255;
|
|
596
|
+
|
|
597
|
+
const max = Math.max(red, green, blue);
|
|
598
|
+
const min = Math.min(red, green, blue);
|
|
599
|
+
const delta = max - min;
|
|
600
|
+
|
|
601
|
+
let hue = 0;
|
|
602
|
+
if (delta !== 0) {
|
|
603
|
+
if (max === red) {
|
|
604
|
+
hue = ((green - blue) / delta) % 6;
|
|
605
|
+
} else if (max === green) {
|
|
606
|
+
hue = (blue - red) / delta + 2;
|
|
607
|
+
} else {
|
|
608
|
+
hue = (red - green) / delta + 4;
|
|
609
|
+
}
|
|
610
|
+
hue *= 60;
|
|
611
|
+
if (hue < 0) {
|
|
612
|
+
hue += 360;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const saturation = max === 0 ? 0 : (delta / max) * 100;
|
|
617
|
+
const value = max * 100;
|
|
618
|
+
|
|
619
|
+
return {
|
|
620
|
+
hue: Math.round(hue),
|
|
621
|
+
saturation: Math.round(saturation),
|
|
622
|
+
value: Math.round(value),
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
private async refreshDimmerDetails(): Promise<void> {
|
|
627
|
+
if (!this.isDimmerDevice) {
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const dimmer = this.device as VeSyncDimmerSwitch;
|
|
632
|
+
try {
|
|
633
|
+
const now = Date.now();
|
|
634
|
+
if (
|
|
635
|
+
now - this.lastDimmerRefresh >= LightAccessory.DIMMER_REFRESH_DEBOUNCE_MS &&
|
|
636
|
+
typeof (dimmer as any).getDetails === 'function'
|
|
637
|
+
) {
|
|
638
|
+
await (dimmer as any).getDetails();
|
|
639
|
+
this.lastDimmerRefresh = now;
|
|
640
|
+
}
|
|
641
|
+
} catch (error) {
|
|
642
|
+
this.platform.log.debug(
|
|
643
|
+
`${this.device.deviceName}: failed to refresh details after state change`,
|
|
644
|
+
error instanceof Error ? error : new Error(String(error))
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
package/src/platform.ts
CHANGED
|
@@ -344,7 +344,27 @@ export class TSVESyncPlatform implements DynamicPlatformPlugin {
|
|
|
344
344
|
* Update device states periodically
|
|
345
345
|
*/
|
|
346
346
|
private async updateDeviceStates() {
|
|
347
|
-
|
|
347
|
+
if (this.refreshInProgress) {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
this.refreshInProgress = true;
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
await this.discoverDevices();
|
|
355
|
+
|
|
356
|
+
const syncTasks = Array.from(this.deviceAccessories.values()).map(async accessory => {
|
|
357
|
+
try {
|
|
358
|
+
await accessory.syncDeviceState();
|
|
359
|
+
} catch (error) {
|
|
360
|
+
this.logger.warn('Failed to sync device state during scheduled refresh', error as Error);
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
await Promise.all(syncTasks);
|
|
365
|
+
} finally {
|
|
366
|
+
this.refreshInProgress = false;
|
|
367
|
+
}
|
|
348
368
|
}
|
|
349
369
|
|
|
350
370
|
/**
|
|
@@ -22,6 +22,7 @@ export interface VeSyncDeviceWithPower extends VeSyncBaseDevice {
|
|
|
22
22
|
deviceStatus: string;
|
|
23
23
|
turnOn(): Promise<boolean>;
|
|
24
24
|
turnOff(): Promise<boolean>;
|
|
25
|
+
getDetails?: (...args: any[]) => any;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
export interface VeSyncDeviceWithBrightness extends VeSyncDeviceWithPower {
|
|
@@ -84,8 +85,25 @@ export interface VeSyncFan extends VeSyncDeviceWithPower, VeSyncDeviceWithSpeed,
|
|
|
84
85
|
setMode(mode: 'normal' | 'auto' | 'sleep' | 'turbo'): Promise<boolean>;
|
|
85
86
|
}
|
|
86
87
|
|
|
88
|
+
export interface VeSyncDimmerSwitch extends VeSyncDeviceWithBrightness {
|
|
89
|
+
rgbLightStatus: string;
|
|
90
|
+
indicatorLightStatus: string;
|
|
91
|
+
rgbLightValue: {
|
|
92
|
+
red: number;
|
|
93
|
+
green: number;
|
|
94
|
+
blue: number;
|
|
95
|
+
};
|
|
96
|
+
rgbColorSet(red: number, green: number, blue: number): Promise<boolean>;
|
|
97
|
+
rgbColorOn(): Promise<boolean>;
|
|
98
|
+
rgbColorOff(): Promise<boolean>;
|
|
99
|
+
indicatorLightOn(): Promise<boolean>;
|
|
100
|
+
indicatorLightOff(): Promise<boolean>;
|
|
101
|
+
}
|
|
102
|
+
|
|
87
103
|
export interface VeSyncBulb extends VeSyncDeviceWithPower, VeSyncDeviceWithBrightness, VeSyncDeviceWithColor {}
|
|
88
104
|
|
|
105
|
+
export type VeSyncLightDevice = VeSyncBulb | VeSyncDimmerSwitch;
|
|
106
|
+
|
|
89
107
|
export interface VeSyncOutlet extends VeSyncDeviceWithPower {
|
|
90
108
|
power?: number;
|
|
91
109
|
voltage?: number;
|
|
@@ -9,11 +9,12 @@ import { OutletAccessory } from '../accessories/outlet.accessory';
|
|
|
9
9
|
import { SwitchAccessory } from '../accessories/switch.accessory';
|
|
10
10
|
import { BaseAccessory } from '../accessories/base.accessory';
|
|
11
11
|
import { AirQualitySensorAccessory } from '../accessories/air-quality-sensor.accessory';
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
13
|
VeSyncAirPurifier,
|
|
14
14
|
VeSyncHumidifier,
|
|
15
15
|
VeSyncFan,
|
|
16
16
|
VeSyncBulb,
|
|
17
|
+
VeSyncDimmerSwitch,
|
|
17
18
|
VeSyncOutlet,
|
|
18
19
|
VeSyncSwitch
|
|
19
20
|
} from '../types/device.types';
|
|
@@ -38,7 +39,9 @@ const OUTLET_MODELS = [
|
|
|
38
39
|
'ESW10-USA', 'wifi-switch-1.3'
|
|
39
40
|
];
|
|
40
41
|
|
|
41
|
-
const SWITCH_MODELS = ['
|
|
42
|
+
const SWITCH_MODELS = ['ESWL01', 'ESWL03'];
|
|
43
|
+
|
|
44
|
+
const DIMMER_MODELS = ['ESWD16'];
|
|
42
45
|
|
|
43
46
|
export class DeviceFactory {
|
|
44
47
|
private static modelMatches(deviceType: string, models: string[]): boolean {
|
|
@@ -66,6 +69,10 @@ export class DeviceFactory {
|
|
|
66
69
|
return deviceType.startsWith('ESL') || deviceType === 'XYD0001';
|
|
67
70
|
}
|
|
68
71
|
|
|
72
|
+
private static isDimmer(deviceType: string): boolean {
|
|
73
|
+
return this.modelMatches(deviceType, DIMMER_MODELS);
|
|
74
|
+
}
|
|
75
|
+
|
|
69
76
|
private static isOutlet(deviceType: string): boolean {
|
|
70
77
|
return this.modelMatches(deviceType, OUTLET_MODELS);
|
|
71
78
|
}
|
|
@@ -96,6 +103,11 @@ export class DeviceFactory {
|
|
|
96
103
|
return new FanAccessory(platform, accessory, device as VeSyncFan);
|
|
97
104
|
}
|
|
98
105
|
|
|
106
|
+
// Dimmers
|
|
107
|
+
if (this.isDimmer(deviceType)) {
|
|
108
|
+
return new LightAccessory(platform, accessory, device as VeSyncDimmerSwitch);
|
|
109
|
+
}
|
|
110
|
+
|
|
99
111
|
// Bulbs
|
|
100
112
|
if (this.isBulb(deviceType)) {
|
|
101
113
|
return new LightAccessory(platform, accessory, device as VeSyncBulb);
|
|
@@ -131,7 +143,7 @@ export class DeviceFactory {
|
|
|
131
143
|
return Categories.FAN;
|
|
132
144
|
}
|
|
133
145
|
|
|
134
|
-
if (this.isBulb(type)) {
|
|
146
|
+
if (this.isDimmer(type) || this.isBulb(type)) {
|
|
135
147
|
return Categories.LIGHTBULB;
|
|
136
148
|
}
|
|
137
149
|
|
|
@@ -179,4 +191,4 @@ export class DeviceFactory {
|
|
|
179
191
|
platform.log.debug(`Not creating AQ sensor for ${device.deviceName} - no air quality support detected`);
|
|
180
192
|
return null;
|
|
181
193
|
}
|
|
182
|
-
}
|
|
194
|
+
}
|