homebridge-tuya-plus 3.6.0 → 3.8.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.
@@ -260,14 +260,14 @@
260
260
  "type": "integer",
261
261
  "placeholder": "255",
262
262
  "condition": {
263
- "functionBody": "return model.devices && model.devices[arrayIndices] && ['RGBTWLight','RGBTWOutlet', 'FanLight'].includes(model.devices[arrayIndices].type);"
263
+ "functionBody": "return model.devices && model.devices[arrayIndices] && ['TWLight','RGBTWLight','RGBTWOutlet', 'FanLight'].includes(model.devices[arrayIndices].type);"
264
264
  }
265
265
  },
266
266
  "scaleWhiteColor": {
267
267
  "type": "integer",
268
268
  "placeholder": "255",
269
269
  "condition": {
270
- "functionBody": "return model.devices && model.devices[arrayIndices] && ['RGBTWLight','RGBTWOutlet'].includes(model.devices[arrayIndices].type);"
270
+ "functionBody": "return model.devices && model.devices[arrayIndices] && ['TWLight','RGBTWLight','RGBTWOutlet'].includes(model.devices[arrayIndices].type);"
271
271
  }
272
272
  },
273
273
  "outletCount": {
@@ -82,13 +82,19 @@ class BaseAccessory {
82
82
  setMultiStateLegacy(dps, callback) {
83
83
  //Adding back the original set multistate command as the multistate command from PR #267 Breaks the Fan Code by splitting the command into two.
84
84
  //For devices like the DETA Smart Fan Controller Switch that by default set the speed as 3 the new code in the setMultiState function causes issues.
85
- if (!this.device.connected) return callback(true);
85
+ if (!this.device.connected) {
86
+ this.log.debug(`${this.device.context.name}: skipping write, device not connected`);
87
+ return callback && callback();
88
+ }
86
89
  const ret = this.device.update(dps);
87
90
  callback && callback(!ret);
88
91
  }
89
92
 
90
93
  setMultiState(dps, callback) {
91
- if (!this.device.connected) return callback(true);
94
+ if (!this.device.connected) {
95
+ this.log.debug(`${this.device.context.name}: skipping write, device not connected`);
96
+ return callback && callback();
97
+ }
92
98
  for (const dp in dps) {
93
99
  if (dps.hasOwnProperty(dp) && dps[dp] !== this.device.state[dp]){
94
100
  this.__ret = this.device.update({[dp.toString()] : dps[dp]});
@@ -121,7 +127,10 @@ class BaseAccessory {
121
127
  }
122
128
 
123
129
  setMultiStateAsync(dps) {
124
- if (!this.device.connected) throw new Error('Not connected');
130
+ if (!this.device.connected) {
131
+ this.log.debug(`${this.device.context.name}: skipping write, device not connected`);
132
+ return;
133
+ }
125
134
  for (const dp in dps) {
126
135
  if (dps.hasOwnProperty(dp) && dps[dp] !== this.device.state[dp]) {
127
136
  this.device.update({[dp.toString()]: dps[dp]});
@@ -130,7 +139,10 @@ class BaseAccessory {
130
139
  }
131
140
 
132
141
  setMultiStateLegacyAsync(dps) {
133
- if (!this.device.connected) throw new Error('Not connected');
142
+ if (!this.device.connected) {
143
+ this.log.debug(`${this.device.context.name}: skipping write, device not connected`);
144
+ return;
145
+ }
134
146
  this.device.update(dps);
135
147
  }
136
148
 
@@ -1,5 +1,8 @@
1
1
  const BaseAccessory = require('./BaseAccessory');
2
2
 
3
+ const DEFAULT_MIN_WHITE_COLOR = 140;
4
+ const DEFAULT_MAX_WHITE_COLOR = 400;
5
+
3
6
  class RGBTWLightAccessory extends BaseAccessory {
4
7
  static getCategory(Categories) {
5
8
  return Categories.LIGHTBULB;
@@ -28,6 +31,9 @@ class RGBTWLightAccessory extends BaseAccessory {
28
31
  this.dpColorTemperature = this._getCustomDP(this.device.context.dpColorTemperature) || '4';
29
32
  this.dpColor = this._getCustomDP(this.device.context.dpColor) || '5';
30
33
 
34
+ this.minWhiteColor = this.device.context.minWhiteColor ?? DEFAULT_MIN_WHITE_COLOR;
35
+ this.maxWhiteColor = this.device.context.maxWhiteColor ?? DEFAULT_MAX_WHITE_COLOR;
36
+
31
37
  this._detectColorFunction(dps[this.dpColor]);
32
38
 
33
39
  this.cmdWhite = 'white';
@@ -57,10 +63,10 @@ class RGBTWLightAccessory extends BaseAccessory {
57
63
 
58
64
  const characteristicColorTemperature = service.getCharacteristic(Characteristic.ColorTemperature)
59
65
  .setProps({
60
- minValue: this.device.context.minWhiteColor,
61
- maxValue: this.device.context.maxWhiteColor
66
+ minValue: this.minWhiteColor,
67
+ maxValue: this.maxWhiteColor
62
68
  })
63
- .updateValue(dps[this.dpMode] === this.cmdWhite ? this.convertColorTemperatureFromTuyaToHomeKit(dps[this.dpColorTemperature]) : this.device.context.minWhiteColor)
69
+ .updateValue(dps[this.dpMode] === this.cmdWhite ? this.convertColorTemperatureFromTuyaToHomeKit(dps[this.dpColorTemperature]) : this.minWhiteColor)
64
70
  .onGet(() => this.getColorTemperature())
65
71
  .onSet(value => this.setColorTemperature(value));
66
72
 
@@ -119,9 +125,9 @@ class RGBTWLightAccessory extends BaseAccessory {
119
125
  if (oldColor.b !== newColor.b) characteristicBrightness.updateValue(newColor.b);
120
126
  if (oldColor.h !== newColor.h) characteristicHue.updateValue(newColor.h);
121
127
  if (oldColor.s !== newColor.s) characteristicSaturation.updateValue(newColor.s);
122
- if (characteristicColorTemperature.value !== this.device.context.minWhiteColor) characteristicColorTemperature.updateValue(this.device.context.minWhiteColor);
128
+ if (characteristicColorTemperature.value !== this.minWhiteColor) characteristicColorTemperature.updateValue(this.minWhiteColor);
123
129
  } else if (changes[this.dpMode]) {
124
- if (characteristicColorTemperature.value !== this.device.context.minWhiteColor) characteristicColorTemperature.updateValue(this.device.context.minWhiteColor);
130
+ if (characteristicColorTemperature.value !== this.minWhiteColor) characteristicColorTemperature.updateValue(this.minWhiteColor);
125
131
  }
126
132
  }
127
133
  });
@@ -139,7 +145,7 @@ class RGBTWLightAccessory extends BaseAccessory {
139
145
 
140
146
  getColorTemperature() {
141
147
  this.log.debug(`getColorTemperature`);
142
- if (this.device.state[this.dpMode] !== this.cmdWhite) return this.device.context.minWhiteColor;
148
+ if (this.device.state[this.dpMode] !== this.cmdWhite) return this.minWhiteColor;
143
149
  return this.convertColorTemperatureFromTuyaToHomeKit(this.device.state[this.dpColorTemperature]);
144
150
  }
145
151
 
@@ -204,7 +210,7 @@ class RGBTWLightAccessory extends BaseAccessory {
204
210
  if (!(this.device.state[this.dpMode] === this.cmdWhite && isSham)) {
205
211
  this.setMultiStateAsync({[this.dpMode]: this.cmdColor, [this.dpColor]: newValue});
206
212
  }
207
- this.characteristicColorTemperature.updateValue(this.device.context.minWhiteColor);
213
+ this.characteristicColorTemperature.updateValue(this.minWhiteColor);
208
214
 
209
215
  resolvers.forEach(r => r());
210
216
  }, 500);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homebridge-tuya-plus",
3
- "version": "3.6.0",
3
+ "version": "3.8.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": {
@@ -178,10 +178,11 @@ describe('setStateAsync', () => {
178
178
  expect(device.update).not.toHaveBeenCalled();
179
179
  });
180
180
 
181
- test('throws when device is not connected', () => {
181
+ test('skips silently when device is not connected (issue #34)', () => {
182
182
  const { instance, device } = make({ '1': false });
183
183
  device.connected = false;
184
- expect(() => instance.setStateAsync('1', true)).toThrow('Not connected');
184
+ expect(() => instance.setStateAsync('1', true)).not.toThrow();
185
+ expect(device.update).not.toHaveBeenCalled();
185
186
  });
186
187
  });
187
188
 
@@ -199,6 +200,13 @@ describe('setMultiStateAsync', () => {
199
200
  instance.setMultiStateAsync({ '1': true, '2': 50 });
200
201
  expect(device.update).not.toHaveBeenCalled();
201
202
  });
203
+
204
+ test('skips silently when device is not connected (issue #34)', () => {
205
+ const { instance, device } = make({ '1': false });
206
+ device.connected = false;
207
+ expect(() => instance.setMultiStateAsync({ '1': true, '2': 50 })).not.toThrow();
208
+ expect(device.update).not.toHaveBeenCalled();
209
+ });
202
210
  });
203
211
 
204
212
  describe('setMultiStateLegacyAsync', () => {
@@ -208,6 +216,13 @@ describe('setMultiStateLegacyAsync', () => {
208
216
  expect(device.update).toHaveBeenCalledTimes(1);
209
217
  expect(device.update).toHaveBeenCalledWith({ '1': true, '3': '2' });
210
218
  });
219
+
220
+ test('skips silently when device is not connected (issue #34)', () => {
221
+ const { instance, device } = make();
222
+ device.connected = false;
223
+ expect(() => instance.setMultiStateLegacyAsync({ '1': true })).not.toThrow();
224
+ expect(device.update).not.toHaveBeenCalled();
225
+ });
211
226
  });
212
227
 
213
228
  describe('getDividedStateAsync', () => {
@@ -222,6 +237,34 @@ describe('getDividedStateAsync', () => {
222
237
  });
223
238
  });
224
239
 
240
+ describe('setMultiState (legacy callback)', () => {
241
+ test('skips silently and invokes callback without error when not connected (issue #34)', () => {
242
+ const { instance, device } = make({ '1': false });
243
+ device.connected = false;
244
+ const cb = jest.fn();
245
+ instance.setMultiState({ '1': true }, cb);
246
+ expect(device.update).not.toHaveBeenCalled();
247
+ expect(cb).toHaveBeenCalledWith();
248
+ });
249
+
250
+ test('tolerates a missing callback when not connected', () => {
251
+ const { instance, device } = make({ '1': false });
252
+ device.connected = false;
253
+ expect(() => instance.setMultiState({ '1': true })).not.toThrow();
254
+ });
255
+ });
256
+
257
+ describe('setMultiStateLegacy (legacy callback)', () => {
258
+ test('skips silently and invokes callback without error when not connected (issue #34)', () => {
259
+ const { instance, device } = make();
260
+ device.connected = false;
261
+ const cb = jest.fn();
262
+ instance.setMultiStateLegacy({ '1': true }, cb);
263
+ expect(device.update).not.toHaveBeenCalled();
264
+ expect(cb).toHaveBeenCalledWith();
265
+ });
266
+ });
267
+
225
268
  // ---------------------------------------------------------------------------
226
269
  // Utility helpers
227
270
  // ---------------------------------------------------------------------------
@@ -5,7 +5,7 @@ const { makeInstance, makeMockCharacteristic } = require('./support/mocks');
5
5
 
6
6
  function makeLight(state = {}, context = {}) {
7
7
  const result = makeInstance(RGBTWLightAccessory, state, { colorFunction: 'HEXHSB', ...context });
8
- const { instance } = result;
8
+ const { instance, device } = result;
9
9
 
10
10
  // Set up the state that _registerCharacteristics would normally establish
11
11
  instance.dpPower = '1';
@@ -16,6 +16,8 @@ function makeLight(state = {}, context = {}) {
16
16
  instance.cmdWhite = 'white';
17
17
  instance.cmdColor = 'colour';
18
18
  instance.colorFunction = 'HEXHSB';
19
+ instance.minWhiteColor = device.context.minWhiteColor ?? 140;
20
+ instance.maxWhiteColor = device.context.maxWhiteColor ?? 400;
19
21
 
20
22
  // Provide the cross-characteristic references _setHueSaturation writes to
21
23
  instance.characteristicColorTemperature = makeMockCharacteristic(0);
@@ -64,8 +66,7 @@ describe('RGBTWLightAccessory.setBrightness', () => {
64
66
  // ---------------------------------------------------------------------------
65
67
  describe('RGBTWLightAccessory.getColorTemperature', () => {
66
68
  test('returns minWhiteColor when in color mode', () => {
67
- const { instance, device } = makeLight({ '2': 'colour' });
68
- device.context.minWhiteColor = 140;
69
+ const { instance } = makeLight({ '2': 'colour' }, { minWhiteColor: 140 });
69
70
  expect(instance.getColorTemperature()).toBe(140);
70
71
  });
71
72
 
@@ -74,6 +75,35 @@ describe('RGBTWLightAccessory.getColorTemperature', () => {
74
75
  // convertColorTemperatureFromTuyaToHomeKit(255) = 140
75
76
  expect(instance.getColorTemperature()).toBe(140);
76
77
  });
78
+
79
+ test('returns a finite default when minWhiteColor is not configured (issue #34)', () => {
80
+ const { instance } = makeLight({ '2': 'colour' });
81
+ const result = instance.getColorTemperature();
82
+ expect(typeof result).toBe('number');
83
+ expect(Number.isFinite(result)).toBe(true);
84
+ });
85
+ });
86
+
87
+ // ---------------------------------------------------------------------------
88
+ // setColorTemperature
89
+ // ---------------------------------------------------------------------------
90
+ describe('RGBTWLightAccessory.setColorTemperature', () => {
91
+ test('does not throw when device is not connected (issue #34)', () => {
92
+ const { instance, device } = makeLight({ '2': 'white', '4': 255 });
93
+ instance.characteristicHue = makeMockCharacteristic(0);
94
+ instance.characteristicSaturation = makeMockCharacteristic(0);
95
+ device.connected = false;
96
+ expect(() => instance.setColorTemperature(200)).not.toThrow();
97
+ expect(device.update).not.toHaveBeenCalled();
98
+ });
99
+
100
+ test('writes color temperature when device is connected', () => {
101
+ const { instance, device } = makeLight({ '2': 'white', '4': 255 });
102
+ instance.characteristicHue = makeMockCharacteristic(0);
103
+ instance.characteristicSaturation = makeMockCharacteristic(0);
104
+ instance.setColorTemperature(200);
105
+ expect(device.update).toHaveBeenCalled();
106
+ });
77
107
  });
78
108
 
79
109
  // ---------------------------------------------------------------------------
@@ -133,11 +163,21 @@ describe('RGBTWLightAccessory._setHueSaturation', () => {
133
163
  });
134
164
 
135
165
  test('updates characteristicColorTemperature to minWhiteColor after firing', async () => {
136
- const { instance } = makeLight({ '2': 'colour', '5': '00000000b46464' });
137
- instance.device.context.minWhiteColor = 140;
166
+ const { instance } = makeLight({ '2': 'colour', '5': '00000000b46464' }, { minWhiteColor: 140 });
138
167
  const p = instance.setHue(180);
139
168
  jest.advanceTimersByTime(500);
140
169
  await p;
141
170
  expect(instance.characteristicColorTemperature.updateValue).toHaveBeenCalledWith(140);
142
171
  });
172
+
173
+ test('updates characteristicColorTemperature to a finite default when minWhiteColor unset (issue #34)', async () => {
174
+ const { instance } = makeLight({ '2': 'colour', '5': '00000000b46464' });
175
+ const p = instance.setHue(180);
176
+ jest.advanceTimersByTime(500);
177
+ await p;
178
+ const calls = instance.characteristicColorTemperature.updateValue.mock.calls;
179
+ const lastValue = calls[calls.length - 1][0];
180
+ expect(typeof lastValue).toBe('number');
181
+ expect(Number.isFinite(lastValue)).toBe(true);
182
+ });
143
183
  });