@homebridge-plugins/homebridge-tuya 2.3.0-beta.6 → 2.4.0-beta.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/ADVANCED_OPTIONS.md +17 -0
- package/CHANGELOG.md +31 -0
- package/README.md +2 -0
- package/config.schema.json +5 -85
- package/dist/accessory/AccessoryFactory.js +0 -1
- package/dist/accessory/AccessoryFactory.js.map +1 -1
- package/dist/accessory/AirConditionerAccessory.js +21 -15
- package/dist/accessory/AirConditionerAccessory.js.map +1 -1
- package/dist/accessory/BaseAccessory.js +1 -29
- package/dist/accessory/BaseAccessory.js.map +1 -1
- package/dist/accessory/GarageDoorAccessory.js +19 -2
- package/dist/accessory/GarageDoorAccessory.js.map +1 -1
- package/dist/accessory/SwitchAccessory.js +6 -3
- package/dist/accessory/SwitchAccessory.js.map +1 -1
- package/dist/accessory/characteristic/EnergyUsage.js.map +1 -1
- package/dist/accessory/characteristic/Name.js +10 -2
- package/dist/accessory/characteristic/Name.js.map +1 -1
- package/dist/accessory/characteristic/On.js +1 -1
- package/dist/accessory/characteristic/On.js.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/core/TuyaOpenAPI.js +2 -3
- package/dist/core/TuyaOpenAPI.js.map +1 -1
- package/dist/core/TuyaOpenMQ.js +2 -3
- package/dist/core/TuyaOpenMQ.js.map +1 -1
- package/dist/device/TuyaDeviceManager.js +2 -3
- package/dist/device/TuyaDeviceManager.js.map +1 -1
- package/dist/platform.js +4 -2
- package/dist/platform.js.map +1 -1
- package/dist/util/Logger.js +69 -9
- package/dist/util/Logger.js.map +1 -1
- package/dist/util/util.js +3 -32
- package/dist/util/util.js.map +1 -1
- package/package.json +5 -3
- package/test/accessory/airConditionerAccessory.test.ts +729 -0
- package/dist/accessory/characteristic/EnumOn.js +0 -48
- package/dist/accessory/characteristic/EnumOn.js.map +0 -1
- package/dist/accessory/characteristic/ExtraCharacteristicAdapter.js +0 -197
- package/dist/accessory/characteristic/ExtraCharacteristicAdapter.js.map +0 -1
- package/dist/accessory/characteristic/RawOn.js +0 -93
- package/dist/accessory/characteristic/RawOn.js.map +0 -1
|
@@ -0,0 +1,729 @@
|
|
|
1
|
+
import { describe, expect, test, beforeEach, jest } from '@jest/globals';
|
|
2
|
+
import AirConditionerAccessory from '../../src/accessory/AirConditionerAccessory';
|
|
3
|
+
import TuyaDevice, { TuyaDeviceSchema, TuyaDeviceStatus, TuyaDeviceSchemaMode, TuyaDeviceSchemaType } from '../../src/device/TuyaDevice';
|
|
4
|
+
|
|
5
|
+
// Mock data
|
|
6
|
+
const mockSchema = (code: string, type: string, mode: string, property: any): TuyaDeviceSchema => ({
|
|
7
|
+
code,
|
|
8
|
+
mode: mode as TuyaDeviceSchemaMode,
|
|
9
|
+
type: type as TuyaDeviceSchemaType,
|
|
10
|
+
property,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const mockStatus = (code: string, value: string | number | boolean): TuyaDeviceStatus => ({
|
|
14
|
+
code,
|
|
15
|
+
value,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// Create mock device with AC capabilities
|
|
19
|
+
const createMockDevice = (overrides?: Partial<TuyaDevice>): TuyaDevice => {
|
|
20
|
+
const device = new TuyaDevice({
|
|
21
|
+
id: 'test-device-001',
|
|
22
|
+
uuid: 'uuid-001',
|
|
23
|
+
name: 'Test Air Conditioner',
|
|
24
|
+
online: true,
|
|
25
|
+
owner_id: 'home-001',
|
|
26
|
+
product_id: 'ac-product-001',
|
|
27
|
+
product_name: 'Smart Air Conditioner',
|
|
28
|
+
model: 'AC-2024',
|
|
29
|
+
icon: 'ac.png',
|
|
30
|
+
category: 'ac',
|
|
31
|
+
schema: [
|
|
32
|
+
mockSchema('switch', 'Boolean', 'rw', {}),
|
|
33
|
+
mockSchema('mode', 'Enum', 'rw', { range: ['auto', 'cold', 'hot', 'wet', 'wind'] }),
|
|
34
|
+
mockSchema('work_status', 'Enum', 'ro', { range: ['heating', 'cooling', 'idle'] }),
|
|
35
|
+
mockSchema('temp_current', 'Integer', 'ro', { min: 0, max: 100, scale: 0, step: 1, unit: '°C' }),
|
|
36
|
+
mockSchema('temp_set', 'Integer', 'rw', { min: 16, max: 30, scale: 0, step: 1, unit: '°C' }),
|
|
37
|
+
mockSchema('fan_speed_enum', 'Enum', 'rw', { range: ['auto', 'low', 'middle', 'high'] }),
|
|
38
|
+
mockSchema('lock', 'Boolean', 'rw', {}),
|
|
39
|
+
mockSchema('humidity_current', 'Integer', 'ro', { min: 0, max: 100, scale: 0, step: 1, unit: '%' }),
|
|
40
|
+
mockSchema('humidity_set', 'Integer', 'rw', { min: 30, max: 80, scale: 0, step: 1, unit: '%' }),
|
|
41
|
+
mockSchema('temp_unit_convert', 'Enum', 'rw', { range: ['C', 'F'] }),
|
|
42
|
+
],
|
|
43
|
+
status: [
|
|
44
|
+
mockStatus('switch', true),
|
|
45
|
+
mockStatus('mode', 'cold'),
|
|
46
|
+
mockStatus('work_status', 'cooling'),
|
|
47
|
+
mockStatus('temp_current', 24),
|
|
48
|
+
mockStatus('temp_set', 22),
|
|
49
|
+
mockStatus('fan_speed_enum', 'auto'),
|
|
50
|
+
mockStatus('lock', false),
|
|
51
|
+
mockStatus('humidity_current', 60),
|
|
52
|
+
mockStatus('humidity_set', 60),
|
|
53
|
+
mockStatus('temp_unit_convert', 'C'),
|
|
54
|
+
],
|
|
55
|
+
sub: false,
|
|
56
|
+
...overrides,
|
|
57
|
+
});
|
|
58
|
+
return device;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Create mock platform
|
|
62
|
+
const createMockPlatform = (): any => {
|
|
63
|
+
// Mock the HAP objects needed by the accessory
|
|
64
|
+
const MockService = {
|
|
65
|
+
HeaterCooler: 'HeaterCooler',
|
|
66
|
+
HumidifierDehumidifier: 'HumidifierDehumidifier',
|
|
67
|
+
Fanv2: 'Fanv2',
|
|
68
|
+
AccessoryInformation: 'AccessoryInformation',
|
|
69
|
+
Battery: 'Battery',
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const MockCharacteristic = {
|
|
73
|
+
Active: { ACTIVE: 1, INACTIVE: 0 },
|
|
74
|
+
CurrentHeaterCoolerState: { INACTIVE: 0, HEATING: 1, COOLING: 2 },
|
|
75
|
+
TargetHeaterCoolerState: { AUTO: 0, HEAT: 1, COOL: 2 },
|
|
76
|
+
CurrentHumidifierDehumidifierState: { DEHUMIDIFYING: 2 },
|
|
77
|
+
TargetHumidifierDehumidifierState: { DEHUMIDIFIER: 2 },
|
|
78
|
+
CurrentRelativeHumidity: 'CurrentRelativeHumidity',
|
|
79
|
+
RelativeHumidityDehumidifierThreshold: 'RelativeHumidityDehumidifierThreshold',
|
|
80
|
+
CurrentTemperature: 'CurrentTemperature',
|
|
81
|
+
CoolingThresholdTemperature: 'CoolingThresholdTemperature',
|
|
82
|
+
HeatingThresholdTemperature: 'HeatingThresholdTemperature',
|
|
83
|
+
RotationSpeed: 'RotationSpeed',
|
|
84
|
+
LockPhysicalControls: 'LockPhysicalControls',
|
|
85
|
+
Manufacturer: 'Manufacturer',
|
|
86
|
+
Model: 'Model',
|
|
87
|
+
Name: 'Name',
|
|
88
|
+
ConfiguredName: 'ConfiguredName',
|
|
89
|
+
SerialNumber: 'SerialNumber',
|
|
90
|
+
StatusLowBattery: { BATTERY_LEVEL_NORMAL: 0, BATTERY_LEVEL_LOW: 1 },
|
|
91
|
+
BatteryLevel: 'BatteryLevel',
|
|
92
|
+
ChargingState: { NOT_CHARGING: 0, CHARGING: 1 },
|
|
93
|
+
StatusActive: 'StatusActive',
|
|
94
|
+
ProgrammableSwitchEvent: { UUID: 'ProgrammableSwitchEvent' },
|
|
95
|
+
TemperatureDisplayUnits: { CELSIUS: 0, FAHRENHEIT: 1 },
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
api: {
|
|
100
|
+
hap: {
|
|
101
|
+
Service: MockService,
|
|
102
|
+
Characteristic: MockCharacteristic,
|
|
103
|
+
HapStatusError: class HapStatusError extends Error {
|
|
104
|
+
constructor(public status: number) {
|
|
105
|
+
super();
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
HAPStatus: {
|
|
109
|
+
SERVICE_COMMUNICATION_FAILURE: 72,
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
deviceManager: {
|
|
114
|
+
getDevice: jest.fn(),
|
|
115
|
+
sendCommands: jest.fn(),
|
|
116
|
+
},
|
|
117
|
+
options: {
|
|
118
|
+
debug: false,
|
|
119
|
+
debugLevel: '',
|
|
120
|
+
},
|
|
121
|
+
getDeviceSchemaConfig: jest.fn(() => undefined),
|
|
122
|
+
};
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// Create mock accessory
|
|
126
|
+
const createMockAccessory = (): any => ({
|
|
127
|
+
context: { deviceID: 'test-device-001' },
|
|
128
|
+
displayName: 'Test Air Conditioner',
|
|
129
|
+
services: [],
|
|
130
|
+
getService: jest.fn(),
|
|
131
|
+
addService: jest.fn(),
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const createMockCharacteristic = () => {
|
|
135
|
+
const characteristic: any = {
|
|
136
|
+
getHandler: undefined as any,
|
|
137
|
+
setHandler: undefined as any,
|
|
138
|
+
onGet: jest.fn().mockImplementation(function (fn: any) {
|
|
139
|
+
characteristic.getHandler = fn;
|
|
140
|
+
return characteristic;
|
|
141
|
+
}),
|
|
142
|
+
onSet: jest.fn().mockImplementation(function (fn: any) {
|
|
143
|
+
characteristic.setHandler = fn;
|
|
144
|
+
return characteristic;
|
|
145
|
+
}),
|
|
146
|
+
updateValue: jest.fn().mockReturnThis(),
|
|
147
|
+
setProps: jest.fn().mockReturnThis(),
|
|
148
|
+
};
|
|
149
|
+
return characteristic;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const createMockService = () => {
|
|
153
|
+
const characteristics = new Map<string, any>();
|
|
154
|
+
const getCharacteristic = jest.fn((key: any) => {
|
|
155
|
+
const keyName = typeof key === 'string' ? key : JSON.stringify(key);
|
|
156
|
+
if (!characteristics.has(keyName)) {
|
|
157
|
+
characteristics.set(keyName, createMockCharacteristic());
|
|
158
|
+
}
|
|
159
|
+
return characteristics.get(keyName);
|
|
160
|
+
});
|
|
161
|
+
return {
|
|
162
|
+
getCharacteristic,
|
|
163
|
+
setCharacteristic: jest.fn().mockReturnThis(),
|
|
164
|
+
addOptionalCharacteristic: jest.fn(),
|
|
165
|
+
testCharacteristic: jest.fn(() => false),
|
|
166
|
+
characteristics,
|
|
167
|
+
};
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
describe('AirConditionerAccessory', () => {
|
|
171
|
+
let platform: any;
|
|
172
|
+
let accessory: any;
|
|
173
|
+
let device: TuyaDevice;
|
|
174
|
+
let airConditionerAccessory: AirConditionerAccessory;
|
|
175
|
+
|
|
176
|
+
beforeEach(() => {
|
|
177
|
+
platform = createMockPlatform();
|
|
178
|
+
accessory = createMockAccessory();
|
|
179
|
+
device = createMockDevice();
|
|
180
|
+
|
|
181
|
+
platform.deviceManager.getDevice.mockReturnValue(device);
|
|
182
|
+
|
|
183
|
+
// Setup mock services and characteristics
|
|
184
|
+
const mockHeaterCoolerService = {
|
|
185
|
+
getCharacteristic: jest.fn(),
|
|
186
|
+
setCharacteristic: jest.fn().mockReturnThis(),
|
|
187
|
+
addOptionalCharacteristic: jest.fn(),
|
|
188
|
+
testCharacteristic: jest.fn(() => false),
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const mockHumidifierService = {
|
|
192
|
+
getCharacteristic: jest.fn(),
|
|
193
|
+
setCharacteristic: jest.fn().mockReturnThis(),
|
|
194
|
+
addOptionalCharacteristic: jest.fn(),
|
|
195
|
+
testCharacteristic: jest.fn(() => false),
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const mockFanService = {
|
|
199
|
+
getCharacteristic: jest.fn(),
|
|
200
|
+
setCharacteristic: jest.fn().mockReturnThis(),
|
|
201
|
+
addOptionalCharacteristic: jest.fn(),
|
|
202
|
+
testCharacteristic: jest.fn(() => false),
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const mockAccessoryInformationService = {
|
|
206
|
+
setCharacteristic: jest.fn().mockReturnThis(),
|
|
207
|
+
getCharacteristic: jest.fn(),
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const mockBatteryService = {
|
|
211
|
+
getCharacteristic: jest.fn(),
|
|
212
|
+
setCharacteristic: jest.fn().mockReturnThis(),
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
// Create a service map for easier mocking
|
|
216
|
+
const serviceMap: any = {
|
|
217
|
+
'HeaterCooler': mockHeaterCoolerService,
|
|
218
|
+
'HumidifierDehumidifier': mockHumidifierService,
|
|
219
|
+
'Fanv2': mockFanService,
|
|
220
|
+
'AccessoryInformation': mockAccessoryInformationService,
|
|
221
|
+
'Battery': mockBatteryService,
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
// Both getService and addService should return the same mock services
|
|
225
|
+
accessory.getService.mockImplementation((serviceType: any) => {
|
|
226
|
+
const serviceName = typeof serviceType === 'string' ? serviceType : serviceType?.UUID;
|
|
227
|
+
return serviceMap[serviceName] || mockAccessoryInformationService; // Default to AccessoryInformation
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
accessory.addService.mockImplementation((serviceType: any, ...args: any[]) => {
|
|
231
|
+
const serviceName = typeof serviceType === 'string' ? serviceType : serviceType?.UUID;
|
|
232
|
+
return serviceMap[serviceName] || mockAccessoryInformationService; // Default to AccessoryInformation
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// Mock characteristic getters
|
|
236
|
+
const mockCharacteristic = {
|
|
237
|
+
onGet: jest.fn().mockReturnThis(),
|
|
238
|
+
onSet: jest.fn().mockReturnThis(),
|
|
239
|
+
updateValue: jest.fn().mockReturnThis(),
|
|
240
|
+
setProps: jest.fn().mockReturnThis(),
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
mockHeaterCoolerService.getCharacteristic.mockReturnValue(mockCharacteristic);
|
|
244
|
+
mockHumidifierService.getCharacteristic.mockReturnValue(mockCharacteristic);
|
|
245
|
+
mockFanService.getCharacteristic.mockReturnValue(mockCharacteristic);
|
|
246
|
+
mockAccessoryInformationService.getCharacteristic.mockReturnValue(mockCharacteristic);
|
|
247
|
+
mockBatteryService.getCharacteristic.mockReturnValue(mockCharacteristic);
|
|
248
|
+
|
|
249
|
+
// Store the service map for testing
|
|
250
|
+
(accessory as any).serviceMap = serviceMap;
|
|
251
|
+
|
|
252
|
+
airConditionerAccessory = new AirConditionerAccessory(platform, accessory);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
describe('requiredSchema', () => {
|
|
256
|
+
test('should return required schema codes for AC', () => {
|
|
257
|
+
const required = airConditionerAccessory.requiredSchema();
|
|
258
|
+
expect(required).toBeDefined();
|
|
259
|
+
expect(Array.isArray(required)).toBe(true);
|
|
260
|
+
expect(required.length).toBeGreaterThan(0);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test('should include switch, mode, work_status, and temp_current', () => {
|
|
264
|
+
const required = airConditionerAccessory.requiredSchema();
|
|
265
|
+
expect(required).toContainEqual(['switch']);
|
|
266
|
+
expect(required).toContainEqual(['mode']);
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
describe('configureServices', () => {
|
|
271
|
+
test('should configure all three services (AC, Dehumidifier, Fan)', () => {
|
|
272
|
+
// Reset the mock to track new calls during configureServices
|
|
273
|
+
jest.clearAllMocks();
|
|
274
|
+
|
|
275
|
+
airConditionerAccessory.configureServices();
|
|
276
|
+
|
|
277
|
+
// Check that services are being configured (getService/addService will be called)
|
|
278
|
+
expect(accessory.getService).toHaveBeenCalled();
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test('should configure main HeaterCooler service', () => {
|
|
282
|
+
airConditionerAccessory.configureServices();
|
|
283
|
+
|
|
284
|
+
const mainService = accessory.getService('HeaterCooler');
|
|
285
|
+
expect(mainService).toBeDefined();
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
describe('configureAirConditioner', () => {
|
|
290
|
+
test('should setup Active characteristic handlers', () => {
|
|
291
|
+
const mockService = {
|
|
292
|
+
getCharacteristic: jest.fn(),
|
|
293
|
+
setCharacteristic: jest.fn(),
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
jest.spyOn(airConditionerAccessory, 'mainService').mockReturnValue(mockService as any);
|
|
297
|
+
|
|
298
|
+
const mockCharacteristic = {
|
|
299
|
+
onGet: jest.fn().mockReturnThis(),
|
|
300
|
+
onSet: jest.fn().mockReturnThis(),
|
|
301
|
+
updateValue: jest.fn().mockReturnThis(),
|
|
302
|
+
setProps: jest.fn().mockReturnThis(),
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
mockService.getCharacteristic.mockReturnValue(mockCharacteristic);
|
|
306
|
+
|
|
307
|
+
airConditionerAccessory.configureAirConditioner();
|
|
308
|
+
|
|
309
|
+
expect(mockService.getCharacteristic).toHaveBeenCalled();
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
test('should handle Active get returns ACTIVE when device is on and in AC mode', async () => {
|
|
313
|
+
device.status = [
|
|
314
|
+
mockStatus('switch', true),
|
|
315
|
+
mockStatus('mode', 'cold'),
|
|
316
|
+
mockStatus('work_status', 'cooling'),
|
|
317
|
+
mockStatus('temp_current', 24),
|
|
318
|
+
mockStatus('temp_set', 22),
|
|
319
|
+
mockStatus('fan_speed_enum', 'auto'),
|
|
320
|
+
mockStatus('lock', false),
|
|
321
|
+
mockStatus('humidity_current', 60),
|
|
322
|
+
mockStatus('humidity_set', 60),
|
|
323
|
+
mockStatus('temp_unit_convert', 'C'),
|
|
324
|
+
];
|
|
325
|
+
|
|
326
|
+
const activeStatus = airConditionerAccessory.getStatus('switch');
|
|
327
|
+
const modeStatus = airConditionerAccessory.getStatus('mode');
|
|
328
|
+
|
|
329
|
+
expect(activeStatus?.value).toBe(true);
|
|
330
|
+
expect(modeStatus?.value).toBe('cold');
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test('should handle Active get returns INACTIVE when device is off', () => {
|
|
334
|
+
device.status[0] = mockStatus('switch', false); // Turn off
|
|
335
|
+
|
|
336
|
+
const activeStatus = airConditionerAccessory.getStatus('switch');
|
|
337
|
+
expect(activeStatus?.value).toBe(false);
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
describe('configureDehumidifier', () => {
|
|
342
|
+
test('should configure dehumidifier service when mode includes wet', () => {
|
|
343
|
+
const mockService = {
|
|
344
|
+
getCharacteristic: jest.fn(),
|
|
345
|
+
setCharacteristic: jest.fn(),
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
jest.spyOn(airConditionerAccessory, 'dehumidifierService').mockReturnValue(mockService as any);
|
|
349
|
+
|
|
350
|
+
const mockCharacteristic = {
|
|
351
|
+
onGet: jest.fn().mockReturnThis(),
|
|
352
|
+
onSet: jest.fn().mockReturnThis(),
|
|
353
|
+
updateValue: jest.fn().mockReturnThis(),
|
|
354
|
+
setProps: jest.fn().mockReturnThis(),
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
mockService.getCharacteristic.mockReturnValue(mockCharacteristic);
|
|
358
|
+
|
|
359
|
+
airConditionerAccessory.configureDehumidifier();
|
|
360
|
+
|
|
361
|
+
expect(mockService.getCharacteristic).toHaveBeenCalled();
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test('should not configure dehumidifier when mode does not include wet', () => {
|
|
365
|
+
const deviceWithoutWet = createMockDevice({
|
|
366
|
+
schema: [
|
|
367
|
+
mockSchema('switch', 'Boolean', 'rw', {}),
|
|
368
|
+
mockSchema('mode', 'Enum', 'rw', { range: ['auto', 'cold', 'hot'] }),
|
|
369
|
+
],
|
|
370
|
+
});
|
|
371
|
+
platform.deviceManager.getDevice.mockReturnValue(deviceWithoutWet);
|
|
372
|
+
|
|
373
|
+
const ac = new AirConditionerAccessory(platform, accessory);
|
|
374
|
+
const mockService = {
|
|
375
|
+
getCharacteristic: jest.fn(),
|
|
376
|
+
setCharacteristic: jest.fn(),
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
jest.spyOn(ac, 'dehumidifierService').mockReturnValue(mockService as any);
|
|
380
|
+
|
|
381
|
+
ac.configureDehumidifier();
|
|
382
|
+
|
|
383
|
+
// Should not configure if wet mode is not available
|
|
384
|
+
expect(mockService.getCharacteristic).not.toHaveBeenCalled();
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
describe('configureFan', () => {
|
|
389
|
+
test('should configure fan service when mode includes wind', () => {
|
|
390
|
+
const mockService = {
|
|
391
|
+
getCharacteristic: jest.fn(),
|
|
392
|
+
setCharacteristic: jest.fn(),
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
jest.spyOn(airConditionerAccessory, 'fanService').mockReturnValue(mockService as any);
|
|
396
|
+
|
|
397
|
+
const mockCharacteristic = {
|
|
398
|
+
onGet: jest.fn().mockReturnThis(),
|
|
399
|
+
onSet: jest.fn().mockReturnThis(),
|
|
400
|
+
updateValue: jest.fn().mockReturnThis(),
|
|
401
|
+
setProps: jest.fn().mockReturnThis(),
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
mockService.getCharacteristic.mockReturnValue(mockCharacteristic);
|
|
405
|
+
|
|
406
|
+
airConditionerAccessory.configureFan();
|
|
407
|
+
|
|
408
|
+
expect(mockService.getCharacteristic).toHaveBeenCalled();
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
test('should not configure fan when mode does not include wind', () => {
|
|
412
|
+
const deviceWithoutWind = createMockDevice({
|
|
413
|
+
schema: [
|
|
414
|
+
mockSchema('switch', 'Boolean', 'rw', {}),
|
|
415
|
+
mockSchema('mode', 'Enum', 'rw', { range: ['auto', 'cold', 'hot'] }),
|
|
416
|
+
],
|
|
417
|
+
});
|
|
418
|
+
platform.deviceManager.getDevice.mockReturnValue(deviceWithoutWind);
|
|
419
|
+
|
|
420
|
+
const ac = new AirConditionerAccessory(platform, accessory);
|
|
421
|
+
const mockService = {
|
|
422
|
+
getCharacteristic: jest.fn(),
|
|
423
|
+
setCharacteristic: jest.fn(),
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
jest.spyOn(ac, 'fanService').mockReturnValue(mockService as any);
|
|
427
|
+
|
|
428
|
+
ac.configureFan();
|
|
429
|
+
|
|
430
|
+
// Should not configure if wind mode is not available
|
|
431
|
+
expect(mockService.getCharacteristic).not.toHaveBeenCalled();
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
describe('Characteristic handlers', () => {
|
|
436
|
+
test('HeaterCooler Active onGet/onSet should reflect AC active state and send commands', async () => {
|
|
437
|
+
const service = createMockService();
|
|
438
|
+
jest.spyOn(airConditionerAccessory, 'mainService').mockReturnValue(service as any);
|
|
439
|
+
|
|
440
|
+
airConditionerAccessory.configureAirConditioner();
|
|
441
|
+
|
|
442
|
+
const activeChar = service.characteristics.get(JSON.stringify(platform.api.hap.Characteristic.Active));
|
|
443
|
+
expect(activeChar).toBeDefined();
|
|
444
|
+
expect(activeChar.getHandler).toBeDefined();
|
|
445
|
+
expect(activeChar.setHandler).toBeDefined();
|
|
446
|
+
|
|
447
|
+
const activeValue = activeChar.getHandler();
|
|
448
|
+
expect(activeValue).toBe(platform.api.hap.Characteristic.Active.ACTIVE);
|
|
449
|
+
|
|
450
|
+
jest.useFakeTimers();
|
|
451
|
+
activeChar.setHandler(platform.api.hap.Characteristic.Active.ACTIVE);
|
|
452
|
+
jest.runAllTimers();
|
|
453
|
+
jest.useRealTimers();
|
|
454
|
+
|
|
455
|
+
expect(platform.deviceManager.sendCommands).toHaveBeenCalledWith('test-device-001', [{ code: 'switch', value: true }]);
|
|
456
|
+
expect(airConditionerAccessory.getStatus('switch')?.value).toBe(true);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
test('TargetHeaterCoolerState onGet/onSet should map mode values correctly', async () => {
|
|
460
|
+
const service = createMockService();
|
|
461
|
+
jest.spyOn(airConditionerAccessory, 'mainService').mockReturnValue(service as any);
|
|
462
|
+
|
|
463
|
+
airConditionerAccessory.configureAirConditioner();
|
|
464
|
+
|
|
465
|
+
const targetChar = service.characteristics.get(JSON.stringify(platform.api.hap.Characteristic.TargetHeaterCoolerState));
|
|
466
|
+
expect(targetChar).toBeDefined();
|
|
467
|
+
expect(targetChar.getHandler).toBeDefined();
|
|
468
|
+
expect(targetChar.setHandler).toBeDefined();
|
|
469
|
+
|
|
470
|
+
device.status = device.status.map(status => status.code === 'mode' ? mockStatus('mode', 'hot') : status);
|
|
471
|
+
const targetValue = targetChar.getHandler();
|
|
472
|
+
expect(targetValue).toBe(platform.api.hap.Characteristic.TargetHeaterCoolerState.HEAT);
|
|
473
|
+
|
|
474
|
+
jest.useFakeTimers();
|
|
475
|
+
targetChar.setHandler(platform.api.hap.Characteristic.TargetHeaterCoolerState.COOL);
|
|
476
|
+
jest.runAllTimers();
|
|
477
|
+
jest.useRealTimers();
|
|
478
|
+
|
|
479
|
+
expect(platform.deviceManager.sendCommands).toHaveBeenCalledWith('test-device-001', [{ code: 'mode', value: 'cold' }]);
|
|
480
|
+
expect(airConditionerAccessory.getStatus('mode')?.value).toBe('cold');
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
test('CoolingThresholdTemperature onGet/onSet should respect auto mode limits', async () => {
|
|
484
|
+
const service = createMockService();
|
|
485
|
+
jest.spyOn(airConditionerAccessory, 'mainService').mockReturnValue(service as any);
|
|
486
|
+
airConditionerAccessory.configureAirConditioner();
|
|
487
|
+
|
|
488
|
+
const temperatureKey = platform.api.hap.Characteristic.CoolingThresholdTemperature;
|
|
489
|
+
const coolingChar = service.getCharacteristic(temperatureKey);
|
|
490
|
+
expect(coolingChar).toBeDefined();
|
|
491
|
+
expect(coolingChar.getHandler).toBeDefined();
|
|
492
|
+
expect(coolingChar.setHandler).toBeDefined();
|
|
493
|
+
|
|
494
|
+
device.status = device.status.map(status => status.code === 'mode' ? mockStatus('mode', 'auto') : status);
|
|
495
|
+
const getValue = coolingChar.getHandler();
|
|
496
|
+
expect(getValue).toBe(16);
|
|
497
|
+
|
|
498
|
+
jest.useFakeTimers();
|
|
499
|
+
coolingChar.setHandler(18);
|
|
500
|
+
jest.runAllTimers();
|
|
501
|
+
jest.useRealTimers();
|
|
502
|
+
|
|
503
|
+
expect(service.getCharacteristic(temperatureKey).updateValue).toHaveBeenCalledWith(16);
|
|
504
|
+
expect(airConditionerAccessory.getStatus('temp_set')?.value).toBe(22);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
test('HeatingThresholdTemperature onGet/onSet should respect auto mode limits', async () => {
|
|
508
|
+
const service = createMockService();
|
|
509
|
+
jest.spyOn(airConditionerAccessory, 'mainService').mockReturnValue(service as any);
|
|
510
|
+
airConditionerAccessory.configureAirConditioner();
|
|
511
|
+
|
|
512
|
+
const temperatureKey = platform.api.hap.Characteristic.HeatingThresholdTemperature;
|
|
513
|
+
const heatingChar = service.getCharacteristic(temperatureKey);
|
|
514
|
+
expect(heatingChar).toBeDefined();
|
|
515
|
+
expect(heatingChar.getHandler).toBeDefined();
|
|
516
|
+
expect(heatingChar.setHandler).toBeDefined();
|
|
517
|
+
|
|
518
|
+
device.status = device.status.map(status => status.code === 'mode' ? mockStatus('mode', 'auto') : status);
|
|
519
|
+
const getValue = heatingChar.getHandler();
|
|
520
|
+
expect(getValue).toBe(30);
|
|
521
|
+
|
|
522
|
+
jest.useFakeTimers();
|
|
523
|
+
heatingChar.setHandler(25);
|
|
524
|
+
jest.runAllTimers();
|
|
525
|
+
jest.useRealTimers();
|
|
526
|
+
|
|
527
|
+
expect(service.getCharacteristic(temperatureKey).updateValue).toHaveBeenCalledWith(30);
|
|
528
|
+
expect(airConditionerAccessory.getStatus('temp_set')?.value).toBe(22);
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
test('CurrentHeaterCoolerState onGet should return correct operating state', () => {
|
|
532
|
+
const service = createMockService();
|
|
533
|
+
jest.spyOn(airConditionerAccessory, 'mainService').mockReturnValue(service as any);
|
|
534
|
+
airConditionerAccessory.configureAirConditioner();
|
|
535
|
+
|
|
536
|
+
const currentStateChar = service.characteristics.get(JSON.stringify(platform.api.hap.Characteristic.CurrentHeaterCoolerState));
|
|
537
|
+
expect(currentStateChar).toBeDefined();
|
|
538
|
+
|
|
539
|
+
device.status = device.status.map(status => status.code === 'work_status' ? mockStatus('work_status', 'heating') : status);
|
|
540
|
+
expect(currentStateChar.getHandler()).toBe(platform.api.hap.Characteristic.CurrentHeaterCoolerState.HEATING);
|
|
541
|
+
|
|
542
|
+
device.status = device.status.map(status => status.code === 'work_status' ? mockStatus('work_status', 'idle') : status);
|
|
543
|
+
expect(currentStateChar.getHandler()).toBe(platform.api.hap.Characteristic.CurrentHeaterCoolerState.INACTIVE);
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
test('Dehumidifier Active onGet/onSet should send wet mode commands', async () => {
|
|
547
|
+
const service = createMockService();
|
|
548
|
+
jest.spyOn(airConditionerAccessory, 'dehumidifierService').mockReturnValue(service as any);
|
|
549
|
+
airConditionerAccessory.configureDehumidifier();
|
|
550
|
+
|
|
551
|
+
const activeChar = service.characteristics.get(JSON.stringify(platform.api.hap.Characteristic.Active));
|
|
552
|
+
expect(activeChar).toBeDefined();
|
|
553
|
+
expect(activeChar.getHandler).toBeDefined();
|
|
554
|
+
expect(activeChar.setHandler).toBeDefined();
|
|
555
|
+
|
|
556
|
+
device.status = device.status.map(status => status.code === 'mode' ? mockStatus('mode', 'wet') : status);
|
|
557
|
+
const activeValue = activeChar.getHandler();
|
|
558
|
+
expect(activeValue).toBe(platform.api.hap.Characteristic.Active.ACTIVE);
|
|
559
|
+
|
|
560
|
+
jest.useFakeTimers();
|
|
561
|
+
activeChar.setHandler(platform.api.hap.Characteristic.Active.INACTIVE);
|
|
562
|
+
jest.runAllTimers();
|
|
563
|
+
jest.useRealTimers();
|
|
564
|
+
|
|
565
|
+
expect(platform.deviceManager.sendCommands).toHaveBeenCalledWith('test-device-001', [
|
|
566
|
+
{ code: 'switch', value: false },
|
|
567
|
+
{ code: 'mode', value: 'wet' },
|
|
568
|
+
]);
|
|
569
|
+
expect(airConditionerAccessory.getStatus('switch')?.value).toBe(false);
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
test('Fan Active onGet/onSet should send wind mode commands', async () => {
|
|
573
|
+
const service = createMockService();
|
|
574
|
+
jest.spyOn(airConditionerAccessory, 'fanService').mockReturnValue(service as any);
|
|
575
|
+
airConditionerAccessory.configureFan();
|
|
576
|
+
|
|
577
|
+
const activeChar = service.characteristics.get(JSON.stringify(platform.api.hap.Characteristic.Active));
|
|
578
|
+
expect(activeChar).toBeDefined();
|
|
579
|
+
expect(activeChar.getHandler).toBeDefined();
|
|
580
|
+
expect(activeChar.setHandler).toBeDefined();
|
|
581
|
+
|
|
582
|
+
device.status = device.status.map(status => status.code === 'mode' ? mockStatus('mode', 'wind') : status);
|
|
583
|
+
const activeValue = activeChar.getHandler();
|
|
584
|
+
expect(activeValue).toBe(platform.api.hap.Characteristic.Active.ACTIVE);
|
|
585
|
+
|
|
586
|
+
jest.useFakeTimers();
|
|
587
|
+
activeChar.setHandler(platform.api.hap.Characteristic.Active.ACTIVE);
|
|
588
|
+
jest.runAllTimers();
|
|
589
|
+
jest.useRealTimers();
|
|
590
|
+
|
|
591
|
+
expect(platform.deviceManager.sendCommands).toHaveBeenCalledWith('test-device-001', [
|
|
592
|
+
{ code: 'switch', value: true },
|
|
593
|
+
{ code: 'mode', value: 'wind' },
|
|
594
|
+
]);
|
|
595
|
+
expect(airConditionerAccessory.getStatus('switch')?.value).toBe(true);
|
|
596
|
+
});
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
describe('getSchema', () => {
|
|
600
|
+
test('should return schema for valid code', () => {
|
|
601
|
+
const schema = airConditionerAccessory.getSchema('switch');
|
|
602
|
+
expect(schema).toBeDefined();
|
|
603
|
+
expect(schema?.code).toBe('switch');
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
test('should return schema for alternative code names', () => {
|
|
607
|
+
// SCHEMA_CODE.FAN_SPEED_ENUM should map to 'fan_speed_enum'
|
|
608
|
+
const schema = airConditionerAccessory.getSchema('windspeed', 'fan_speed_enum');
|
|
609
|
+
expect(schema).toBeDefined();
|
|
610
|
+
expect(device.schema.some((s) => s.code === 'fan_speed_enum')).toBeTruthy();
|
|
611
|
+
expect(schema?.code).toBe('fan_speed_enum');
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
test('should return undefined for non-existent code', () => {
|
|
615
|
+
const schema = airConditionerAccessory.getSchema('non_existent_code');
|
|
616
|
+
expect(schema).toBeUndefined();
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
test('should be case-insensitive', () => {
|
|
620
|
+
const schema1 = airConditionerAccessory.getSchema('SWITCH');
|
|
621
|
+
const schema2 = airConditionerAccessory.getSchema('switch');
|
|
622
|
+
expect(schema1?.code).toBe(schema2?.code);
|
|
623
|
+
});
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
describe('getStatus', () => {
|
|
627
|
+
test('should return status for valid code', () => {
|
|
628
|
+
const status = airConditionerAccessory.getStatus('switch');
|
|
629
|
+
expect(status).toBeDefined();
|
|
630
|
+
expect(status?.code).toBe('switch');
|
|
631
|
+
expect(status?.value).toBe(true);
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
test('should return status for temperature', () => {
|
|
635
|
+
const status = airConditionerAccessory.getStatus('temp_current');
|
|
636
|
+
expect(status?.value).toBe(24);
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
test('should return undefined for non-existent code', () => {
|
|
640
|
+
const status = airConditionerAccessory.getStatus('non_existent');
|
|
641
|
+
expect(status).toBeUndefined();
|
|
642
|
+
});
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
describe('service methods', () => {
|
|
646
|
+
test('mainService should return HeaterCooler service', () => {
|
|
647
|
+
const service = airConditionerAccessory.mainService();
|
|
648
|
+
expect(service).toBeDefined();
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
test('dehumidifierService should return HumidifierDehumidifier service', () => {
|
|
652
|
+
const service = airConditionerAccessory.dehumidifierService();
|
|
653
|
+
expect(service).toBeDefined();
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
test('fanService should return Fanv2 service', () => {
|
|
657
|
+
const service = airConditionerAccessory.fanService();
|
|
658
|
+
expect(service).toBeDefined();
|
|
659
|
+
});
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
describe('device offline handling', () => {
|
|
663
|
+
test('should update characteristic when device status changes', () => {
|
|
664
|
+
device.status[0] = mockStatus('switch', false);
|
|
665
|
+
|
|
666
|
+
const status = airConditionerAccessory.getStatus('switch');
|
|
667
|
+
expect(status?.value).toBe(false);
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
test('should handle multiple mode values correctly', () => {
|
|
671
|
+
const modeSchema = airConditionerAccessory.getSchema('mode');
|
|
672
|
+
expect(modeSchema).toBeDefined();
|
|
673
|
+
const property = modeSchema?.property as any;
|
|
674
|
+
expect(property.range).toContain('auto');
|
|
675
|
+
expect(property.range).toContain('cold');
|
|
676
|
+
expect(property.range).toContain('hot');
|
|
677
|
+
});
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
describe('temperature configuration', () => {
|
|
681
|
+
test('should have correct temperature range', () => {
|
|
682
|
+
const schema = airConditionerAccessory.getSchema('temp_set');
|
|
683
|
+
expect(schema).toBeDefined();
|
|
684
|
+
const property = schema?.property as any;
|
|
685
|
+
expect(property.min).toBe(16);
|
|
686
|
+
expect(property.max).toBe(30);
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
test('should have correct humidity range', () => {
|
|
690
|
+
const schema = airConditionerAccessory.getSchema('humidity_set');
|
|
691
|
+
expect(schema).toBeDefined();
|
|
692
|
+
const property = schema?.property as any;
|
|
693
|
+
expect(property.min).toBe(30);
|
|
694
|
+
expect(property.max).toBe(80);
|
|
695
|
+
});
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
describe('mode switching', () => {
|
|
699
|
+
test('should support cold mode', () => {
|
|
700
|
+
device.status[1] = mockStatus('mode', 'cold');
|
|
701
|
+
const status = airConditionerAccessory.getStatus('mode');
|
|
702
|
+
expect(status?.value).toBe('cold');
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
test('should support hot mode', () => {
|
|
706
|
+
device.status[1] = mockStatus('mode', 'hot');
|
|
707
|
+
const status = airConditionerAccessory.getStatus('mode');
|
|
708
|
+
expect(status?.value).toBe('hot');
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
test('should support auto mode', () => {
|
|
712
|
+
device.status[1] = mockStatus('mode', 'auto');
|
|
713
|
+
const status = airConditionerAccessory.getStatus('mode');
|
|
714
|
+
expect(status?.value).toBe('auto');
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
test('should support wet (dehumidifier) mode', () => {
|
|
718
|
+
device.status[1] = mockStatus('mode', 'wet');
|
|
719
|
+
const status = airConditionerAccessory.getStatus('mode');
|
|
720
|
+
expect(status?.value).toBe('wet');
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
test('should support wind (fan) mode', () => {
|
|
724
|
+
device.status[1] = mockStatus('mode', 'wind');
|
|
725
|
+
const status = airConditionerAccessory.getStatus('mode');
|
|
726
|
+
expect(status?.value).toBe('wind');
|
|
727
|
+
});
|
|
728
|
+
});
|
|
729
|
+
});
|