@homebridge-plugins/homebridge-tuya 2.6.0 → 2.7.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.
- package/CHANGELOG.md +39 -0
- package/README.md +10 -10
- package/SUPPORTED_DEVICES.md +2 -1
- package/dist/accessory/AccessoryFactory.js +7 -0
- package/dist/accessory/AccessoryFactory.js.map +1 -1
- package/dist/accessory/BaseAccessory.js +2 -2
- package/dist/accessory/BaseAccessory.js.map +1 -1
- package/dist/accessory/CatToiletAccessory.js +130 -0
- package/dist/accessory/CatToiletAccessory.js.map +1 -0
- package/dist/accessory/FanAccessory.js +14 -2
- package/dist/accessory/FanAccessory.js.map +1 -1
- package/dist/accessory/IRAirConditionerAccessory.js +2 -2
- package/dist/accessory/IRAirConditionerAccessory.js.map +1 -1
- package/dist/accessory/LocationWeatherAccessory.js +2 -2
- package/dist/accessory/LocationWeatherAccessory.js.map +1 -1
- package/dist/accessory/LockAccessory.js +254 -3
- package/dist/accessory/LockAccessory.js.map +1 -1
- package/dist/accessory/SaunaAccessory.js +144 -0
- package/dist/accessory/SaunaAccessory.js.map +1 -0
- package/dist/accessory/SecuritySystemAccessory.js +2 -2
- package/dist/accessory/SecuritySystemAccessory.js.map +1 -1
- package/dist/accessory/characteristic/Light.js +25 -4
- package/dist/accessory/characteristic/Light.js.map +1 -1
- package/dist/core/TuyaOpenAPI.js +20 -6
- package/dist/core/TuyaOpenAPI.js.map +1 -1
- package/dist/core/TuyaOpenMQ.js +0 -1
- package/dist/core/TuyaOpenMQ.js.map +1 -1
- package/dist/device/TuyaDeviceManager.js +37 -0
- package/dist/device/TuyaDeviceManager.js.map +1 -1
- package/dist/platform.js +0 -1
- package/dist/platform.js.map +1 -1
- package/dist/util/Logger.js +15 -13
- package/dist/util/Logger.js.map +1 -1
- package/dist/util/TuyaStreamDelegate.js.map +1 -1
- package/dist/util/util.js +48 -0
- package/dist/util/util.js.map +1 -1
- package/package.json +1 -1
- package/test/FanAccessory.test.ts +457 -0
- package/test/Light.test.ts +186 -0
- package/test/accessory/lockAccessory.test.ts +549 -0
package/dist/util/util.js
CHANGED
|
@@ -7,6 +7,8 @@ exports.sanitizeName = sanitizeName;
|
|
|
7
7
|
exports.uuidFromSeed = uuidFromSeed;
|
|
8
8
|
exports.updateBase64 = updateBase64;
|
|
9
9
|
exports.updateHex = updateHex;
|
|
10
|
+
exports.maskSecret = maskSecret;
|
|
11
|
+
exports.redactSensitive = redactSensitive;
|
|
10
12
|
const crypto_1 = require("crypto");
|
|
11
13
|
function remap(value, srcStart, srcEnd, dstStart, dstEnd) {
|
|
12
14
|
const percent = (value - srcStart) / (srcEnd - srcStart);
|
|
@@ -101,4 +103,50 @@ function updateBuffer(buf, byteIndex, value) {
|
|
|
101
103
|
valueBuf.copy(buf, byteIndex);
|
|
102
104
|
return buf;
|
|
103
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Keys whose values must never reach the log, no matter which log level is
|
|
108
|
+
* active. Note that `PrefixLogger` disables its own masking in debug mode
|
|
109
|
+
* (`mask = !debugMode`), so redaction has to happen at the call site.
|
|
110
|
+
*/
|
|
111
|
+
const SENSITIVE_KEY_PATTERN = /(password|secret|access_?key|access_?token|refresh_?token|ticket_key|api_?key|tuya_?key)/i;
|
|
112
|
+
/**
|
|
113
|
+
* Mask an identifier that is not secret enough to print, but useful enough to
|
|
114
|
+
* correlate log lines (e.g. a smart lock `ticket_id`).
|
|
115
|
+
*/
|
|
116
|
+
function maskSecret(value, visible = 4) {
|
|
117
|
+
if (value === undefined || value === null) {
|
|
118
|
+
return '<none>';
|
|
119
|
+
}
|
|
120
|
+
const str = `${value}`;
|
|
121
|
+
if (str.length === 0) {
|
|
122
|
+
return '<empty>';
|
|
123
|
+
}
|
|
124
|
+
if (str.length <= visible) {
|
|
125
|
+
return '***';
|
|
126
|
+
}
|
|
127
|
+
return `***${str.slice(-visible)}`;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Deep-copy a JSON-ish value, replacing the values of sensitive keys with
|
|
131
|
+
* `***`. Used before dumping API payloads to the log.
|
|
132
|
+
*/
|
|
133
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
134
|
+
function redactSensitive(value, depth = 0) {
|
|
135
|
+
if (depth > 10 || value === null || typeof value !== 'object') {
|
|
136
|
+
return value;
|
|
137
|
+
}
|
|
138
|
+
if (Array.isArray(value)) {
|
|
139
|
+
return value.map(item => redactSensitive(item, depth + 1));
|
|
140
|
+
}
|
|
141
|
+
const result = {};
|
|
142
|
+
for (const [key, item] of Object.entries(value)) {
|
|
143
|
+
if (SENSITIVE_KEY_PATTERN.test(key)) {
|
|
144
|
+
result[key] = '***';
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
result[key] = redactSensitive(item, depth + 1);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
104
152
|
//# sourceMappingURL=util.js.map
|
package/dist/util/util.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"util.js","sourceRoot":"","sources":["../../src/util/util.ts"],"names":[],"mappings":";;AAGA,sBAUC;AAED,sBASC;AAED,sCA+BC;AAED,oCAcC;AAGD,oCAUC;AACD,oCAGC;AAED,8BAGC;
|
|
1
|
+
{"version":3,"file":"util.js","sourceRoot":"","sources":["../../src/util/util.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homebridge-plugins/homebridge-tuya",
|
|
3
3
|
"displayName": "Homebridge Tuya",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.7.0",
|
|
5
5
|
"description": "Forked from 0x5e/homebridge-tuya-platform, with a focus on fixing bugs and adding new device support.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { describe, expect, test, jest, beforeEach } from '@jest/globals';
|
|
3
|
+
import { API, PlatformAccessory } from 'homebridge';
|
|
4
|
+
import FanAccessory from '../src/accessory/FanAccessory';
|
|
5
|
+
import TuyaDevice, { TuyaDeviceSchemaMode, TuyaDeviceSchemaType } from '../src/device/TuyaDevice';
|
|
6
|
+
import { TuyaPlatform } from '../src/platform';
|
|
7
|
+
|
|
8
|
+
const mockConfigureLight = jest.fn();
|
|
9
|
+
const mockConfigureOn = jest.fn();
|
|
10
|
+
jest.mock('../src/accessory/characteristic/Light', () => ({
|
|
11
|
+
configureLight: (...args: any[]) => mockConfigureLight(...args),
|
|
12
|
+
}));
|
|
13
|
+
jest.mock('../src/accessory/characteristic/On', () => ({
|
|
14
|
+
configureOn: (...args: any[]) => mockConfigureOn(...args),
|
|
15
|
+
}));
|
|
16
|
+
jest.mock('../src/accessory/characteristic/Active');
|
|
17
|
+
jest.mock('../src/accessory/characteristic/RotationSpeed');
|
|
18
|
+
jest.mock('../src/accessory/characteristic/SwingMode');
|
|
19
|
+
jest.mock('../src/accessory/characteristic/LockPhysicalControls');
|
|
20
|
+
|
|
21
|
+
describe('FanAccessory', () => {
|
|
22
|
+
let mockPlatform: any;
|
|
23
|
+
let mockAccessory: any;
|
|
24
|
+
let mockAPI: any;
|
|
25
|
+
let mockDeviceManager: any;
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
mockConfigureLight.mockClear();
|
|
29
|
+
mockConfigureOn.mockClear();
|
|
30
|
+
|
|
31
|
+
mockAPI = {
|
|
32
|
+
hap: {
|
|
33
|
+
Service: {
|
|
34
|
+
Fan: jest.fn(),
|
|
35
|
+
Fanv2: jest.fn(),
|
|
36
|
+
Lightbulb: jest.fn(),
|
|
37
|
+
Switch: jest.fn(),
|
|
38
|
+
AccessoryInformation: jest.fn(),
|
|
39
|
+
},
|
|
40
|
+
Characteristic: {
|
|
41
|
+
On: jest.fn(),
|
|
42
|
+
Active: jest.fn(),
|
|
43
|
+
RotationSpeed: jest.fn(),
|
|
44
|
+
RotationDirection: jest.fn(),
|
|
45
|
+
Brightness: jest.fn(),
|
|
46
|
+
},
|
|
47
|
+
uuid: {
|
|
48
|
+
generate: jest.fn(() => 'mock-uuid'),
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
user: {
|
|
52
|
+
persistPath: jest.fn(() => '/mock/path'),
|
|
53
|
+
},
|
|
54
|
+
} as unknown as API;
|
|
55
|
+
|
|
56
|
+
mockDeviceManager = {
|
|
57
|
+
getDevice: jest.fn(),
|
|
58
|
+
sendCommands: jest.fn(),
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
mockPlatform = {
|
|
62
|
+
api: mockAPI,
|
|
63
|
+
Service: mockAPI.hap.Service,
|
|
64
|
+
Characteristic: mockAPI.hap.Characteristic,
|
|
65
|
+
log: {
|
|
66
|
+
info: jest.fn(),
|
|
67
|
+
warn: jest.fn(),
|
|
68
|
+
error: jest.fn(),
|
|
69
|
+
debug: jest.fn(),
|
|
70
|
+
},
|
|
71
|
+
options: {
|
|
72
|
+
debug: false,
|
|
73
|
+
debugLevel: '',
|
|
74
|
+
},
|
|
75
|
+
deviceManager: mockDeviceManager,
|
|
76
|
+
getDeviceConfig: jest.fn(() => undefined),
|
|
77
|
+
getDeviceSchemaConfig: jest.fn(() => undefined),
|
|
78
|
+
} as unknown as TuyaPlatform;
|
|
79
|
+
|
|
80
|
+
mockAccessory = {
|
|
81
|
+
UUID: 'mock-uuid',
|
|
82
|
+
displayName: 'Test Fan',
|
|
83
|
+
context: {
|
|
84
|
+
deviceID: 'test-device-id',
|
|
85
|
+
},
|
|
86
|
+
services: [],
|
|
87
|
+
getService: jest.fn((name: string) => {
|
|
88
|
+
return mockAccessory.services.find((s: any) => s.displayName === name || s.UUID === name);
|
|
89
|
+
}),
|
|
90
|
+
addService: jest.fn((serviceType: any, name?: string, subtype?: string) => {
|
|
91
|
+
const service = {
|
|
92
|
+
UUID: subtype || name || 'mock-service',
|
|
93
|
+
displayName: name || 'Mock Service',
|
|
94
|
+
subtype: subtype,
|
|
95
|
+
getCharacteristic: jest.fn(() => ({
|
|
96
|
+
onGet: jest.fn().mockReturnThis(),
|
|
97
|
+
onSet: jest.fn().mockReturnThis(),
|
|
98
|
+
setProps: jest.fn().mockReturnThis(),
|
|
99
|
+
updateValue: jest.fn().mockReturnThis(),
|
|
100
|
+
})),
|
|
101
|
+
setCharacteristic: jest.fn().mockReturnThis(),
|
|
102
|
+
};
|
|
103
|
+
mockAccessory.services.push(service);
|
|
104
|
+
return service;
|
|
105
|
+
}),
|
|
106
|
+
removeService: jest.fn(),
|
|
107
|
+
} as unknown as PlatformAccessory;
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
function createMockDevice(codes: string[]): TuyaDevice {
|
|
111
|
+
const schema = codes.map(code => ({
|
|
112
|
+
code,
|
|
113
|
+
mode: TuyaDeviceSchemaMode.READ_WRITE,
|
|
114
|
+
type: code.includes('bright') ? TuyaDeviceSchemaType.Integer : TuyaDeviceSchemaType.Boolean,
|
|
115
|
+
property: code.includes('bright')
|
|
116
|
+
? { min: 10, max: 1000, scale: 0, step: 1 }
|
|
117
|
+
: {},
|
|
118
|
+
}));
|
|
119
|
+
|
|
120
|
+
const status = codes.map(code => ({
|
|
121
|
+
code,
|
|
122
|
+
value: code.includes('bright') ? 500 : true,
|
|
123
|
+
}));
|
|
124
|
+
|
|
125
|
+
return new TuyaDevice({
|
|
126
|
+
id: 'test-device-id',
|
|
127
|
+
uuid: 'test-uuid',
|
|
128
|
+
name: 'Test Fan',
|
|
129
|
+
online: true,
|
|
130
|
+
owner_id: 'owner-1',
|
|
131
|
+
product_id: 'fs',
|
|
132
|
+
product_name: 'Smart Fan',
|
|
133
|
+
category: 'fs',
|
|
134
|
+
schema,
|
|
135
|
+
status,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function setupAndConfigure(codes: string[]) {
|
|
140
|
+
const device = createMockDevice(codes);
|
|
141
|
+
mockDeviceManager.getDevice.mockReturnValue(device);
|
|
142
|
+
const fanAccessory = new FanAccessory(mockPlatform, mockAccessory);
|
|
143
|
+
fanAccessory.configureServices();
|
|
144
|
+
return fanAccessory;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function getDualLightServices() {
|
|
148
|
+
return mockAccessory.addService.mock.calls.filter((call: any[]) =>
|
|
149
|
+
call[1] === 'Warm Light' || call[1] === 'White Light',
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function expectNoDualLight() {
|
|
154
|
+
expect(getDualLightServices().length).toBe(0);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function expectLightCall(index: number, onCode: string, brightCode?: string) {
|
|
158
|
+
const args = mockConfigureLight.mock.calls[index];
|
|
159
|
+
expect(args[2]).toEqual(expect.objectContaining({ code: onCode }));
|
|
160
|
+
if (brightCode) {
|
|
161
|
+
expect(args[3]).toEqual(expect.objectContaining({ code: brightCode }));
|
|
162
|
+
} else {
|
|
163
|
+
expect(args[3]).toBeUndefined();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ─── Fan service type ─────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
describe('fan service type', () => {
|
|
170
|
+
test('should use Fanv2 when child_lock present', () => {
|
|
171
|
+
const fanAccessory = setupAndConfigure(['switch', 'fan_speed', 'child_lock']);
|
|
172
|
+
expect(fanAccessory.fanServiceType()).toBe(mockAPI.hap.Service.Fanv2);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('should use Fanv2 when swing present', () => {
|
|
176
|
+
const fanAccessory = setupAndConfigure(['switch', 'fan_speed', 'switch_horizontal']);
|
|
177
|
+
expect(fanAccessory.fanServiceType()).toBe(mockAPI.hap.Service.Fanv2);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('should use Fan when no lock or swing present', () => {
|
|
181
|
+
const fanAccessory = setupAndConfigure(['switch', 'fan_speed']);
|
|
182
|
+
expect(fanAccessory.fanServiceType()).toBe(mockAPI.hap.Service.Fan);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// ─── A. Dual-light path ───────────────────────────────────────────
|
|
187
|
+
// Requires ALL 4: light + bright_value + switch_led + bright_value_1
|
|
188
|
+
|
|
189
|
+
describe('dual-light (all 4 DPs present)', () => {
|
|
190
|
+
test('light + bright_value + switch_led + bright_value_1', () => {
|
|
191
|
+
setupAndConfigure([
|
|
192
|
+
'switch', 'fan_speed',
|
|
193
|
+
'light', 'bright_value', 'switch_led', 'bright_value_1',
|
|
194
|
+
]);
|
|
195
|
+
|
|
196
|
+
const lightServices = getDualLightServices();
|
|
197
|
+
expect(lightServices.length).toBe(2);
|
|
198
|
+
expect(lightServices[0][1]).toBe('Warm Light');
|
|
199
|
+
expect(lightServices[0][2]).toBe('warm_light');
|
|
200
|
+
expect(lightServices[1][1]).toBe('White Light');
|
|
201
|
+
expect(lightServices[1][2]).toBe('white_light');
|
|
202
|
+
|
|
203
|
+
expect(mockConfigureLight).toHaveBeenCalledTimes(2);
|
|
204
|
+
expectLightCall(0, 'light', 'bright_value');
|
|
205
|
+
expectLightCall(1, 'switch_led', 'bright_value_1');
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test('dual-light with extra bright_value_v2 (ignored)', () => {
|
|
209
|
+
setupAndConfigure([
|
|
210
|
+
'switch', 'fan_speed',
|
|
211
|
+
'light', 'bright_value', 'bright_value_v2', 'switch_led', 'bright_value_1',
|
|
212
|
+
]);
|
|
213
|
+
|
|
214
|
+
expect(getDualLightServices().length).toBe(2);
|
|
215
|
+
expect(mockConfigureLight).toHaveBeenCalledTimes(2);
|
|
216
|
+
expectLightCall(0, 'light', 'bright_value');
|
|
217
|
+
expectLightCall(1, 'switch_led', 'bright_value_1');
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test('dual-light with extra temp_value (not passed to dual-light configureLight)', () => {
|
|
221
|
+
setupAndConfigure([
|
|
222
|
+
'switch', 'fan_speed',
|
|
223
|
+
'light', 'bright_value', 'switch_led', 'bright_value_1', 'temp_value',
|
|
224
|
+
]);
|
|
225
|
+
|
|
226
|
+
expect(getDualLightServices().length).toBe(2);
|
|
227
|
+
expect(mockConfigureLight).toHaveBeenCalledTimes(2);
|
|
228
|
+
// Dual-light calls only pass on+bright, not temp/color/mode
|
|
229
|
+
expect(mockConfigureLight.mock.calls[0].length).toBe(4);
|
|
230
|
+
expect(mockConfigureLight.mock.calls[1].length).toBe(4);
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// ─── B. Single-light Lightbulb path ───────────────────────────────
|
|
235
|
+
// Has LIGHT_ON match + at least one of: bright_value/v2, temp_value/v2, colour_data, work_mode
|
|
236
|
+
|
|
237
|
+
describe('single-light as Lightbulb', () => {
|
|
238
|
+
test('light + bright_value', () => {
|
|
239
|
+
setupAndConfigure(['switch', 'fan_speed', 'light', 'bright_value']);
|
|
240
|
+
|
|
241
|
+
expectNoDualLight();
|
|
242
|
+
expect(mockConfigureLight).toHaveBeenCalledTimes(1);
|
|
243
|
+
expectLightCall(0, 'light', 'bright_value');
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test('light + bright_value_v2', () => {
|
|
247
|
+
setupAndConfigure(['switch', 'fan_speed', 'light', 'bright_value_v2']);
|
|
248
|
+
|
|
249
|
+
expectNoDualLight();
|
|
250
|
+
expect(mockConfigureLight).toHaveBeenCalledTimes(1);
|
|
251
|
+
expectLightCall(0, 'light', 'bright_value_v2');
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test('switch_led + bright_value', () => {
|
|
255
|
+
setupAndConfigure(['switch', 'fan_speed', 'switch_led', 'bright_value']);
|
|
256
|
+
|
|
257
|
+
expectNoDualLight();
|
|
258
|
+
expect(mockConfigureLight).toHaveBeenCalledTimes(1);
|
|
259
|
+
expectLightCall(0, 'switch_led', 'bright_value');
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test('switch_led + bright_value_v2', () => {
|
|
263
|
+
setupAndConfigure(['switch', 'fan_speed', 'switch_led', 'bright_value_v2']);
|
|
264
|
+
|
|
265
|
+
expectNoDualLight();
|
|
266
|
+
expect(mockConfigureLight).toHaveBeenCalledTimes(1);
|
|
267
|
+
expectLightCall(0, 'switch_led', 'bright_value_v2');
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test('light + bright_value + both bright versions (prefers bright_value)', () => {
|
|
271
|
+
setupAndConfigure(['switch', 'fan_speed', 'light', 'bright_value', 'bright_value_v2']);
|
|
272
|
+
|
|
273
|
+
expectNoDualLight();
|
|
274
|
+
expect(mockConfigureLight).toHaveBeenCalledTimes(1);
|
|
275
|
+
expectLightCall(0, 'light', 'bright_value');
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test('light + temp_value only (no brightness)', () => {
|
|
279
|
+
setupAndConfigure(['switch', 'fan_speed', 'light', 'temp_value']);
|
|
280
|
+
|
|
281
|
+
expectNoDualLight();
|
|
282
|
+
expect(mockConfigureLight).toHaveBeenCalledTimes(1);
|
|
283
|
+
const args = mockConfigureLight.mock.calls[0];
|
|
284
|
+
expect(args[2]).toEqual(expect.objectContaining({ code: 'light' }));
|
|
285
|
+
expect(args[3]).toBeUndefined(); // no brightness DP
|
|
286
|
+
expect(args[4]).toEqual(expect.objectContaining({ code: 'temp_value' }));
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test('switch_led + temp_value_v2 only', () => {
|
|
290
|
+
setupAndConfigure(['switch', 'fan_speed', 'switch_led', 'temp_value_v2']);
|
|
291
|
+
|
|
292
|
+
expectNoDualLight();
|
|
293
|
+
expect(mockConfigureLight).toHaveBeenCalledTimes(1);
|
|
294
|
+
const args = mockConfigureLight.mock.calls[0];
|
|
295
|
+
expect(args[2]).toEqual(expect.objectContaining({ code: 'switch_led' }));
|
|
296
|
+
expect(args[4]).toEqual(expect.objectContaining({ code: 'temp_value_v2' }));
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test('light + bright_value + temp_value + colour_data + work_mode (full feature set)', () => {
|
|
300
|
+
setupAndConfigure([
|
|
301
|
+
'switch', 'fan_speed',
|
|
302
|
+
'light', 'bright_value', 'temp_value', 'colour_data', 'work_mode',
|
|
303
|
+
]);
|
|
304
|
+
|
|
305
|
+
expectNoDualLight();
|
|
306
|
+
expect(mockConfigureLight).toHaveBeenCalledTimes(1);
|
|
307
|
+
const args = mockConfigureLight.mock.calls[0];
|
|
308
|
+
expect(args[2]).toEqual(expect.objectContaining({ code: 'light' }));
|
|
309
|
+
expect(args[3]).toEqual(expect.objectContaining({ code: 'bright_value' }));
|
|
310
|
+
expect(args[4]).toEqual(expect.objectContaining({ code: 'temp_value' }));
|
|
311
|
+
expect(args[5]).toEqual(expect.objectContaining({ code: 'colour_data' }));
|
|
312
|
+
expect(args[6]).toEqual(expect.objectContaining({ code: 'work_mode' }));
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test('both on/off DPs + switch_led fallback: prefers light (LIGHT_ON order)', () => {
|
|
316
|
+
setupAndConfigure(['switch', 'fan_speed', 'light', 'switch_led', 'bright_value']);
|
|
317
|
+
|
|
318
|
+
expectNoDualLight();
|
|
319
|
+
expect(mockConfigureLight).toHaveBeenCalledTimes(1);
|
|
320
|
+
expectLightCall(0, 'light', 'bright_value');
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// ─── C. Single-light Switch path ──────────────────────────────────
|
|
325
|
+
// Has LIGHT_ON match but NO brightness/temp/color/mode DPs
|
|
326
|
+
|
|
327
|
+
describe('single-light as Switch (on/off only)', () => {
|
|
328
|
+
test('light only', () => {
|
|
329
|
+
setupAndConfigure(['switch', 'fan_speed', 'light']);
|
|
330
|
+
|
|
331
|
+
expectNoDualLight();
|
|
332
|
+
expect(mockConfigureLight).not.toHaveBeenCalled();
|
|
333
|
+
expect(mockConfigureOn).toHaveBeenCalledWith(
|
|
334
|
+
expect.anything(),
|
|
335
|
+
undefined,
|
|
336
|
+
expect.objectContaining({ code: 'light' }),
|
|
337
|
+
);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test('switch_led only', () => {
|
|
341
|
+
setupAndConfigure(['switch', 'fan_speed', 'switch_led']);
|
|
342
|
+
|
|
343
|
+
expectNoDualLight();
|
|
344
|
+
expect(mockConfigureLight).not.toHaveBeenCalled();
|
|
345
|
+
expect(mockConfigureOn).toHaveBeenCalledWith(
|
|
346
|
+
expect.anything(),
|
|
347
|
+
undefined,
|
|
348
|
+
expect.objectContaining({ code: 'switch_led' }),
|
|
349
|
+
);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test('light + switch_led, no brightness (prefers light)', () => {
|
|
353
|
+
setupAndConfigure(['switch', 'fan_speed', 'light', 'switch_led']);
|
|
354
|
+
|
|
355
|
+
expectNoDualLight();
|
|
356
|
+
expect(mockConfigureLight).not.toHaveBeenCalled();
|
|
357
|
+
expect(mockConfigureOn).toHaveBeenCalledWith(
|
|
358
|
+
expect.anything(),
|
|
359
|
+
undefined,
|
|
360
|
+
expect.objectContaining({ code: 'light' }),
|
|
361
|
+
);
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
// ─── D. No light ──────────────────────────────────────────────────
|
|
366
|
+
|
|
367
|
+
describe('no light at all', () => {
|
|
368
|
+
test('fan-only device', () => {
|
|
369
|
+
setupAndConfigure(['switch', 'fan_speed']);
|
|
370
|
+
|
|
371
|
+
expectNoDualLight();
|
|
372
|
+
expect(mockConfigureLight).not.toHaveBeenCalled();
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
test('brightness DPs without any on/off DP are ignored', () => {
|
|
376
|
+
setupAndConfigure(['switch', 'fan_speed', 'bright_value']);
|
|
377
|
+
|
|
378
|
+
expectNoDualLight();
|
|
379
|
+
expect(mockConfigureLight).not.toHaveBeenCalled();
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
test('bright_value_1 alone is ignored', () => {
|
|
383
|
+
setupAndConfigure(['switch', 'fan_speed', 'bright_value_1']);
|
|
384
|
+
|
|
385
|
+
expectNoDualLight();
|
|
386
|
+
expect(mockConfigureLight).not.toHaveBeenCalled();
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
// ─── E. Edge cases: dual-light guard must NOT match ────────────────
|
|
391
|
+
// These are the critical backward-compatibility scenarios.
|
|
392
|
+
// Each has 3 of the 4 required DPs but not the 4th,
|
|
393
|
+
// so must fall through to the single-light path.
|
|
394
|
+
|
|
395
|
+
describe('partial dual-light DPs (must NOT trigger dual-light)', () => {
|
|
396
|
+
test('missing bright_value_1: light + bright_value + switch_led', () => {
|
|
397
|
+
setupAndConfigure([
|
|
398
|
+
'switch', 'fan_speed',
|
|
399
|
+
'light', 'bright_value', 'switch_led',
|
|
400
|
+
]);
|
|
401
|
+
|
|
402
|
+
expectNoDualLight();
|
|
403
|
+
expect(mockConfigureLight).toHaveBeenCalledTimes(1);
|
|
404
|
+
expectLightCall(0, 'light', 'bright_value');
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
test('missing switch_led: light + bright_value + bright_value_1', () => {
|
|
408
|
+
setupAndConfigure([
|
|
409
|
+
'switch', 'fan_speed',
|
|
410
|
+
'light', 'bright_value', 'bright_value_1',
|
|
411
|
+
]);
|
|
412
|
+
|
|
413
|
+
expectNoDualLight();
|
|
414
|
+
expect(mockConfigureLight).toHaveBeenCalledTimes(1);
|
|
415
|
+
expectLightCall(0, 'light', 'bright_value');
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
test('missing bright_value: light + switch_led + bright_value_1', () => {
|
|
419
|
+
setupAndConfigure([
|
|
420
|
+
'switch', 'fan_speed',
|
|
421
|
+
'light', 'switch_led', 'bright_value_1',
|
|
422
|
+
]);
|
|
423
|
+
|
|
424
|
+
expectNoDualLight();
|
|
425
|
+
// lightServiceType: no bright_value/v2, no temp, no color, no mode → Switch
|
|
426
|
+
expect(mockConfigureLight).not.toHaveBeenCalled();
|
|
427
|
+
expect(mockConfigureOn).toHaveBeenCalledWith(
|
|
428
|
+
expect.anything(),
|
|
429
|
+
undefined,
|
|
430
|
+
expect.objectContaining({ code: 'light' }),
|
|
431
|
+
);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
test('missing light: switch_led + bright_value + bright_value_1', () => {
|
|
435
|
+
setupAndConfigure([
|
|
436
|
+
'switch', 'fan_speed',
|
|
437
|
+
'switch_led', 'bright_value', 'bright_value_1',
|
|
438
|
+
]);
|
|
439
|
+
|
|
440
|
+
expectNoDualLight();
|
|
441
|
+
expect(mockConfigureLight).toHaveBeenCalledTimes(1);
|
|
442
|
+
expectLightCall(0, 'switch_led', 'bright_value');
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
test('has bright_value_v2 instead of bright_value: light + bright_value_v2 + switch_led + bright_value_1', () => {
|
|
446
|
+
setupAndConfigure([
|
|
447
|
+
'switch', 'fan_speed',
|
|
448
|
+
'light', 'bright_value_v2', 'switch_led', 'bright_value_1',
|
|
449
|
+
]);
|
|
450
|
+
|
|
451
|
+
// bright_value is MISSING so dual-light guard fails
|
|
452
|
+
expectNoDualLight();
|
|
453
|
+
expect(mockConfigureLight).toHaveBeenCalledTimes(1);
|
|
454
|
+
expectLightCall(0, 'light', 'bright_value_v2');
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
});
|