homebridge-tuya-plus 3.14.0-pr.94.55 → 4.1.0-dev.63

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/Changelog.md CHANGED
@@ -4,6 +4,7 @@ All notable changes to this project will be documented in this file. This projec
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ * [+] **Garage door: support DASPI "Passage" openers.** Set `"manufacturer": "Daspi"` on a `GarageDoor` device to select its data-points (`dpAction` `1`, `dpStatus` `7`, still overridable). DASPI reports state as strings on DP `7` (`full_opened`/`opening`/`open_softstop`/`full_closed`/`closing`/`close_softstop`) and takes capitalized `Open`/`Close` commands on DP `1`; the soft-stop phases map to HomeKit's Opening/Closing so the tile animates correctly mid-travel.
7
8
  * [+] **Tuya Cloud is now a transparent, global fallback for every device.** What started as an experiment to reach battery-powered "sleepy" irrigation timers (which never appear on the LAN) is now a general, optional fallback: add a top-level `cloud` credentials block once and the plugin keeps a single Tuya Cloud/MQTT session alive in the background, falling back to it whenever a device can't be reached on the LAN — a flaky moment, or hardware that's never local. The plugin stays **LAN-first** (local is always tried first and preferred) and cloud remains **strictly opt-in** (nothing runs unless credentials are present).
8
9
  * **No per-device configuration.** There is one global session and no per-device cloud settings. Every device automatically uses the LAN first and the cloud as a fallback; a device with no local `key` is reached over the cloud only. Existing LAN configs (numeric data-points) keep working over the cloud — the data-point id↔code map is learned from Tuya's device shadow (`/v2.0/cloud/thing/{id}/shadow/properties`) — and if both transports fail, HomeKit shows "No Response" as before. This mirrors the official Tuya/Smart Life app (LAN when possible, cloud as a backup).
9
10
  * Realtime updates arrive over Tuya's **MQTT** message service (via the optional `mqtt` dependency, installed automatically); initial state and control use the Tuya OpenAPI. There is no polling.
@@ -448,7 +448,7 @@
448
448
  },
449
449
  "fanSpeedValues": {
450
450
  "type": "array",
451
- "description": "Ordered string values used by the device for each fan speed (low → high), e.g. ['low', 'middle', 'high']. Overrides fanSpeedSteps.",
451
+ "description": "Ordered string values used by the device for each fan speed (low → high), e.g. [\"low\", \"middle\", \"high\"]. Overrides fanSpeedSteps.",
452
452
  "items": {
453
453
  "type": "string"
454
454
  },
@@ -12,8 +12,16 @@ const GARAGE_DOOR_OPENNING = 'openning';
12
12
  const GARAGE_DOOR_OPENING = 'opening';
13
13
  const GARAGE_DOOR_CLOSING = 'closing';
14
14
 
15
+ // For Daspi garage door
16
+ const GARAGE_DOOR_FULL_OPENED = 'full_opened';
17
+ const GARAGE_DOOR_FULL_CLOSED = 'full_closed';
18
+ const GARAGE_DOOR_OPEN_SOFTSTOP = 'open_softstop';
19
+ const GARAGE_DOOR_CLOSE_SOFTSTOP = 'close_softstop';
20
+ // Daspi garage door appears to have no stopped status
21
+
15
22
  const GARAGE_DOOR_MANUFACTURER_KOGAN = 'Kogan';
16
23
  const GARAGE_DOOR_MANUFACTURER_WOFEA = 'Wofea';
24
+ const GARAGE_DOOR_MANUFACTURER_DASPI = 'Daspi';
17
25
 
18
26
  class GarageDoorAccessory extends BaseAccessory {
19
27
  static getCategory(Categories) {
@@ -50,6 +58,10 @@ class GarageDoorAccessory extends BaseAccessory {
50
58
  return this.manufacturer === GARAGE_DOOR_MANUFACTURER_WOFEA.trim();
51
59
  }
52
60
 
61
+ _isDaspi() {
62
+ return this.manufacturer === GARAGE_DOOR_MANUFACTURER_DASPI.trim();
63
+ }
64
+
53
65
  _registerCharacteristics(dps) {
54
66
  const {Service, Characteristic} = this.hap;
55
67
  const service = this.accessory.getService(Service.GarageDoorOpener);
@@ -59,6 +71,8 @@ class GarageDoorAccessory extends BaseAccessory {
59
71
  this.manufacturer = GARAGE_DOOR_MANUFACTURER_KOGAN.trim();
60
72
  } else if (this.device.context.manufacturer.trim().toLowerCase() === GARAGE_DOOR_MANUFACTURER_WOFEA.trim().toLowerCase()) {
61
73
  this.manufacturer = this.device.context.manufacturer.trim();
74
+ } else if (this.device.context.manufacturer.trim().toLowerCase() === GARAGE_DOOR_MANUFACTURER_DASPI.trim().toLowerCase()) {
75
+ this.manufacturer = GARAGE_DOOR_MANUFACTURER_DASPI.trim();
62
76
  } else if (this.device.context.manufacturer) {
63
77
  this.manufacturer = this.device.context.manufacturer.trim();
64
78
  } else {
@@ -73,6 +87,10 @@ class GarageDoorAccessory extends BaseAccessory {
73
87
  this._debugLog('_registerCharacteristics setting dpAction and dpStatus for ' + GARAGE_DOOR_MANUFACTURER_WOFEA + ' garage door');
74
88
  this.dpAction = this._getCustomDP(this.device.context.dpAction) || '1';
75
89
  this.dpStatus = this._getCustomDP(this.device.context.dpStatus) || '101';
90
+ } else if (this._isDaspi()) {
91
+ this._debugLog('_registerCharacteristics setting dpAction and dpStatus for ' + GARAGE_DOOR_MANUFACTURER_DASPI + ' garage door');
92
+ this.dpAction = this._getCustomDP(this.device.context.dpAction) || '1';
93
+ this.dpStatus = this._getCustomDP(this.device.context.dpStatus) || '7';
76
94
  } else {
77
95
  this._debugLog('_registerCharacteristics setting dpAction and dpStatus for generic door');
78
96
  this.dpAction = this._getCustomDP(this.device.context.dpAction) || '1';
@@ -143,6 +161,23 @@ class GarageDoorAccessory extends BaseAccessory {
143
161
  case GARAGE_DOOR_CLOSING:
144
162
  return this.targetClosed;
145
163
 
164
+ default:
165
+ this._alwaysLog('_getTargetDoorState UNKNOWN STATE ' + JSON.stringify(dp));
166
+ }
167
+ } else if (this._isDaspi()) {
168
+ switch (dp.toLowerCase()) {
169
+ case GARAGE_DOOR_OPEN:
170
+ case GARAGE_DOOR_OPENING:
171
+ case GARAGE_DOOR_OPEN_SOFTSTOP:
172
+ case GARAGE_DOOR_FULL_OPENED:
173
+ return this.targetOpen;
174
+
175
+ case GARAGE_DOOR_CLOSE:
176
+ case GARAGE_DOOR_CLOSING:
177
+ case GARAGE_DOOR_CLOSE_SOFTSTOP:
178
+ case GARAGE_DOOR_FULL_CLOSED:
179
+ return this.targetClosed;
180
+
146
181
  default:
147
182
  this._alwaysLog('_getTargetDoorState UNKNOWN STATE ' + JSON.stringify(dp));
148
183
  }
@@ -161,13 +196,14 @@ class GarageDoorAccessory extends BaseAccessory {
161
196
  this._debugLog('setTargetDoorState value ' + value + ' targetOpen ' + this.targetOpen + ' targetClosed ' + this.targetClosed);
162
197
  let newValue = GARAGE_DOOR_CLOSE;
163
198
 
164
- if (this._isKogan()) {
199
+ if (this._isKogan() || this._isDaspi()) {
200
+ // Daspi expects capitalized command strings ('Open'/'Close'); Kogan uses lowercase.
165
201
  switch (value) {
166
202
  case this.targetOpen:
167
- newValue = GARAGE_DOOR_OPEN;
203
+ newValue = this._isDaspi() ? 'Open' : GARAGE_DOOR_OPEN;
168
204
  break;
169
205
  case this.targetClosed:
170
- newValue = GARAGE_DOOR_CLOSE;
206
+ newValue = this._isDaspi() ? 'Close' : GARAGE_DOOR_CLOSE;
171
207
  break;
172
208
  default:
173
209
  this._alwaysLog('setTargetDoorState UNKNOWN STATE ' + JSON.stringify(value));
@@ -210,6 +246,25 @@ class GarageDoorAccessory extends BaseAccessory {
210
246
  default:
211
247
  this._alwaysLog('_getCurrentDoorState UNKNOWN STATUS ' + JSON.stringify(dpStatusValue));
212
248
  }
249
+ } else if (this._isDaspi()) {
250
+ switch (dpStatusValue.toLowerCase()) {
251
+ case GARAGE_DOOR_FULL_OPENED:
252
+ return this.currentOpen;
253
+
254
+ case GARAGE_DOOR_OPEN_SOFTSTOP:
255
+ case GARAGE_DOOR_OPENING:
256
+ return this.currentOpening;
257
+
258
+ case GARAGE_DOOR_CLOSE_SOFTSTOP:
259
+ case GARAGE_DOOR_CLOSING:
260
+ return this.currentClosing;
261
+
262
+ case GARAGE_DOOR_FULL_CLOSED:
263
+ return this.currentClosed;
264
+
265
+ default:
266
+ this._alwaysLog('_getCurrentDoorState UNKNOWN STATUS ' + JSON.stringify(dpStatusValue));
267
+ }
213
268
  } else {
214
269
  if (dpStatusValue === true) {
215
270
  return this.currentOpen;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homebridge-tuya-plus",
3
- "version": "3.14.0-pr.94.55",
3
+ "version": "4.1.0-dev.63",
4
4
  "description": "A community-maintained Homebridge plugin for controlling Tuya devices locally over LAN, with an optional Tuya Cloud fallback for whatever the LAN can't reach. Includes new features, fixes, and updated device support. PRs welcome: if it runs, it ships (almost).",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -52,6 +52,21 @@ describe('GarageDoorAccessory._getTargetDoorState — Kogan (string)', () => {
52
52
  test('"closing" → targetClosed', () => expect(instance._getTargetDoorState('closing')).toBe(TDS.CLOSED));
53
53
  });
54
54
 
55
+ describe('GarageDoorAccessory._getTargetDoorState — Daspi (string)', () => {
56
+ let instance;
57
+ beforeEach(() => ({ instance } = makeGarage('Daspi')));
58
+
59
+ test('"open" → targetOpen', () => expect(instance._getTargetDoorState('open')).toBe(TDS.OPEN));
60
+ test('"opening" → targetOpen', () => expect(instance._getTargetDoorState('opening')).toBe(TDS.OPEN));
61
+ test('"open_softstop" → targetOpen', () => expect(instance._getTargetDoorState('open_softstop')).toBe(TDS.OPEN));
62
+ test('"full_opened" → targetOpen', () => expect(instance._getTargetDoorState('full_opened')).toBe(TDS.OPEN));
63
+ test('"close" → targetClosed', () => expect(instance._getTargetDoorState('close')).toBe(TDS.CLOSED));
64
+ test('"closing" → targetClosed', () => expect(instance._getTargetDoorState('closing')).toBe(TDS.CLOSED));
65
+ test('"close_softstop" → targetClosed', () => expect(instance._getTargetDoorState('close_softstop')).toBe(TDS.CLOSED));
66
+ test('"full_closed" → targetClosed', () => expect(instance._getTargetDoorState('full_closed')).toBe(TDS.CLOSED));
67
+ test('mixed case "FULL_OPENED" → targetOpen', () => expect(instance._getTargetDoorState('FULL_OPENED')).toBe(TDS.OPEN));
68
+ });
69
+
55
70
  // ---------------------------------------------------------------------------
56
71
  // _getCurrentDoorState
57
72
  // ---------------------------------------------------------------------------
@@ -78,6 +93,19 @@ describe('GarageDoorAccessory._getCurrentDoorState — Kogan (string)', () => {
78
93
  test('"closed" → CLOSED', () => expect(instance._getCurrentDoorState('closed')).toBe(CDS.CLOSED));
79
94
  });
80
95
 
96
+ describe('GarageDoorAccessory._getCurrentDoorState — Daspi (string)', () => {
97
+ let instance;
98
+ beforeEach(() => ({ instance } = makeGarage('Daspi')));
99
+
100
+ test('"full_opened" → OPEN', () => expect(instance._getCurrentDoorState('full_opened')).toBe(CDS.OPEN));
101
+ test('"open_softstop" → OPENING', () => expect(instance._getCurrentDoorState('open_softstop')).toBe(CDS.OPENING));
102
+ test('"opening" → OPENING', () => expect(instance._getCurrentDoorState('opening')).toBe(CDS.OPENING));
103
+ test('"close_softstop" → CLOSING', () => expect(instance._getCurrentDoorState('close_softstop')).toBe(CDS.CLOSING));
104
+ test('"closing" → CLOSING', () => expect(instance._getCurrentDoorState('closing')).toBe(CDS.CLOSING));
105
+ test('"full_closed" → CLOSED', () => expect(instance._getCurrentDoorState('full_closed')).toBe(CDS.CLOSED));
106
+ test('mixed case "FULL_CLOSED" → CLOSED', () => expect(instance._getCurrentDoorState('FULL_CLOSED')).toBe(CDS.CLOSED));
107
+ });
108
+
81
109
  // ---------------------------------------------------------------------------
82
110
  // setTargetDoorState — device command
83
111
  // ---------------------------------------------------------------------------
@@ -113,6 +141,23 @@ describe('GarageDoorAccessory.setTargetDoorState — Kogan', () => {
113
141
  });
114
142
  });
115
143
 
144
+ describe('GarageDoorAccessory.setTargetDoorState — Daspi', () => {
145
+ // Daspi expects capitalized command strings, unlike Kogan's lowercase.
146
+ test('OPEN sends "Open" to dpAction (1)', () => {
147
+ const { instance, device } = makeGarage('Daspi');
148
+ device.state['1'] = 'full_closed';
149
+ instance.setTargetDoorState(TDS.OPEN);
150
+ expect(device.update).toHaveBeenCalledWith({ '1': 'Open' });
151
+ });
152
+
153
+ test('CLOSED sends "Close" to dpAction (1)', () => {
154
+ const { instance, device } = makeGarage('Daspi');
155
+ device.state['1'] = 'full_opened';
156
+ instance.setTargetDoorState(TDS.CLOSED);
157
+ expect(device.update).toHaveBeenCalledWith({ '1': 'Close' });
158
+ });
159
+ });
160
+
116
161
  // ---------------------------------------------------------------------------
117
162
  // flipState
118
163
  // ---------------------------------------------------------------------------
@@ -466,6 +466,15 @@ While still in early testing, you can use this to open and close the garage door
466
466
  }
467
467
  ```
468
468
 
469
+ **Manufacturer-specific defaults.** Most openers report a plain boolean state on a numeric data-point. Some report string states and use different action/status data-points; set `manufacturer` and the plugin picks sensible defaults for you (each still overridable with `dpAction`/`dpStatus`):
470
+
471
+ | `manufacturer` | `dpAction` | `dpStatus` | State values |
472
+ |---|---|---|---|
473
+ | _(any other)_ | `1` | `2` | boolean (`true`/`false`) |
474
+ | `Kogan` | `101` | `102` | strings (`opened` / `openning` / `opening` / `closing` / `closed`) |
475
+ | `Wofea` | `1` | `101` | boolean (`true`/`false`) |
476
+ | `Daspi` | `1` | `7` | strings (`full_opened` / `opening` / `open_softstop` / `full_closed` / `closing` / `close_softstop`); commands are capitalized `Open` / `Close` |
477
+
469
478
  ### Simple Garage Doors
470
479
  For sliding gate openers and garage door controllers that expose momentary
471
480
  open/stop/close action DPs plus a single status DP that reports the gate's