homebridge-tuya-plus 3.14.0-dev.46 → 3.14.0-dev.47
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.
|
@@ -257,6 +257,21 @@ class SimpleGarageDoorAccessory extends BaseAccessory {
|
|
|
257
257
|
// the partial switch tapped OFF) is manual control: cancel any pending
|
|
258
258
|
// partial-open auto-stop so it can't halt this movement part-way.
|
|
259
259
|
this._cancelPartialStop();
|
|
260
|
+
|
|
261
|
+
// The opener is level-triggered: act only when the requested target
|
|
262
|
+
// differs from the one already committed. HomeKit re-sends the same
|
|
263
|
+
// target after a tap — a second controller echoing it, or its own retry
|
|
264
|
+
// a few seconds later — and acting on that repeat would fire another
|
|
265
|
+
// open/close. For a close that means a second stop-before-close into the
|
|
266
|
+
// already-closing gate, halting it mid-travel and restarting it. A real
|
|
267
|
+
// request always flips the target (the Home app toggles to the opposite
|
|
268
|
+
// state), and the reported state keeps cachedTargetDoorState honest — a
|
|
269
|
+
// command the controller drops is reverted by the next status read — so
|
|
270
|
+
// this never swallows a genuine request, only the redundant echoes.
|
|
271
|
+
if (value === this.accessory.context.cachedTargetDoorState) {
|
|
272
|
+
this.log.debug(`[SimpleGarageDoor] ${this.device.context.name}: target unchanged — ignoring repeat request`);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
260
275
|
this._applyTarget(value);
|
|
261
276
|
}
|
|
262
277
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "homebridge-tuya-plus",
|
|
3
|
-
"version": "3.14.0-dev.
|
|
3
|
+
"version": "3.14.0-dev.47",
|
|
4
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": {
|
|
@@ -226,9 +226,18 @@ describe('SimpleGarageDoorAccessory.setTargetDoorState — close (stop-before-cl
|
|
|
226
226
|
beforeEach(() => jest.useFakeTimers());
|
|
227
227
|
afterEach(() => jest.useRealTimers());
|
|
228
228
|
|
|
229
|
+
// A close is only ever issued from an open gate, so the committed target
|
|
230
|
+
// (cachedTargetDoorState) starts OPEN — the state HomeKit shows before the
|
|
231
|
+
// tap. Without it the very first close would match the helper's default
|
|
232
|
+
// committed target and be (correctly) treated as a redundant repeat.
|
|
233
|
+
function openGate(instance, device, state = STATE_OPENING_OR_OPEN) {
|
|
234
|
+
if (state !== undefined) device.state['105'] = state;
|
|
235
|
+
instance.accessory.context.cachedTargetDoorState = TDS.OPEN;
|
|
236
|
+
}
|
|
237
|
+
|
|
229
238
|
test('CLOSE fires immediately when the gate is already stopped (state 11)', () => {
|
|
230
239
|
const { instance, device, accessory } = makeSimpleGarage();
|
|
231
|
-
device
|
|
240
|
+
openGate(instance, device, STATE_STOPPED);
|
|
232
241
|
|
|
233
242
|
instance.setTargetDoorState(TDS.CLOSED);
|
|
234
243
|
|
|
@@ -242,7 +251,7 @@ describe('SimpleGarageDoorAccessory.setTargetDoorState — close (stop-before-cl
|
|
|
242
251
|
test('CLOSE while opening (state 12) fires stop, then close after stopBeforeCloseMs', () => {
|
|
243
252
|
const { instance, device } = makeSimpleGarage();
|
|
244
253
|
instance.stopBeforeCloseMs = 1500;
|
|
245
|
-
device
|
|
254
|
+
openGate(instance, device, STATE_OPENING_OR_OPEN);
|
|
246
255
|
|
|
247
256
|
instance.setTargetDoorState(TDS.CLOSED);
|
|
248
257
|
|
|
@@ -259,10 +268,10 @@ describe('SimpleGarageDoorAccessory.setTargetDoorState — close (stop-before-cl
|
|
|
259
268
|
expect(instance.pendingCloseTimer).toBeNull();
|
|
260
269
|
});
|
|
261
270
|
|
|
262
|
-
test('CLOSE
|
|
271
|
+
test('CLOSE that reads state 13 still uses stop-before-close (13 is not "stopped")', () => {
|
|
263
272
|
const { instance, device } = makeSimpleGarage();
|
|
264
273
|
instance.stopBeforeCloseMs = 1500;
|
|
265
|
-
device
|
|
274
|
+
openGate(instance, device, STATE_CLOSING_OR_CLOSED);
|
|
266
275
|
|
|
267
276
|
instance.setTargetDoorState(TDS.CLOSED);
|
|
268
277
|
expect(device.update).toHaveBeenNthCalledWith(1, { '103': true });
|
|
@@ -274,7 +283,7 @@ describe('SimpleGarageDoorAccessory.setTargetDoorState — close (stop-before-cl
|
|
|
274
283
|
test('CLOSE with no reported state yet uses stop-before-close', () => {
|
|
275
284
|
const { instance, device } = makeSimpleGarage();
|
|
276
285
|
instance.stopBeforeCloseMs = 1500;
|
|
277
|
-
// device.state['105']
|
|
286
|
+
openGate(instance, device, undefined); // device.state['105'] left undefined
|
|
278
287
|
|
|
279
288
|
instance.setTargetDoorState(TDS.CLOSED);
|
|
280
289
|
expect(device.update).toHaveBeenNthCalledWith(1, { '103': true });
|
|
@@ -286,7 +295,7 @@ describe('SimpleGarageDoorAccessory.setTargetDoorState — close (stop-before-cl
|
|
|
286
295
|
test('A duplicate CLOSE during the wait does not restart or double-fire', () => {
|
|
287
296
|
const { instance, device } = makeSimpleGarage();
|
|
288
297
|
instance.stopBeforeCloseMs = 1500;
|
|
289
|
-
device
|
|
298
|
+
openGate(instance, device, STATE_OPENING_OR_OPEN);
|
|
290
299
|
|
|
291
300
|
instance.setTargetDoorState(TDS.CLOSED);
|
|
292
301
|
expect(device.update).toHaveBeenCalledTimes(1); // stop
|
|
@@ -300,10 +309,52 @@ describe('SimpleGarageDoorAccessory.setTargetDoorState — close (stop-before-cl
|
|
|
300
309
|
expect(device.update).toHaveBeenNthCalledWith(2, { '102': true });
|
|
301
310
|
});
|
|
302
311
|
|
|
312
|
+
test('A repeat close once committed is swallowed — no second stop-before-close into the moving gate', () => {
|
|
313
|
+
// HomeKit re-sends the same target seconds after a tap (a second
|
|
314
|
+
// controller echoing it, or its own retry). Acting on it fired another
|
|
315
|
+
// stop-before-close that halted the closing gate and restarted it — the
|
|
316
|
+
// reported stutter. Level-triggered dispatch makes the repeat a no-op.
|
|
317
|
+
const { instance, device } = makeSimpleGarage();
|
|
318
|
+
instance.stopBeforeCloseMs = 1500;
|
|
319
|
+
openGate(instance, device, STATE_OPENING_OR_OPEN);
|
|
320
|
+
|
|
321
|
+
instance.setTargetDoorState(TDS.CLOSED);
|
|
322
|
+
jest.advanceTimersByTime(1500);
|
|
323
|
+
expect(device.update).toHaveBeenCalledTimes(2); // stop + close
|
|
324
|
+
expect(device.update).toHaveBeenNthCalledWith(2, { '102': true });
|
|
325
|
+
|
|
326
|
+
// The gate is now committed closed and still travelling (reports 13).
|
|
327
|
+
emitState(device, STATE_CLOSING_OR_CLOSED);
|
|
328
|
+
jest.advanceTimersByTime(3000);
|
|
329
|
+
instance.setTargetDoorState(TDS.CLOSED); // HomeKit's repeat close
|
|
330
|
+
expect(device.update).toHaveBeenCalledTimes(2); // swallowed — no second stop/close
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test('A close runs again once the gate is reported open (committed target tracks reports)', () => {
|
|
334
|
+
const { instance, device } = makeSimpleGarage();
|
|
335
|
+
instance.stopBeforeCloseMs = 1500;
|
|
336
|
+
openGate(instance, device, STATE_OPENING_OR_OPEN);
|
|
337
|
+
|
|
338
|
+
instance.setTargetDoorState(TDS.CLOSED);
|
|
339
|
+
jest.advanceTimersByTime(1500);
|
|
340
|
+
expect(device.update).toHaveBeenCalledTimes(2); // stop + close
|
|
341
|
+
emitState(device, STATE_CLOSING_OR_CLOSED); // committed = closed
|
|
342
|
+
|
|
343
|
+
instance.setTargetDoorState(TDS.CLOSED); // repeat — swallowed
|
|
344
|
+
expect(device.update).toHaveBeenCalledTimes(2);
|
|
345
|
+
|
|
346
|
+
// The gate is opened again (here, externally) — a close is a real
|
|
347
|
+
// transition once more and runs.
|
|
348
|
+
emitState(device, STATE_OPENING_OR_OPEN); // committed = open
|
|
349
|
+
instance.setTargetDoorState(TDS.CLOSED);
|
|
350
|
+
expect(device.update).toHaveBeenCalledTimes(3);
|
|
351
|
+
expect(device.update).toHaveBeenNthCalledWith(3, { '103': true });
|
|
352
|
+
});
|
|
353
|
+
|
|
303
354
|
test('OPEN during the wait cancels the pending close and opens instead', () => {
|
|
304
355
|
const { instance, device } = makeSimpleGarage();
|
|
305
356
|
instance.stopBeforeCloseMs = 1500;
|
|
306
|
-
device
|
|
357
|
+
openGate(instance, device, STATE_OPENING_OR_OPEN);
|
|
307
358
|
|
|
308
359
|
instance.setTargetDoorState(TDS.CLOSED);
|
|
309
360
|
expect(device.update).toHaveBeenNthCalledWith(1, { '103': true }); // stop
|
|
@@ -321,7 +372,7 @@ describe('SimpleGarageDoorAccessory.setTargetDoorState — close (stop-before-cl
|
|
|
321
372
|
test('Status reports are ignored during the stop-before-close window', () => {
|
|
322
373
|
const { instance, device, accessory } = makeSimpleGarage();
|
|
323
374
|
instance.stopBeforeCloseMs = 1500;
|
|
324
|
-
device
|
|
375
|
+
openGate(instance, device, STATE_OPENING_OR_OPEN);
|
|
325
376
|
instance.currentDoorState = CDS.OPEN;
|
|
326
377
|
instance.characteristicCurrentDoorState.value = CDS.OPEN;
|
|
327
378
|
|
|
@@ -345,7 +396,7 @@ describe('SimpleGarageDoorAccessory.setTargetDoorState — close (stop-before-cl
|
|
|
345
396
|
test('stopBeforeCloseMs is configurable', () => {
|
|
346
397
|
const { instance, device } = makeSimpleGarage();
|
|
347
398
|
instance.stopBeforeCloseMs = 300;
|
|
348
|
-
device
|
|
399
|
+
openGate(instance, device, STATE_OPENING_OR_OPEN);
|
|
349
400
|
|
|
350
401
|
instance.setTargetDoorState(TDS.CLOSED);
|
|
351
402
|
expect(device.update).toHaveBeenNthCalledWith(1, { '103': true });
|
|
@@ -361,7 +412,7 @@ describe('SimpleGarageDoorAccessory.setTargetDoorState — close (stop-before-cl
|
|
|
361
412
|
instance.dpClose = '2';
|
|
362
413
|
instance.dpStop = '4';
|
|
363
414
|
instance.stopBeforeCloseMs = 1000;
|
|
364
|
-
device
|
|
415
|
+
openGate(instance, device, STATE_OPENING_OR_OPEN);
|
|
365
416
|
|
|
366
417
|
instance.setTargetDoorState(TDS.CLOSED);
|
|
367
418
|
expect(device.update).toHaveBeenNthCalledWith(1, { '4': true });
|
|
@@ -580,6 +631,7 @@ describe('SimpleGarageDoorAccessory — force switches', () => {
|
|
|
580
631
|
test('Force Close fires the close action (immediately when already stopped)', () => {
|
|
581
632
|
const { instance, device, accessory } = makeSimpleGarage();
|
|
582
633
|
device.state['105'] = STATE_STOPPED;
|
|
634
|
+
accessory.context.cachedTargetDoorState = TDS.OPEN; // gate open → close is a real transition
|
|
583
635
|
|
|
584
636
|
instance.setTargetDoorState(TDS.CLOSED);
|
|
585
637
|
|