homebridge-tuya-plus 3.5.1 → 3.5.3
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/.github/workflows/test.yml +23 -0
- package/config.schema.json +7 -7
- package/lib/CustomMultiOutletAccessory.js +1 -1
- package/lib/MultiOutletAccessory.js +1 -1
- package/lib/OilDiffuserAccessory.js +4 -4
- package/lib/RGBTWOutletAccessory.js +4 -4
- package/lib/SwitchAccessory.js +1 -1
- package/package.json +1 -1
- package/test/uuidStability.test.js +75 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
name: Test Code
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
|
|
15
|
+
- name: Use Node.js
|
|
16
|
+
uses: actions/setup-node@v4
|
|
17
|
+
with:
|
|
18
|
+
node-version: "23"
|
|
19
|
+
|
|
20
|
+
- run: npm ci
|
|
21
|
+
|
|
22
|
+
- name: Run Jest
|
|
23
|
+
run: npm run test
|
package/config.schema.json
CHANGED
|
@@ -7,23 +7,27 @@
|
|
|
7
7
|
"schema": {
|
|
8
8
|
"type": "object",
|
|
9
9
|
"properties": {
|
|
10
|
+
"name": {
|
|
11
|
+
"title": "Name",
|
|
12
|
+
"type": "string",
|
|
13
|
+
"default": "Tuya LAN"
|
|
14
|
+
},
|
|
10
15
|
"discoverTimeout": {
|
|
11
16
|
"title": "Device IP auto-discovery timeout",
|
|
12
17
|
"type": "integer",
|
|
13
18
|
"minimum": 0,
|
|
14
19
|
"placeholder": "Timeout (millisecond) for device IP address discovery",
|
|
15
|
-
"default": 60000
|
|
16
|
-
"required": false
|
|
20
|
+
"default": 60000
|
|
17
21
|
},
|
|
18
22
|
"devices": {
|
|
19
23
|
"type": "array",
|
|
20
24
|
"orderable": false,
|
|
21
25
|
"items": {
|
|
22
26
|
"type": "object",
|
|
27
|
+
"required": ["type", "name", "id", "key"],
|
|
23
28
|
"properties": {
|
|
24
29
|
"type": {
|
|
25
30
|
"type": "string",
|
|
26
|
-
"required": true,
|
|
27
31
|
"default": "null",
|
|
28
32
|
"oneOf": [
|
|
29
33
|
{
|
|
@@ -107,7 +111,6 @@
|
|
|
107
111
|
"name": {
|
|
108
112
|
"type": "string",
|
|
109
113
|
"description": "Anything you'd like to use to identify this device. You can always change the name from within the Home app later.",
|
|
110
|
-
"required": true,
|
|
111
114
|
"condition": {
|
|
112
115
|
"functionBody": "return model.devices && model.devices[arrayIndices].type !== 'null';"
|
|
113
116
|
}
|
|
@@ -116,7 +119,6 @@
|
|
|
116
119
|
"type": "string",
|
|
117
120
|
"title": "Tuya ID",
|
|
118
121
|
"description": "If you don't have the Tuya ID or Key, follow the steps found on the <a href='https://github.com/adrianjagielak/homebridge-tuya-plus/blob/main/wiki/Get-Local-Keys-for-Your-Devices.md' target='_blank'>Setup Instructions</a> page.",
|
|
119
|
-
"required": true,
|
|
120
122
|
"condition": {
|
|
121
123
|
"functionBody": "return model.devices && model.devices[arrayIndices].type !== 'null';"
|
|
122
124
|
}
|
|
@@ -124,7 +126,6 @@
|
|
|
124
126
|
"key": {
|
|
125
127
|
"title": "Tuya Key",
|
|
126
128
|
"type": "string",
|
|
127
|
-
"required": true,
|
|
128
129
|
"condition": {
|
|
129
130
|
"functionBody": "return model.devices && model.devices[arrayIndices].type !== 'null';"
|
|
130
131
|
}
|
|
@@ -132,7 +133,6 @@
|
|
|
132
133
|
"ip": {
|
|
133
134
|
"title": "IP Address",
|
|
134
135
|
"type": "string",
|
|
135
|
-
"required": false,
|
|
136
136
|
"condition": {
|
|
137
137
|
"functionBody": "return model.devices && model.devices[arrayIndices].type !== 'null';"
|
|
138
138
|
}
|
|
@@ -30,7 +30,7 @@ class CustomMultiOutletAccessory extends BaseAccessory {
|
|
|
30
30
|
throw new Error('The outlet definition #${i} is missing or is malformed: ' + outlet);
|
|
31
31
|
|
|
32
32
|
const name = ((outlet.name || '').trim() || 'Unnamed') + ' - ' + this.device.context.name;
|
|
33
|
-
let service = this.accessory.getServiceById(Service.Outlet
|
|
33
|
+
let service = this.accessory.getServiceById(Service.Outlet, 'outlet ' + outlet.dp);
|
|
34
34
|
if (service) this._checkServiceName(service, name);
|
|
35
35
|
else service = this.accessory.addService(Service.Outlet, name, 'outlet ' + outlet.dp);
|
|
36
36
|
|
|
@@ -24,7 +24,7 @@ class MultiOutletAccessory extends BaseAccessory {
|
|
|
24
24
|
const outletCount = parseInt(this.device.context.outletCount) || 1;
|
|
25
25
|
const _validServices = [];
|
|
26
26
|
for (let i = 0; i++ < outletCount;) {
|
|
27
|
-
let service = this.accessory.getServiceById(Service.Outlet
|
|
27
|
+
let service = this.accessory.getServiceById(Service.Outlet, 'outlet ' + i);
|
|
28
28
|
if (service) this._checkServiceName(service, this.device.context.name + ' ' + i);
|
|
29
29
|
else service = this.accessory.addService(Service.Outlet, this.device.context.name + ' ' + i, 'outlet ' + i);
|
|
30
30
|
|
|
@@ -34,12 +34,12 @@ class OilDiffuserAccessory extends BaseAccessory {
|
|
|
34
34
|
const {Service} = this.hap;
|
|
35
35
|
|
|
36
36
|
const humidifierName = this.device.context.name;
|
|
37
|
-
let humidifierService = this.accessory.getServiceById(Service.HumidifierDehumidifier
|
|
37
|
+
let humidifierService = this.accessory.getServiceById(Service.HumidifierDehumidifier, 'humidifier');
|
|
38
38
|
if (humidifierService) this._checkServiceName(humidifierService, humidifierName);
|
|
39
39
|
else humidifierService = this.accessory.addService(Service.HumidifierDehumidifier, humidifierName, 'humidifier');
|
|
40
40
|
|
|
41
41
|
const lightName = this.device.context.name + ' Light';
|
|
42
|
-
let lightService = this.accessory.getServiceById(Service.Lightbulb
|
|
42
|
+
let lightService = this.accessory.getServiceById(Service.Lightbulb, 'lightbulb');
|
|
43
43
|
if (lightService) this._checkServiceName(lightService, lightName);
|
|
44
44
|
else lightService = this.accessory.addService(Service.Lightbulb, lightName, 'lightbulb');
|
|
45
45
|
|
|
@@ -55,8 +55,8 @@ class OilDiffuserAccessory extends BaseAccessory {
|
|
|
55
55
|
|
|
56
56
|
const {Service, AdaptiveLightingController, Characteristic} = this.hap;
|
|
57
57
|
|
|
58
|
-
const humidifierService = this.accessory.getServiceById(Service.HumidifierDehumidifier
|
|
59
|
-
const lightService = this.accessory.getServiceById(Service.Lightbulb
|
|
58
|
+
const humidifierService = this.accessory.getServiceById(Service.HumidifierDehumidifier, 'humidifier');
|
|
59
|
+
const lightService = this.accessory.getServiceById(Service.Lightbulb, 'lightbulb');
|
|
60
60
|
|
|
61
61
|
this.dpLight = this._getCustomDP(this.device.context.dpLight) || '5';
|
|
62
62
|
this.dpMode = this._getCustomDP(this.device.context.dpMode) || '6';
|
|
@@ -22,12 +22,12 @@ class RGBTWOutletAccessory extends BaseAccessory {
|
|
|
22
22
|
const {Service} = this.hap;
|
|
23
23
|
|
|
24
24
|
const outletName = 'Outlet - ' + this.device.context.name;
|
|
25
|
-
let outletService = this.accessory.getServiceById(Service.Outlet
|
|
25
|
+
let outletService = this.accessory.getServiceById(Service.Outlet, 'outlet');
|
|
26
26
|
if (outletService) this._checkServiceName(outletService, outletName);
|
|
27
27
|
else outletService = this.accessory.addService(Service.Outlet, outletName, 'outlet');
|
|
28
28
|
|
|
29
29
|
const lightName = 'RGBTWLight - ' + this.device.context.name;
|
|
30
|
-
let lightService = this.accessory.getServiceById(Service.Lightbulb
|
|
30
|
+
let lightService = this.accessory.getServiceById(Service.Lightbulb, 'lightbulb');
|
|
31
31
|
if (lightService) this._checkServiceName(lightService, lightName);
|
|
32
32
|
else lightService = this.accessory.addService(Service.Lightbulb, lightName, 'lightbulb');
|
|
33
33
|
|
|
@@ -43,8 +43,8 @@ class RGBTWOutletAccessory extends BaseAccessory {
|
|
|
43
43
|
|
|
44
44
|
const {Service, Characteristic, EnergyCharacteristics} = this.hap;
|
|
45
45
|
|
|
46
|
-
const outletService = this.accessory.getServiceById(Service.Outlet
|
|
47
|
-
const lightService = this.accessory.getServiceById(Service.Lightbulb
|
|
46
|
+
const outletService = this.accessory.getServiceById(Service.Outlet, 'outlet');
|
|
47
|
+
const lightService = this.accessory.getServiceById(Service.Lightbulb, 'lightbulb');
|
|
48
48
|
|
|
49
49
|
this.dpLight = this._getCustomDP(this.device.context.dpLight) || '1';
|
|
50
50
|
this.dpMode = this._getCustomDP(this.device.context.dpMode) || '2';
|
package/lib/SwitchAccessory.js
CHANGED
|
@@ -24,7 +24,7 @@ class SwitchAccessory extends BaseAccessory {
|
|
|
24
24
|
const switchCount = parseInt(this.device.context.switchCount) || 1;
|
|
25
25
|
const _validServices = [];
|
|
26
26
|
for (let i = 0; i++ < switchCount;) {
|
|
27
|
-
let service = this.accessory.getServiceById(Service.Switch
|
|
27
|
+
let service = this.accessory.getServiceById(Service.Switch, 'switch ' + i);
|
|
28
28
|
if (service) this._checkServiceName(service, this.device.context.name + ' ' + i);
|
|
29
29
|
else service = this.accessory.addService(Service.Switch, this.device.context.name + ' ' + i, 'switch ' + i);
|
|
30
30
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "homebridge-tuya-plus",
|
|
3
|
-
"version": "3.5.
|
|
3
|
+
"version": "3.5.3",
|
|
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": {
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Regression guard for HomeKit accessory identity.
|
|
4
|
+
//
|
|
5
|
+
// HomeKit identifies accessories by UUID. Every UUID this plugin produces
|
|
6
|
+
// must be derived from the legacy seed 'homebridge-tuya' (the PLUGIN_NAME
|
|
7
|
+
// used in v3.4.0 and earlier) - NOT from the current npm package name
|
|
8
|
+
// 'homebridge-tuya-plus'. v3.5.0 broke this and reset every device in
|
|
9
|
+
// HomeKit; v3.5.1 restored it.
|
|
10
|
+
//
|
|
11
|
+
// These tests fail loudly if anyone "cleans up" the seed back to PLUGIN_NAME.
|
|
12
|
+
|
|
13
|
+
const crypto = require('crypto');
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
|
|
17
|
+
const indexSource = fs.readFileSync(path.join(__dirname, '..', 'index.js'), 'utf8');
|
|
18
|
+
|
|
19
|
+
// HAP-NodeJS's uuid.generate algorithm, reproduced here so the test does not
|
|
20
|
+
// require pulling in homebridge or hap-nodejs. Matches
|
|
21
|
+
// https://github.com/homebridge/HAP-NodeJS/blob/master/src/lib/util/uuid.ts
|
|
22
|
+
function hapGenerate(data) {
|
|
23
|
+
const sha1 = crypto.createHash('sha1').update(data).digest('hex');
|
|
24
|
+
let i = -1;
|
|
25
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
26
|
+
i += 1;
|
|
27
|
+
if (c === 'y') return ((parseInt(sha1[i], 16) & 0x3) | 0x8).toString(16);
|
|
28
|
+
return sha1[i];
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('index.js UUID seed constants', () => {
|
|
33
|
+
test("UUID_SEED is 'homebridge-tuya' (legacy v3.4.0 PLUGIN_NAME)", () => {
|
|
34
|
+
expect(indexSource).toMatch(/const\s+UUID_SEED\s*=\s*'homebridge-tuya'\s*;/);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("PLUGIN_NAME is 'homebridge-tuya-plus' (current npm package name)", () => {
|
|
38
|
+
expect(indexSource).toMatch(/const\s+PLUGIN_NAME\s*=\s*'homebridge-tuya-plus'\s*;/);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('every UUID.generate(...) call uses UUID_SEED, never PLUGIN_NAME', () => {
|
|
42
|
+
const calls = indexSource.match(/UUID\.generate\([^)]*\)/g) || [];
|
|
43
|
+
expect(calls.length).toBeGreaterThan(0);
|
|
44
|
+
for (const call of calls) {
|
|
45
|
+
expect(call).toMatch(/UUID_SEED/);
|
|
46
|
+
expect(call).not.toMatch(/PLUGIN_NAME/);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('UUID stability against v3.4.0', () => {
|
|
52
|
+
// Pinned outputs derived from `homebridge-tuya:<deviceId>` (the v3.4.0
|
|
53
|
+
// seed). If any of these change, every existing user's HomeKit identity
|
|
54
|
+
// resets on upgrade.
|
|
55
|
+
const fixtures = [
|
|
56
|
+
{
|
|
57
|
+
seed: 'homebridge-tuya:bf1234567890abcdef1234',
|
|
58
|
+
uuid: 'f7bd0a51-7bb8-4405-9ea6-d3da81023f2f',
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
seed: 'homebridge-tuya:fake:device-001',
|
|
62
|
+
uuid: '4922e84f-cbd0-415f-8ad4-43d7891ace87',
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
test.each(fixtures)('UUID for "$seed" is stable', ({ seed, uuid }) => {
|
|
67
|
+
expect(hapGenerate(seed)).toBe(uuid);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('legacy seed and v3.5.0 broken seed produce different UUIDs', () => {
|
|
71
|
+
const id = 'bf1234567890abcdef1234';
|
|
72
|
+
expect(hapGenerate('homebridge-tuya:' + id))
|
|
73
|
+
.not.toBe(hapGenerate('homebridge-tuya-plus:' + id));
|
|
74
|
+
});
|
|
75
|
+
});
|