homebridge-tuya-plus 3.14.0-dev.5 → 3.14.0-dev.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/.github/workflows/publish-dev.yml +298 -31
  2. package/AGENTS.md +162 -0
  3. package/CLAUDE.md +1 -0
  4. package/Changelog.md +16 -3
  5. package/Readme.MD +8 -32
  6. package/config.schema.json +79 -79
  7. package/index.js +599 -358
  8. package/lib/AirPurifierAccessory.js +1 -1
  9. package/lib/BaseAccessory.js +54 -17
  10. package/lib/ConvectorAccessory.js +1 -1
  11. package/lib/CustomMultiOutletAccessory.js +2 -1
  12. package/lib/DoorbellAccessory.js +9 -16
  13. package/lib/GarageDoorAccessory.js +1 -1
  14. package/lib/IrrigationSystemAccessory.js +260 -120
  15. package/lib/MultiOutletAccessory.js +3 -2
  16. package/lib/OilDiffuserAccessory.js +10 -8
  17. package/lib/RGBTWLightAccessory.js +13 -11
  18. package/lib/RGBTWOutletAccessory.js +11 -9
  19. package/lib/SimpleBlindsAccessory.js +5 -5
  20. package/lib/SimpleDimmer2Accessory.js +1 -1
  21. package/lib/SimpleDimmerAccessory.js +10 -6
  22. package/lib/SimpleGarageDoorAccessory.js +114 -25
  23. package/lib/SimpleHeaterAccessory.js +4 -4
  24. package/lib/SimpleLightAccessory.js +1 -1
  25. package/lib/SwitchAccessory.js +3 -2
  26. package/lib/TWLightAccessory.js +2 -2
  27. package/lib/TuyaAccessory.js +75 -42
  28. package/lib/TuyaCloudApi.js +121 -8
  29. package/lib/TuyaCloudDevice.js +434 -31
  30. package/lib/TuyaCloudMessaging.js +34 -41
  31. package/lib/TuyaDevice.js +269 -0
  32. package/lib/TuyaDiscovery.js +25 -9
  33. package/lib/ValveAccessory.js +16 -12
  34. package/lib/VerticalBlindsWithTilt.js +27 -25
  35. package/lib/WledDimmerAccessory.js +46 -81
  36. package/package.json +5 -6
  37. package/scripts/cleanup-pr-tags.js +122 -0
  38. package/test/BaseAccessory.test.js +94 -15
  39. package/test/IrrigationSystemAccessory.test.js +293 -67
  40. package/test/MultiOutletAccessory.test.js +18 -3
  41. package/test/RGBTWLightAccessory.test.js +3 -3
  42. package/test/SimpleFanLightAccessory.test.js +3 -3
  43. package/test/SimpleGarageDoorAccessory.test.js +146 -19
  44. package/test/TuyaAccessory.protocol.test.js +76 -3
  45. package/test/TuyaCloudApi.test.js +110 -1
  46. package/test/TuyaCloudDevice.test.js +564 -2
  47. package/test/TuyaCloudMessaging.test.js +24 -5
  48. package/test/TuyaDevice.test.js +266 -0
  49. package/test/getCategory.test.js +1 -0
  50. package/test/index.test.js +271 -0
  51. package/test/support/mocks.js +13 -0
  52. package/wiki/Supported-Device-Types.md +64 -34
  53. package/wiki/Tuya-Cloud-Setup.md +31 -108
@@ -38,16 +38,16 @@ class VerticalBlindsWithTilt extends BaseAccessory {
38
38
 
39
39
  const characteristicCurrentPosition = service.getCharacteristic(Characteristic.CurrentPosition)
40
40
  .updateValue(this.currentPosition)
41
- .onGet(() => this.currentPosition);
41
+ .onGet(() => { if (!this.device.connected) throw this._commError(); return this.currentPosition; });
42
42
 
43
43
  const characteristicTargetPosition = service.getCharacteristic(Characteristic.TargetPosition)
44
44
  .updateValue(this.currentPosition)
45
- .onGet(() => this.currentPosition)
45
+ .onGet(() => { if (!this.device.connected) throw this._commError(); return this.currentPosition; })
46
46
  .onSet(value => this.setPosition(value));
47
47
 
48
48
  const characteristicPositionState = service.getCharacteristic(Characteristic.PositionState)
49
49
  .updateValue(Characteristic.PositionState.STOPPED)
50
- .onGet(() => Characteristic.PositionState.STOPPED);
50
+ .onGet(() => { if (!this.device.connected) throw this._commError(); return Characteristic.PositionState.STOPPED; });
51
51
 
52
52
  const characteristicCurrentHorizontalTilt = service.getCharacteristic(Characteristic.CurrentHorizontalTiltAngle)
53
53
  .updateValue(this._getTiltAngle(dps[this.dpTiltState] || dps[this.dpTilt]))
@@ -67,33 +67,34 @@ class VerticalBlindsWithTilt extends BaseAccessory {
67
67
 
68
68
  _getInitialPosition(dps) {
69
69
  if (this.accessory.context.cachedPosition !== undefined) {
70
- this.log('[TuyaAccessory] Restored position from cache:', this.accessory.context.cachedPosition + '%');
70
+ this.log.debug('[TuyaAccessory] Restored position from cache:', this.accessory.context.cachedPosition + '%');
71
71
  return this.accessory.context.cachedPosition;
72
72
  }
73
- this.log('[TuyaAccessory] Initial position unknown (first time setup), defaulting to open (100%)');
73
+ this.log.debug('[TuyaAccessory] Initial position unknown (first time setup), defaulting to open (100%)');
74
74
  return 100;
75
75
  }
76
76
 
77
77
  setPosition(value) {
78
78
  const {Characteristic} = this.hap;
79
+ if (!this.device.connected) throw this._commError();
79
80
 
80
81
  if (value === 0) {
81
82
  if (this.currentPosition === 0) {
82
- this.log('[TuyaAccessory] Blinds already closed, skipping close command');
83
+ this.log.debug('[TuyaAccessory] Blinds already closed, skipping close command');
83
84
  return;
84
85
  }
85
86
 
86
- this.log('[TuyaAccessory] Closing blinds');
87
+ this.log.debug('[TuyaAccessory] Closing blinds');
87
88
  this.lastCloseTime = Date.now();
88
89
  this.isMoving = true;
89
90
 
90
91
  if (this.lastTiltCommand && (Date.now() - this.lastTiltCommand.time < this.timeToClose * 1000)) {
91
- this.log('[TuyaAccessory] Close command received while tilt was pending - rescheduling tilt delay');
92
+ this.log.debug('[TuyaAccessory] Close command received while tilt was pending - rescheduling tilt delay');
92
93
  if (this.tiltBufferTimeout) { clearTimeout(this.tiltBufferTimeout); this.tiltBufferTimeout = null; }
93
94
  if (this.tiltDelayTimeout) clearTimeout(this.tiltDelayTimeout);
94
95
  const delayMs = this.timeToClose * 1000;
95
96
  this.tiltDelayTimeout = setTimeout(() => {
96
- this.log('[TuyaAccessory] Executing delayed tilt angle:', this.lastTiltCommand.angle, '-> Tuya percent_control:', this.lastTiltCommand.value);
97
+ this.log.debug('[TuyaAccessory] Executing delayed tilt angle:', this.lastTiltCommand.angle, '-> Tuya percent_control:', this.lastTiltCommand.value);
97
98
  this._executeTilt(this.lastTiltCommand.value);
98
99
  this.tiltDelayTimeout = null;
99
100
  this.lastTiltCommand = null;
@@ -105,7 +106,7 @@ class VerticalBlindsWithTilt extends BaseAccessory {
105
106
 
106
107
  if (this.positionStateTimeout) clearTimeout(this.positionStateTimeout);
107
108
  this.positionStateTimeout = setTimeout(() => {
108
- this.log('[TuyaAccessory] Blinds finished closing');
109
+ this.log.debug('[TuyaAccessory] Blinds finished closing');
109
110
  this.currentPosition = 0;
110
111
  this.accessory.context.cachedPosition = 0;
111
112
  this.characteristicCurrentPosition.updateValue(0);
@@ -119,19 +120,19 @@ class VerticalBlindsWithTilt extends BaseAccessory {
119
120
 
120
121
  } else if (value === 100) {
121
122
  if (this.currentPosition === 100) {
122
- this.log('[TuyaAccessory] Blinds already open, skipping open command');
123
+ this.log.debug('[TuyaAccessory] Blinds already open, skipping open command');
123
124
  return;
124
125
  }
125
126
 
126
- this.log('[TuyaAccessory] Opening blinds');
127
+ this.log.debug('[TuyaAccessory] Opening blinds');
127
128
 
128
129
  if (this.lastTiltCommand && (Date.now() - this.lastTiltCommand.time < this.timeToClose * 1000)) {
129
- this.log('[TuyaAccessory] Open command received while tilt was pending - rescheduling tilt delay');
130
+ this.log.debug('[TuyaAccessory] Open command received while tilt was pending - rescheduling tilt delay');
130
131
  if (this.tiltBufferTimeout) { clearTimeout(this.tiltBufferTimeout); this.tiltBufferTimeout = null; }
131
132
  if (this.tiltDelayTimeout) clearTimeout(this.tiltDelayTimeout);
132
133
  const delayMs = this.timeToClose * 1000;
133
134
  this.tiltDelayTimeout = setTimeout(() => {
134
- this.log('[TuyaAccessory] Executing delayed tilt angle:', this.lastTiltCommand.angle, '-> Tuya percent_control:', this.lastTiltCommand.value);
135
+ this.log.debug('[TuyaAccessory] Executing delayed tilt angle:', this.lastTiltCommand.angle, '-> Tuya percent_control:', this.lastTiltCommand.value);
135
136
  this._executeTilt(this.lastTiltCommand.value);
136
137
  this.tiltDelayTimeout = null;
137
138
  this.lastTiltCommand = null;
@@ -148,7 +149,7 @@ class VerticalBlindsWithTilt extends BaseAccessory {
148
149
 
149
150
  if (this.positionStateTimeout) clearTimeout(this.positionStateTimeout);
150
151
  this.positionStateTimeout = setTimeout(() => {
151
- this.log('[TuyaAccessory] Blinds finished opening');
152
+ this.log.debug('[TuyaAccessory] Blinds finished opening');
152
153
  this.currentPosition = 100;
153
154
  this.accessory.context.cachedPosition = 100;
154
155
  this.characteristicCurrentPosition.updateValue(100);
@@ -160,15 +161,15 @@ class VerticalBlindsWithTilt extends BaseAccessory {
160
161
  return this.setStateAsync(this.dpAction, 'open');
161
162
 
162
163
  } else {
163
- this.log('[TuyaAccessory] Partial position requested, opening fully');
164
+ this.log.debug('[TuyaAccessory] Partial position requested, opening fully');
164
165
 
165
166
  if (this.lastTiltCommand && (Date.now() - this.lastTiltCommand.time < this.timeToClose * 1000)) {
166
- this.log('[TuyaAccessory] Partial open command received while tilt was pending - rescheduling tilt delay');
167
+ this.log.debug('[TuyaAccessory] Partial open command received while tilt was pending - rescheduling tilt delay');
167
168
  if (this.tiltBufferTimeout) { clearTimeout(this.tiltBufferTimeout); this.tiltBufferTimeout = null; }
168
169
  if (this.tiltDelayTimeout) clearTimeout(this.tiltDelayTimeout);
169
170
  const delayMs = this.timeToClose * 1000;
170
171
  this.tiltDelayTimeout = setTimeout(() => {
171
- this.log('[TuyaAccessory] Executing delayed tilt angle:', this.lastTiltCommand.angle, '-> Tuya percent_control:', this.lastTiltCommand.value);
172
+ this.log.debug('[TuyaAccessory] Executing delayed tilt angle:', this.lastTiltCommand.angle, '-> Tuya percent_control:', this.lastTiltCommand.value);
172
173
  this._executeTilt(this.lastTiltCommand.value);
173
174
  this.tiltDelayTimeout = null;
174
175
  this.lastTiltCommand = null;
@@ -185,7 +186,7 @@ class VerticalBlindsWithTilt extends BaseAccessory {
185
186
 
186
187
  if (this.positionStateTimeout) clearTimeout(this.positionStateTimeout);
187
188
  this.positionStateTimeout = setTimeout(() => {
188
- this.log('[TuyaAccessory] Blinds finished opening');
189
+ this.log.debug('[TuyaAccessory] Blinds finished opening');
189
190
  this.currentPosition = 100;
190
191
  this.accessory.context.cachedPosition = 100;
191
192
  this.characteristicCurrentPosition.updateValue(100);
@@ -205,6 +206,7 @@ class VerticalBlindsWithTilt extends BaseAccessory {
205
206
  }
206
207
 
207
208
  setTiltAngle(value) {
209
+ if (!this.device.connected) throw this._commError();
208
210
  const tuyaValue = Math.round((value / 1.8) + 50);
209
211
  const clampedValue = Math.max(0, Math.min(100, tuyaValue));
210
212
 
@@ -221,12 +223,12 @@ class VerticalBlindsWithTilt extends BaseAccessory {
221
223
  ? (this.timeToClose * 1000) - timeSinceClose
222
224
  : (this.timeToClose * 1000) - timeSinceOpen;
223
225
  const action = shouldDelayForClose ? 'close' : 'open';
224
- this.log('[TuyaAccessory] Tilt command received during', action, '- delaying by', Math.round(delayMs / 1000), 'seconds');
226
+ this.log.debug('[TuyaAccessory] Tilt command received during', action, '- delaying by', Math.round(delayMs / 1000), 'seconds');
225
227
 
226
228
  this._cancelPendingTilts();
227
229
 
228
230
  this.tiltDelayTimeout = setTimeout(() => {
229
- this.log('[TuyaAccessory] Executing delayed tilt angle:', value, '-> Tuya percent_control:', clampedValue);
231
+ this.log.debug('[TuyaAccessory] Executing delayed tilt angle:', value, '-> Tuya percent_control:', clampedValue);
230
232
  this._executeTilt(clampedValue);
231
233
  this.tiltDelayTimeout = null;
232
234
  this.lastCloseTime = 0;
@@ -235,7 +237,7 @@ class VerticalBlindsWithTilt extends BaseAccessory {
235
237
  }, delayMs);
236
238
 
237
239
  } else {
238
- this.log('[TuyaAccessory] Setting tilt angle:', value, '-> Tuya percent_control:', clampedValue);
240
+ this.log.debug('[TuyaAccessory] Setting tilt angle:', value, '-> Tuya percent_control:', clampedValue);
239
241
 
240
242
  if (this.tiltBufferTimeout) clearTimeout(this.tiltBufferTimeout);
241
243
 
@@ -256,7 +258,7 @@ class VerticalBlindsWithTilt extends BaseAccessory {
256
258
 
257
259
  _executeTilt(value) {
258
260
  if (!this.device.connected) {
259
- this.log('[TuyaAccessory] Cannot execute tilt - device not connected');
261
+ this.log.debug('[TuyaAccessory] Cannot execute tilt - device not connected');
260
262
  return;
261
263
  }
262
264
  this.device.update({[this.dpTilt]: value});
@@ -266,12 +268,12 @@ class VerticalBlindsWithTilt extends BaseAccessory {
266
268
  if (this.tiltBufferTimeout) {
267
269
  clearTimeout(this.tiltBufferTimeout);
268
270
  this.tiltBufferTimeout = null;
269
- this.log('[TuyaAccessory] Cleared pending tilt buffer');
271
+ this.log.debug('[TuyaAccessory] Cleared pending tilt buffer');
270
272
  }
271
273
  if (this.tiltDelayTimeout) {
272
274
  clearTimeout(this.tiltDelayTimeout);
273
275
  this.tiltDelayTimeout = null;
274
- this.log('[TuyaAccessory] Cleared pending tilt delay');
276
+ this.log.debug('[TuyaAccessory] Cleared pending tilt delay');
275
277
  }
276
278
  }
277
279
 
@@ -1,4 +1,4 @@
1
- const BaseAccessory = require('./BaseAccessory');
1
+ const SimpleDimmerAccessory = require('./SimpleDimmerAccessory');
2
2
  const http = require('http');
3
3
  const maxWledBrightness = 255;
4
4
 
@@ -6,30 +6,14 @@ const maxWledBrightness = 255;
6
6
  const wledDebounceMs = 50;
7
7
  const wledWarmupMs = 8000;
8
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
-
9
+ class WledDimmerAccessory extends SimpleDimmerAccessory {
26
10
  _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';
11
+ // The Lightbulb service, On/Brightness characteristics, default DPs and
12
+ // the device 'change' listener are all set up by SimpleDimmerAccessory.
13
+ // WLED only layers optional brightness sync and preset-effect switches
14
+ // on top, so without syncBrightnessToWled configured this behaves
15
+ // exactly like a plain SimpleDimmer.
16
+ super._registerCharacteristics(dps);
33
17
 
34
18
  // State for debounced / delayed WLED updates
35
19
  this._wledPendingBri = null;
@@ -47,32 +31,20 @@ class WledDimmerAccessory extends BaseAccessory {
47
31
  null;
48
32
  this.syncBrightnessToWled = (syncCfg && ('' + syncCfg).trim()) || null;
49
33
 
50
- this.log.info(
34
+ this.log.debug(
51
35
  '[WLED Sync] %s: syncBrightnessToWled=%s',
52
36
  this.device.context.name,
53
37
  this.syncBrightnessToWled || 'disabled'
54
38
  );
55
39
 
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
40
  if (this.syncBrightnessToWled) {
66
41
  // 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));
42
+ this._characteristicBrightness.updateValue(100);
71
43
 
72
44
  // Force Tuya dimmer brightness to 100% on startup
73
45
  const maxTuyaBrightness = this.convertBrightnessFromHomeKitToTuya(100);
74
46
  if (dps[this.dpBrightness] !== maxTuyaBrightness) {
75
- this.log.info(
47
+ this.log.debug(
76
48
  '[WLED Sync] %s: setting Tuya brightness DP%s to max (%s)',
77
49
  this.device.context.name,
78
50
  this.dpBrightness,
@@ -80,19 +52,6 @@ class WledDimmerAccessory extends BaseAccessory {
80
52
  );
81
53
  this.setState(this.dpBrightness, maxTuyaBrightness, () => {});
82
54
  }
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
55
  }
97
56
 
98
57
  // Optional: create one HomeKit Switch per configured WLED preset effect.
@@ -148,7 +107,7 @@ class WledDimmerAccessory extends BaseAccessory {
148
107
  return callback();
149
108
  }
150
109
 
151
- this.log.info(
110
+ this.log.debug(
152
111
  '[WLED Sync] %s: setting preset effect "%s" (id=%s, index=%s)',
153
112
  this.device.context.name,
154
113
  effectName,
@@ -196,6 +155,7 @@ class WledDimmerAccessory extends BaseAccessory {
196
155
  });
197
156
  })
198
157
  .on('get', cb => {
158
+ if (!this.device.connected) return cb(this._commError());
199
159
  cb(null, this.device.state[this.dpPower] && this._wledCurrentEffectIndex === (index + 1));
200
160
  });
201
161
 
@@ -211,15 +171,17 @@ class WledDimmerAccessory extends BaseAccessory {
211
171
  .filter(service => service.UUID === HapService.Switch.UUID && service.subtype && service.subtype.startsWith('wledEffect '))
212
172
  .forEach(service => {
213
173
  if (!validEffectServices.includes(service)) {
214
- this.log.info('Removing', service.displayName);
174
+ this.log.debug('Removing', service.displayName);
215
175
  this.accessory.removeService(service);
216
176
  }
217
177
  });
218
178
  }
179
+ }
219
180
 
181
+ _registerChangeListener() {
220
182
  this.device.on('change', changes => {
221
183
  // Log full DPS changes so we can see exactly what Tuya reported
222
- this.log.info(
184
+ this.log.debug(
223
185
  '%s DPS changes: %s %s was:',
224
186
  this.device.context.name,
225
187
  JSON.stringify(changes),
@@ -228,10 +190,10 @@ class WledDimmerAccessory extends BaseAccessory {
228
190
  );
229
191
 
230
192
  if (changes.hasOwnProperty(this.dpPower)) {
231
- const oldPower = !!characteristicOn.value;
193
+ const oldPower = !!this._characteristicOn.value;
232
194
  const newPower = !!changes[this.dpPower];
233
195
 
234
- this.log.info(
196
+ this.log.debug(
235
197
  '%s power change detected on DP%s: %s -> %s',
236
198
  this.device.context.name,
237
199
  this.dpPower,
@@ -240,13 +202,13 @@ class WledDimmerAccessory extends BaseAccessory {
240
202
  );
241
203
 
242
204
  if (oldPower !== newPower) {
243
- characteristicOn.updateValue(newPower);
205
+ this._characteristicOn.updateValue(newPower);
244
206
  }
245
207
 
246
208
  // Track warmup window for WLED after power ON
247
209
  if (this.syncBrightnessToWled && newPower) {
248
210
  this._wledReadyAt = Date.now() + wledWarmupMs;
249
- this.log.info(
211
+ this.log.debug(
250
212
  '[WLED Sync] %s: power ON -> delaying WLED API calls for %sms',
251
213
  this.device.context.name,
252
214
  wledWarmupMs
@@ -270,7 +232,7 @@ class WledDimmerAccessory extends BaseAccessory {
270
232
 
271
233
  // Ignore the "forced back to 100%" update to avoid feedback loops
272
234
  if (tuyaValue === maxTuyaBrightness) {
273
- this.log.info(
235
+ this.log.debug(
274
236
  '[WLED Sync] %s: Tuya DP%s reported max brightness (%s), ignoring',
275
237
  this.device.context.name,
276
238
  this.dpBrightness,
@@ -282,7 +244,7 @@ class WledDimmerAccessory extends BaseAccessory {
282
244
  const percent = this.convertBrightnessFromTuyaToHomeKit(tuyaValue);
283
245
  const bri = Math.max(0, Math.min(maxWledBrightness, Math.round((percent / 100) * maxWledBrightness)));
284
246
 
285
- this.log.info(
247
+ this.log.debug(
286
248
  '[WLED Sync] %s: Tuya DP%s changed to %s -> %s%% -> WLED bri=%s (debounced)',
287
249
  this.device.context.name,
288
250
  this.dpBrightness,
@@ -303,14 +265,14 @@ class WledDimmerAccessory extends BaseAccessory {
303
265
  }
304
266
 
305
267
  // Reflect that brightness back into HomeKit
306
- characteristicBrightness.updateValue(percent);
268
+ this._characteristicBrightness.updateValue(percent);
307
269
 
308
270
  // After a short delay, force Tuya brightness back to 100% so it stops dimming the strip
309
271
  if (this._wledForceMaxTimeout) {
310
272
  clearTimeout(this._wledForceMaxTimeout);
311
273
  }
312
274
  this._wledForceMaxTimeout = setTimeout(() => {
313
- this.log.info(
275
+ this.log.debug(
314
276
  '[WLED Sync] %s: forcing Tuya DP%s back to max (%s) after Tuya app change',
315
277
  this.device.context.name,
316
278
  this.dpBrightness,
@@ -319,17 +281,18 @@ class WledDimmerAccessory extends BaseAccessory {
319
281
  this.setState(this.dpBrightness, maxTuyaBrightness, () => {});
320
282
  }, 5000);
321
283
  });
322
- } else if (!this.syncBrightnessToWled && changes.hasOwnProperty(this.dpBrightness) && this.convertBrightnessFromHomeKitToTuya(characteristicBrightness.value) !== changes[this.dpBrightness]) {
323
- characteristicBrightness.updateValue(this.convertBrightnessFromTuyaToHomeKit(changes[this.dpBrightness]));
284
+ } else if (!this.syncBrightnessToWled && changes.hasOwnProperty(this.dpBrightness) && this.convertBrightnessFromHomeKitToTuya(this._characteristicBrightness.value) !== changes[this.dpBrightness]) {
285
+ this._characteristicBrightness.updateValue(this.convertBrightnessFromTuyaToHomeKit(changes[this.dpBrightness]));
324
286
  }
325
287
  });
326
288
  }
327
289
 
328
290
  getBrightness() {
291
+ if (!this.device.connected) throw this._commError();
329
292
  if (this.syncBrightnessToWled) {
330
293
  // If we already have a cached WLED brightness, return it immediately.
331
294
  if (this._lastWledPercent != null) {
332
- this.log.info(
295
+ this.log.debug(
333
296
  '[WLED Sync] %s: getBrightness() -> using cached %s%%',
334
297
  this.device.context.name,
335
298
  this._lastWledPercent
@@ -338,13 +301,14 @@ class WledDimmerAccessory extends BaseAccessory {
338
301
  }
339
302
  return 50;
340
303
  } else {
341
- return this.convertBrightnessFromTuyaToHomeKit(this.device.state[this.dpBrightness]);
304
+ // No WLED sync: behave as a plain SimpleDimmer.
305
+ return super.getBrightness();
342
306
  }
343
307
  }
344
308
 
345
309
  setBrightness(value) {
346
310
  if (this.syncBrightnessToWled) {
347
- this.log.info(
311
+ this.log.debug(
348
312
  '[WLED Sync] %s: setBrightness(%s%%)',
349
313
  this.device.context.name,
350
314
  value
@@ -356,19 +320,19 @@ class WledDimmerAccessory extends BaseAccessory {
356
320
  // Ensure Tuya stays at 100% so it doesn't interfere with WLED.
357
321
  const maxTuyaBrightness = this.convertBrightnessFromHomeKitToTuya(100);
358
322
  if (this.device.state[this.dpBrightness] !== maxTuyaBrightness) {
359
- this.log.info(
323
+ this.log.debug(
360
324
  '[WLED Sync] %s: ensuring Tuya DP%s=%s (max)',
361
325
  this.device.context.name,
362
326
  this.dpBrightness,
363
327
  maxTuyaBrightness
364
328
  );
365
329
  // Fire-and-forget; don't tie HomeKit callback to Tuya I/O.
366
- this.setStateAsync(this.dpBrightness, maxTuyaBrightness);
330
+ this.setStateInBackground(this.dpBrightness, maxTuyaBrightness);
367
331
  }
368
332
 
369
333
  // Compute desired WLED brightness once.
370
334
  const bri = Math.max(0, Math.min(maxWledBrightness, Math.round((value / 100) * maxWledBrightness)));
371
- this.log.info(
335
+ this.log.debug(
372
336
  '[WLED Sync] %s: mapped HomeKit %s%% -> WLED bri=%s (debounced background)',
373
337
  this.device.context.name,
374
338
  value,
@@ -381,7 +345,8 @@ class WledDimmerAccessory extends BaseAccessory {
381
345
  // Return to HomeKit immediately; don't wait for network I/O.
382
346
  return null;
383
347
  } else {
384
- return this.setStateAsync(this.dpBrightness, this.convertBrightnessFromHomeKitToTuya(value));
348
+ // No WLED sync: behave as a plain SimpleDimmer.
349
+ return super.setBrightness(value);
385
350
  }
386
351
  }
387
352
 
@@ -390,7 +355,7 @@ class WledDimmerAccessory extends BaseAccessory {
390
355
  const parts = String(this.syncBrightnessToWled).split(':');
391
356
  const host = parts[0];
392
357
  const port = parts[1] ? parseInt(parts[1], 10) || 80 : 80;
393
- this.log.info(
358
+ this.log.debug(
394
359
  '[WLED Sync] %s: target host=%s port=%s',
395
360
  this.device.context.name,
396
361
  host,
@@ -424,7 +389,7 @@ class WledDimmerAccessory extends BaseAccessory {
424
389
  timeout: 1000
425
390
  };
426
391
 
427
- this.log.info(
392
+ this.log.debug(
428
393
  '[WLED Sync] %s: GET http://%s:%s/json/state',
429
394
  this.device.context.name,
430
395
  target.host,
@@ -447,7 +412,7 @@ class WledDimmerAccessory extends BaseAccessory {
447
412
  );
448
413
  return done(true);
449
414
  }
450
- this.log.info(
415
+ this.log.debug(
451
416
  '[WLED Sync] %s: /json/state -> bri=%s',
452
417
  this.device.context.name,
453
418
  bri
@@ -506,7 +471,7 @@ class WledDimmerAccessory extends BaseAccessory {
506
471
  timeout: 1000
507
472
  };
508
473
 
509
- this.log.info(
474
+ this.log.debug(
510
475
  '[WLED Sync] %s: POST http://%s:%s/json/state body=%s',
511
476
  this.device.context.name,
512
477
  target.host,
@@ -518,7 +483,7 @@ class WledDimmerAccessory extends BaseAccessory {
518
483
  // Consume response and ignore body
519
484
  res.on('data', () => {});
520
485
  res.on('end', () => {
521
- this.log.info(
486
+ this.log.debug(
522
487
  '[WLED Sync] %s: WLED brightness set, HTTP %s',
523
488
  this.device.context.name,
524
489
  res.statusCode
@@ -581,7 +546,7 @@ class WledDimmerAccessory extends BaseAccessory {
581
546
  timeout: 1000
582
547
  };
583
548
 
584
- this.log.info(
549
+ this.log.debug(
585
550
  '[WLED Sync] %s: POST http://%s:%s/json/state body=%s (preset effect)',
586
551
  this.device.context.name,
587
552
  target.host,
@@ -593,7 +558,7 @@ class WledDimmerAccessory extends BaseAccessory {
593
558
  // Consume response and ignore body
594
559
  res.on('data', () => {});
595
560
  res.on('end', () => {
596
- this.log.info(
561
+ this.log.debug(
597
562
  '[WLED Sync] %s: WLED effect set to %s, HTTP %s',
598
563
  this.device.context.name,
599
564
  effectId,
@@ -657,7 +622,7 @@ class WledDimmerAccessory extends BaseAccessory {
657
622
  const warmupDelay = this._wledReadyAt && now < this._wledReadyAt ? (this._wledReadyAt - now) : 0;
658
623
  const delay = warmupDelay + wledDebounceMs;
659
624
 
660
- this.log.info(
625
+ this.log.debug(
661
626
  '[WLED Sync] %s: scheduling WLED bri=%s in %sms (%s)',
662
627
  this.device.context.name,
663
628
  brightness,
@@ -670,7 +635,7 @@ class WledDimmerAccessory extends BaseAccessory {
670
635
 
671
636
  // If the light is now off, skip the call.
672
637
  if (!this.device.state[this.dpPower]) {
673
- this.log.info(
638
+ this.log.debug(
674
639
  '[WLED Sync] %s: skipping scheduled WLED bri=%s because power is OFF',
675
640
  this.device.context.name,
676
641
  this._wledPendingBri
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "homebridge-tuya-plus",
3
- "version": "3.14.0-dev.5",
4
- "description": "A community-maintained Homebridge plugin for controlling Tuya devices locally over LAN. Includes new features, fixes, and updated device support.",
3
+ "version": "3.14.0-dev.51",
4
+ "description": "A community-maintained Homebridge plugin for controlling Tuya devices in HomeKit. LAN-first, with an optional Tuya Cloud fallback for any device the LAN can't reach.",
5
5
  "main": "index.js",
6
6
  "scripts": {
7
7
  "test": "jest",
8
- "lint": "eslint index.js lib/**.js bin/**.js"
8
+ "lint": "eslint index.js lib/**.js bin/**.js",
9
+ "cleanup:pr-tags": "node scripts/cleanup-pr-tags.js"
9
10
  },
10
11
  "bin": {
11
12
  "tuya-lan": "./bin/cli.js",
@@ -28,12 +29,10 @@
28
29
  "fs-extra": "^8.1.0",
29
30
  "http-mitm-proxy": "^1.1.0",
30
31
  "json5": "^2.2.0",
32
+ "mqtt": "^5.15.1",
31
33
  "qrcode": "^1.4.1",
32
34
  "yaml": "^1.6.0"
33
35
  },
34
- "optionalDependencies": {
35
- "mqtt": "^5.15.1"
36
- },
37
36
  "keywords": [
38
37
  "homebridge-plugin",
39
38
  "homebridge",