@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.
Files changed (40) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +10 -10
  3. package/SUPPORTED_DEVICES.md +2 -1
  4. package/dist/accessory/AccessoryFactory.js +7 -0
  5. package/dist/accessory/AccessoryFactory.js.map +1 -1
  6. package/dist/accessory/BaseAccessory.js +2 -2
  7. package/dist/accessory/BaseAccessory.js.map +1 -1
  8. package/dist/accessory/CatToiletAccessory.js +130 -0
  9. package/dist/accessory/CatToiletAccessory.js.map +1 -0
  10. package/dist/accessory/FanAccessory.js +14 -2
  11. package/dist/accessory/FanAccessory.js.map +1 -1
  12. package/dist/accessory/IRAirConditionerAccessory.js +2 -2
  13. package/dist/accessory/IRAirConditionerAccessory.js.map +1 -1
  14. package/dist/accessory/LocationWeatherAccessory.js +2 -2
  15. package/dist/accessory/LocationWeatherAccessory.js.map +1 -1
  16. package/dist/accessory/LockAccessory.js +254 -3
  17. package/dist/accessory/LockAccessory.js.map +1 -1
  18. package/dist/accessory/SaunaAccessory.js +144 -0
  19. package/dist/accessory/SaunaAccessory.js.map +1 -0
  20. package/dist/accessory/SecuritySystemAccessory.js +2 -2
  21. package/dist/accessory/SecuritySystemAccessory.js.map +1 -1
  22. package/dist/accessory/characteristic/Light.js +25 -4
  23. package/dist/accessory/characteristic/Light.js.map +1 -1
  24. package/dist/core/TuyaOpenAPI.js +20 -6
  25. package/dist/core/TuyaOpenAPI.js.map +1 -1
  26. package/dist/core/TuyaOpenMQ.js +0 -1
  27. package/dist/core/TuyaOpenMQ.js.map +1 -1
  28. package/dist/device/TuyaDeviceManager.js +37 -0
  29. package/dist/device/TuyaDeviceManager.js.map +1 -1
  30. package/dist/platform.js +0 -1
  31. package/dist/platform.js.map +1 -1
  32. package/dist/util/Logger.js +15 -13
  33. package/dist/util/Logger.js.map +1 -1
  34. package/dist/util/TuyaStreamDelegate.js.map +1 -1
  35. package/dist/util/util.js +48 -0
  36. package/dist/util/util.js.map +1 -1
  37. package/package.json +1 -1
  38. package/test/FanAccessory.test.ts +457 -0
  39. package/test/Light.test.ts +186 -0
  40. 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
@@ -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;AA/FD,mCAAoC;AAGpC,SAAgB,KAAK,CACnB,KAAa,EACb,QAAgB,EAChB,MAAc,EACd,QAAgB,EAChB,MAAc;IAEd,MAAM,OAAO,GAAG,CAAC,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,MAAM,GAAG,QAAQ,CAAC,CAAC;IACzD,MAAM,MAAM,GAAG,OAAO,GAAG,CAAC,MAAM,GAAG,QAAQ,CAAC,GAAG,QAAQ,CAAC;IACxD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAgB,KAAK,CACnB,KAAa,EACb,KAAa,EACb,GAAW;IAEX,IAAI,MAAM,GAAG,KAAK,CAAC;IACnB,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IAC/B,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;IACjC,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAgB,aAAa,CAC3B,QAAkC;IAElC,OAAO,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE;QAC3D,QAAQ,GAAG,EAAE,CAAC;YACZ,KAAK,KAAK,CAAC,CAAC,CAAC;gBACX,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;gBAChE,GAAG,CAAC,UAAU,CAAC,GAAG,KAAK,GAAG,QAAQ,CAAC;gBACnC,MAAM;YACR,CAAC;YACD,KAAK,KAAK,CAAC,CAAC,CAAC;gBACX,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;gBAChE,GAAG,CAAC,UAAU,CAAC,GAAG,KAAK,GAAG,QAAQ,CAAC;gBACnC,MAAM;YACR,CAAC;YACD,KAAK,MAAM,CAAC,CAAC,CAAC;gBACZ,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;gBAChE,GAAG,CAAC,SAAS,CAAC,GAAG,KAAK,GAAG,QAAQ,CAAC;gBAClC,MAAM;YACR,CAAC;YACD,KAAK,OAAO,CAAC,CAAC,CAAC;gBACb,GAAG,CAAC,aAAa,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC;gBAChC,MAAM;YACR,CAAC;YACD,OAAO,CAAC,CAAC,CAAC;gBACR,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;gBACjB,MAAM;YACR,CAAC;QACH,CAAC;QACD,OAAO,GAAG,CAAC;IACb,CAAC,EAAE,EAAE,CAAC,CAAC;AACT,CAAC;AAED,SAAgB,YAAY,CAAC,IAAa;IACxC,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,+FAA+F;IAC/F,qFAAqF;IACrF,IAAI,CAAC,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,oBAAoB,EAAE,GAAG,CAAC,CAAC;IAC3D,sBAAsB;IACtB,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IAClC,iEAAiE;IACjE,IAAI,CAAC,+BAA+B,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7C,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC;AAGD,SAAgB,YAAY,CAAC,IAAY;IACvC,MAAM,IAAI,GAAG,IAAA,mBAAU,EAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAE7D,OAAO;QACL,IAAI,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC;QACpB,IAAI,CAAC,SAAS,CAAC,CAAC,EAAE,EAAE,CAAC;QACrB,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,YAAY;QAC1C,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,CAAC;QAC1F,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,CAAC;KACvB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACd,CAAC;AACD,SAAgB,YAAY,CAAC,MAAc,EAAE,SAAiB,EAAE,KAAsC;IACpG,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IAC1C,OAAO,YAAY,CAAC,GAAG,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;AAChE,CAAC;AAED,SAAgB,SAAS,CAAC,GAAW,EAAE,SAAiB,EAAE,KAAsC;IAC9F,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,EAAE,KAAK,CAAC,CAAC;IACvD,OAAO,YAAY,CAAC,GAAG,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AAC7D,CAAC;AAED,SAAS,YAAY,CAAC,GAAW,EAAE,SAAiB,EAAE,KAAsC;IAC1F,IAAI,QAAgB,CAAC;IAErB,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC;QAC5D,QAAQ,GAAG,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;QACpC,QAAQ,CAAC,WAAW,CAAC,KAAK,EAAE,CAAC,EAAE,UAAU,CAAC,CAAC;IAC7C,CAAC;SAAM,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;QAClC,QAAQ,GAAG,KAAK,CAAC;IACnB,CAAC;SAAM,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QAChC,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAChC,CAAC;SAAM,CAAC;QACN,MAAM,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAC;IAC/D,CAAC;IAED,QAAQ,CAAC,IAAI,CAAC,GAAiB,EAAE,SAAS,CAAC,CAAC;IAE5C,OAAO,GAAG,CAAC;AACb,CAAC"}
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.6.0",
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
+ });