tsvesync 1.0.89 → 1.0.91

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/README.md CHANGED
@@ -201,42 +201,7 @@ The library supports configuration through environment variables:
201
201
  ```env
202
202
  VESYNC_USERNAME=your_email@example.com
203
203
  VESYNC_PASSWORD=your_password
204
- VESYNC_API_URL=https://smartapi.vesync.com # Optional, auto-detects regional endpoint
205
- ```
206
-
207
- ### Regional API Support
208
-
209
- The library automatically detects the correct regional API endpoint based on your timezone:
210
-
211
- - **Europe**: `https://smartapi.vesync.eu` (for European timezones like `Europe/Berlin`, `Europe/London`)
212
- - **Asia**: `https://smartapi.vesync.com.cn` (for Asian timezones like `Asia/Shanghai`, `Asia/Tokyo`)
213
- - **US/Others**: `https://smartapi.vesync.com` (default for all other timezones)
214
-
215
- **For German users experiencing authentication issues:**
216
-
217
- ```typescript
218
- // This will automatically use the EU API endpoint
219
- const manager = new VeSync(
220
- 'your_email@example.com',
221
- 'your_password',
222
- 'Europe/Berlin' // This triggers EU endpoint selection
223
- );
224
-
225
- await manager.login();
226
- ```
227
-
228
- **Manual API endpoint override:**
229
-
230
- ```typescript
231
- // Force a specific regional endpoint
232
- const manager = new VeSync(
233
- 'your_email@example.com',
234
- 'your_password',
235
- 'Europe/Berlin',
236
- false, // debug
237
- true, // redact
238
- 'https://smartapi.vesync.eu' // Force EU endpoint
239
- );
204
+ VESYNC_API_URL=https://smartapi.vesync.com # Optional, defaults to standard API URL
240
205
  ```
241
206
 
242
207
  ## Testing
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Unit tests for VeSyncAir131 air quality data parsing and filter life handling
3
+ */
4
+ export {};
@@ -0,0 +1,365 @@
1
+ "use strict";
2
+ /**
3
+ * Unit tests for VeSyncAir131 air quality data parsing and filter life handling
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const air131_1 = require("../../lib/fans/air131");
7
+ // Mock VeSync manager
8
+ const mockManager = {
9
+ username: 'test@example.com',
10
+ password: 'testpass',
11
+ token: 'testtoken',
12
+ accountID: 'testaccount',
13
+ apiKey: 'testapikey',
14
+ apiBase: 'testbase',
15
+ timezone: 'UTC'
16
+ };
17
+ describe('VeSyncAir131 Air Quality Features', () => {
18
+ let device;
19
+ beforeEach(() => {
20
+ const deviceDetails = {
21
+ deviceName: 'LV-PUR131S Test',
22
+ deviceType: 'LV-PUR131S',
23
+ cid: 'test-cid',
24
+ uuid: 'test-uuid',
25
+ deviceStatus: 'on',
26
+ deviceRegion: 'US',
27
+ configModule: 'VeSyncAir131',
28
+ macId: '00:11:22:33:44:55',
29
+ deviceCategory: 'fan',
30
+ connectionStatus: 'online'
31
+ };
32
+ device = new air131_1.VeSyncAir131(deviceDetails, mockManager);
33
+ });
34
+ describe('Air Quality Data Parsing', () => {
35
+ it('should parse air quality data with PM2.5 values', () => {
36
+ const mockResponse = {
37
+ result: {
38
+ code: 0,
39
+ result: {
40
+ enabled: true,
41
+ mode: 'auto',
42
+ level: 2,
43
+ speed: 2,
44
+ display: true,
45
+ child_lock: false,
46
+ night_light: 'off',
47
+ air_quality: 2,
48
+ air_quality_value: 28,
49
+ filter_life: {
50
+ percent: 68,
51
+ replace_indicator: false
52
+ },
53
+ timer_remain: 0
54
+ }
55
+ }
56
+ };
57
+ device.details = mockResponse.result.result;
58
+ expect(device.airQualityLevel).toBe(2);
59
+ expect(device.airQualityValue).toBe(28);
60
+ expect(device.filterLife).toBe(68);
61
+ });
62
+ it('should handle air quality levels 1-5 mapping', () => {
63
+ const airQualityMappings = [
64
+ { level: 1, pm25: 8, description: 'Excellent' },
65
+ { level: 2, pm25: 22, description: 'Good' },
66
+ { level: 3, pm25: 38, description: 'Fair' },
67
+ { level: 4, pm25: 75, description: 'Poor' },
68
+ { level: 5, pm25: 150, description: 'Very Poor' }
69
+ ];
70
+ airQualityMappings.forEach(mapping => {
71
+ device.details = {
72
+ air_quality: mapping.level,
73
+ air_quality_value: mapping.pm25,
74
+ filter_life: { percent: 50 }
75
+ };
76
+ expect(device.airQualityLevel).toBe(mapping.level);
77
+ expect(device.airQualityValue).toBe(mapping.pm25);
78
+ });
79
+ });
80
+ it('should handle missing air quality data', () => {
81
+ device.details = {
82
+ enabled: true,
83
+ mode: 'manual',
84
+ level: 3,
85
+ speed: 3,
86
+ filter_life: { percent: 45 }
87
+ // air_quality and air_quality_value missing
88
+ };
89
+ expect(device.airQualityLevel).toBe('');
90
+ expect(device.airQualityValue).toBe(0);
91
+ });
92
+ it('should handle zero PM2.5 readings (perfect air)', () => {
93
+ device.details = {
94
+ air_quality: 1,
95
+ air_quality_value: 0,
96
+ filter_life: { percent: 85 }
97
+ };
98
+ expect(device.airQualityLevel).toBe(1);
99
+ expect(device.airQualityValue).toBe(0);
100
+ });
101
+ it('should handle hazardous air quality levels', () => {
102
+ device.details = {
103
+ air_quality: 5,
104
+ air_quality_value: 500,
105
+ filter_life: { percent: 25 }
106
+ };
107
+ expect(device.airQualityLevel).toBe(5);
108
+ expect(device.airQualityValue).toBe(500);
109
+ });
110
+ });
111
+ describe('Filter Life Object Format', () => {
112
+ it('should parse filter life object with percent', () => {
113
+ device.details = {
114
+ filter_life: {
115
+ percent: 72,
116
+ replace_indicator: false
117
+ }
118
+ };
119
+ expect(device.filterLife).toBe(72);
120
+ });
121
+ it('should handle filter replacement indicator', () => {
122
+ device.details = {
123
+ filter_life: {
124
+ percent: 8,
125
+ replace_indicator: true
126
+ }
127
+ };
128
+ expect(device.filterLife).toBe(8);
129
+ });
130
+ it('should handle new filter (100%)', () => {
131
+ device.details = {
132
+ filter_life: {
133
+ percent: 100,
134
+ replace_indicator: false
135
+ }
136
+ };
137
+ expect(device.filterLife).toBe(100);
138
+ });
139
+ it('should handle filter needs replacement (0%)', () => {
140
+ device.details = {
141
+ filter_life: {
142
+ percent: 0,
143
+ replace_indicator: true
144
+ }
145
+ };
146
+ expect(device.filterLife).toBe(0);
147
+ });
148
+ it('should handle missing percent in filter object', () => {
149
+ device.details = {
150
+ filter_life: {
151
+ replace_indicator: true
152
+ // percent missing
153
+ }
154
+ };
155
+ expect(device.filterLife).toBe(0);
156
+ });
157
+ it('should handle null filter life object', () => {
158
+ device.details = {
159
+ filter_life: null
160
+ };
161
+ expect(device.filterLife).toBe(0);
162
+ });
163
+ it('should handle empty filter life object', () => {
164
+ device.details = {
165
+ filter_life: {}
166
+ };
167
+ expect(device.filterLife).toBe(0);
168
+ });
169
+ it('should handle malformed filter life object', () => {
170
+ device.details = {
171
+ filter_life: {
172
+ percent: 'not_a_number',
173
+ replace_indicator: 'not_a_boolean'
174
+ }
175
+ };
176
+ expect(device.filterLife).toBe(0);
177
+ });
178
+ });
179
+ describe('Device Configuration and Features', () => {
180
+ it('should support air quality features for LV-PUR131S', () => {
181
+ expect(device.hasFeature('air_quality')).toBe(true);
182
+ expect(device.hasFeature('display')).toBe(true);
183
+ expect(device.hasFeature('child_lock')).toBe(true);
184
+ expect(device.hasFeature('night_light')).toBe(true);
185
+ expect(device.hasFeature('timer')).toBe(true);
186
+ expect(device.hasFeature('fan_speed')).toBe(true);
187
+ expect(device.hasFeature('auto_mode')).toBe(true);
188
+ });
189
+ it('should return correct maximum speed for LV-PUR131S', () => {
190
+ expect(device.getMaxFanSpeed()).toBe(3); // LV series has 3 speeds
191
+ });
192
+ it('should handle LV-RH131S without air quality', () => {
193
+ const rh131s = new air131_1.VeSyncAir131({
194
+ deviceName: 'LV-RH131S Test',
195
+ deviceType: 'LV-RH131S',
196
+ cid: 'test-cid',
197
+ uuid: 'test-uuid'
198
+ }, mockManager);
199
+ expect(rh131s.hasFeature('air_quality')).toBe(false);
200
+ expect(rh131s.hasFeature('display')).toBe(true);
201
+ });
202
+ });
203
+ describe('Mode-specific Behavior', () => {
204
+ it('should support air quality in auto mode', () => {
205
+ device.details.mode = 'auto';
206
+ expect(device.isFeatureSupportedInCurrentMode('air_quality')).toBe(true);
207
+ });
208
+ it('should support air quality in manual mode', () => {
209
+ device.details.mode = 'manual';
210
+ expect(device.isFeatureSupportedInCurrentMode('air_quality')).toBe(true);
211
+ });
212
+ it('should restrict fan speed control to manual mode', () => {
213
+ device.details.mode = 'manual';
214
+ expect(device.isFeatureSupportedInCurrentMode('fan_speed')).toBe(true);
215
+ device.details.mode = 'auto';
216
+ expect(device.isFeatureSupportedInCurrentMode('fan_speed')).toBe(false);
217
+ });
218
+ it('should disable display and child lock in sleep mode', () => {
219
+ device.details.mode = 'sleep';
220
+ expect(device.isFeatureSupportedInCurrentMode('display')).toBe(false);
221
+ expect(device.isFeatureSupportedInCurrentMode('child_lock')).toBe(false);
222
+ });
223
+ });
224
+ describe('State Tracking and Updates', () => {
225
+ it('should track complete device state', () => {
226
+ device.details = {
227
+ enabled: true,
228
+ mode: 'auto',
229
+ level: 2,
230
+ speed: 2,
231
+ display: true,
232
+ child_lock: false,
233
+ night_light: 'dim',
234
+ air_quality: 2,
235
+ air_quality_value: 32,
236
+ filter_life: {
237
+ percent: 55,
238
+ replace_indicator: false
239
+ },
240
+ timer_remain: 1800
241
+ };
242
+ expect(device.deviceStatus).toBe('on');
243
+ expect(device.mode).toBe('auto');
244
+ expect(device.speed).toBe(2);
245
+ expect(device.screenStatus).toBe('on'); // display: true maps to 'on'
246
+ expect(device.childLock).toBe(false);
247
+ expect(device.airQualityLevel).toBe(2);
248
+ expect(device.airQualityValue).toBe(32);
249
+ expect(device.filterLife).toBe(55);
250
+ });
251
+ it('should handle boolean display values correctly', () => {
252
+ device.details = { display: true };
253
+ expect(device.screenStatus).toBe('on');
254
+ device.details = { display: false };
255
+ expect(device.screenStatus).toBe('off');
256
+ });
257
+ it('should track filter replacement warnings', () => {
258
+ // Filter life getting low
259
+ device.details = {
260
+ filter_life: {
261
+ percent: 15,
262
+ replace_indicator: false
263
+ }
264
+ };
265
+ expect(device.filterLife).toBe(15);
266
+ // Filter replacement needed
267
+ device.details = {
268
+ filter_life: {
269
+ percent: 5,
270
+ replace_indicator: true
271
+ }
272
+ };
273
+ expect(device.filterLife).toBe(5);
274
+ });
275
+ });
276
+ describe('Air Quality History and Trends', () => {
277
+ it('should handle air quality improvements', () => {
278
+ // Poor air quality initially
279
+ device.details = {
280
+ air_quality: 4,
281
+ air_quality_value: 85,
282
+ filter_life: { percent: 40 }
283
+ };
284
+ expect(device.airQualityLevel).toBe(4);
285
+ // Air quality improves
286
+ device.details.air_quality = 2;
287
+ device.details.air_quality_value = 25;
288
+ expect(device.airQualityLevel).toBe(2);
289
+ expect(device.airQualityValue).toBe(25);
290
+ });
291
+ it('should handle air quality deterioration', () => {
292
+ // Good air quality initially
293
+ device.details = {
294
+ air_quality: 2,
295
+ air_quality_value: 20,
296
+ filter_life: { percent: 60 }
297
+ };
298
+ expect(device.airQualityLevel).toBe(2);
299
+ // Air quality worsens
300
+ device.details.air_quality = 4;
301
+ device.details.air_quality_value = 90;
302
+ expect(device.airQualityLevel).toBe(4);
303
+ expect(device.airQualityValue).toBe(90);
304
+ });
305
+ });
306
+ describe('Error Handling and Edge Cases', () => {
307
+ it('should handle completely missing details', () => {
308
+ device.details = undefined;
309
+ expect(device.airQualityLevel).toBe('');
310
+ expect(device.airQualityValue).toBe(0);
311
+ expect(device.filterLife).toBe(0);
312
+ });
313
+ it('should handle partial API responses', () => {
314
+ device.details = {
315
+ mode: 'auto',
316
+ speed: 1
317
+ // All other fields missing
318
+ };
319
+ expect(device.mode).toBe('auto');
320
+ expect(device.speed).toBe(1);
321
+ expect(device.airQualityLevel).toBe('');
322
+ expect(device.airQualityValue).toBe(0);
323
+ expect(device.filterLife).toBe(0);
324
+ });
325
+ it('should handle network errors gracefully', () => {
326
+ // Simulate network error - details remain from last update
327
+ device.details = {
328
+ air_quality: 2,
329
+ air_quality_value: 30,
330
+ filter_life: { percent: 70 }
331
+ };
332
+ // Values preserved from last successful update
333
+ expect(device.airQualityLevel).toBe(2);
334
+ expect(device.airQualityValue).toBe(30);
335
+ expect(device.filterLife).toBe(70);
336
+ });
337
+ it('should handle invalid air quality levels', () => {
338
+ device.details = {
339
+ air_quality: 10, // Invalid level (should be 1-5)
340
+ air_quality_value: 50,
341
+ filter_life: { percent: 50 }
342
+ };
343
+ expect(device.airQualityLevel).toBe(10); // Return raw value for debugging
344
+ expect(device.airQualityValue).toBe(50);
345
+ });
346
+ it('should handle negative filter life percent', () => {
347
+ device.details = {
348
+ filter_life: {
349
+ percent: -10,
350
+ replace_indicator: true
351
+ }
352
+ };
353
+ expect(device.filterLife).toBe(-10); // Allow for error detection
354
+ });
355
+ it('should handle filter life percent over 100', () => {
356
+ device.details = {
357
+ filter_life: {
358
+ percent: 150,
359
+ replace_indicator: false
360
+ }
361
+ };
362
+ expect(device.filterLife).toBe(150); // Allow for error detection
363
+ });
364
+ });
365
+ });
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Unit tests for VeSyncAirBaseV2 air quality data parsing
3
+ */
4
+ export {};