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.
|
package/config.schema.json
CHANGED
|
@@ -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. [
|
|
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
|
+
"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
|