homebridge-tuya-plus 3.14.0-dev.17 → 3.14.0-dev.19
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 +5 -0
- package/config.schema.json +1 -1
- package/index.js +2 -1
- package/lib/BaseAccessory.js +40 -13
- package/lib/ConvectorAccessory.js +1 -1
- package/lib/CustomMultiOutletAccessory.js +2 -1
- package/lib/MultiOutletAccessory.js +2 -1
- 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 +31 -18
- package/lib/SimpleHeaterAccessory.js +2 -2
- package/lib/SwitchAccessory.js +2 -1
- package/lib/TWLightAccessory.js +2 -2
- package/lib/TuyaCloudDevice.js +15 -5
- package/lib/ValveAccessory.js +16 -12
- package/lib/VerticalBlindsWithTilt.js +5 -3
- package/lib/WledDimmerAccessory.js +46 -81
- package/package.json +1 -1
- package/test/BaseAccessory.test.js +83 -10
- package/test/MultiOutletAccessory.test.js +12 -0
- package/test/RGBTWLightAccessory.test.js +3 -3
- package/test/SimpleFanLightAccessory.test.js +3 -3
- package/test/SimpleGarageDoorAccessory.test.js +4 -2
- package/test/TuyaCloudDevice.test.js +22 -1
- package/test/getCategory.test.js +1 -0
- package/wiki/Supported-Device-Types.md +8 -6
|
@@ -181,10 +181,10 @@ describe('setStateAsync', () => {
|
|
|
181
181
|
expect(device.update).not.toHaveBeenCalled();
|
|
182
182
|
});
|
|
183
183
|
|
|
184
|
-
test('
|
|
184
|
+
test('rejects with a comm-failure HapStatusError when device is not connected', async () => {
|
|
185
185
|
const { instance, device } = make({ '1': false });
|
|
186
186
|
device.connected = false;
|
|
187
|
-
expect(
|
|
187
|
+
await expect(instance.setStateAsync('1', true)).rejects.toBeInstanceOf(HAP.HapStatusError);
|
|
188
188
|
expect(device.update).not.toHaveBeenCalled();
|
|
189
189
|
});
|
|
190
190
|
});
|
|
@@ -204,12 +204,29 @@ describe('setMultiStateAsync', () => {
|
|
|
204
204
|
expect(device.update).not.toHaveBeenCalled();
|
|
205
205
|
});
|
|
206
206
|
|
|
207
|
-
test('
|
|
207
|
+
test('resolves when the device accepts the writes', async () => {
|
|
208
|
+
const { instance } = make({ '1': false });
|
|
209
|
+
await expect(instance.setMultiStateAsync({ '1': true })).resolves.toBeUndefined();
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test('rejects with a comm-failure HapStatusError when device is not connected', async () => {
|
|
208
213
|
const { instance, device } = make({ '1': false });
|
|
209
214
|
device.connected = false;
|
|
210
|
-
expect(
|
|
215
|
+
await expect(instance.setMultiStateAsync({ '1': true, '2': 50 })).rejects.toBeInstanceOf(HAP.HapStatusError);
|
|
211
216
|
expect(device.update).not.toHaveBeenCalled();
|
|
212
217
|
});
|
|
218
|
+
|
|
219
|
+
test('rejects when the device write is not accepted (returns false)', async () => {
|
|
220
|
+
const { instance, device } = make({ '1': false });
|
|
221
|
+
device.update.mockReturnValue(false);
|
|
222
|
+
await expect(instance.setMultiStateAsync({ '1': true })).rejects.toBeInstanceOf(HAP.HapStatusError);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test('awaits an async device write result (cloud) and rejects on failure', async () => {
|
|
226
|
+
const { instance, device } = make({ '1': false });
|
|
227
|
+
device.update.mockResolvedValue(false);
|
|
228
|
+
await expect(instance.setMultiStateAsync({ '1': true })).rejects.toBeInstanceOf(HAP.HapStatusError);
|
|
229
|
+
});
|
|
213
230
|
});
|
|
214
231
|
|
|
215
232
|
describe('setMultiStateLegacyAsync', () => {
|
|
@@ -220,12 +237,34 @@ describe('setMultiStateLegacyAsync', () => {
|
|
|
220
237
|
expect(device.update).toHaveBeenCalledWith({ '1': true, '3': '2' });
|
|
221
238
|
});
|
|
222
239
|
|
|
223
|
-
test('
|
|
240
|
+
test('rejects with a comm-failure HapStatusError when device is not connected', async () => {
|
|
224
241
|
const { instance, device } = make();
|
|
225
242
|
device.connected = false;
|
|
226
|
-
expect(
|
|
243
|
+
await expect(instance.setMultiStateLegacyAsync({ '1': true })).rejects.toBeInstanceOf(HAP.HapStatusError);
|
|
227
244
|
expect(device.update).not.toHaveBeenCalled();
|
|
228
245
|
});
|
|
246
|
+
|
|
247
|
+
test('rejects when the device write is not accepted (returns false)', async () => {
|
|
248
|
+
const { instance, device } = make();
|
|
249
|
+
device.update.mockReturnValue(false);
|
|
250
|
+
await expect(instance.setMultiStateLegacyAsync({ '1': true })).rejects.toBeInstanceOf(HAP.HapStatusError);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
describe('background write helpers (never throw/reject)', () => {
|
|
255
|
+
test('setStateInBackground swallows a disconnected-device failure', async () => {
|
|
256
|
+
const { instance, device } = make({ '1': false });
|
|
257
|
+
device.connected = false;
|
|
258
|
+
expect(() => instance.setStateInBackground('1', true)).not.toThrow();
|
|
259
|
+
// Give the rejected inner promise a tick to settle; it must be caught.
|
|
260
|
+
await Promise.resolve();
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test('setMultiStateLegacyInBackground still dispatches the write when connected', () => {
|
|
264
|
+
const { instance, device } = make();
|
|
265
|
+
instance.setMultiStateLegacyInBackground({ '1': true });
|
|
266
|
+
expect(device.update).toHaveBeenCalledWith({ '1': true });
|
|
267
|
+
});
|
|
229
268
|
});
|
|
230
269
|
|
|
231
270
|
describe('getDividedStateAsync', () => {
|
|
@@ -243,14 +282,48 @@ describe('getDividedStateAsync', () => {
|
|
|
243
282
|
});
|
|
244
283
|
});
|
|
245
284
|
|
|
285
|
+
describe('getState (callback)', () => {
|
|
286
|
+
test('returns the DP value via the callback when connected', done => {
|
|
287
|
+
const { instance } = make({ '1': true });
|
|
288
|
+
instance.getState('1', (err, value) => {
|
|
289
|
+
expect(err).toBeNull();
|
|
290
|
+
expect(value).toBe(true);
|
|
291
|
+
done();
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test('invokes the callback with a comm-failure error when not connected', () => {
|
|
296
|
+
const { instance, device } = make({ '1': true });
|
|
297
|
+
device.connected = false;
|
|
298
|
+
const cb = jest.fn();
|
|
299
|
+
instance.getState('1', cb);
|
|
300
|
+
expect(cb).toHaveBeenCalledWith(expect.any(HAP.HapStatusError));
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
|
|
246
304
|
describe('setMultiState (legacy callback)', () => {
|
|
247
|
-
test('
|
|
305
|
+
test('invokes the callback without error on a successful write', () => {
|
|
306
|
+
const { instance } = make({ '1': false });
|
|
307
|
+
const cb = jest.fn();
|
|
308
|
+
instance.setMultiState({ '1': true }, cb);
|
|
309
|
+
expect(cb).toHaveBeenCalledWith(null);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
test('invokes the callback with a comm-failure error when not connected', () => {
|
|
248
313
|
const { instance, device } = make({ '1': false });
|
|
249
314
|
device.connected = false;
|
|
250
315
|
const cb = jest.fn();
|
|
251
316
|
instance.setMultiState({ '1': true }, cb);
|
|
252
317
|
expect(device.update).not.toHaveBeenCalled();
|
|
253
|
-
expect(cb).toHaveBeenCalledWith();
|
|
318
|
+
expect(cb).toHaveBeenCalledWith(expect.any(HAP.HapStatusError));
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test('invokes the callback with a comm-failure error when the write is not accepted', () => {
|
|
322
|
+
const { instance, device } = make({ '1': false });
|
|
323
|
+
device.update.mockReturnValue(false);
|
|
324
|
+
const cb = jest.fn();
|
|
325
|
+
instance.setMultiState({ '1': true }, cb);
|
|
326
|
+
expect(cb).toHaveBeenCalledWith(expect.any(HAP.HapStatusError));
|
|
254
327
|
});
|
|
255
328
|
|
|
256
329
|
test('tolerates a missing callback when not connected', () => {
|
|
@@ -261,13 +334,13 @@ describe('setMultiState (legacy callback)', () => {
|
|
|
261
334
|
});
|
|
262
335
|
|
|
263
336
|
describe('setMultiStateLegacy (legacy callback)', () => {
|
|
264
|
-
test('
|
|
337
|
+
test('invokes the callback with a comm-failure error when not connected', () => {
|
|
265
338
|
const { instance, device } = make();
|
|
266
339
|
device.connected = false;
|
|
267
340
|
const cb = jest.fn();
|
|
268
341
|
instance.setMultiStateLegacy({ '1': true }, cb);
|
|
269
342
|
expect(device.update).not.toHaveBeenCalled();
|
|
270
|
-
expect(cb).toHaveBeenCalledWith();
|
|
343
|
+
expect(cb).toHaveBeenCalledWith(expect.any(HAP.HapStatusError));
|
|
271
344
|
});
|
|
272
345
|
});
|
|
273
346
|
|
|
@@ -86,4 +86,16 @@ describe('MultiOutletAccessory.setPower — debounce batching', () => {
|
|
|
86
86
|
await p;
|
|
87
87
|
expect(device.update).not.toHaveBeenCalled();
|
|
88
88
|
});
|
|
89
|
+
|
|
90
|
+
test('surfaces a comm-failure error (No Response) when disconnected', () => {
|
|
91
|
+
// A tap on an unreachable outlet must fail in HomeKit instead of being
|
|
92
|
+
// silently dropped after the debounce.
|
|
93
|
+
const { instance, device } = makeOutlet({ '1': false });
|
|
94
|
+
device.connected = false;
|
|
95
|
+
let err;
|
|
96
|
+
try { instance.setPower('1', true); } catch (e) { err = e; }
|
|
97
|
+
expect(err).toBeInstanceOf(HAP.HapStatusError);
|
|
98
|
+
expect(err.hapStatus).toBe(HAP.HAPStatus.SERVICE_COMMUNICATION_FAILURE);
|
|
99
|
+
expect(device.update).not.toHaveBeenCalled();
|
|
100
|
+
});
|
|
89
101
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const RGBTWLightAccessory = require('../lib/RGBTWLightAccessory');
|
|
4
|
-
const { makeInstance, makeMockCharacteristic } = require('./support/mocks');
|
|
4
|
+
const { makeInstance, makeMockCharacteristic, HAP } = require('./support/mocks');
|
|
5
5
|
|
|
6
6
|
function makeLight(state = {}, context = {}) {
|
|
7
7
|
const result = makeInstance(RGBTWLightAccessory, state, { colorFunction: 'HEXHSB', ...context });
|
|
@@ -88,12 +88,12 @@ describe('RGBTWLightAccessory.getColorTemperature', () => {
|
|
|
88
88
|
// setColorTemperature
|
|
89
89
|
// ---------------------------------------------------------------------------
|
|
90
90
|
describe('RGBTWLightAccessory.setColorTemperature', () => {
|
|
91
|
-
test('
|
|
91
|
+
test('rejects (No Response) and writes nothing when device is not connected', async () => {
|
|
92
92
|
const { instance, device } = makeLight({ '2': 'white', '4': 255 });
|
|
93
93
|
instance.characteristicHue = makeMockCharacteristic(0);
|
|
94
94
|
instance.characteristicSaturation = makeMockCharacteristic(0);
|
|
95
95
|
device.connected = false;
|
|
96
|
-
expect(
|
|
96
|
+
await expect(instance.setColorTemperature(200)).rejects.toBeInstanceOf(HAP.HapStatusError);
|
|
97
97
|
expect(device.update).not.toHaveBeenCalled();
|
|
98
98
|
});
|
|
99
99
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const SimpleFanLightAccessory = require('../lib/SimpleFanLightAccessory');
|
|
4
|
-
const { makeInstance } = require('./support/mocks');
|
|
4
|
+
const { makeInstance, HAP } = require('./support/mocks');
|
|
5
5
|
|
|
6
6
|
// Mirror the state that _registerCharacteristics would establish (the mock
|
|
7
7
|
// service shares a single characteristic, so wiring the fields manually keeps
|
|
@@ -290,10 +290,10 @@ describe('SimpleFanLightAccessory.getColorTemp / setColorTemp', () => {
|
|
|
290
290
|
expect(device.update).toHaveBeenCalledWith({ '23': 1000 });
|
|
291
291
|
});
|
|
292
292
|
|
|
293
|
-
test('setColorTemp
|
|
293
|
+
test('setColorTemp rejects (No Response) and writes nothing when disconnected', async () => {
|
|
294
294
|
const { instance, device } = makeFanLight({ '23': 500 });
|
|
295
295
|
device.connected = false;
|
|
296
|
-
expect(
|
|
296
|
+
await expect(instance.setColorTemp(370)).rejects.toBeInstanceOf(HAP.HapStatusError);
|
|
297
297
|
expect(device.update).not.toHaveBeenCalled();
|
|
298
298
|
});
|
|
299
299
|
});
|
|
@@ -373,11 +373,13 @@ describe('SimpleGarageDoorAccessory.setTargetDoorState — close (stop-before-cl
|
|
|
373
373
|
// Disconnect handling
|
|
374
374
|
// ---------------------------------------------------------------------------
|
|
375
375
|
describe('SimpleGarageDoorAccessory — disconnected', () => {
|
|
376
|
-
test('
|
|
376
|
+
test('Surfaces a No Response error and writes nothing when disconnected', () => {
|
|
377
377
|
const { instance, device } = makeSimpleGarage();
|
|
378
378
|
device.connected = false;
|
|
379
379
|
|
|
380
|
-
|
|
380
|
+
// A tap on an unreachable gate must fail in HomeKit instead of being
|
|
381
|
+
// silently dropped while HomeKit believes the open/close succeeded.
|
|
382
|
+
expect(() => instance.setTargetDoorState(TDS.OPEN)).toThrow(HAP.HapStatusError);
|
|
381
383
|
|
|
382
384
|
expect(device.update).not.toHaveBeenCalled();
|
|
383
385
|
});
|
|
@@ -56,10 +56,31 @@ describe('TuyaCloudDevice', () => {
|
|
|
56
56
|
const api = makeApi();
|
|
57
57
|
const dev = makeDevice(api);
|
|
58
58
|
expect(dev.connected).toBe(false);
|
|
59
|
-
expect(
|
|
59
|
+
expect(dev.update({switch_1: true})).toBe(false);
|
|
60
60
|
expect(api.sendCommands).not.toHaveBeenCalled();
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
+
test('update resolves to the cloud command result so failures are awaitable', async () => {
|
|
64
|
+
const api = makeApi();
|
|
65
|
+
const dev = makeDevice(api);
|
|
66
|
+
await dev._connect();
|
|
67
|
+
|
|
68
|
+
api.sendCommands.mockResolvedValueOnce(true);
|
|
69
|
+
await expect(dev.update({switch_1: true})).resolves.toBe(true);
|
|
70
|
+
|
|
71
|
+
api.sendCommands.mockResolvedValueOnce(false);
|
|
72
|
+
await expect(dev.update({switch_1: true})).resolves.toBe(false);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('update resolves to false (never rejects) when the cloud request throws', async () => {
|
|
76
|
+
const api = makeApi();
|
|
77
|
+
const dev = makeDevice(api);
|
|
78
|
+
await dev._connect();
|
|
79
|
+
|
|
80
|
+
api.sendCommands.mockRejectedValueOnce(new Error('network down'));
|
|
81
|
+
await expect(dev.update({switch_1: true})).resolves.toBe(false);
|
|
82
|
+
});
|
|
83
|
+
|
|
63
84
|
test('applyStatus emits only the changed data-points', async () => {
|
|
64
85
|
const api = makeApi();
|
|
65
86
|
const dev = makeDevice(api);
|
package/test/getCategory.test.js
CHANGED
|
@@ -23,6 +23,7 @@ const CLASS_DEF = {
|
|
|
23
23
|
garagedoor: require('../lib/GarageDoorAccessory'),
|
|
24
24
|
simplegaragedoor: require('../lib/SimpleGarageDoorAccessory'),
|
|
25
25
|
simpledimmer: require('../lib/SimpleDimmerAccessory'),
|
|
26
|
+
wleddimmer: require('../lib/WledDimmerAccessory'),
|
|
26
27
|
simpledimmer2: require('../lib/SimpleDimmer2Accessory'),
|
|
27
28
|
simpleblinds: require('../lib/SimpleBlindsAccessory'),
|
|
28
29
|
simpleheater: require('../lib/SimpleHeaterAccessory'),
|
|
@@ -15,7 +15,8 @@ If you are looking for verified configurations for your specific device, please
|
|
|
15
15
|
|Barely Smart Power Strip|`Outlet`|Smart power strips that don't allow individual control of the outlets|
|
|
16
16
|
|Air Conditioner|`AirConditioner`<sup>[6](#air-conditioners)</sup>|Cooling and heating devices <small>([instructions](#air-conditioners))</small>|
|
|
17
17
|
|Heat Convector|`Convector`<sup>[7](#heat-convectors)</sup>|Heating panels <small>([instructions](#heat-convectors))</small>|
|
|
18
|
-
|
|
|
18
|
+
|Simple Dimmer|`SimpleDimmer`<sup>[8](#simple-dimmers)</sup>|Dimmer switches with power control <small>([instructions](#simple-dimmers))</small>|
|
|
19
|
+
|WLED Dimmer|`WledDimmer`<sup>[8](#simple-dimmers)</sup>|Dimmer switches with power control, plus optional WLED brightness sync and preset-effect switches <small>([instructions](#simple-dimmers))</small>|
|
|
19
20
|
|Simple Heater|`SimpleHeater`<sup>[9](#simple-heaters)</sup>|Heating solutions with only temperature control <small>([instructions](#simple-heaters))</small>|
|
|
20
21
|
|Garage Door|`GarageDoor`<sup>[10](#garage-doors)</sup>|Smart garage doors or garage door openers <small>([instructions](#garage-doors))</small>|
|
|
21
22
|
|Simple Garage Door|`SimpleGarageDoor`<sup>[10](#simple-garage-doors)</sup>|Sliding gate openers and garage door controllers with open/stop/close action DPs and a simple three-value status DP <small>([instructions](#simple-garage-doors))</small>|
|
|
@@ -345,14 +346,15 @@ If your signature doesn't have a variation of _low_ or _high_, `SimpleHeater` wo
|
|
|
345
346
|
```
|
|
346
347
|
|
|
347
348
|
### Simple Dimmers / WLED Dimmers
|
|
348
|
-
These are switches that allow turning on and off, and dimming.
|
|
349
|
+
These are switches that allow turning on and off, and dimming. Two distinct types are available:
|
|
349
350
|
|
|
350
|
-
|
|
351
|
+
- `SimpleDimmer` — a plain dimmer with power and brightness control.
|
|
352
|
+
- `WledDimmer` — a dimmer that can additionally drive a [WLED](https://kno.wled.ge/) controller (e.g. a Tuya-based relay/dimmer feeding power to a WLED strip). With none of the WLED options below configured, it behaves exactly like a `SimpleDimmer`.
|
|
351
353
|
|
|
352
|
-
|
|
353
|
-
- `presetEffects`: array of effect configs to expose as switches in HomeKit (each turns on a WLED fx preset, optionally with staticColor).
|
|
354
|
+
The following options apply to `WledDimmer` only (they are ignored by `SimpleDimmer`):
|
|
354
355
|
|
|
355
|
-
|
|
356
|
+
- `syncBrightnessToWled`: set to the WLED device IP (e.g. "192.168.1.50" or "192.168.1.50:80") to sync HomeKit brightness changes directly to WLED over HTTP, keeping the Tuya dimmer at 100%.
|
|
357
|
+
- `presetEffects`: array of effect configs to expose as switches in HomeKit (each turns on a WLED fx preset, optionally with staticColor).
|
|
356
358
|
|
|
357
359
|
```json5
|
|
358
360
|
{
|