homebridge-tuya-plus 3.10.0 → 3.11.0

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.
@@ -82,6 +82,10 @@
82
82
  "title": "Garage Door",
83
83
  "enum": ["GarageDoor"]
84
84
  },
85
+ {
86
+ "title": "Simple Garage Door (Open/Stop/Close)",
87
+ "enum": ["SimpleGarageDoor"]
88
+ },
85
89
  {
86
90
  "title": "Simple Blinds",
87
91
  "enum": ["SimpleBlinds"]
@@ -493,6 +497,27 @@
493
497
  "functionBody": "return model.devices && model.devices[arrayIndices] && ['GarageDoor'].includes(model.devices[arrayIndices].type);"
494
498
  }
495
499
  },
500
+ "dpOpen": {
501
+ "type": "integer",
502
+ "placeholder": "1",
503
+ "condition": {
504
+ "functionBody": "return model.devices && model.devices[arrayIndices] && ['SimpleGarageDoor'].includes(model.devices[arrayIndices].type);"
505
+ }
506
+ },
507
+ "dpStop": {
508
+ "type": "integer",
509
+ "placeholder": "2",
510
+ "condition": {
511
+ "functionBody": "return model.devices && model.devices[arrayIndices] && ['SimpleGarageDoor'].includes(model.devices[arrayIndices].type);"
512
+ }
513
+ },
514
+ "dpClose": {
515
+ "type": "integer",
516
+ "placeholder": "3",
517
+ "condition": {
518
+ "functionBody": "return model.devices && model.devices[arrayIndices] && ['SimpleGarageDoor'].includes(model.devices[arrayIndices].type);"
519
+ }
520
+ },
496
521
  "flipState": {
497
522
  "type": "boolean",
498
523
  "condition": {
package/index.js CHANGED
@@ -13,6 +13,7 @@ const AirPurifierAccessory = require('./lib/AirPurifierAccessory');
13
13
  const DehumidifierAccessory = require('./lib/DehumidifierAccessory');
14
14
  const ConvectorAccessory = require('./lib/ConvectorAccessory');
15
15
  const GarageDoorAccessory = require('./lib/GarageDoorAccessory');
16
+ const SimpleGarageDoorAccessory = require('./lib/SimpleGarageDoorAccessory');
16
17
  const SimpleDimmerAccessory = require('./lib/SimpleDimmerAccessory');
17
18
  const SimpleDimmer2Accessory = require('./lib/SimpleDimmer2Accessory');
18
19
  const SimpleBlindsAccessory = require('./lib/SimpleBlindsAccessory');
@@ -47,6 +48,7 @@ const CLASS_DEF = {
47
48
  dehumidifier: DehumidifierAccessory,
48
49
  convector: ConvectorAccessory,
49
50
  garagedoor: GarageDoorAccessory,
51
+ simplegaragedoor: SimpleGarageDoorAccessory,
50
52
  simpledimmer: SimpleDimmerAccessory,
51
53
  simpledimmer2: SimpleDimmer2Accessory,
52
54
  simpleblinds: SimpleBlindsAccessory,
@@ -0,0 +1,84 @@
1
+ const BaseAccessory = require('./BaseAccessory');
2
+
3
+ // Window during which CurrentDoorState lags TargetDoorState so HomeKit's
4
+ // "Opening..."/"Closing..." caption is visible after a toggle. The device
5
+ // has no position feedback, so this is purely cosmetic.
6
+ const CURRENT_STATE_DELAY_MS = 1000;
7
+
8
+ class SimpleGarageDoorAccessory extends BaseAccessory {
9
+ static getCategory(Categories) {
10
+ return Categories.GARAGE_DOOR_OPENER;
11
+ }
12
+
13
+ constructor(...props) {
14
+ super(...props);
15
+ }
16
+
17
+ _registerPlatformAccessory() {
18
+ const {Service} = this.hap;
19
+
20
+ this.accessory.addService(Service.GarageDoorOpener, this.device.context.name);
21
+
22
+ super._registerPlatformAccessory();
23
+ }
24
+
25
+ _registerCharacteristics() {
26
+ const {Service, Characteristic} = this.hap;
27
+ const service = this.accessory.getService(Service.GarageDoorOpener);
28
+ this._checkServiceName(service, this.device.context.name);
29
+
30
+ this.dpOpen = this._getCustomDP(this.device.context.dpOpen) || '1';
31
+ this.dpStop = this._getCustomDP(this.device.context.dpStop) || '2';
32
+ this.dpClose = this._getCustomDP(this.device.context.dpClose) || '3';
33
+
34
+ // The device only exposes momentary action DPs, so the target state is
35
+ // tracked locally and persisted via the homebridge accessory context.
36
+ if (this.accessory.context.cachedTargetDoorState !== Characteristic.TargetDoorState.OPEN &&
37
+ this.accessory.context.cachedTargetDoorState !== Characteristic.TargetDoorState.CLOSED) {
38
+ this.accessory.context.cachedTargetDoorState = Characteristic.TargetDoorState.OPEN;
39
+ }
40
+ const initialTarget = this.accessory.context.cachedTargetDoorState;
41
+ this.currentDoorState = initialTarget === Characteristic.TargetDoorState.OPEN
42
+ ? Characteristic.CurrentDoorState.OPEN
43
+ : Characteristic.CurrentDoorState.CLOSED;
44
+
45
+ this.characteristicTargetDoorState = service.getCharacteristic(Characteristic.TargetDoorState)
46
+ .updateValue(initialTarget)
47
+ .onGet(() => this.accessory.context.cachedTargetDoorState)
48
+ .onSet(value => this.setTargetDoorState(value));
49
+
50
+ this.characteristicCurrentDoorState = service.getCharacteristic(Characteristic.CurrentDoorState)
51
+ .updateValue(this.currentDoorState)
52
+ .onGet(() => this.currentDoorState);
53
+
54
+ service.getCharacteristic(Characteristic.ObstructionDetected)
55
+ .updateValue(false)
56
+ .onGet(() => false);
57
+ }
58
+
59
+ setTargetDoorState(value) {
60
+ const {Characteristic} = this.hap;
61
+
62
+ this.accessory.context.cachedTargetDoorState = value;
63
+
64
+ // Send stop first so reversing direction mid-motion works; if the gate
65
+ // is already idle, the stop is a no-op on the device side.
66
+ this.setMultiStateLegacyAsync({[this.dpStop]: true});
67
+ if (value === Characteristic.TargetDoorState.OPEN) {
68
+ this.setMultiStateLegacyAsync({[this.dpOpen]: true});
69
+ } else {
70
+ this.setMultiStateLegacyAsync({[this.dpClose]: true});
71
+ }
72
+
73
+ if (this.currentStateTimeout) clearTimeout(this.currentStateTimeout);
74
+ this.currentStateTimeout = setTimeout(() => {
75
+ this.currentStateTimeout = null;
76
+ this.currentDoorState = value === Characteristic.TargetDoorState.OPEN
77
+ ? Characteristic.CurrentDoorState.OPEN
78
+ : Characteristic.CurrentDoorState.CLOSED;
79
+ this.characteristicCurrentDoorState.updateValue(this.currentDoorState);
80
+ }, CURRENT_STATE_DELAY_MS);
81
+ }
82
+ }
83
+
84
+ module.exports = SimpleGarageDoorAccessory;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homebridge-tuya-plus",
3
- "version": "3.10.0",
3
+ "version": "3.11.0",
4
4
  "description": "A community-maintained Homebridge plugin for controlling Tuya devices locally over LAN. Includes new features, fixes, and updated device support.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -0,0 +1,129 @@
1
+ 'use strict';
2
+
3
+ const SimpleGarageDoorAccessory = require('../lib/SimpleGarageDoorAccessory');
4
+ const { HAP, makeInstance } = require('./support/mocks');
5
+
6
+ const { CurrentDoorState: CDS, TargetDoorState: TDS } = HAP.Characteristic;
7
+
8
+ function makeSimpleGarage(contextOverrides = {}) {
9
+ const { instance, device, accessory, platform } = makeInstance(
10
+ SimpleGarageDoorAccessory,
11
+ {},
12
+ { manufacturer: 'Generic', ...contextOverrides }
13
+ );
14
+
15
+ // Replicate what _registerCharacteristics would set up.
16
+ instance.dpOpen = '1';
17
+ instance.dpStop = '2';
18
+ instance.dpClose = '3';
19
+ instance.currentDoorState = CDS.OPEN;
20
+ instance.characteristicCurrentDoorState = {
21
+ value: CDS.OPEN,
22
+ updateValue: jest.fn().mockImplementation(function(v) { this.value = v; return this; }),
23
+ };
24
+
25
+ return { instance, device, accessory, platform };
26
+ }
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // setTargetDoorState — device commands
30
+ // ---------------------------------------------------------------------------
31
+ describe('SimpleGarageDoorAccessory.setTargetDoorState — device commands', () => {
32
+ test('OPEN sends stop=true then open=true', () => {
33
+ const { instance, device } = makeSimpleGarage();
34
+ instance.setTargetDoorState(TDS.OPEN);
35
+ expect(device.update).toHaveBeenNthCalledWith(1, { '2': true });
36
+ expect(device.update).toHaveBeenNthCalledWith(2, { '1': true });
37
+ });
38
+
39
+ test('CLOSED sends stop=true then close=true', () => {
40
+ const { instance, device } = makeSimpleGarage();
41
+ instance.setTargetDoorState(TDS.CLOSED);
42
+ expect(device.update).toHaveBeenNthCalledWith(1, { '2': true });
43
+ expect(device.update).toHaveBeenNthCalledWith(2, { '3': true });
44
+ });
45
+
46
+ test('Skips writes when the device is disconnected', () => {
47
+ const { instance, device } = makeSimpleGarage();
48
+ device.connected = false;
49
+ instance.setTargetDoorState(TDS.OPEN);
50
+ expect(device.update).not.toHaveBeenCalled();
51
+ });
52
+
53
+ test('Custom DPs are respected', () => {
54
+ const { instance, device } = makeSimpleGarage();
55
+ instance.dpOpen = '101';
56
+ instance.dpStop = '102';
57
+ instance.dpClose = '103';
58
+ instance.setTargetDoorState(TDS.OPEN);
59
+ expect(device.update).toHaveBeenNthCalledWith(1, { '102': true });
60
+ expect(device.update).toHaveBeenNthCalledWith(2, { '101': true });
61
+ });
62
+ });
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // CurrentDoorState transition
66
+ // ---------------------------------------------------------------------------
67
+ describe('SimpleGarageDoorAccessory.setTargetDoorState — CurrentDoorState transition', () => {
68
+ beforeEach(() => jest.useFakeTimers());
69
+ afterEach(() => jest.useRealTimers());
70
+
71
+ test('CurrentDoorState lags the target by 1s when opening', () => {
72
+ const { instance } = makeSimpleGarage();
73
+ instance.currentDoorState = CDS.CLOSED;
74
+ instance.characteristicCurrentDoorState.value = CDS.CLOSED;
75
+
76
+ instance.setTargetDoorState(TDS.OPEN);
77
+ expect(instance.characteristicCurrentDoorState.value).toBe(CDS.CLOSED);
78
+
79
+ jest.advanceTimersByTime(999);
80
+ expect(instance.characteristicCurrentDoorState.value).toBe(CDS.CLOSED);
81
+
82
+ jest.advanceTimersByTime(1);
83
+ expect(instance.currentDoorState).toBe(CDS.OPEN);
84
+ expect(instance.characteristicCurrentDoorState.value).toBe(CDS.OPEN);
85
+ });
86
+
87
+ test('CurrentDoorState lags the target by 1s when closing', () => {
88
+ const { instance } = makeSimpleGarage();
89
+ instance.currentDoorState = CDS.OPEN;
90
+ instance.characteristicCurrentDoorState.value = CDS.OPEN;
91
+
92
+ instance.setTargetDoorState(TDS.CLOSED);
93
+ expect(instance.characteristicCurrentDoorState.value).toBe(CDS.OPEN);
94
+
95
+ jest.advanceTimersByTime(1000);
96
+ expect(instance.currentDoorState).toBe(CDS.CLOSED);
97
+ expect(instance.characteristicCurrentDoorState.value).toBe(CDS.CLOSED);
98
+ });
99
+
100
+ test('Reversing direction within the 1s window resets the timer', () => {
101
+ const { instance } = makeSimpleGarage();
102
+ instance.currentDoorState = CDS.OPEN;
103
+ instance.characteristicCurrentDoorState.value = CDS.OPEN;
104
+
105
+ instance.setTargetDoorState(TDS.CLOSED);
106
+ jest.advanceTimersByTime(500);
107
+ instance.setTargetDoorState(TDS.OPEN);
108
+
109
+ jest.advanceTimersByTime(500);
110
+ expect(instance.characteristicCurrentDoorState.value).toBe(CDS.OPEN);
111
+
112
+ jest.advanceTimersByTime(500);
113
+ expect(instance.currentDoorState).toBe(CDS.OPEN);
114
+ expect(instance.characteristicCurrentDoorState.value).toBe(CDS.OPEN);
115
+ });
116
+ });
117
+
118
+ // ---------------------------------------------------------------------------
119
+ // Persistence
120
+ // ---------------------------------------------------------------------------
121
+ describe('SimpleGarageDoorAccessory persistence', () => {
122
+ test('Stores the latest target on the accessory context', () => {
123
+ const { instance, accessory } = makeSimpleGarage();
124
+ instance.setTargetDoorState(TDS.CLOSED);
125
+ expect(accessory.context.cachedTargetDoorState).toBe(TDS.CLOSED);
126
+ instance.setTargetDoorState(TDS.OPEN);
127
+ expect(accessory.context.cachedTargetDoorState).toBe(TDS.OPEN);
128
+ });
129
+ });