homebridge-tuya-plus 3.14.0-dev.4 → 3.14.0-dev.41
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 +63 -78
- 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 +93 -114
- 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 +78 -24
- 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 +115 -7
- package/lib/TuyaCloudDevice.js +351 -28
- package/lib/TuyaCloudMessaging.js +28 -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 +122 -68
- 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 +63 -9
- package/test/TuyaAccessory.protocol.test.js +76 -3
- package/test/TuyaCloudApi.test.js +110 -1
- package/test/TuyaCloudDevice.test.js +438 -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 +48 -30
- package/wiki/Tuya-Cloud-Setup.md +31 -108
|
@@ -203,8 +203,8 @@ class OilDiffuserAccessory extends BaseAccessory {
|
|
|
203
203
|
}
|
|
204
204
|
|
|
205
205
|
getBrightness() {
|
|
206
|
-
if (this.
|
|
207
|
-
return this.convertColorFromTuyaToHomeKit(this.
|
|
206
|
+
if (this.getStateAsync(this.dpMode) === this.cmdWhite) return this.convertBrightnessFromTuyaToHomeKit(this.getStateAsync(this.dpColor)).b;
|
|
207
|
+
return this.convertColorFromTuyaToHomeKit(this.getStateAsync(this.dpColor)).b;
|
|
208
208
|
}
|
|
209
209
|
|
|
210
210
|
setBrightness(value) {
|
|
@@ -218,8 +218,8 @@ class OilDiffuserAccessory extends BaseAccessory {
|
|
|
218
218
|
}
|
|
219
219
|
|
|
220
220
|
getColorTemperature() {
|
|
221
|
-
if (this.
|
|
222
|
-
return this.convertColorTemperatureFromTuyaToHomeKit(this.
|
|
221
|
+
if (this.getStateAsync(this.dpMode) !== this.cmdWhite) return 0;
|
|
222
|
+
return this.convertColorTemperatureFromTuyaToHomeKit(this.getStateAsync(this.dpColorTemperature));
|
|
223
223
|
}
|
|
224
224
|
|
|
225
225
|
setColorTemperature(value) {
|
|
@@ -234,8 +234,8 @@ class OilDiffuserAccessory extends BaseAccessory {
|
|
|
234
234
|
}
|
|
235
235
|
|
|
236
236
|
getHue() {
|
|
237
|
-
if (this.
|
|
238
|
-
return this.convertColorFromTuyaToHomeKit(this.
|
|
237
|
+
if (this.getStateAsync(this.dpMode) === this.cmdWhite) return 0;
|
|
238
|
+
return this.convertColorFromTuyaToHomeKit(this.getStateAsync(this.dpColor)).h;
|
|
239
239
|
}
|
|
240
240
|
|
|
241
241
|
setHue(value) {
|
|
@@ -243,7 +243,7 @@ class OilDiffuserAccessory extends BaseAccessory {
|
|
|
243
243
|
}
|
|
244
244
|
|
|
245
245
|
getSaturation() {
|
|
246
|
-
return this.convertColorFromTuyaToHomeKit(this.
|
|
246
|
+
return this.convertColorFromTuyaToHomeKit(this.getStateAsync(this.dpColor)).s;
|
|
247
247
|
}
|
|
248
248
|
|
|
249
249
|
setSaturation(value) {
|
|
@@ -251,6 +251,8 @@ class OilDiffuserAccessory extends BaseAccessory {
|
|
|
251
251
|
}
|
|
252
252
|
|
|
253
253
|
_setHueSaturation(prop) {
|
|
254
|
+
if (!this.device.connected) throw this._commError();
|
|
255
|
+
|
|
254
256
|
if (!this._pendingHueSaturation) {
|
|
255
257
|
this._pendingHueSaturation = {props: {}, resolvers: []};
|
|
256
258
|
}
|
|
@@ -266,7 +268,7 @@ class OilDiffuserAccessory extends BaseAccessory {
|
|
|
266
268
|
this._pendingHueSaturation = null;
|
|
267
269
|
|
|
268
270
|
const newValue = this.convertColorFromHomeKitToTuya(props);
|
|
269
|
-
this.
|
|
271
|
+
this.setMultiStateInBackground({[this.dpMode]: this.cmdColor, [this.dpColor]: newValue});
|
|
270
272
|
|
|
271
273
|
resolvers.forEach(r => r());
|
|
272
274
|
}, 500);
|
|
@@ -134,8 +134,8 @@ class RGBTWLightAccessory extends BaseAccessory {
|
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
getBrightness() {
|
|
137
|
-
if (this.
|
|
138
|
-
return this.convertColorFromTuyaToHomeKit(this.
|
|
137
|
+
if (this.getStateAsync(this.dpMode) === this.cmdWhite) return this.convertBrightnessFromTuyaToHomeKit(this.getStateAsync(this.dpBrightness));
|
|
138
|
+
return this.convertColorFromTuyaToHomeKit(this.getStateAsync(this.dpColor)).b;
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
setBrightness(value) {
|
|
@@ -145,8 +145,8 @@ class RGBTWLightAccessory extends BaseAccessory {
|
|
|
145
145
|
|
|
146
146
|
getColorTemperature() {
|
|
147
147
|
this.log.debug(`getColorTemperature`);
|
|
148
|
-
if (this.
|
|
149
|
-
return this.convertColorTemperatureFromTuyaToHomeKit(this.
|
|
148
|
+
if (this.getStateAsync(this.dpMode) !== this.cmdWhite) return this.minWhiteColor;
|
|
149
|
+
return this.convertColorTemperatureFromTuyaToHomeKit(this.getStateAsync(this.dpColorTemperature));
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
setColorTemperature(value) {
|
|
@@ -165,10 +165,10 @@ class RGBTWLightAccessory extends BaseAccessory {
|
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
getHue() {
|
|
168
|
-
if (this.
|
|
169
|
-
return this.convertHomeKitColorTemperatureToHomeKitColor(this.convertColorTemperatureFromTuyaToHomeKit(this.
|
|
168
|
+
if (this.getStateAsync(this.dpMode) === this.cmdWhite) {
|
|
169
|
+
return this.convertHomeKitColorTemperatureToHomeKitColor(this.convertColorTemperatureFromTuyaToHomeKit(this.getStateAsync(this.dpColorTemperature))).h;
|
|
170
170
|
}
|
|
171
|
-
return this.convertColorFromTuyaToHomeKit(this.
|
|
171
|
+
return this.convertColorFromTuyaToHomeKit(this.getStateAsync(this.dpColor)).h;
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
setHue(value) {
|
|
@@ -176,10 +176,10 @@ class RGBTWLightAccessory extends BaseAccessory {
|
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
getSaturation() {
|
|
179
|
-
if (this.
|
|
180
|
-
return this.convertHomeKitColorTemperatureToHomeKitColor(this.convertColorTemperatureFromTuyaToHomeKit(this.
|
|
179
|
+
if (this.getStateAsync(this.dpMode) === this.cmdWhite) {
|
|
180
|
+
return this.convertHomeKitColorTemperatureToHomeKitColor(this.convertColorTemperatureFromTuyaToHomeKit(this.getStateAsync(this.dpColorTemperature))).s;
|
|
181
181
|
}
|
|
182
|
-
return this.convertColorFromTuyaToHomeKit(this.
|
|
182
|
+
return this.convertColorFromTuyaToHomeKit(this.getStateAsync(this.dpColor)).s;
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
setSaturation(value) {
|
|
@@ -187,6 +187,8 @@ class RGBTWLightAccessory extends BaseAccessory {
|
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
_setHueSaturation(prop) {
|
|
190
|
+
if (!this.device.connected) throw this._commError();
|
|
191
|
+
|
|
190
192
|
if (!this._pendingHueSaturation) {
|
|
191
193
|
this._pendingHueSaturation = {props: {}, resolvers: []};
|
|
192
194
|
}
|
|
@@ -208,7 +210,7 @@ class RGBTWLightAccessory extends BaseAccessory {
|
|
|
208
210
|
const newValue = this.convertColorFromHomeKitToTuya(props);
|
|
209
211
|
|
|
210
212
|
if (!(this.device.state[this.dpMode] === this.cmdWhite && isSham)) {
|
|
211
|
-
this.
|
|
213
|
+
this.setMultiStateInBackground({[this.dpMode]: this.cmdColor, [this.dpColor]: newValue});
|
|
212
214
|
}
|
|
213
215
|
this.characteristicColorTemperature.updateValue(this.minWhiteColor);
|
|
214
216
|
|
|
@@ -196,8 +196,8 @@ class RGBTWOutletAccessory extends BaseAccessory {
|
|
|
196
196
|
}
|
|
197
197
|
|
|
198
198
|
getBrightness() {
|
|
199
|
-
if (this.
|
|
200
|
-
return this.convertColorFromTuyaToHomeKit(this.
|
|
199
|
+
if (this.getStateAsync(this.dpMode) === this.cmdWhite) return this.convertBrightnessFromTuyaToHomeKit(this.getStateAsync(this.dpBrightness));
|
|
200
|
+
return this.convertColorFromTuyaToHomeKit(this.getStateAsync(this.dpColor)).b;
|
|
201
201
|
}
|
|
202
202
|
|
|
203
203
|
setBrightness(value) {
|
|
@@ -206,8 +206,8 @@ class RGBTWOutletAccessory extends BaseAccessory {
|
|
|
206
206
|
}
|
|
207
207
|
|
|
208
208
|
getColorTemperature() {
|
|
209
|
-
if (this.
|
|
210
|
-
return this.convertColorTemperatureFromTuyaToHomeKit(this.
|
|
209
|
+
if (this.getStateAsync(this.dpMode) !== this.cmdWhite) return 0;
|
|
210
|
+
return this.convertColorTemperatureFromTuyaToHomeKit(this.getStateAsync(this.dpColorTemperature));
|
|
211
211
|
}
|
|
212
212
|
|
|
213
213
|
setColorTemperature(value) {
|
|
@@ -221,8 +221,8 @@ class RGBTWOutletAccessory extends BaseAccessory {
|
|
|
221
221
|
}
|
|
222
222
|
|
|
223
223
|
getHue() {
|
|
224
|
-
if (this.
|
|
225
|
-
return this.convertColorFromTuyaToHomeKit(this.
|
|
224
|
+
if (this.getStateAsync(this.dpMode) === this.cmdWhite) return 0;
|
|
225
|
+
return this.convertColorFromTuyaToHomeKit(this.getStateAsync(this.dpColor)).h;
|
|
226
226
|
}
|
|
227
227
|
|
|
228
228
|
setHue(value) {
|
|
@@ -230,8 +230,8 @@ class RGBTWOutletAccessory extends BaseAccessory {
|
|
|
230
230
|
}
|
|
231
231
|
|
|
232
232
|
getSaturation() {
|
|
233
|
-
if (this.
|
|
234
|
-
return this.convertColorFromTuyaToHomeKit(this.
|
|
233
|
+
if (this.getStateAsync(this.dpMode) === this.cmdWhite) return 0;
|
|
234
|
+
return this.convertColorFromTuyaToHomeKit(this.getStateAsync(this.dpColor)).s;
|
|
235
235
|
}
|
|
236
236
|
|
|
237
237
|
setSaturation(value) {
|
|
@@ -239,6 +239,8 @@ class RGBTWOutletAccessory extends BaseAccessory {
|
|
|
239
239
|
}
|
|
240
240
|
|
|
241
241
|
_setHueSaturation(prop) {
|
|
242
|
+
if (!this.device.connected) throw this._commError();
|
|
243
|
+
|
|
242
244
|
if (!this._pendingHueSaturation) {
|
|
243
245
|
this._pendingHueSaturation = {props: {}, resolvers: []};
|
|
244
246
|
}
|
|
@@ -257,7 +259,7 @@ class RGBTWOutletAccessory extends BaseAccessory {
|
|
|
257
259
|
const newValue = this.convertColorFromHomeKitToTuya(props);
|
|
258
260
|
|
|
259
261
|
if (!(this.device.state[this.dpMode] === this.cmdWhite && isSham)) {
|
|
260
|
-
this.
|
|
262
|
+
this.setMultiStateInBackground({[this.dpMode]: this.cmdColor, [this.dpColor]: newValue});
|
|
261
263
|
}
|
|
262
264
|
this.characteristicColorTemperature.updateValue(0);
|
|
263
265
|
|
|
@@ -92,7 +92,7 @@ class SimpleBlindsAccessory extends BaseAccessory {
|
|
|
92
92
|
|
|
93
93
|
const characteristicPositionState = service.getCharacteristic(Characteristic.PositionState)
|
|
94
94
|
.updateValue(this._getPositionState())
|
|
95
|
-
.onGet(() => this._getPositionState());
|
|
95
|
+
.onGet(() => { if (!this.device.connected) throw this._commError(); return this._getPositionState(); });
|
|
96
96
|
|
|
97
97
|
this.device.on('change', changes => {
|
|
98
98
|
this.log.debug('[TuyaAccessory] Blinds saw change to ' + changes[this.dpAction]);
|
|
@@ -176,7 +176,7 @@ class SimpleBlindsAccessory extends BaseAccessory {
|
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
getCurrentPosition() {
|
|
179
|
-
return this._getCurrentPosition(this.
|
|
179
|
+
return this._getCurrentPosition(this.getStateAsync(this.dpAction));
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
_getCurrentPosition(dp) {
|
|
@@ -191,7 +191,7 @@ class SimpleBlindsAccessory extends BaseAccessory {
|
|
|
191
191
|
}
|
|
192
192
|
|
|
193
193
|
getTargetPosition() {
|
|
194
|
-
return this._getTargetPosition(this.
|
|
194
|
+
return this._getTargetPosition(this.getStateAsync(this.dpAction));
|
|
195
195
|
}
|
|
196
196
|
|
|
197
197
|
_getTargetPosition(dp) {
|
|
@@ -234,7 +234,7 @@ class SimpleBlindsAccessory extends BaseAccessory {
|
|
|
234
234
|
this.log.debug('[TuyaAccessory] Blinds will stop in ' + duration + 'ms');
|
|
235
235
|
this.changeTimeout = setTimeout(() => {
|
|
236
236
|
this.log.debug('[TuyaAccessory] Blinds asked to stop');
|
|
237
|
-
this.
|
|
237
|
+
this.setStateInBackground(this.dpAction, this.cmdStop);
|
|
238
238
|
}, duration);
|
|
239
239
|
}
|
|
240
240
|
return result;
|
|
@@ -246,7 +246,7 @@ class SimpleBlindsAccessory extends BaseAccessory {
|
|
|
246
246
|
this.log.debug('[TuyaAccessory] Blinds will stop in ' + duration + 'ms');
|
|
247
247
|
this.changeTimeout = setTimeout(() => {
|
|
248
248
|
this.log.debug('[TuyaAccessory] Blinds asked to stop');
|
|
249
|
-
this.
|
|
249
|
+
this.setStateInBackground(this.dpAction, this.cmdStop);
|
|
250
250
|
}, duration);
|
|
251
251
|
}
|
|
252
252
|
return result;
|
|
@@ -43,7 +43,7 @@ class SimpleDimmer2Accessory extends BaseAccessory {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
getBrightness() {
|
|
46
|
-
return this.convertBrightnessFromTuyaToHomeKit(this.
|
|
46
|
+
return this.convertBrightnessFromTuyaToHomeKit(this.getStateAsync(this.dpBrightness));
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
setBrightness(value) {
|
|
@@ -25,25 +25,29 @@ class SimpleDimmerAccessory extends BaseAccessory {
|
|
|
25
25
|
this.dpPower = this._getCustomDP(this.device.context.dpPower) || '1';
|
|
26
26
|
this.dpBrightness = this._getCustomDP(this.device.context.dpBrightness) || this._getCustomDP(this.device.context.dp) || '2';
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
this._characteristicOn = service.getCharacteristic(Characteristic.On)
|
|
29
29
|
.updateValue(dps[this.dpPower])
|
|
30
30
|
.onGet(() => this.getStateAsync(this.dpPower))
|
|
31
31
|
.onSet(value => this.setStateAsync(this.dpPower, value));
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
this._characteristicBrightness = service.getCharacteristic(Characteristic.Brightness)
|
|
34
34
|
.updateValue(this.convertBrightnessFromTuyaToHomeKit(dps[this.dpBrightness]))
|
|
35
35
|
.onGet(() => this.getBrightness())
|
|
36
36
|
.onSet(value => this.setBrightness(value));
|
|
37
37
|
|
|
38
|
+
this._registerChangeListener();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
_registerChangeListener() {
|
|
38
42
|
this.device.on('change', changes => {
|
|
39
|
-
if (changes.hasOwnProperty(this.dpPower) &&
|
|
40
|
-
if (changes.hasOwnProperty(this.dpBrightness) && this.convertBrightnessFromHomeKitToTuya(
|
|
41
|
-
|
|
43
|
+
if (changes.hasOwnProperty(this.dpPower) && this._characteristicOn.value !== changes[this.dpPower]) this._characteristicOn.updateValue(changes[this.dpPower]);
|
|
44
|
+
if (changes.hasOwnProperty(this.dpBrightness) && this.convertBrightnessFromHomeKitToTuya(this._characteristicBrightness.value) !== changes[this.dpBrightness])
|
|
45
|
+
this._characteristicBrightness.updateValue(this.convertBrightnessFromTuyaToHomeKit(changes[this.dpBrightness]));
|
|
42
46
|
});
|
|
43
47
|
}
|
|
44
48
|
|
|
45
49
|
getBrightness() {
|
|
46
|
-
return this.convertBrightnessFromTuyaToHomeKit(this.
|
|
50
|
+
return this.convertBrightnessFromTuyaToHomeKit(this.getStateAsync(this.dpBrightness));
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
setBrightness(value) {
|
|
@@ -98,6 +98,12 @@ class SimpleGarageDoorAccessory extends BaseAccessory {
|
|
|
98
98
|
// and the close that trails a stop in the stop-before-close path.
|
|
99
99
|
this.partialStopTimer = null;
|
|
100
100
|
this.pendingCloseTimer = null;
|
|
101
|
+
// A partial open that has fired its open but is still waiting for the
|
|
102
|
+
// controller to report it's moving before arming the auto-stop.
|
|
103
|
+
this.partialPending = false;
|
|
104
|
+
// Bumped whenever a partial open starts or is superseded/cancelled, so a
|
|
105
|
+
// stale auto-stop (and its re-sends) from an earlier flow bails out.
|
|
106
|
+
this.partialGeneration = 0;
|
|
101
107
|
|
|
102
108
|
// Seed the initial state from whatever the device has already reported.
|
|
103
109
|
// If it hasn't reported yet, fall back to the persisted target, then to
|
|
@@ -117,25 +123,25 @@ class SimpleGarageDoorAccessory extends BaseAccessory {
|
|
|
117
123
|
|
|
118
124
|
this.characteristicTargetDoorState = service.getCharacteristic(Characteristic.TargetDoorState)
|
|
119
125
|
.updateValue(initialTarget)
|
|
120
|
-
.onGet(() => this.accessory.context.cachedTargetDoorState)
|
|
126
|
+
.onGet(() => this._reportDoorState(this.accessory.context.cachedTargetDoorState))
|
|
121
127
|
.onSet(value => this.setTargetDoorState(value));
|
|
122
128
|
|
|
123
129
|
this.characteristicCurrentDoorState = service.getCharacteristic(Characteristic.CurrentDoorState)
|
|
124
130
|
.updateValue(this.currentDoorState)
|
|
125
|
-
.onGet(() => this.currentDoorState);
|
|
131
|
+
.onGet(() => this._reportDoorState(this.currentDoorState));
|
|
126
132
|
|
|
127
133
|
// The controller exposes limit switches (l_open/l_close) but they don't
|
|
128
134
|
// work in practice, and there's no obstruction sensor wired up, so this
|
|
129
135
|
// is always reported clear.
|
|
130
136
|
service.getCharacteristic(Characteristic.ObstructionDetected)
|
|
131
137
|
.updateValue(false)
|
|
132
|
-
.onGet(() => false);
|
|
138
|
+
.onGet(() => this._reportDoorState(false));
|
|
133
139
|
|
|
134
140
|
const partialSwitch = this.accessory.getServiceById(Service.Switch, 'partialOpen');
|
|
135
141
|
if (partialSwitch) {
|
|
136
142
|
const onChar = partialSwitch.getCharacteristic(Characteristic.On)
|
|
137
143
|
.updateValue(isOpen)
|
|
138
|
-
.onGet(() => this.currentDoorState === Characteristic.CurrentDoorState.OPEN)
|
|
144
|
+
.onGet(() => this._reportDoorState(this.currentDoorState === Characteristic.CurrentDoorState.OPEN))
|
|
139
145
|
.onSet(value => {
|
|
140
146
|
// Stateful: the switch mirrors CurrentDoorState (ON while
|
|
141
147
|
// the gate is open in HomeKit's view — see
|
|
@@ -197,7 +203,7 @@ class SimpleGarageDoorAccessory extends BaseAccessory {
|
|
|
197
203
|
if (this.pendingCloseTimer) return;
|
|
198
204
|
const isOpen = this._mapDpState(changes[this.dpState]);
|
|
199
205
|
if (isOpen === null) {
|
|
200
|
-
this.log.
|
|
206
|
+
this.log.debug(`[SimpleGarageDoor] ${this.device.context.name}: ignoring unknown state DP value ${JSON.stringify(changes[this.dpState])}`);
|
|
201
207
|
return;
|
|
202
208
|
}
|
|
203
209
|
this._applyReportedState(isOpen);
|
|
@@ -227,9 +233,26 @@ class SimpleGarageDoorAccessory extends BaseAccessory {
|
|
|
227
233
|
if (this.characteristicPartialOpen) {
|
|
228
234
|
this.characteristicPartialOpen.updateValue(isOpen);
|
|
229
235
|
}
|
|
236
|
+
|
|
237
|
+
// A partial open fires its open and then waits for the controller to
|
|
238
|
+
// confirm the gate is actually moving before starting the auto-stop
|
|
239
|
+
// countdown — see _handlePartialOpen.
|
|
240
|
+
if (this.partialPending && isOpen) this._armPartialStop();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// onGet helper: a cached/optimistic door value is fine to report while the
|
|
244
|
+
// gate is reachable, but when it's offline HomeKit must show "No Response"
|
|
245
|
+
// instead of a stale state that makes the gate look online and accept taps.
|
|
246
|
+
_reportDoorState(value) {
|
|
247
|
+
if (!this.device.connected) throw this._commError();
|
|
248
|
+
return value;
|
|
230
249
|
}
|
|
231
250
|
|
|
232
251
|
setTargetDoorState(value) {
|
|
252
|
+
// Surface "No Response" for a tap on an unreachable gate instead of
|
|
253
|
+
// silently dropping the command (the write helpers would otherwise just
|
|
254
|
+
// log and skip, leaving HomeKit to believe the open/close succeeded).
|
|
255
|
+
if (!this.device.connected) throw this._commError();
|
|
233
256
|
// A direct open/close (the GarageDoorOpener target, a Force switch, or
|
|
234
257
|
// the partial switch tapped OFF) is manual control: cancel any pending
|
|
235
258
|
// partial-open auto-stop so it can't halt this movement part-way.
|
|
@@ -254,8 +277,8 @@ class SimpleGarageDoorAccessory extends BaseAccessory {
|
|
|
254
277
|
// own, even mid-close. Abandon any pending stop-before-close.
|
|
255
278
|
_sendOpen() {
|
|
256
279
|
this._cancelPendingClose();
|
|
257
|
-
this.log.
|
|
258
|
-
this.
|
|
280
|
+
this.log.debug(`[SimpleGarageDoor] ${this.device.context.name}: open (dp${this.dpOpen})`);
|
|
281
|
+
this.setMultiStateLegacyInBackground({[this.dpOpen]: true});
|
|
259
282
|
}
|
|
260
283
|
|
|
261
284
|
// Close is ignored while the gate is actively moving, so fire it directly
|
|
@@ -266,20 +289,20 @@ class SimpleGarageDoorAccessory extends BaseAccessory {
|
|
|
266
289
|
if (this.pendingCloseTimer) {
|
|
267
290
|
// A stop-before-close is already running — don't restart it (and
|
|
268
291
|
// push the close out) on a duplicate or retransmitted request.
|
|
269
|
-
this.log.
|
|
292
|
+
this.log.debug(`[SimpleGarageDoor] ${name}: close ignored — a stop-before-close is already running`);
|
|
270
293
|
return;
|
|
271
294
|
}
|
|
272
295
|
if (this._isStopped()) {
|
|
273
|
-
this.log.
|
|
274
|
-
this.
|
|
296
|
+
this.log.debug(`[SimpleGarageDoor] ${name}: close (dp${this.dpClose}) — gate already stopped`);
|
|
297
|
+
this.setMultiStateLegacyInBackground({[this.dpClose]: true});
|
|
275
298
|
return;
|
|
276
299
|
}
|
|
277
|
-
this.log.
|
|
278
|
-
this.
|
|
300
|
+
this.log.debug(`[SimpleGarageDoor] ${name}: stop-before-close — stop (dp${this.dpStop}) now, close (dp${this.dpClose}) in ${this.stopBeforeCloseMs}ms`);
|
|
301
|
+
this.setMultiStateLegacyInBackground({[this.dpStop]: true});
|
|
279
302
|
this.pendingCloseTimer = setTimeout(() => {
|
|
280
303
|
this.pendingCloseTimer = null;
|
|
281
|
-
this.log.
|
|
282
|
-
this.
|
|
304
|
+
this.log.debug(`[SimpleGarageDoor] ${name}: stop-before-close — firing close (dp${this.dpClose})`);
|
|
305
|
+
this.setMultiStateLegacyInBackground({[this.dpClose]: true});
|
|
283
306
|
}, this.stopBeforeCloseMs);
|
|
284
307
|
}
|
|
285
308
|
|
|
@@ -298,32 +321,63 @@ class SimpleGarageDoorAccessory extends BaseAccessory {
|
|
|
298
321
|
}
|
|
299
322
|
}
|
|
300
323
|
|
|
301
|
-
// Partial open: fire the open action,
|
|
302
|
-
// so the gate ends up parked part-way.
|
|
303
|
-
//
|
|
304
|
-
//
|
|
324
|
+
// Partial open: fire the open action, wait partialOpenMs, then fire a stop
|
|
325
|
+
// so the gate ends up parked part-way.
|
|
326
|
+
//
|
|
327
|
+
// The auto-stop is anchored to the controller *reporting the gate is moving*
|
|
328
|
+
// (the status DP flipping to OPEN), not to the button press: the open
|
|
329
|
+
// command takes time to reach the gate, and a stop fired before the gate is
|
|
330
|
+
// actually moving lands as a no-op the controller drops — letting the gate
|
|
331
|
+
// run all the way open. If the gate already reads open/opening, no fresh
|
|
332
|
+
// report is coming, so we arm straight away.
|
|
305
333
|
_handlePartialOpen() {
|
|
306
334
|
const {Characteristic} = this.hap;
|
|
307
335
|
const name = this.device.context.name;
|
|
336
|
+
if (!this.device.connected) throw this._commError();
|
|
308
337
|
if (!this.partialOpenMs) return;
|
|
309
|
-
if (this.partialStopTimer) {
|
|
310
|
-
// Re-entrant press while a partial is already
|
|
338
|
+
if (this.partialPending || this.partialStopTimer) {
|
|
339
|
+
// Re-entrant press while a partial is already in progress (e.g. a
|
|
311
340
|
// HomeKit/iOS WRITE retransmit) — ignore so the stop isn't pushed
|
|
312
341
|
// out, which would let the gate run further than intended.
|
|
313
|
-
this.log.
|
|
342
|
+
this.log.debug(`[SimpleGarageDoor] ${name}: partial press ignored — a partial open is already running`);
|
|
314
343
|
return;
|
|
315
344
|
}
|
|
316
345
|
|
|
317
|
-
this.log.
|
|
346
|
+
this.log.debug(`[SimpleGarageDoor] ${name}: partial open — opening, will stop ${this.partialOpenMs}ms after the gate starts moving`);
|
|
347
|
+
this.partialGeneration++;
|
|
348
|
+
this.partialPending = true;
|
|
318
349
|
this._applyTarget(Characteristic.TargetDoorState.OPEN);
|
|
350
|
+
|
|
351
|
+
const reported = this.device.state ? this.device.state[this.dpState] : undefined;
|
|
352
|
+
if (this._mapDpState(reported) === true) this._armPartialStop();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
_armPartialStop() {
|
|
356
|
+
if (!this.partialPending || this.partialStopTimer) return;
|
|
357
|
+
this.partialPending = false;
|
|
358
|
+
const name = this.device.context.name;
|
|
359
|
+
const generation = this.partialGeneration;
|
|
319
360
|
this.partialStopTimer = setTimeout(() => {
|
|
320
361
|
this.partialStopTimer = null;
|
|
321
|
-
this.log.
|
|
322
|
-
this.
|
|
362
|
+
this.log.debug(`[SimpleGarageDoor] ${name}: partial open — firing stop (dp${this.dpStop})`);
|
|
363
|
+
this._sendPartialStop(generation, 1);
|
|
323
364
|
}, this.partialOpenMs);
|
|
324
365
|
}
|
|
325
366
|
|
|
367
|
+
// The controller occasionally drops a lone write (its command queue can
|
|
368
|
+
// coalesce back-to-back writes, and brief Wi-Fi blips lose individual
|
|
369
|
+
// packets), and a dropped stop here is exactly what lets a partial open run
|
|
370
|
+
// all the way. Re-send it a few times; a stop on an already-parked gate is a
|
|
371
|
+
// harmless no-op. Bails out if the flow was superseded mid-way.
|
|
372
|
+
_sendPartialStop(generation, attempt) {
|
|
373
|
+
if (generation !== this.partialGeneration) return;
|
|
374
|
+
this.setMultiStateLegacyInBackground({[this.dpStop]: true});
|
|
375
|
+
if (attempt < 3) setTimeout(() => this._sendPartialStop(generation, attempt + 1), 300);
|
|
376
|
+
}
|
|
377
|
+
|
|
326
378
|
_cancelPartialStop() {
|
|
379
|
+
this.partialPending = false;
|
|
380
|
+
this.partialGeneration++;
|
|
327
381
|
if (this.partialStopTimer) {
|
|
328
382
|
clearTimeout(this.partialStopTimer);
|
|
329
383
|
this.partialStopTimer = null;
|
|
@@ -45,7 +45,7 @@ class SimpleHeaterAccessory extends BaseAccessory {
|
|
|
45
45
|
|
|
46
46
|
const characteristicCurrentHeaterCoolerState = service.getCharacteristic(Characteristic.CurrentHeaterCoolerState)
|
|
47
47
|
.updateValue(this._getCurrentHeaterCoolerState(dps))
|
|
48
|
-
.onGet(() => this.currentHeaterCoolerState);
|
|
48
|
+
.onGet(() => { if (!this.device.connected) throw this._commError(); return this.currentHeaterCoolerState; });
|
|
49
49
|
|
|
50
50
|
service.getCharacteristic(Characteristic.TargetHeaterCoolerState)
|
|
51
51
|
.setProps({
|
|
@@ -54,7 +54,7 @@ class SimpleHeaterAccessory extends BaseAccessory {
|
|
|
54
54
|
validValues: [Characteristic.TargetHeaterCoolerState.HEAT]
|
|
55
55
|
})
|
|
56
56
|
.updateValue(this._getTargetHeaterCoolerState())
|
|
57
|
-
.onGet(() => this._getTargetHeaterCoolerState())
|
|
57
|
+
.onGet(() => { if (!this.device.connected) throw this._commError(); return this._getTargetHeaterCoolerState(); })
|
|
58
58
|
.onSet(() => this.setStateAsync(this.dpActive, true));
|
|
59
59
|
|
|
60
60
|
const characteristicCurrentTemperature = service.getCharacteristic(Characteristic.CurrentTemperature)
|
|
@@ -90,7 +90,7 @@ class SimpleHeaterAccessory extends BaseAccessory {
|
|
|
90
90
|
characteristicCurrentTemperature.updateValue(this._getDividedState(changes[this.dpCurrentTemperature], this.temperatureDivisor, this.currentTemperatureOffset));
|
|
91
91
|
|
|
92
92
|
characteristicCurrentHeaterCoolerState.updateValue(this._getCurrentHeaterCoolerState(state));
|
|
93
|
-
this.log.
|
|
93
|
+
this.log.debug('SimpleHeater changed: ' + JSON.stringify(state));
|
|
94
94
|
});
|
|
95
95
|
}
|
|
96
96
|
|
|
@@ -147,7 +147,7 @@ class SimpleHeaterAccessory extends BaseAccessory {
|
|
|
147
147
|
|
|
148
148
|
_getCurrentTempAsync() {
|
|
149
149
|
const data = this.getStateAsync(this.dpCurrentTemperature);
|
|
150
|
-
if (!isFinite(data)) throw
|
|
150
|
+
if (!isFinite(data)) throw this._commError();
|
|
151
151
|
return this._getDividedState(data, this.temperatureDivisor, this.currentTemperatureOffset);
|
|
152
152
|
}
|
|
153
153
|
}
|
|
@@ -31,7 +31,7 @@ class SimpleLightAccessory extends BaseAccessory {
|
|
|
31
31
|
|
|
32
32
|
this.device.on('change', (changes, state) => {
|
|
33
33
|
if (changes.hasOwnProperty(this.dpPower) && characteristicOn.value !== changes[this.dpPower]) characteristicOn.updateValue(changes[this.dpPower]);
|
|
34
|
-
this.log.
|
|
34
|
+
this.log.debug('SimpleLight changed: ' + JSON.stringify(state));
|
|
35
35
|
});
|
|
36
36
|
}
|
|
37
37
|
}
|
package/lib/SwitchAccessory.js
CHANGED
|
@@ -34,7 +34,7 @@ class SwitchAccessory extends BaseAccessory {
|
|
|
34
34
|
this.accessory.services
|
|
35
35
|
.filter(service => service.UUID === Service.Switch.UUID && !_validServices.includes(service))
|
|
36
36
|
.forEach(service => {
|
|
37
|
-
this.log.
|
|
37
|
+
this.log.debug('Removing', service.displayName);
|
|
38
38
|
this.accessory.removeService(service);
|
|
39
39
|
});
|
|
40
40
|
}
|
|
@@ -69,6 +69,7 @@ class SwitchAccessory extends BaseAccessory {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
setPower(dp, value) {
|
|
72
|
+
if (!this.device.connected) throw this._commError();
|
|
72
73
|
if (!this._pendingPower) {
|
|
73
74
|
this._pendingPower = {props: {}, resolvers: []};
|
|
74
75
|
}
|
|
@@ -82,7 +83,7 @@ class SwitchAccessory extends BaseAccessory {
|
|
|
82
83
|
this._pendingPower.timer = setTimeout(() => {
|
|
83
84
|
const {props, resolvers} = this._pendingPower;
|
|
84
85
|
this._pendingPower = null;
|
|
85
|
-
this.
|
|
86
|
+
this.setMultiStateInBackground(props);
|
|
86
87
|
resolvers.forEach(r => r());
|
|
87
88
|
}, 500);
|
|
88
89
|
});
|
package/lib/TWLightAccessory.js
CHANGED
|
@@ -72,7 +72,7 @@ class TWLightAccessory extends BaseAccessory {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
getBrightness() {
|
|
75
|
-
return this.convertBrightnessFromTuyaToHomeKit(this.
|
|
75
|
+
return this.convertBrightnessFromTuyaToHomeKit(this.getStateAsync(this.dpBrightness));
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
setBrightness(value) {
|
|
@@ -80,7 +80,7 @@ class TWLightAccessory extends BaseAccessory {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
getColorTemperature() {
|
|
83
|
-
return this.convertColorTemperatureFromTuyaToHomeKit(this.
|
|
83
|
+
return this.convertColorTemperatureFromTuyaToHomeKit(this.getStateAsync(this.dpColorTemperature));
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
setColorTemperature(value) {
|