tsvesync 1.3.8 → 1.3.10
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/lib/fans/airBaseV2.d.ts +8 -10
- package/dist/lib/fans/airBaseV2.js +63 -67
- package/dist/lib/vesyncBulb.d.ts +23 -6
- package/dist/lib/vesyncBulb.js +144 -32
- package/dist/lib/vesyncBulbImpl.d.ts +16 -1
- package/dist/lib/vesyncBulbImpl.js +296 -47
- package/package.json +1 -1
- package/src/lib/fans/airBaseV2.ts +65 -71
- package/src/lib/vesyncBulb.ts +160 -34
- package/src/lib/vesyncBulbImpl.ts +404 -52
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { VeSyncAirBypass } from './airBypass';
|
|
2
2
|
import { VeSync } from '../vesync';
|
|
3
|
-
import { Helpers } from '../helpers';
|
|
4
3
|
import { logger } from '../logger';
|
|
5
4
|
|
|
6
5
|
/**
|
|
@@ -55,29 +54,23 @@ export class VeSyncAirBaseV2 extends VeSyncAirBypass {
|
|
|
55
54
|
}
|
|
56
55
|
|
|
57
56
|
/**
|
|
58
|
-
* Check response for
|
|
59
|
-
* These devices return success at API level but may have inner errors
|
|
57
|
+
* Check response for bypassV2 purifiers that surface nested result codes.
|
|
60
58
|
*/
|
|
61
|
-
protected
|
|
62
|
-
// First check basic response
|
|
59
|
+
protected checkV2Response(response: any, status: number, method: string): boolean {
|
|
60
|
+
// First check basic response structure
|
|
63
61
|
if (!this.checkResponse([response, status], method)) {
|
|
64
62
|
return false;
|
|
65
63
|
}
|
|
66
64
|
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
return true; // Not a Vital series device, standard check is enough
|
|
65
|
+
// Only apply extended handling to bypassV2 purifiers (Vital/Everest)
|
|
66
|
+
if (!this.requiresPowerSwitchPayload()) {
|
|
67
|
+
return true;
|
|
71
68
|
}
|
|
72
69
|
|
|
73
|
-
//
|
|
70
|
+
// These devices frequently succeed with a nested non-zero code
|
|
74
71
|
if (response.result && response.result.code !== undefined && response.result.code !== 0) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
logger.debug(`Vital series device ${this.deviceName} returned inner code ${response.result.code} for ${method}`);
|
|
78
|
-
logger.debug('This is normal for Vital series devices - proceeding with operation');
|
|
79
|
-
|
|
80
|
-
// Keep the device online despite inner error
|
|
72
|
+
logger.debug(`${this.deviceName} returned inner code ${response.result.code} for ${method}`);
|
|
73
|
+
logger.debug('BypassV2 purifier reports non-zero inner code but request still succeeded');
|
|
81
74
|
this.connectionStatus = 'online';
|
|
82
75
|
return true;
|
|
83
76
|
}
|
|
@@ -99,8 +92,7 @@ export class VeSyncAirBaseV2 extends VeSyncAirBypass {
|
|
|
99
92
|
head
|
|
100
93
|
);
|
|
101
94
|
|
|
102
|
-
|
|
103
|
-
const isVitalSeries = this.deviceType.includes('LAP-V102S') || this.deviceType.includes('LAP-V201S');
|
|
95
|
+
const needsV2Handling = this.requiresPowerSwitchPayload();
|
|
104
96
|
|
|
105
97
|
// First check the outer response
|
|
106
98
|
if (!response || status !== 200) {
|
|
@@ -109,15 +101,14 @@ export class VeSyncAirBaseV2 extends VeSyncAirBypass {
|
|
|
109
101
|
return false;
|
|
110
102
|
}
|
|
111
103
|
|
|
112
|
-
// For
|
|
113
|
-
|
|
114
|
-
if (isVitalSeries && response.result) {
|
|
104
|
+
// For bypassV2 purifier families the API nests the result and may surface non-zero codes
|
|
105
|
+
if (needsV2Handling && response.result) {
|
|
115
106
|
// Check if we have a valid getPurifierStatus response
|
|
116
107
|
if (response.code === 0 && response.result?.result) {
|
|
117
108
|
const statusResponse = response.result.result;
|
|
118
109
|
|
|
119
110
|
// If we have valid status data, use it regardless of inner result code
|
|
120
|
-
logger.debug(`Using status data for
|
|
111
|
+
logger.debug(`Using nested status data for bypassV2 purifier: ${this.deviceName}`);
|
|
121
112
|
this.buildPurifierDict(statusResponse);
|
|
122
113
|
if (statusResponse.configuration) {
|
|
123
114
|
this.buildConfigDict(statusResponse.configuration);
|
|
@@ -128,7 +119,7 @@ export class VeSyncAirBaseV2 extends VeSyncAirBypass {
|
|
|
128
119
|
}
|
|
129
120
|
}
|
|
130
121
|
|
|
131
|
-
// Standard processing for non-
|
|
122
|
+
// Standard processing for non-bypassV2 devices or when no status data is available
|
|
132
123
|
if (!this.checkResponse([response, status], 'getDetails') || !response?.result) {
|
|
133
124
|
logger.debug('Error getting purifier details');
|
|
134
125
|
this.connectionStatus = 'offline';
|
|
@@ -159,10 +150,12 @@ export class VeSyncAirBaseV2 extends VeSyncAirBypass {
|
|
|
159
150
|
this.enabled = powerSwitch;
|
|
160
151
|
this.deviceStatus = powerSwitch ? 'on' : 'off';
|
|
161
152
|
this.mode = devDict.workMode ?? 'manual';
|
|
162
|
-
|
|
163
|
-
|
|
153
|
+
|
|
154
|
+
const reportedFanSpeed = devDict.fanSpeedLevel ?? 0;
|
|
155
|
+
const isAutoSentinel = reportedFanSpeed === 255;
|
|
156
|
+
this.speed = isAutoSentinel ? 0 : reportedFanSpeed;
|
|
164
157
|
this.setSpeedLevel = devDict.manualSpeedLevel ?? 1;
|
|
165
|
-
|
|
158
|
+
|
|
166
159
|
// Parse filter life with proper fallback handling and logging
|
|
167
160
|
let filterLife = 0;
|
|
168
161
|
if (devDict.filterLifePercent !== undefined) {
|
|
@@ -186,7 +179,11 @@ export class VeSyncAirBaseV2 extends VeSyncAirBypass {
|
|
|
186
179
|
light_detection_switch: Boolean(devDict.lightDetectionSwitch ?? 0),
|
|
187
180
|
environment_light_state: Boolean(devDict.environmentLightState ?? 0),
|
|
188
181
|
screen_switch: Boolean(devDict.screenSwitch ?? 0),
|
|
189
|
-
screenStatus: Boolean(devDict.screenSwitch ?? 0) ? 'on' : 'off'
|
|
182
|
+
screenStatus: Boolean(devDict.screenSwitch ?? 0) ? 'on' : 'off',
|
|
183
|
+
manualSpeedLevel: this.setSpeedLevel,
|
|
184
|
+
mode: this.mode,
|
|
185
|
+
speed: this.speed,
|
|
186
|
+
isAutoSentinel
|
|
190
187
|
};
|
|
191
188
|
|
|
192
189
|
if (this.hasFeature('air_quality')) {
|
|
@@ -212,25 +209,24 @@ export class VeSyncAirBaseV2 extends VeSyncAirBypass {
|
|
|
212
209
|
}
|
|
213
210
|
|
|
214
211
|
/**
|
|
215
|
-
*
|
|
216
|
-
* @returns true if this is a LAP-V102S or LAP-V201S device
|
|
212
|
+
* Determine whether this purifier requires the powerSwitch payload and nested response handling.
|
|
217
213
|
*/
|
|
218
|
-
private
|
|
219
|
-
|
|
214
|
+
private requiresPowerSwitchPayload(): boolean {
|
|
215
|
+
const type = (this.deviceType || '').toUpperCase();
|
|
216
|
+
return type.startsWith('LAP-V') || type.startsWith('LAP-EL');
|
|
220
217
|
}
|
|
221
218
|
|
|
222
219
|
/**
|
|
223
|
-
* Override turn on
|
|
220
|
+
* Override turn on to use the powerSwitch payload required by bypassV2 purifiers.
|
|
224
221
|
*/
|
|
225
222
|
override async turnOn(): Promise<boolean> {
|
|
226
223
|
logger.info(`Turning on device: ${this.deviceName}`);
|
|
227
224
|
|
|
228
225
|
const [head, body] = this.buildApiDict('setSwitch');
|
|
229
226
|
|
|
230
|
-
//
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
logger.debug(`Using Vital series specific payload for ${this.deviceName}`);
|
|
227
|
+
// BypassV2 purifiers require powerSwitch toggles; others fall back to the base implementation
|
|
228
|
+
if (this.requiresPowerSwitchPayload()) {
|
|
229
|
+
logger.debug(`Using bypassV2 power payload for ${this.deviceName}`);
|
|
234
230
|
body.payload.data = {
|
|
235
231
|
powerSwitch: 1,
|
|
236
232
|
switchIdx: 0
|
|
@@ -243,8 +239,8 @@ export class VeSyncAirBaseV2 extends VeSyncAirBypass {
|
|
|
243
239
|
head
|
|
244
240
|
);
|
|
245
241
|
|
|
246
|
-
// Use
|
|
247
|
-
const success = this.
|
|
242
|
+
// Use bypassV2 response checking
|
|
243
|
+
const success = this.checkV2Response(response, status, 'turnOn');
|
|
248
244
|
if (success) {
|
|
249
245
|
this.deviceStatus = 'on';
|
|
250
246
|
|
|
@@ -257,23 +253,22 @@ export class VeSyncAirBaseV2 extends VeSyncAirBypass {
|
|
|
257
253
|
}
|
|
258
254
|
return success;
|
|
259
255
|
} else {
|
|
260
|
-
// Use parent implementation for non-
|
|
256
|
+
// Use parent implementation for non-bypassV2 models
|
|
261
257
|
return super.turnOn();
|
|
262
258
|
}
|
|
263
259
|
}
|
|
264
260
|
|
|
265
261
|
/**
|
|
266
|
-
* Override turn off
|
|
262
|
+
* Override turn off to use the powerSwitch payload required by bypassV2 purifiers.
|
|
267
263
|
*/
|
|
268
264
|
override async turnOff(): Promise<boolean> {
|
|
269
265
|
logger.info(`Turning off device: ${this.deviceName}`);
|
|
270
266
|
|
|
271
267
|
const [head, body] = this.buildApiDict('setSwitch');
|
|
272
268
|
|
|
273
|
-
//
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
logger.debug(`Using Vital series specific payload for ${this.deviceName}`);
|
|
269
|
+
// BypassV2 purifiers require powerSwitch toggles; others fall back to the base implementation
|
|
270
|
+
if (this.requiresPowerSwitchPayload()) {
|
|
271
|
+
logger.debug(`Using bypassV2 power payload for ${this.deviceName}`);
|
|
277
272
|
body.payload.data = {
|
|
278
273
|
powerSwitch: 0,
|
|
279
274
|
switchIdx: 0
|
|
@@ -286,8 +281,8 @@ export class VeSyncAirBaseV2 extends VeSyncAirBypass {
|
|
|
286
281
|
head
|
|
287
282
|
);
|
|
288
283
|
|
|
289
|
-
// Use
|
|
290
|
-
const success = this.
|
|
284
|
+
// Use bypassV2 response checking
|
|
285
|
+
const success = this.checkV2Response(response, status, 'turnOff');
|
|
291
286
|
if (success) {
|
|
292
287
|
this.deviceStatus = 'off';
|
|
293
288
|
|
|
@@ -300,13 +295,13 @@ export class VeSyncAirBaseV2 extends VeSyncAirBypass {
|
|
|
300
295
|
}
|
|
301
296
|
return success;
|
|
302
297
|
} else {
|
|
303
|
-
// Use parent implementation for non-
|
|
298
|
+
// Use parent implementation for non-bypassV2 models
|
|
304
299
|
return super.turnOff();
|
|
305
300
|
}
|
|
306
301
|
}
|
|
307
302
|
|
|
308
303
|
/**
|
|
309
|
-
* Override setMode
|
|
304
|
+
* Override setMode to send workMode payloads for bypassV2 purifiers.
|
|
310
305
|
*/
|
|
311
306
|
override async setMode(mode: string): Promise<boolean> {
|
|
312
307
|
if (!this.modes.includes(mode as any)) {
|
|
@@ -321,10 +316,9 @@ export class VeSyncAirBaseV2 extends VeSyncAirBypass {
|
|
|
321
316
|
return this.setMode('manual');
|
|
322
317
|
}
|
|
323
318
|
|
|
324
|
-
//
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
logger.debug(`Using Vital series specific response handling for ${this.deviceName}`);
|
|
319
|
+
// BypassV2 purifiers need the workMode payload; other models use the base implementation
|
|
320
|
+
if (this.requiresPowerSwitchPayload()) {
|
|
321
|
+
logger.debug(`Using bypassV2 mode payload for ${this.deviceName}`);
|
|
328
322
|
const [head, body] = this.buildApiDict('setPurifierMode');
|
|
329
323
|
body.payload.data = {
|
|
330
324
|
workMode: mode
|
|
@@ -337,7 +331,7 @@ export class VeSyncAirBaseV2 extends VeSyncAirBypass {
|
|
|
337
331
|
head
|
|
338
332
|
);
|
|
339
333
|
|
|
340
|
-
const success = this.
|
|
334
|
+
const success = this.checkV2Response(response, status, 'setMode');
|
|
341
335
|
if (success) {
|
|
342
336
|
this.details.mode = mode;
|
|
343
337
|
this._mode = mode;
|
|
@@ -355,13 +349,13 @@ export class VeSyncAirBaseV2 extends VeSyncAirBypass {
|
|
|
355
349
|
return false;
|
|
356
350
|
}
|
|
357
351
|
} else {
|
|
358
|
-
// Use parent implementation for non-
|
|
352
|
+
// Use parent implementation for non-bypassV2 models
|
|
359
353
|
return super.setMode(mode);
|
|
360
354
|
}
|
|
361
355
|
}
|
|
362
356
|
|
|
363
357
|
/**
|
|
364
|
-
* Override changeFanSpeed
|
|
358
|
+
* Override changeFanSpeed to use manualSpeedLevel for bypassV2 purifiers.
|
|
365
359
|
*/
|
|
366
360
|
override async changeFanSpeed(speed: number): Promise<boolean> {
|
|
367
361
|
const speeds = this.config.levels ?? [];
|
|
@@ -374,10 +368,10 @@ export class VeSyncAirBaseV2 extends VeSyncAirBypass {
|
|
|
374
368
|
|
|
375
369
|
const [head, body] = this.buildApiDict('setLevel');
|
|
376
370
|
|
|
377
|
-
// Specific handling for
|
|
378
|
-
if (this.
|
|
379
|
-
logger.debug(`Using
|
|
380
|
-
// For
|
|
371
|
+
// Specific handling for bypassV2 devices
|
|
372
|
+
if (this.requiresPowerSwitchPayload()) {
|
|
373
|
+
logger.debug(`Using bypassV2 speed payload for ${this.deviceName}`);
|
|
374
|
+
// For these devices, use levelIdx, levelType, and manualSpeedLevel per YAML spec
|
|
381
375
|
body.payload.data = {
|
|
382
376
|
levelIdx: 0,
|
|
383
377
|
levelType: 'wind',
|
|
@@ -391,7 +385,7 @@ export class VeSyncAirBaseV2 extends VeSyncAirBypass {
|
|
|
391
385
|
head
|
|
392
386
|
);
|
|
393
387
|
|
|
394
|
-
const success = this.
|
|
388
|
+
const success = this.checkV2Response(response, status, 'changeFanSpeed');
|
|
395
389
|
if (success) {
|
|
396
390
|
this.speed = speed;
|
|
397
391
|
this.details.manualSpeedLevel = speed;
|
|
@@ -409,7 +403,7 @@ export class VeSyncAirBaseV2 extends VeSyncAirBypass {
|
|
|
409
403
|
return false;
|
|
410
404
|
}
|
|
411
405
|
} else {
|
|
412
|
-
// For non-
|
|
406
|
+
// For non-bypassV2 models, use id, level, mode, and type
|
|
413
407
|
body.payload.data = {
|
|
414
408
|
id: 0,
|
|
415
409
|
level: speed,
|
|
@@ -459,16 +453,16 @@ export class VeSyncAirBaseV2 extends VeSyncAirBypass {
|
|
|
459
453
|
);
|
|
460
454
|
|
|
461
455
|
// Use the appropriate response checking method based on device type
|
|
462
|
-
const success = this.
|
|
463
|
-
? this.
|
|
456
|
+
const success = this.requiresPowerSwitchPayload()
|
|
457
|
+
? this.checkV2Response(response, status, 'setAutoPreference')
|
|
464
458
|
: this.checkResponse([response, status], 'setAutoPreference');
|
|
465
459
|
|
|
466
460
|
if (success) {
|
|
467
461
|
this.details.auto_preference = preference;
|
|
468
462
|
this.details.auto_preference_type = preference;
|
|
469
463
|
|
|
470
|
-
// Force a details update to sync state for
|
|
471
|
-
if (this.
|
|
464
|
+
// Force a details update to sync state for bypassV2 purifiers
|
|
465
|
+
if (this.requiresPowerSwitchPayload()) {
|
|
472
466
|
setTimeout(() => {
|
|
473
467
|
this.getDetails().catch(e => {
|
|
474
468
|
logger.debug(`Background state refresh failed: ${e}`);
|
|
@@ -496,15 +490,15 @@ export class VeSyncAirBaseV2 extends VeSyncAirBypass {
|
|
|
496
490
|
);
|
|
497
491
|
|
|
498
492
|
// Use the appropriate response checking method based on device type
|
|
499
|
-
const success = this.
|
|
500
|
-
? this.
|
|
493
|
+
const success = this.requiresPowerSwitchPayload()
|
|
494
|
+
? this.checkV2Response(response, status, 'setLightDetection')
|
|
501
495
|
: this.checkResponse([response, status], 'setLightDetection');
|
|
502
496
|
|
|
503
497
|
if (success) {
|
|
504
498
|
this.details.light_detection_switch = enabled;
|
|
505
499
|
|
|
506
|
-
// Force a details update to sync state for
|
|
507
|
-
if (this.
|
|
500
|
+
// Force a details update to sync state for bypassV2 purifiers
|
|
501
|
+
if (this.requiresPowerSwitchPayload()) {
|
|
508
502
|
setTimeout(() => {
|
|
509
503
|
this.getDetails().catch(e => {
|
|
510
504
|
logger.debug(`Background state refresh failed: ${e}`);
|
|
@@ -595,8 +589,8 @@ export class VeSyncAirBaseV2 extends VeSyncAirBypass {
|
|
|
595
589
|
);
|
|
596
590
|
|
|
597
591
|
// Use the appropriate response checking method based on device type
|
|
598
|
-
const success = this.
|
|
599
|
-
? this.
|
|
592
|
+
const success = this.requiresPowerSwitchPayload()
|
|
593
|
+
? this.checkV2Response(response, status, 'setOscillation')
|
|
600
594
|
: this.checkResponse([response, status], 'setOscillation');
|
|
601
595
|
|
|
602
596
|
if (success) {
|
package/src/lib/vesyncBulb.ts
CHANGED
|
@@ -7,6 +7,12 @@ import { VeSync } from './vesync';
|
|
|
7
7
|
import { Helpers } from './helpers';
|
|
8
8
|
import { logger } from './logger';
|
|
9
9
|
|
|
10
|
+
interface RgbValues {
|
|
11
|
+
red: number;
|
|
12
|
+
green: number;
|
|
13
|
+
blue: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
10
16
|
interface BulbConfig {
|
|
11
17
|
[key: string]: {
|
|
12
18
|
module: string;
|
|
@@ -50,7 +56,7 @@ export abstract class VeSyncBulb extends VeSyncBaseDevice {
|
|
|
50
56
|
protected colorSaturation: number;
|
|
51
57
|
protected colorMode: string;
|
|
52
58
|
protected features: string[];
|
|
53
|
-
protected rgbValues:
|
|
59
|
+
protected rgbValues: RgbValues;
|
|
54
60
|
|
|
55
61
|
constructor(details: Record<string, any>, manager: VeSync) {
|
|
56
62
|
super(details, manager);
|
|
@@ -68,6 +74,18 @@ export abstract class VeSyncBulb extends VeSyncBaseDevice {
|
|
|
68
74
|
};
|
|
69
75
|
}
|
|
70
76
|
|
|
77
|
+
hasFeature(feature: string): boolean {
|
|
78
|
+
return this.features.includes(feature);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
getColorModel(): string {
|
|
82
|
+
return bulbConfig[this.deviceType]?.colorModel ?? 'none';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
getBrightness(): number {
|
|
86
|
+
return this.brightness;
|
|
87
|
+
}
|
|
88
|
+
|
|
71
89
|
/**
|
|
72
90
|
* Get bulb details
|
|
73
91
|
*/
|
|
@@ -101,29 +119,7 @@ export abstract class VeSyncBulb extends VeSyncBaseDevice {
|
|
|
101
119
|
Helpers.reqHeaders(this.manager)
|
|
102
120
|
);
|
|
103
121
|
|
|
104
|
-
|
|
105
|
-
if (success && response?.result) {
|
|
106
|
-
this.deviceStatus = response.result.enabled ? 'on' : 'off';
|
|
107
|
-
this.brightness = response.result.brightness || this.brightness;
|
|
108
|
-
if (this.features.includes('color_temp')) {
|
|
109
|
-
this.colorTemp = response.result.colorTemp || this.colorTemp;
|
|
110
|
-
}
|
|
111
|
-
if (this.features.includes('rgb_shift')) {
|
|
112
|
-
if (bulbConfig[this.deviceType].colorModel === 'rgb') {
|
|
113
|
-
this.rgbValues = {
|
|
114
|
-
red: response.result.red || this.rgbValues.red,
|
|
115
|
-
green: response.result.green || this.rgbValues.green,
|
|
116
|
-
blue: response.result.blue || this.rgbValues.blue
|
|
117
|
-
};
|
|
118
|
-
} else {
|
|
119
|
-
this.colorHue = response.result.hue || this.colorHue;
|
|
120
|
-
this.colorSaturation = response.result.saturation || this.colorSaturation;
|
|
121
|
-
this.colorValue = response.result.value || this.colorValue;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
logger.debug(`[${this.deviceName}] Successfully retrieved bulb details`);
|
|
125
|
-
}
|
|
126
|
-
return success;
|
|
122
|
+
return this.processStandardBypassDetails(response, statusCode);
|
|
127
123
|
}
|
|
128
124
|
|
|
129
125
|
/**
|
|
@@ -239,7 +235,8 @@ export abstract class VeSyncBulb extends VeSyncBaseDevice {
|
|
|
239
235
|
return false;
|
|
240
236
|
}
|
|
241
237
|
|
|
242
|
-
|
|
238
|
+
const brightnessLevel = VeSyncBulb.clamp(Math.round(brightness), 0, 100);
|
|
239
|
+
logger.debug(`[${this.deviceName}] Setting brightness to ${brightnessLevel}`);
|
|
243
240
|
|
|
244
241
|
const isV2Device = this.deviceType === 'XYD0001';
|
|
245
242
|
const endpoint = isV2Device ? '/cloud/v2/deviceManaged/bypassV2' : '/cloud/v1/deviceManaged/bypass';
|
|
@@ -249,7 +246,7 @@ export abstract class VeSyncBulb extends VeSyncBaseDevice {
|
|
|
249
246
|
configModule: this.configModule,
|
|
250
247
|
payload: {
|
|
251
248
|
data: {
|
|
252
|
-
brightness:
|
|
249
|
+
brightness: brightnessLevel,
|
|
253
250
|
colorMode: '',
|
|
254
251
|
colorTemp: '',
|
|
255
252
|
force: 0,
|
|
@@ -266,7 +263,7 @@ export abstract class VeSyncBulb extends VeSyncBaseDevice {
|
|
|
266
263
|
configModule: this.configModule,
|
|
267
264
|
jsonCmd: {
|
|
268
265
|
light: {
|
|
269
|
-
brightness:
|
|
266
|
+
brightness: brightnessLevel
|
|
270
267
|
}
|
|
271
268
|
}
|
|
272
269
|
};
|
|
@@ -279,8 +276,8 @@ export abstract class VeSyncBulb extends VeSyncBaseDevice {
|
|
|
279
276
|
);
|
|
280
277
|
|
|
281
278
|
if (response?.code === 0) {
|
|
282
|
-
this.brightness =
|
|
283
|
-
logger.info(`[${this.deviceName}] Successfully set brightness to ${
|
|
279
|
+
this.brightness = brightnessLevel;
|
|
280
|
+
logger.info(`[${this.deviceName}] Successfully set brightness to ${brightnessLevel}`);
|
|
284
281
|
return true;
|
|
285
282
|
}
|
|
286
283
|
logger.error(`[${this.deviceName}] Failed to set brightness: ${JSON.stringify(response)}`);
|
|
@@ -290,10 +287,6 @@ export abstract class VeSyncBulb extends VeSyncBaseDevice {
|
|
|
290
287
|
/**
|
|
291
288
|
* Get bulb brightness
|
|
292
289
|
*/
|
|
293
|
-
getBrightness(): number {
|
|
294
|
-
return this.brightness;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
290
|
/**
|
|
298
291
|
* Get color temperature in Kelvin
|
|
299
292
|
*/
|
|
@@ -354,8 +347,141 @@ export abstract class VeSyncBulb extends VeSyncBaseDevice {
|
|
|
354
347
|
return this.rgbValues;
|
|
355
348
|
}
|
|
356
349
|
|
|
350
|
+
/**
|
|
351
|
+
* Set color temperature wrapper to maintain compatibility with consumers
|
|
352
|
+
*/
|
|
353
|
+
async setColorTemperature(colorTemp: number): Promise<boolean> {
|
|
354
|
+
return this.setColorTemp(colorTemp);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Set color via hue/saturation/value when supported
|
|
359
|
+
*/
|
|
360
|
+
async setColor(hue: number, saturation: number, value: number = 100): Promise<boolean> {
|
|
361
|
+
if (!this.features.includes('rgb_shift')) {
|
|
362
|
+
logger.error(`[${this.deviceName}] Color control not supported`);
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const colorModel = bulbConfig[this.deviceType]?.colorModel;
|
|
367
|
+
if (colorModel === 'rgb' && typeof (this as any).setRgb === 'function') {
|
|
368
|
+
const rgb = VeSyncBulb.hsvToRgb(hue, saturation, value);
|
|
369
|
+
return (this as any).setRgb(rgb.red, rgb.green, rgb.blue);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (typeof (this as any).setHsv === 'function') {
|
|
373
|
+
return (this as any).setHsv(hue, saturation, value);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
logger.error(`[${this.deviceName}] No compatible color handler found`);
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Process standard bypass (v1) detail responses shared by multiple bulbs
|
|
382
|
+
*/
|
|
383
|
+
protected processStandardBypassDetails(response: any, statusCode: number): boolean {
|
|
384
|
+
const success = this.checkResponse([response, statusCode], 'getDetails');
|
|
385
|
+
if (success && response?.result) {
|
|
386
|
+
const innerResult = response.result?.result || response.result;
|
|
387
|
+
if (innerResult) {
|
|
388
|
+
if (typeof innerResult.enabled === 'boolean') {
|
|
389
|
+
this.deviceStatus = innerResult.enabled ? 'on' : 'off';
|
|
390
|
+
} else if (innerResult.action) {
|
|
391
|
+
this.deviceStatus = innerResult.action === 'on' ? 'on' : 'off';
|
|
392
|
+
}
|
|
393
|
+
if (innerResult.connectionStatus) {
|
|
394
|
+
this.connectionStatus = innerResult.connectionStatus;
|
|
395
|
+
}
|
|
396
|
+
if (this.features.includes('dimmable') && innerResult.brightness !== undefined) {
|
|
397
|
+
this.brightness = Number(innerResult.brightness);
|
|
398
|
+
}
|
|
399
|
+
if (this.features.includes('color_temp')) {
|
|
400
|
+
if (innerResult.colorTemp !== undefined) {
|
|
401
|
+
this.colorTemp = Number(innerResult.colorTemp);
|
|
402
|
+
} else if (innerResult.colorTempe !== undefined) {
|
|
403
|
+
this.colorTemp = Number(innerResult.colorTempe);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (this.features.includes('rgb_shift')) {
|
|
407
|
+
if (bulbConfig[this.deviceType]?.colorModel === 'rgb') {
|
|
408
|
+
this.rgbValues = {
|
|
409
|
+
red: Number(innerResult.red ?? this.rgbValues.red),
|
|
410
|
+
green: Number(innerResult.green ?? this.rgbValues.green),
|
|
411
|
+
blue: Number(innerResult.blue ?? this.rgbValues.blue)
|
|
412
|
+
};
|
|
413
|
+
} else {
|
|
414
|
+
this.colorHue = Number(innerResult.hue ?? this.colorHue);
|
|
415
|
+
this.colorSaturation = Number(innerResult.saturation ?? this.colorSaturation);
|
|
416
|
+
this.colorValue = Number(innerResult.value ?? this.colorValue);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
logger.debug(`[${this.deviceName}] Successfully retrieved bulb details`);
|
|
421
|
+
}
|
|
422
|
+
return success;
|
|
423
|
+
}
|
|
424
|
+
|
|
357
425
|
/**
|
|
358
426
|
* Set color temperature - Abstract method to be implemented by subclasses
|
|
359
427
|
*/
|
|
360
428
|
abstract setColorTemp(colorTemp: number): Promise<boolean>;
|
|
361
|
-
|
|
429
|
+
|
|
430
|
+
protected static clamp(value: number, min: number, max: number): number {
|
|
431
|
+
return Math.min(Math.max(value, min), max);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
protected static hsvToRgb(h: number, s: number, v: number): RgbValues {
|
|
435
|
+
const saturation = VeSyncBulb.clamp(s, 0, 100) / 100;
|
|
436
|
+
const value = VeSyncBulb.clamp(v, 0, 100) / 100;
|
|
437
|
+
const hue = ((h % 360) + 360) % 360 / 60;
|
|
438
|
+
const i = Math.floor(hue);
|
|
439
|
+
const f = hue - i;
|
|
440
|
+
const p = value * (1 - saturation);
|
|
441
|
+
const q = value * (1 - saturation * f);
|
|
442
|
+
const t = value * (1 - saturation * (1 - f));
|
|
443
|
+
|
|
444
|
+
let r = 0;
|
|
445
|
+
let g = 0;
|
|
446
|
+
let b = 0;
|
|
447
|
+
|
|
448
|
+
switch (i) {
|
|
449
|
+
case 0:
|
|
450
|
+
r = value;
|
|
451
|
+
g = t;
|
|
452
|
+
b = p;
|
|
453
|
+
break;
|
|
454
|
+
case 1:
|
|
455
|
+
r = q;
|
|
456
|
+
g = value;
|
|
457
|
+
b = p;
|
|
458
|
+
break;
|
|
459
|
+
case 2:
|
|
460
|
+
r = p;
|
|
461
|
+
g = value;
|
|
462
|
+
b = t;
|
|
463
|
+
break;
|
|
464
|
+
case 3:
|
|
465
|
+
r = p;
|
|
466
|
+
g = q;
|
|
467
|
+
b = value;
|
|
468
|
+
break;
|
|
469
|
+
case 4:
|
|
470
|
+
r = t;
|
|
471
|
+
g = p;
|
|
472
|
+
b = value;
|
|
473
|
+
break;
|
|
474
|
+
default:
|
|
475
|
+
r = value;
|
|
476
|
+
g = p;
|
|
477
|
+
b = q;
|
|
478
|
+
break;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return {
|
|
482
|
+
red: Math.round(r * 255),
|
|
483
|
+
green: Math.round(g * 255),
|
|
484
|
+
blue: Math.round(b * 255)
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
}
|