homebridge-tuya-plus 3.14.0-dev.21 → 3.14.0-dev.22
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/index.js +19 -1
- package/package.json +1 -1
- package/test/index.test.js +49 -1
package/index.js
CHANGED
|
@@ -123,6 +123,7 @@ class TuyaLan {
|
|
|
123
123
|
const lanDeviceIds = []; // ids we still want to find on the LAN
|
|
124
124
|
const connectedDevices = []; // ids discovered on the LAN
|
|
125
125
|
const fakeDevices = [];
|
|
126
|
+
const seenUUIDs = new Set(); // accessory UUIDs already configured this run
|
|
126
127
|
|
|
127
128
|
this.config.devices.forEach(device => {
|
|
128
129
|
try {
|
|
@@ -137,6 +138,15 @@ class TuyaLan {
|
|
|
137
138
|
if (!device.type) return this.log.error('%s (%s) doesn\'t have a type defined.', device.name || 'Unnamed device', device.id);
|
|
138
139
|
if (!CLASS_DEF[device.type.toLowerCase()]) return this.log.error('%s (%s) doesn\'t have a valid type defined.', device.name || 'Unnamed device', device.id);
|
|
139
140
|
|
|
141
|
+
// Two config entries that resolve to the same accessory UUID (the same
|
|
142
|
+
// Tuya id, real or fake) make addAccessory process that UUID twice. The
|
|
143
|
+
// second pass reads back the accessory wrapper the first pass cached
|
|
144
|
+
// instead of a PlatformAccessory and crashes the child bridge while
|
|
145
|
+
// trying to unregister it. Keep the first entry; skip the rest.
|
|
146
|
+
const uuid = UUID.generate(UUID_SEED + (device.fake ? ':fake:' : ':') + device.id);
|
|
147
|
+
if (seenUUIDs.has(uuid)) return this.log.warn('%s (%s) is configured more than once; ignoring the duplicate entry.', device.name || 'Unnamed device', device.id);
|
|
148
|
+
seenUUIDs.add(uuid);
|
|
149
|
+
|
|
140
150
|
if (device.fake) {
|
|
141
151
|
fakeDevices.push({name: device.id.slice(8), ...device});
|
|
142
152
|
return;
|
|
@@ -337,9 +347,17 @@ class TuyaLan {
|
|
|
337
347
|
removeAccessory(homebridgeAccessory) {
|
|
338
348
|
if (!homebridgeAccessory) return;
|
|
339
349
|
|
|
350
|
+
// Only real PlatformAccessory instances can be unregistered. cachedAccessories
|
|
351
|
+
// also holds accessory wrappers (set by addAccessory), and Homebridge throws a
|
|
352
|
+
// fatal TypeError when handed anything else, taking down the whole child bridge.
|
|
353
|
+
// Guard against it rather than trust every caller.
|
|
354
|
+
if (!(homebridgeAccessory instanceof PlatformAccessory)) {
|
|
355
|
+
return this.log.warn('Skipped unregistering %s: not a PlatformAccessory.', homebridgeAccessory.displayName);
|
|
356
|
+
}
|
|
357
|
+
|
|
340
358
|
this.log.warn('Unregistering', homebridgeAccessory.displayName);
|
|
341
359
|
|
|
342
|
-
|
|
360
|
+
this.cachedAccessories.delete(homebridgeAccessory.UUID);
|
|
343
361
|
this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [homebridgeAccessory]);
|
|
344
362
|
}
|
|
345
363
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "homebridge-tuya-plus",
|
|
3
|
-
"version": "3.14.0-dev.
|
|
3
|
+
"version": "3.14.0-dev.22",
|
|
4
4
|
"description": "A community-maintained Homebridge plugin for controlling Tuya devices in HomeKit. LAN-first, with an optional Tuya Cloud fallback for any device the LAN can't reach.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
package/test/index.test.js
CHANGED
|
@@ -24,6 +24,7 @@ jest.mock('../lib/TuyaDiscovery', () => ({
|
|
|
24
24
|
}));
|
|
25
25
|
|
|
26
26
|
const TuyaDevice = require('../lib/TuyaDevice');
|
|
27
|
+
const TuyaAccessory = require('../lib/TuyaAccessory');
|
|
27
28
|
const TuyaCloudApi = require('../lib/TuyaCloudApi');
|
|
28
29
|
const TuyaCloudMessaging = require('../lib/TuyaCloudMessaging');
|
|
29
30
|
const TuyaDiscovery = require('../lib/TuyaDiscovery');
|
|
@@ -56,7 +57,7 @@ function makeLog() {
|
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
function makeApi() {
|
|
59
|
-
return {hap: makeHap(), on: jest.fn(), registerPlatformAccessories: jest.fn()};
|
|
60
|
+
return {hap: makeHap(), on: jest.fn(), registerPlatformAccessories: jest.fn(), unregisterPlatformAccessories: jest.fn()};
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
function run(config) {
|
|
@@ -139,3 +140,50 @@ describe('TuyaLan — discovery routing', () => {
|
|
|
139
140
|
expect(TuyaDiscovery.start).not.toHaveBeenCalled();
|
|
140
141
|
});
|
|
141
142
|
});
|
|
143
|
+
|
|
144
|
+
describe('TuyaLan — duplicate device ids', () => {
|
|
145
|
+
// Two entries sharing an id resolve to one accessory UUID; configuring it twice
|
|
146
|
+
// used to crash the child bridge (addAccessory read back its own wrapper and
|
|
147
|
+
// tried to unregister it). The repeat must be dropped with a warning instead.
|
|
148
|
+
test('a repeated id is configured once and warns', () => {
|
|
149
|
+
const platform = run({devices: [SW(), SW()]});
|
|
150
|
+
expect(TuyaDevice).toHaveBeenCalledTimes(1);
|
|
151
|
+
expect(platform.addAccessory).toHaveBeenCalledTimes(1);
|
|
152
|
+
expect(platform.log.warn).toHaveBeenCalled();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('distinct ids are all configured', () => {
|
|
156
|
+
run({devices: [SW(), SLEEPY()]});
|
|
157
|
+
expect(TuyaDevice).toHaveBeenCalledTimes(2);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('a repeated fake id is configured once', () => {
|
|
161
|
+
run({devices: [SW({fake: true}), SW({fake: true})]});
|
|
162
|
+
expect(TuyaAccessory).toHaveBeenCalledTimes(1);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe('TuyaLan — removeAccessory hardening', () => {
|
|
167
|
+
function platformWith(PlatformAccessory) {
|
|
168
|
+
let cls;
|
|
169
|
+
factory({platformAccessory: PlatformAccessory, hap: makeHap(), registerPlatform: (n, p, c) => { cls = c; }});
|
|
170
|
+
return new cls(makeLog(), {devices: [SW()]}, makeApi());
|
|
171
|
+
}
|
|
172
|
+
const PlatformAccessoryStub = function(name, uuid) { this.displayName = name; this.UUID = uuid; };
|
|
173
|
+
|
|
174
|
+
test('a non-PlatformAccessory (e.g. an accessory wrapper) is never unregistered', () => {
|
|
175
|
+
const platform = platformWith(PlatformAccessoryStub);
|
|
176
|
+
expect(() => platform.removeAccessory({accessory: {}})).not.toThrow();
|
|
177
|
+
expect(platform.api.unregisterPlatformAccessories).not.toHaveBeenCalled();
|
|
178
|
+
expect(platform.log.warn).toHaveBeenCalled();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('a real PlatformAccessory is unregistered and dropped from the cache', () => {
|
|
182
|
+
const platform = platformWith(PlatformAccessoryStub);
|
|
183
|
+
const accessory = new PlatformAccessoryStub('Real', 'uuid:x');
|
|
184
|
+
platform.cachedAccessories.set('uuid:x', accessory);
|
|
185
|
+
platform.removeAccessory(accessory);
|
|
186
|
+
expect(platform.api.unregisterPlatformAccessories).toHaveBeenCalledTimes(1);
|
|
187
|
+
expect(platform.cachedAccessories.has('uuid:x')).toBe(false);
|
|
188
|
+
});
|
|
189
|
+
});
|