homebridge-tuya-plus 3.13.0 → 3.13.1-dev.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,703 @@
1
+ const BaseAccessory = require('./BaseAccessory');
2
+ const http = require('http');
3
+ const maxWledBrightness = 255;
4
+
5
+ // Debounce and warmup settings for WLED calls
6
+ const wledDebounceMs = 50;
7
+ const wledWarmupMs = 8000;
8
+
9
+ class WledDimmerAccessory extends BaseAccessory {
10
+ static getCategory(Categories) {
11
+ return Categories.LIGHTBULB;
12
+ }
13
+
14
+ constructor(...props) {
15
+ super(...props);
16
+ }
17
+
18
+ _registerPlatformAccessory() {
19
+ const {Service} = this.hap;
20
+
21
+ this.accessory.addService(Service.Lightbulb, this.device.context.name);
22
+
23
+ super._registerPlatformAccessory();
24
+ }
25
+
26
+ _registerCharacteristics(dps) {
27
+ const {Service, Characteristic} = this.hap;
28
+ const service = this.accessory.getService(Service.Lightbulb);
29
+ this._checkServiceName(service, this.device.context.name);
30
+
31
+ this.dpPower = this._getCustomDP(this.device.context.dpPower) || '1';
32
+ this.dpBrightness = this._getCustomDP(this.device.context.dpBrightness) || this._getCustomDP(this.device.context.dp) || '2';
33
+
34
+ // State for debounced / delayed WLED updates
35
+ this._wledPendingBri = null;
36
+ this._wledDebounceTimer = null;
37
+ this._wledReadyAt = 0;
38
+
39
+ // State for WLED preset effect switches
40
+ this._wledEffectSwitches = [];
41
+ this._wledCurrentEffectIndex = null;
42
+
43
+ // Allow a couple of different spellings just in case
44
+ const syncCfg =
45
+ this.device.context.syncBrightnessToWled ||
46
+ this.device.context.syncBrightnessToWLED ||
47
+ null;
48
+ this.syncBrightnessToWled = (syncCfg && ('' + syncCfg).trim()) || null;
49
+
50
+ this.log.info(
51
+ '[WLED Sync] %s: syncBrightnessToWled=%s',
52
+ this.device.context.name,
53
+ this.syncBrightnessToWled || 'disabled'
54
+ );
55
+
56
+ const characteristicOn = service.getCharacteristic(Characteristic.On)
57
+ .updateValue(dps[this.dpPower])
58
+ .onGet(() => this.getStateAsync(this.dpPower))
59
+ .onSet(value => this.setStateAsync(this.dpPower, value));
60
+
61
+ const characteristicBrightness = service.getCharacteristic(Characteristic.Brightness);
62
+ // Keep a reference so we can update brightness from background WLED calls
63
+ this._characteristicBrightness = characteristicBrightness;
64
+
65
+ if (this.syncBrightnessToWled) {
66
+ // Start with 100% locally while we fetch the actual WLED brightness
67
+ characteristicBrightness
68
+ .updateValue(100)
69
+ .onGet(() => this.getBrightness())
70
+ .onSet(value => this.setBrightness(value));
71
+
72
+ // Force Tuya dimmer brightness to 100% on startup
73
+ const maxTuyaBrightness = this.convertBrightnessFromHomeKitToTuya(100);
74
+ if (dps[this.dpBrightness] !== maxTuyaBrightness) {
75
+ this.log.info(
76
+ '[WLED Sync] %s: setting Tuya brightness DP%s to max (%s)',
77
+ this.device.context.name,
78
+ this.dpBrightness,
79
+ maxTuyaBrightness
80
+ );
81
+ this.setState(this.dpBrightness, maxTuyaBrightness, () => {});
82
+ }
83
+ } else {
84
+ const initial = this.convertBrightnessFromTuyaToHomeKit(dps[this.dpBrightness]);
85
+ this.log.debug(
86
+ '%s: initial Tuya brightness DP%s=%s -> HomeKit %s%%',
87
+ this.device.context.name,
88
+ this.dpBrightness,
89
+ dps[this.dpBrightness],
90
+ initial
91
+ );
92
+ characteristicBrightness
93
+ .updateValue(initial)
94
+ .onGet(() => this.getBrightness())
95
+ .onSet(value => this.setBrightness(value));
96
+ }
97
+
98
+ // Optional: create one HomeKit Switch per configured WLED preset effect.
99
+ // Each switch, when turned ON, will push the corresponding effect ID to WLED.
100
+ const presetEffects = this.device.context.presetEffects;
101
+ if (Array.isArray(presetEffects) && presetEffects.length) {
102
+ const {Service: HapService, Characteristic: HapCharacteristic} = this.hap;
103
+
104
+ const validEffectServices = [];
105
+ this._wledEffectSwitches = [];
106
+
107
+ presetEffects.forEach((effectCfg, index) => {
108
+ if (!effectCfg) return;
109
+
110
+ const effectId = effectCfg.fx != null ? effectCfg.fx : effectCfg.id;
111
+ if (!isFinite(effectId)) {
112
+ this.log.warn(
113
+ '[WLED Sync] %s: presetEffects[%s] is missing a numeric id/fx, skipping: %s',
114
+ this.device.context.name,
115
+ index,
116
+ JSON.stringify(effectCfg)
117
+ );
118
+ return;
119
+ }
120
+
121
+ const effectName = (effectCfg.name && String(effectCfg.name).trim()) || `Effect ${index + 1}`;
122
+ const staticColor = effectCfg.staticColor && String(effectCfg.staticColor).trim();
123
+ const displayName = effectName;
124
+ const subtype = `wledEffect ${index + 1}`;
125
+
126
+ let effectService = this.accessory.getServiceById(HapService.Switch, subtype);
127
+ if (effectService) {
128
+ this._checkServiceName(effectService, displayName);
129
+ } else {
130
+ effectService = this.accessory.addService(HapService.Switch, displayName, subtype);
131
+ }
132
+
133
+ validEffectServices.push(effectService);
134
+
135
+ const onCharacteristic = effectService
136
+ .getCharacteristic(HapCharacteristic.On)
137
+ .on('set', (value, callback) => {
138
+ // Only react to turning the switch ON; turning OFF just clears the toggle in HomeKit.
139
+ if (!value) {
140
+ return callback();
141
+ }
142
+
143
+ // If the light is off, ignore effect change and turn this switch back off.
144
+ if (!this.device.state[this.dpPower]) {
145
+ setImmediate(() => {
146
+ effectService.getCharacteristic(HapCharacteristic.On).updateValue(false);
147
+ });
148
+ return callback();
149
+ }
150
+
151
+ this.log.info(
152
+ '[WLED Sync] %s: setting preset effect "%s" (id=%s, index=%s)',
153
+ this.device.context.name,
154
+ effectName,
155
+ effectId,
156
+ index + 1
157
+ );
158
+
159
+ const rgb = this._parseStaticColor(staticColor);
160
+
161
+ if (!this.device.state[this.dpPower]) {
162
+ this.log.error(
163
+ '[WLED Sync] %s: lamp is off',
164
+ this.device.context.name
165
+ );
166
+ setImmediate(() => {
167
+ effectService.getCharacteristic(HapCharacteristic.On).updateValue(false);
168
+ });
169
+ return callback();
170
+ }
171
+
172
+ this._setWledEffect(effectId, rgb, err => {
173
+ if (err) {
174
+ this.log.error(
175
+ '[WLED Sync] %s: failed to set preset effect "%s" (id=%s): %s',
176
+ this.device.context.name,
177
+ effectName,
178
+ effectId,
179
+ err
180
+ );
181
+ setImmediate(() => {
182
+ effectService.getCharacteristic(HapCharacteristic.On).updateValue(false);
183
+ });
184
+ return callback();
185
+ }
186
+
187
+ // Remember active effect locally and turn off other switches for a radio-button-style UX.
188
+ this._wledCurrentEffectIndex = (index + 1);
189
+ this._wledEffectSwitches.forEach(sw => {
190
+ if (sw.effectName !== effectName && sw.characteristic.value) {
191
+ sw.characteristic.updateValue(false);
192
+ }
193
+ });
194
+
195
+ callback();
196
+ });
197
+ })
198
+ .on('get', cb => {
199
+ cb(null, this.device.state[this.dpPower] && this._wledCurrentEffectIndex === (index + 1));
200
+ });
201
+
202
+ this._wledEffectSwitches.push({
203
+ effectId,
204
+ name: effectName,
205
+ characteristic: onCharacteristic
206
+ });
207
+ });
208
+
209
+ // Clean up any stale WLED preset effect services that are no longer in config.
210
+ this.accessory.services
211
+ .filter(service => service.UUID === HapService.Switch.UUID && service.subtype && service.subtype.startsWith('wledEffect '))
212
+ .forEach(service => {
213
+ if (!validEffectServices.includes(service)) {
214
+ this.log.info('Removing', service.displayName);
215
+ this.accessory.removeService(service);
216
+ }
217
+ });
218
+ }
219
+
220
+ this.device.on('change', changes => {
221
+ // Log full DPS changes so we can see exactly what Tuya reported
222
+ this.log.info(
223
+ '%s DPS changes: %s %s was:',
224
+ this.device.context.name,
225
+ JSON.stringify(changes),
226
+ 'dpPower=' + this.dpPower,
227
+ this.device.state[this.dpPower]
228
+ );
229
+
230
+ if (changes.hasOwnProperty(this.dpPower)) {
231
+ const oldPower = !!characteristicOn.value;
232
+ const newPower = !!changes[this.dpPower];
233
+
234
+ this.log.info(
235
+ '%s power change detected on DP%s: %s -> %s',
236
+ this.device.context.name,
237
+ this.dpPower,
238
+ oldPower,
239
+ newPower
240
+ );
241
+
242
+ if (oldPower !== newPower) {
243
+ characteristicOn.updateValue(newPower);
244
+ }
245
+
246
+ // Track warmup window for WLED after power ON
247
+ if (this.syncBrightnessToWled && newPower) {
248
+ this._wledReadyAt = Date.now() + wledWarmupMs;
249
+ this.log.info(
250
+ '[WLED Sync] %s: power ON -> delaying WLED API calls for %sms',
251
+ this.device.context.name,
252
+ wledWarmupMs
253
+ );
254
+ }
255
+
256
+ // Cancel any pending brightness updates when turning OFF
257
+ if (this.syncBrightnessToWled && !newPower) {
258
+ if (this._wledDebounceTimer) {
259
+ clearTimeout(this._wledDebounceTimer);
260
+ this._wledDebounceTimer = null;
261
+ }
262
+ this._wledPendingBri = null;
263
+ }
264
+ }
265
+
266
+ // When WLED sync is enabled, also mirror manual Tuya (Smart Life) brightness changes to WLED
267
+ if (this.syncBrightnessToWled && changes.hasOwnProperty(this.dpBrightness)) {
268
+ const tuyaValue = changes[this.dpBrightness];
269
+ const maxTuyaBrightness = this.convertBrightnessFromHomeKitToTuya(100);
270
+
271
+ // Ignore the "forced back to 100%" update to avoid feedback loops
272
+ if (tuyaValue === maxTuyaBrightness) {
273
+ this.log.info(
274
+ '[WLED Sync] %s: Tuya DP%s reported max brightness (%s), ignoring',
275
+ this.device.context.name,
276
+ this.dpBrightness,
277
+ tuyaValue
278
+ );
279
+ return;
280
+ }
281
+
282
+ const percent = this.convertBrightnessFromTuyaToHomeKit(tuyaValue);
283
+ const bri = Math.max(0, Math.min(maxWledBrightness, Math.round((percent / 100) * maxWledBrightness)));
284
+
285
+ this.log.info(
286
+ '[WLED Sync] %s: Tuya DP%s changed to %s -> %s%% -> WLED bri=%s (debounced)',
287
+ this.device.context.name,
288
+ this.dpBrightness,
289
+ tuyaValue,
290
+ percent,
291
+ bri
292
+ );
293
+
294
+ // Queue a debounced WLED update to match the Tuya app change
295
+ this._scheduleWledBrightness(bri, 'Tuya brightness change', err => {
296
+ if (err) {
297
+ this.log.error(
298
+ '[WLED Sync] %s: failed to push Tuya-origin brightness to WLED (err=%s)',
299
+ this.device.context.name,
300
+ err
301
+ );
302
+ return;
303
+ }
304
+
305
+ // Reflect that brightness back into HomeKit
306
+ characteristicBrightness.updateValue(percent);
307
+
308
+ // After a short delay, force Tuya brightness back to 100% so it stops dimming the strip
309
+ if (this._wledForceMaxTimeout) {
310
+ clearTimeout(this._wledForceMaxTimeout);
311
+ }
312
+ this._wledForceMaxTimeout = setTimeout(() => {
313
+ this.log.info(
314
+ '[WLED Sync] %s: forcing Tuya DP%s back to max (%s) after Tuya app change',
315
+ this.device.context.name,
316
+ this.dpBrightness,
317
+ maxTuyaBrightness
318
+ );
319
+ this.setState(this.dpBrightness, maxTuyaBrightness, () => {});
320
+ }, 5000);
321
+ });
322
+ } else if (!this.syncBrightnessToWled && changes.hasOwnProperty(this.dpBrightness) && this.convertBrightnessFromHomeKitToTuya(characteristicBrightness.value) !== changes[this.dpBrightness]) {
323
+ characteristicBrightness.updateValue(this.convertBrightnessFromTuyaToHomeKit(changes[this.dpBrightness]));
324
+ }
325
+ });
326
+ }
327
+
328
+ getBrightness() {
329
+ if (this.syncBrightnessToWled) {
330
+ // If we already have a cached WLED brightness, return it immediately.
331
+ if (this._lastWledPercent != null) {
332
+ this.log.info(
333
+ '[WLED Sync] %s: getBrightness() -> using cached %s%%',
334
+ this.device.context.name,
335
+ this._lastWledPercent
336
+ );
337
+ return this._lastWledPercent;
338
+ }
339
+ return 50;
340
+ } else {
341
+ return this.convertBrightnessFromTuyaToHomeKit(this.device.state[this.dpBrightness]);
342
+ }
343
+ }
344
+
345
+ setBrightness(value) {
346
+ if (this.syncBrightnessToWled) {
347
+ this.log.info(
348
+ '[WLED Sync] %s: setBrightness(%s%%)',
349
+ this.device.context.name,
350
+ value
351
+ );
352
+
353
+ // Remember the target level immediately so HomeKit/UI stays in sync.
354
+ this._lastWledPercent = value;
355
+
356
+ // Ensure Tuya stays at 100% so it doesn't interfere with WLED.
357
+ const maxTuyaBrightness = this.convertBrightnessFromHomeKitToTuya(100);
358
+ if (this.device.state[this.dpBrightness] !== maxTuyaBrightness) {
359
+ this.log.info(
360
+ '[WLED Sync] %s: ensuring Tuya DP%s=%s (max)',
361
+ this.device.context.name,
362
+ this.dpBrightness,
363
+ maxTuyaBrightness
364
+ );
365
+ // Fire-and-forget; don't tie HomeKit callback to Tuya I/O.
366
+ this.setStateAsync(this.dpBrightness, maxTuyaBrightness);
367
+ }
368
+
369
+ // Compute desired WLED brightness once.
370
+ const bri = Math.max(0, Math.min(maxWledBrightness, Math.round((value / 100) * maxWledBrightness)));
371
+ this.log.info(
372
+ '[WLED Sync] %s: mapped HomeKit %s%% -> WLED bri=%s (debounced background)',
373
+ this.device.context.name,
374
+ value,
375
+ bri
376
+ );
377
+
378
+ // Schedule the WLED update in the background with debounce and warmup handling.
379
+ this._scheduleWledBrightness(bri, 'HomeKit setBrightness');
380
+
381
+ // Return to HomeKit immediately; don't wait for network I/O.
382
+ return null;
383
+ } else {
384
+ return this.setStateAsync(this.dpBrightness, this.convertBrightnessFromHomeKitToTuya(value));
385
+ }
386
+ }
387
+
388
+ _getWledTarget() {
389
+ if (!this.syncBrightnessToWled) return null;
390
+ const parts = String(this.syncBrightnessToWled).split(':');
391
+ const host = parts[0];
392
+ const port = parts[1] ? parseInt(parts[1], 10) || 80 : 80;
393
+ this.log.info(
394
+ '[WLED Sync] %s: target host=%s port=%s',
395
+ this.device.context.name,
396
+ host,
397
+ port
398
+ );
399
+ return {host, port};
400
+ }
401
+
402
+ _getWledBrightness(callback) {
403
+ const target = this._getWledTarget();
404
+ if (!target) {
405
+ this.log.error(
406
+ '[WLED Sync] %s: _getWledBrightness but no target configured',
407
+ this.device.context.name
408
+ );
409
+ return callback(true);
410
+ }
411
+
412
+ // Ensure we never call the provided callback more than once
413
+ const done = (err, bri) => {
414
+ if (done.called) return;
415
+ done.called = true;
416
+ callback(err, bri);
417
+ };
418
+
419
+ const options = {
420
+ host: target.host,
421
+ port: target.port,
422
+ path: '/json/state',
423
+ method: 'GET',
424
+ timeout: 1000
425
+ };
426
+
427
+ this.log.info(
428
+ '[WLED Sync] %s: GET http://%s:%s/json/state',
429
+ this.device.context.name,
430
+ target.host,
431
+ target.port
432
+ );
433
+
434
+ const req = http.request(options, res => {
435
+ let data = '';
436
+ res.on('data', chunk => data += chunk);
437
+ res.on('end', () => {
438
+ try {
439
+ const json = JSON.parse(data || '{}');
440
+ // WLED exposes brightness as "bri" in the root of the state response
441
+ const bri = json.bri != null ? json.bri : (json.state && json.state.bri);
442
+ if (!isFinite(bri)) {
443
+ this.log.error(
444
+ '[WLED Sync] %s: /json/state response has no numeric bri: %s',
445
+ this.device.context.name,
446
+ data
447
+ );
448
+ return done(true);
449
+ }
450
+ this.log.info(
451
+ '[WLED Sync] %s: /json/state -> bri=%s',
452
+ this.device.context.name,
453
+ bri
454
+ );
455
+ done(null, bri);
456
+ } catch (e) {
457
+ this.log.error('Failed to parse WLED state from %s: %s', this.syncBrightnessToWled, e.message);
458
+ done(true);
459
+ }
460
+ });
461
+ });
462
+
463
+ req.on('error', err => {
464
+ this.log.error('Error talking to WLED at %s: %s', this.syncBrightnessToWled, err.message);
465
+ done(true);
466
+ });
467
+
468
+ req.on('timeout', () => {
469
+ req.destroy();
470
+ done(true);
471
+ });
472
+
473
+ req.end();
474
+ }
475
+
476
+ _setWledBrightness(brightness, callback) {
477
+ const target = this._getWledTarget();
478
+ if (!target) {
479
+ this.log.error(
480
+ '[WLED Sync] %s: _setWledBrightness but no target configured',
481
+ this.device.context.name
482
+ );
483
+ if (callback) callback(true);
484
+ return;
485
+ }
486
+
487
+ // Ensure we never call the provided callback more than once
488
+ const done = (err) => {
489
+ if (!callback) return;
490
+ if (done.called) return;
491
+ done.called = true;
492
+ callback(err);
493
+ };
494
+
495
+ const body = JSON.stringify({bri: brightness});
496
+
497
+ const options = {
498
+ host: target.host,
499
+ port: target.port,
500
+ path: '/json/state',
501
+ method: 'POST',
502
+ headers: {
503
+ 'Content-Type': 'application/json',
504
+ 'Content-Length': Buffer.byteLength(body)
505
+ },
506
+ timeout: 1000
507
+ };
508
+
509
+ this.log.info(
510
+ '[WLED Sync] %s: POST http://%s:%s/json/state body=%s',
511
+ this.device.context.name,
512
+ target.host,
513
+ target.port,
514
+ body
515
+ );
516
+
517
+ const req = http.request(options, res => {
518
+ // Consume response and ignore body
519
+ res.on('data', () => {});
520
+ res.on('end', () => {
521
+ this.log.info(
522
+ '[WLED Sync] %s: WLED brightness set, HTTP %s',
523
+ this.device.context.name,
524
+ res.statusCode
525
+ );
526
+ done && done();
527
+ });
528
+ });
529
+
530
+ req.on('error', err => {
531
+ this.log.error('Error setting WLED brightness on %s: %s', this.syncBrightnessToWled, err.message);
532
+ done && done(true);
533
+ });
534
+
535
+ req.on('timeout', () => {
536
+ req.destroy();
537
+ done && done(true);
538
+ });
539
+
540
+ req.write(body);
541
+ req.end();
542
+ }
543
+
544
+ _setWledEffect(effectId, rgbColor, callback) {
545
+ const target = this._getWledTarget();
546
+ if (!target) {
547
+ this.log.error(
548
+ '[WLED Sync] %s: _setWledEffect but no target configured',
549
+ this.device.context.name
550
+ );
551
+ if (callback) callback(true);
552
+ return;
553
+ }
554
+
555
+ // Ensure we never call the provided callback more than once
556
+ const done = (err) => {
557
+ if (!callback) return;
558
+ if (done.called) return;
559
+ done.called = true;
560
+ callback(err);
561
+ };
562
+
563
+ // Build WLED JSON: always set effect (fx), and if a valid staticColor
564
+ // is provided, also override the first segment color.
565
+ const seg = {fx: effectId};
566
+ if (Array.isArray(rgbColor) && rgbColor.length === 3 && rgbColor.every(c => Number.isInteger(c))) {
567
+ seg.col = [rgbColor];
568
+ }
569
+
570
+ const body = JSON.stringify({seg: [seg]});
571
+
572
+ const options = {
573
+ host: target.host,
574
+ port: target.port,
575
+ path: '/json/state',
576
+ method: 'POST',
577
+ headers: {
578
+ 'Content-Type': 'application/json',
579
+ 'Content-Length': Buffer.byteLength(body)
580
+ },
581
+ timeout: 1000
582
+ };
583
+
584
+ this.log.info(
585
+ '[WLED Sync] %s: POST http://%s:%s/json/state body=%s (preset effect)',
586
+ this.device.context.name,
587
+ target.host,
588
+ target.port,
589
+ body
590
+ );
591
+
592
+ const req = http.request(options, res => {
593
+ // Consume response and ignore body
594
+ res.on('data', () => {});
595
+ res.on('end', () => {
596
+ this.log.info(
597
+ '[WLED Sync] %s: WLED effect set to %s, HTTP %s',
598
+ this.device.context.name,
599
+ effectId,
600
+ res.statusCode
601
+ );
602
+ done && done();
603
+ });
604
+ });
605
+
606
+ req.on('error', err => {
607
+ this.log.error('Error setting WLED effect on %s: %s', this.syncBrightnessToWled, err.message);
608
+ done && done(true);
609
+ });
610
+
611
+ req.on('timeout', () => {
612
+ req.destroy();
613
+ done && done(true);
614
+ });
615
+
616
+ req.write(body);
617
+ req.end();
618
+ }
619
+
620
+ _parseStaticColor(color) {
621
+ if (!color) return null;
622
+
623
+ let hex = String(color).trim().toLowerCase();
624
+ if (hex[0] === '#') hex = hex.slice(1);
625
+
626
+ if (hex.length !== 6 || !/^[0-9a-f]{6}$/.test(hex)) {
627
+ this.log.warn(
628
+ '[WLED Sync] %s: staticColor "%s" is not a valid 6-digit hex string, ignoring',
629
+ this.device.context.name,
630
+ color
631
+ );
632
+ return null;
633
+ }
634
+
635
+ const r = parseInt(hex.slice(0, 2), 16);
636
+ const g = parseInt(hex.slice(2, 4), 16);
637
+ const b = parseInt(hex.slice(4, 6), 16);
638
+
639
+ return [r, g, b];
640
+ }
641
+
642
+ _scheduleWledBrightness(brightness, reason, callback) {
643
+ if (!this.syncBrightnessToWled) {
644
+ callback && callback(true);
645
+ return;
646
+ }
647
+
648
+ // Remember the last requested brightness; multiple calls within the debounce window collapse into one.
649
+ this._wledPendingBri = brightness;
650
+
651
+ if (this._wledDebounceTimer) {
652
+ clearTimeout(this._wledDebounceTimer);
653
+ this._wledDebounceTimer = null;
654
+ }
655
+
656
+ const now = Date.now();
657
+ const warmupDelay = this._wledReadyAt && now < this._wledReadyAt ? (this._wledReadyAt - now) : 0;
658
+ const delay = warmupDelay + wledDebounceMs;
659
+
660
+ this.log.info(
661
+ '[WLED Sync] %s: scheduling WLED bri=%s in %sms (%s)',
662
+ this.device.context.name,
663
+ brightness,
664
+ delay,
665
+ reason || 'unspecified'
666
+ );
667
+
668
+ this._wledDebounceTimer = setTimeout(() => {
669
+ this._wledDebounceTimer = null;
670
+
671
+ // If the light is now off, skip the call.
672
+ if (!this.device.state[this.dpPower]) {
673
+ this.log.info(
674
+ '[WLED Sync] %s: skipping scheduled WLED bri=%s because power is OFF',
675
+ this.device.context.name,
676
+ this._wledPendingBri
677
+ );
678
+ this._wledPendingBri = null;
679
+ callback && callback(true);
680
+ return;
681
+ }
682
+
683
+ const briToSend = this._wledPendingBri;
684
+ this._wledPendingBri = null;
685
+
686
+ setImmediate(() => {
687
+ this._setWledBrightness(briToSend, err => {
688
+ if (err) {
689
+ this.log.error(
690
+ '[WLED Sync] %s: scheduled WLED bri=%s failed: %s',
691
+ this.device.context.name,
692
+ briToSend,
693
+ err
694
+ );
695
+ }
696
+ callback && callback(err);
697
+ });
698
+ });
699
+ }, delay);
700
+ }
701
+ }
702
+
703
+ module.exports = WledDimmerAccessory;