homebridge-tsvesync 1.1.2 → 1.3.4
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 +312 -0
- package/dist/accessories/air-purifier.accessory.d.ts +1 -0
- package/dist/accessories/air-purifier.accessory.d.ts.map +1 -1
- package/dist/accessories/air-purifier.accessory.js +38 -26
- package/dist/accessories/air-purifier.accessory.js.map +1 -1
- package/dist/accessories/light.accessory.d.ts +25 -3
- package/dist/accessories/light.accessory.d.ts.map +1 -1
- package/dist/accessories/light.accessory.js +290 -14
- package/dist/accessories/light.accessory.js.map +1 -1
- package/dist/platform.d.ts +18 -0
- package/dist/platform.d.ts.map +1 -1
- package/dist/platform.js +224 -11
- 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/api-proxy.d.ts +4 -1
- package/dist/utils/api-proxy.d.ts.map +1 -1
- package/dist/utils/api-proxy.js +5 -3
- package/dist/utils/api-proxy.js.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/dist/utils/session-store.d.ts +28 -0
- package/dist/utils/session-store.d.ts.map +1 -0
- package/dist/utils/session-store.js +92 -0
- package/dist/utils/session-store.js.map +1 -0
- package/package.json +2 -2
- package/src/accessories/air-purifier.accessory.ts +38 -22
- package/src/accessories/light.accessory.ts +415 -34
- package/src/platform.ts +223 -9
- package/src/types/device.types.ts +18 -0
- package/src/utils/api-proxy.ts +8 -2
- package/src/utils/device-factory.ts +16 -4
- package/src/utils/session-store.ts +92 -0
|
@@ -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,16 +9,23 @@ 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:
|
|
12
|
+
protected readonly device: VeSyncLightDevice;
|
|
13
13
|
private readonly capabilities: DeviceCapabilities;
|
|
14
|
+
private readonly isDimmerDevice: boolean;
|
|
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;
|
|
28
|
+
this.isDimmerDevice = this.detectDimmer(device);
|
|
22
29
|
this.capabilities = this.getDeviceCapabilities();
|
|
23
30
|
}
|
|
24
31
|
|
|
@@ -35,7 +42,7 @@ export class LightAccessory extends BaseAccessory {
|
|
|
35
42
|
);
|
|
36
43
|
|
|
37
44
|
// Set up optional characteristics based on device capabilities
|
|
38
|
-
const capabilities = this.
|
|
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
|
+
}
|