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.
- package/.github/workflows/publish-dev.yml +298 -31
- package/AGENTS.md +162 -0
- package/CLAUDE.md +1 -0
- package/Changelog.md +16 -3
- package/Readme.MD +8 -32
- package/config.schema.json +79 -79
- package/index.js +599 -358
- package/lib/AirPurifierAccessory.js +1 -1
- package/lib/BaseAccessory.js +54 -17
- package/lib/ConvectorAccessory.js +1 -1
- package/lib/CustomMultiOutletAccessory.js +2 -1
- package/lib/DoorbellAccessory.js +9 -16
- package/lib/GarageDoorAccessory.js +1 -1
- package/lib/IrrigationSystemAccessory.js +260 -120
- package/lib/MultiOutletAccessory.js +3 -2
- package/lib/OilDiffuserAccessory.js +10 -8
- package/lib/RGBTWLightAccessory.js +13 -11
- package/lib/RGBTWOutletAccessory.js +11 -9
- package/lib/SimpleBlindsAccessory.js +5 -5
- package/lib/SimpleDimmer2Accessory.js +1 -1
- package/lib/SimpleDimmerAccessory.js +10 -6
- package/lib/SimpleGarageDoorAccessory.js +114 -25
- package/lib/SimpleHeaterAccessory.js +4 -4
- package/lib/SimpleLightAccessory.js +1 -1
- package/lib/SwitchAccessory.js +3 -2
- package/lib/TWLightAccessory.js +2 -2
- package/lib/TuyaAccessory.js +75 -42
- package/lib/TuyaCloudApi.js +121 -8
- package/lib/TuyaCloudDevice.js +434 -31
- package/lib/TuyaCloudMessaging.js +34 -41
- package/lib/TuyaDevice.js +269 -0
- package/lib/TuyaDiscovery.js +25 -9
- package/lib/ValveAccessory.js +16 -12
- package/lib/VerticalBlindsWithTilt.js +27 -25
- package/lib/WledDimmerAccessory.js +46 -81
- package/package.json +5 -6
- package/scripts/cleanup-pr-tags.js +122 -0
- package/test/BaseAccessory.test.js +94 -15
- package/test/IrrigationSystemAccessory.test.js +293 -67
- package/test/MultiOutletAccessory.test.js +18 -3
- package/test/RGBTWLightAccessory.test.js +3 -3
- package/test/SimpleFanLightAccessory.test.js +3 -3
- package/test/SimpleGarageDoorAccessory.test.js +146 -19
- package/test/TuyaAccessory.protocol.test.js +76 -3
- package/test/TuyaCloudApi.test.js +110 -1
- package/test/TuyaCloudDevice.test.js +564 -2
- package/test/TuyaCloudMessaging.test.js +24 -5
- package/test/TuyaDevice.test.js +266 -0
- package/test/getCategory.test.js +1 -0
- package/test/index.test.js +271 -0
- package/test/support/mocks.js +13 -0
- package/wiki/Supported-Device-Types.md +64 -34
- 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
|
|
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
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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 = !!
|
|
193
|
+
const oldPower = !!this._characteristicOn.value;
|
|
232
194
|
const newPower = !!changes[this.dpPower];
|
|
233
195
|
|
|
234
|
-
this.log.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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(
|
|
323
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
4
|
-
"description": "A community-maintained Homebridge plugin for controlling Tuya devices
|
|
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",
|