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.
@@ -181,10 +181,10 @@ describe('setStateAsync', () => {
181
181
  expect(device.update).not.toHaveBeenCalled();
182
182
  });
183
183
 
184
- test('skips silently when device is not connected (issue #34)', () => {
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(() => instance.setStateAsync('1', true)).not.toThrow();
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('skips silently when device is not connected (issue #34)', () => {
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(() => instance.setMultiStateAsync({ '1': true, '2': 50 })).not.toThrow();
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('skips silently when device is not connected (issue #34)', () => {
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(() => instance.setMultiStateLegacyAsync({ '1': true })).not.toThrow();
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('skips silently and invokes callback without error when not connected (issue #34)', () => {
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('skips silently and invokes callback without error when not connected (issue #34)', () => {
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('does not throw when device is not connected (issue #34)', () => {
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(() => instance.setColorTemperature(200)).not.toThrow();
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 does not write when the device is disconnected', () => {
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(() => instance.setColorTemp(370)).not.toThrow();
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('Skips writes when the device is disconnected', () => {
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
- instance.setTargetDoorState(TDS.OPEN);
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(() => dev.update({switch_1: true})).not.toThrow();
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);
@@ -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
- |WLED Dimmer|`WledDimmer` (or legacy `SimpleDimmer`)<sup>[8](#simple-dimmers)</sup>|Dimmer switches with power control, with optional WLED sync <small>([instructions](#simple-dimmers))</small>|
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. Use type `WledDimmer` (legacy `SimpleDimmer` alias is also supported for backward compatibility).
349
+ These are switches that allow turning on and off, and dimming. Two distinct types are available:
349
350
 
350
- When using with a WLED controller (e.g. a Tuya-based relay/dimmer feeding power to a WLED strip), you can use advanced sync options:
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
- - `syncBrightnessToWled`: set to 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%.
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
- See the accessory source for full details on WLED integration.
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
  {