homebridge-tuya-plus 3.14.0-dev.5 → 3.14.0-dev.52
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 +122 -26
- 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 +193 -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
|
@@ -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
|
|
@@ -114,28 +120,33 @@ class SimpleGarageDoorAccessory extends BaseAccessory {
|
|
|
114
120
|
? Characteristic.TargetDoorState.OPEN
|
|
115
121
|
: Characteristic.TargetDoorState.CLOSED;
|
|
116
122
|
this.accessory.context.cachedTargetDoorState = initialTarget;
|
|
123
|
+
// The level-triggered dispatch baseline (see setTargetDoorState). Held
|
|
124
|
+
// separately from cachedTargetDoorState because that one mirrors every
|
|
125
|
+
// reported value for HomeKit's benefit — including the transient "stopped"
|
|
126
|
+
// our own stop-before-close produces — which must not move this baseline.
|
|
127
|
+
this._committedTarget = initialTarget;
|
|
117
128
|
|
|
118
129
|
this.characteristicTargetDoorState = service.getCharacteristic(Characteristic.TargetDoorState)
|
|
119
130
|
.updateValue(initialTarget)
|
|
120
|
-
.onGet(() => this.accessory.context.cachedTargetDoorState)
|
|
131
|
+
.onGet(() => this._reportDoorState(this.accessory.context.cachedTargetDoorState))
|
|
121
132
|
.onSet(value => this.setTargetDoorState(value));
|
|
122
133
|
|
|
123
134
|
this.characteristicCurrentDoorState = service.getCharacteristic(Characteristic.CurrentDoorState)
|
|
124
135
|
.updateValue(this.currentDoorState)
|
|
125
|
-
.onGet(() => this.currentDoorState);
|
|
136
|
+
.onGet(() => this._reportDoorState(this.currentDoorState));
|
|
126
137
|
|
|
127
138
|
// The controller exposes limit switches (l_open/l_close) but they don't
|
|
128
139
|
// work in practice, and there's no obstruction sensor wired up, so this
|
|
129
140
|
// is always reported clear.
|
|
130
141
|
service.getCharacteristic(Characteristic.ObstructionDetected)
|
|
131
142
|
.updateValue(false)
|
|
132
|
-
.onGet(() => false);
|
|
143
|
+
.onGet(() => this._reportDoorState(false));
|
|
133
144
|
|
|
134
145
|
const partialSwitch = this.accessory.getServiceById(Service.Switch, 'partialOpen');
|
|
135
146
|
if (partialSwitch) {
|
|
136
147
|
const onChar = partialSwitch.getCharacteristic(Characteristic.On)
|
|
137
148
|
.updateValue(isOpen)
|
|
138
|
-
.onGet(() => this.currentDoorState === Characteristic.CurrentDoorState.OPEN)
|
|
149
|
+
.onGet(() => this._reportDoorState(this.currentDoorState === Characteristic.CurrentDoorState.OPEN))
|
|
139
150
|
.onSet(value => {
|
|
140
151
|
// Stateful: the switch mirrors CurrentDoorState (ON while
|
|
141
152
|
// the gate is open in HomeKit's view — see
|
|
@@ -195,11 +206,24 @@ class SimpleGarageDoorAccessory extends BaseAccessory {
|
|
|
195
206
|
// (=OPEN), which would fight the close we're about to send. Ignore
|
|
196
207
|
// status reports until the close has gone out.
|
|
197
208
|
if (this.pendingCloseTimer) return;
|
|
198
|
-
const
|
|
209
|
+
const raw = typeof changes[this.dpState] === 'string' ? parseInt(changes[this.dpState], 10) : changes[this.dpState];
|
|
210
|
+
const isOpen = this._mapDpState(raw);
|
|
199
211
|
if (isOpen === null) {
|
|
200
|
-
this.log.
|
|
212
|
+
this.log.debug(`[SimpleGarageDoor] ${this.device.context.name}: ignoring unknown state DP value ${JSON.stringify(changes[this.dpState])}`);
|
|
201
213
|
return;
|
|
202
214
|
}
|
|
215
|
+
// Keep the level-triggered dispatch baseline honest with the gate's real
|
|
216
|
+
// position, but only from a DEFINITIVE report — 12 (opening/open) or 13
|
|
217
|
+
// (closing/closed). A bare 11 (stopped) is skipped: it is exactly what our
|
|
218
|
+
// own stop-before-close pulse makes the controller report, and over the LAN
|
|
219
|
+
// that report can land just after the stop→close window. Letting it flip
|
|
220
|
+
// the committed target back to OPEN is what let HomeKit's repeat close fire
|
|
221
|
+
// a second stop-before-close into the already-closing gate. A genuine
|
|
222
|
+
// external open always passes through 12 first, so nothing is missed.
|
|
223
|
+
if (raw === STATE_OPENING_OR_OPEN || raw === STATE_CLOSING_OR_CLOSED) {
|
|
224
|
+
const {Characteristic} = this.hap;
|
|
225
|
+
this._committedTarget = isOpen ? Characteristic.TargetDoorState.OPEN : Characteristic.TargetDoorState.CLOSED;
|
|
226
|
+
}
|
|
203
227
|
this._applyReportedState(isOpen);
|
|
204
228
|
}
|
|
205
229
|
|
|
@@ -227,13 +251,53 @@ class SimpleGarageDoorAccessory extends BaseAccessory {
|
|
|
227
251
|
if (this.characteristicPartialOpen) {
|
|
228
252
|
this.characteristicPartialOpen.updateValue(isOpen);
|
|
229
253
|
}
|
|
254
|
+
|
|
255
|
+
// A partial open fires its open and then waits for the controller to
|
|
256
|
+
// confirm the gate is actually moving before starting the auto-stop
|
|
257
|
+
// countdown — see _handlePartialOpen.
|
|
258
|
+
if (this.partialPending && isOpen) this._armPartialStop();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// onGet helper: a cached/optimistic door value is fine to report while the
|
|
262
|
+
// gate is reachable, but when it's offline HomeKit must show "No Response"
|
|
263
|
+
// instead of a stale state that makes the gate look online and accept taps.
|
|
264
|
+
_reportDoorState(value) {
|
|
265
|
+
if (!this.device.connected) throw this._commError();
|
|
266
|
+
return value;
|
|
230
267
|
}
|
|
231
268
|
|
|
232
269
|
setTargetDoorState(value) {
|
|
270
|
+
// Surface "No Response" for a tap on an unreachable gate instead of
|
|
271
|
+
// silently dropping the command (the write helpers would otherwise just
|
|
272
|
+
// log and skip, leaving HomeKit to believe the open/close succeeded).
|
|
273
|
+
if (!this.device.connected) throw this._commError();
|
|
274
|
+
|
|
275
|
+
// The opener is level-triggered: act only when the requested target
|
|
276
|
+
// differs from the one we've already committed to. HomeKit re-sends the
|
|
277
|
+
// same target after a tap — a second controller echoing it, or its own
|
|
278
|
+
// retry a few seconds later — and acting on that repeat would fire another
|
|
279
|
+
// open/close. For a close that means a second stop-before-close into the
|
|
280
|
+
// already-closing gate, halting it mid-travel and restarting it (the
|
|
281
|
+
// reported stutter). A genuine request always flips the target (the Home
|
|
282
|
+
// app toggles to the opposite state). The committed target is reconciled
|
|
283
|
+
// with the gate's real position in _onDeviceChange so external operation
|
|
284
|
+
// still works — but only from a definitive report, never from the
|
|
285
|
+
// "stopped" transient our own stop produces, so the repeat stays caught.
|
|
286
|
+
if (value === this._committedTarget) {
|
|
287
|
+
this.log.debug(`[SimpleGarageDoor] ${this.device.context.name}: target unchanged — ignoring repeat request`);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
233
291
|
// A direct open/close (the GarageDoorOpener target, a Force switch, or
|
|
234
292
|
// the partial switch tapped OFF) is manual control: cancel any pending
|
|
235
|
-
// partial-open auto-stop so it can't halt this movement part-way.
|
|
293
|
+
// partial-open auto-stop so it can't halt this movement part-way. This
|
|
294
|
+
// must run only for a genuine target change, AFTER the repeat check
|
|
295
|
+
// above: a partial open drives the committed target to OPEN itself, so
|
|
296
|
+
// HomeKit's repeat of that OPEN re-enters here, and cancelling on that
|
|
297
|
+
// swallowed repeat would tear down the auto-stop the partial just armed —
|
|
298
|
+
// letting the gate run all the way open instead of parking part-way.
|
|
236
299
|
this._cancelPartialStop();
|
|
300
|
+
|
|
237
301
|
this._applyTarget(value);
|
|
238
302
|
}
|
|
239
303
|
|
|
@@ -244,6 +308,7 @@ class SimpleGarageDoorAccessory extends BaseAccessory {
|
|
|
244
308
|
_applyTarget(value) {
|
|
245
309
|
const {Characteristic} = this.hap;
|
|
246
310
|
const open = value === Characteristic.TargetDoorState.OPEN;
|
|
311
|
+
this._committedTarget = value;
|
|
247
312
|
this.accessory.context.cachedTargetDoorState = value;
|
|
248
313
|
if (this.characteristicTargetDoorState) this.characteristicTargetDoorState.updateValue(value);
|
|
249
314
|
if (open) this._sendOpen();
|
|
@@ -254,8 +319,8 @@ class SimpleGarageDoorAccessory extends BaseAccessory {
|
|
|
254
319
|
// own, even mid-close. Abandon any pending stop-before-close.
|
|
255
320
|
_sendOpen() {
|
|
256
321
|
this._cancelPendingClose();
|
|
257
|
-
this.log.
|
|
258
|
-
this.
|
|
322
|
+
this.log.debug(`[SimpleGarageDoor] ${this.device.context.name}: open (dp${this.dpOpen})`);
|
|
323
|
+
this.setMultiStateLegacyInBackground({[this.dpOpen]: true});
|
|
259
324
|
}
|
|
260
325
|
|
|
261
326
|
// Close is ignored while the gate is actively moving, so fire it directly
|
|
@@ -266,20 +331,20 @@ class SimpleGarageDoorAccessory extends BaseAccessory {
|
|
|
266
331
|
if (this.pendingCloseTimer) {
|
|
267
332
|
// A stop-before-close is already running — don't restart it (and
|
|
268
333
|
// push the close out) on a duplicate or retransmitted request.
|
|
269
|
-
this.log.
|
|
334
|
+
this.log.debug(`[SimpleGarageDoor] ${name}: close ignored — a stop-before-close is already running`);
|
|
270
335
|
return;
|
|
271
336
|
}
|
|
272
337
|
if (this._isStopped()) {
|
|
273
|
-
this.log.
|
|
274
|
-
this.
|
|
338
|
+
this.log.debug(`[SimpleGarageDoor] ${name}: close (dp${this.dpClose}) — gate already stopped`);
|
|
339
|
+
this.setMultiStateLegacyInBackground({[this.dpClose]: true});
|
|
275
340
|
return;
|
|
276
341
|
}
|
|
277
|
-
this.log.
|
|
278
|
-
this.
|
|
342
|
+
this.log.debug(`[SimpleGarageDoor] ${name}: stop-before-close — stop (dp${this.dpStop}) now, close (dp${this.dpClose}) in ${this.stopBeforeCloseMs}ms`);
|
|
343
|
+
this.setMultiStateLegacyInBackground({[this.dpStop]: true});
|
|
279
344
|
this.pendingCloseTimer = setTimeout(() => {
|
|
280
345
|
this.pendingCloseTimer = null;
|
|
281
|
-
this.log.
|
|
282
|
-
this.
|
|
346
|
+
this.log.debug(`[SimpleGarageDoor] ${name}: stop-before-close — firing close (dp${this.dpClose})`);
|
|
347
|
+
this.setMultiStateLegacyInBackground({[this.dpClose]: true});
|
|
283
348
|
}, this.stopBeforeCloseMs);
|
|
284
349
|
}
|
|
285
350
|
|
|
@@ -298,32 +363,63 @@ class SimpleGarageDoorAccessory extends BaseAccessory {
|
|
|
298
363
|
}
|
|
299
364
|
}
|
|
300
365
|
|
|
301
|
-
// Partial open: fire the open action,
|
|
302
|
-
// so the gate ends up parked part-way.
|
|
303
|
-
//
|
|
304
|
-
//
|
|
366
|
+
// Partial open: fire the open action, wait partialOpenMs, then fire a stop
|
|
367
|
+
// so the gate ends up parked part-way.
|
|
368
|
+
//
|
|
369
|
+
// The auto-stop is anchored to the controller *reporting the gate is moving*
|
|
370
|
+
// (the status DP flipping to OPEN), not to the button press: the open
|
|
371
|
+
// command takes time to reach the gate, and a stop fired before the gate is
|
|
372
|
+
// actually moving lands as a no-op the controller drops — letting the gate
|
|
373
|
+
// run all the way open. If the gate already reads open/opening, no fresh
|
|
374
|
+
// report is coming, so we arm straight away.
|
|
305
375
|
_handlePartialOpen() {
|
|
306
376
|
const {Characteristic} = this.hap;
|
|
307
377
|
const name = this.device.context.name;
|
|
378
|
+
if (!this.device.connected) throw this._commError();
|
|
308
379
|
if (!this.partialOpenMs) return;
|
|
309
|
-
if (this.partialStopTimer) {
|
|
310
|
-
// Re-entrant press while a partial is already
|
|
380
|
+
if (this.partialPending || this.partialStopTimer) {
|
|
381
|
+
// Re-entrant press while a partial is already in progress (e.g. a
|
|
311
382
|
// HomeKit/iOS WRITE retransmit) — ignore so the stop isn't pushed
|
|
312
383
|
// out, which would let the gate run further than intended.
|
|
313
|
-
this.log.
|
|
384
|
+
this.log.debug(`[SimpleGarageDoor] ${name}: partial press ignored — a partial open is already running`);
|
|
314
385
|
return;
|
|
315
386
|
}
|
|
316
387
|
|
|
317
|
-
this.log.
|
|
388
|
+
this.log.debug(`[SimpleGarageDoor] ${name}: partial open — opening, will stop ${this.partialOpenMs}ms after the gate starts moving`);
|
|
389
|
+
this.partialGeneration++;
|
|
390
|
+
this.partialPending = true;
|
|
318
391
|
this._applyTarget(Characteristic.TargetDoorState.OPEN);
|
|
392
|
+
|
|
393
|
+
const reported = this.device.state ? this.device.state[this.dpState] : undefined;
|
|
394
|
+
if (this._mapDpState(reported) === true) this._armPartialStop();
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
_armPartialStop() {
|
|
398
|
+
if (!this.partialPending || this.partialStopTimer) return;
|
|
399
|
+
this.partialPending = false;
|
|
400
|
+
const name = this.device.context.name;
|
|
401
|
+
const generation = this.partialGeneration;
|
|
319
402
|
this.partialStopTimer = setTimeout(() => {
|
|
320
403
|
this.partialStopTimer = null;
|
|
321
|
-
this.log.
|
|
322
|
-
this.
|
|
404
|
+
this.log.debug(`[SimpleGarageDoor] ${name}: partial open — firing stop (dp${this.dpStop})`);
|
|
405
|
+
this._sendPartialStop(generation, 1);
|
|
323
406
|
}, this.partialOpenMs);
|
|
324
407
|
}
|
|
325
408
|
|
|
409
|
+
// The controller occasionally drops a lone write (its command queue can
|
|
410
|
+
// coalesce back-to-back writes, and brief Wi-Fi blips lose individual
|
|
411
|
+
// packets), and a dropped stop here is exactly what lets a partial open run
|
|
412
|
+
// all the way. Re-send it a few times; a stop on an already-parked gate is a
|
|
413
|
+
// harmless no-op. Bails out if the flow was superseded mid-way.
|
|
414
|
+
_sendPartialStop(generation, attempt) {
|
|
415
|
+
if (generation !== this.partialGeneration) return;
|
|
416
|
+
this.setMultiStateLegacyInBackground({[this.dpStop]: true});
|
|
417
|
+
if (attempt < 3) setTimeout(() => this._sendPartialStop(generation, attempt + 1), 300);
|
|
418
|
+
}
|
|
419
|
+
|
|
326
420
|
_cancelPartialStop() {
|
|
421
|
+
this.partialPending = false;
|
|
422
|
+
this.partialGeneration++;
|
|
327
423
|
if (this.partialStopTimer) {
|
|
328
424
|
clearTimeout(this.partialStopTimer);
|
|
329
425
|
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) {
|