tsvesync 1.6.0 → 1.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.
@@ -3,254 +3,190 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.VeSyncTowerFan = void 0;
4
4
  const airBaseV2_1 = require("./airBaseV2");
5
5
  const logger_1 = require("../logger");
6
- /**
7
- * VeSync Tower Fan Implementation
8
- */
6
+ /** LTF-F422S tower fans use different commands from bypassV2 purifiers. */
9
7
  class VeSyncTowerFan extends airBaseV2_1.VeSyncAirBaseV2 {
10
8
  constructor(details, manager) {
11
9
  super(details, manager);
12
- this.towerModes = ['normal', 'advancedSleep', 'off'];
13
- logger_1.logger.debug(`Initialized VeSyncTowerFan device: ${this.deviceName}`);
10
+ this.towerModes = ['normal', 'auto', 'advancedSleep', 'turbo', 'off'];
14
11
  }
15
- /**
16
- * Get device details
17
- */
18
- async getDetails() {
19
- logger_1.logger.debug(`Getting details for device: ${this.deviceName}`);
20
- const [head, body] = this.buildApiDict('getTowerFanStatus');
21
- const [response, status] = await this.callApi('/cloud/v2/deviceManaged/bypassV2', 'post', body, head);
22
- if (!this.checkResponse([response, status], 'getDetails') || !(response === null || response === void 0 ? void 0 : response.result)) {
23
- logger_1.logger.debug('Error getting tower fan details');
24
- this.connectionStatus = 'offline';
12
+ /** A successful cloud envelope can still contain a rejected device command. */
13
+ checkResponse(response, method) {
14
+ var _a, _b, _c;
15
+ if (!super.checkResponse(response, method))
16
+ return false;
17
+ const result = (_a = response[0]) === null || _a === void 0 ? void 0 : _a.result;
18
+ if (!result || result.code !== 0) {
19
+ this.logError(method, `Device error code ${(_b = result === null || result === void 0 ? void 0 : result.code) !== null && _b !== void 0 ? _b : 'missing'}: ${(_c = result === null || result === void 0 ? void 0 : result.msg) !== null && _c !== void 0 ? _c : 'Invalid device response'}`);
25
20
  return false;
26
21
  }
27
- const innerResponse = response.result;
28
- if (innerResponse.code !== 0 || !innerResponse.result) {
29
- logger_1.logger.debug('Error in inner response from tower fan');
22
+ return true;
23
+ }
24
+ async request(method, data = {}) {
25
+ var _a;
26
+ const [head, body] = this.buildApiDict(method);
27
+ body.payload.data = data;
28
+ const response = await this.callApi('/cloud/v2/deviceManaged/bypassV2', 'post', body, head);
29
+ if (!this.checkResponse(response, method))
30
+ return null;
31
+ this.connectionStatus = 'online';
32
+ return (_a = response[0].result.result) !== null && _a !== void 0 ? _a : {};
33
+ }
34
+ async getDetails() {
35
+ var _a, _b;
36
+ const data = await this.request('getTowerFanStatus');
37
+ if (!data || (data.powerSwitch !== 0 && data.powerSwitch !== 1)) {
30
38
  this.connectionStatus = 'offline';
31
39
  return false;
32
40
  }
33
- const deviceData = innerResponse.result;
34
- this.deviceStatus = deviceData.powerSwitch === 1 ? 'on' : 'off';
35
- this.details = {
36
- powerSwitch: deviceData.powerSwitch,
37
- workMode: deviceData.workMode,
38
- manualSpeedLevel: deviceData.manualSpeedLevel,
39
- fanSpeedLevel: deviceData.fanSpeedLevel,
40
- screenState: deviceData.screenState,
41
- screenSwitch: deviceData.screenSwitch,
42
- oscillationState: deviceData.oscillationState,
43
- oscillationSwitch: deviceData.oscillationSwitch,
44
- timerRemain: deviceData.timerRemain,
45
- temperature: deviceData.temperature || 0,
46
- humidity: deviceData.humidity || 0,
47
- thermalComfort: deviceData.thermalComfort,
48
- sleepPreference: deviceData.sleepPreference || {},
49
- scheduleCount: deviceData.scheduleCount,
50
- displayingType: deviceData.displayingType,
51
- errorCode: deviceData.errorCode
52
- };
53
- logger_1.logger.debug(`Successfully got details for device: ${this.deviceName}`);
41
+ this.deviceStatus = data.powerSwitch === 1 ? 'on' : 'off';
42
+ this.details = { ...data };
43
+ this.mode = (_a = data.workMode) !== null && _a !== void 0 ? _a : 'normal';
44
+ // A status poll cannot identify the timer. It may have been replaced in
45
+ // the app, so never carry an older command's id into cancellation.
46
+ this.timerId = undefined;
47
+ this.updateTimerState((_b = data.timerRemain) !== null && _b !== void 0 ? _b : 0, this.deviceStatus === 'on' ? 'off' : 'on');
54
48
  return true;
55
49
  }
56
- /**
57
- * Toggle device power
58
- */
59
50
  async toggleSwitch(toggle) {
60
- if (typeof toggle !== 'boolean') {
61
- logger_1.logger.debug('Invalid toggle value for tower fan switch');
51
+ if (typeof toggle !== 'boolean')
62
52
  return false;
63
- }
64
- const [head, body] = this.buildApiDict('setSwitch');
65
- body.payload.data = {
66
- powerSwitch: toggle ? 1 : 0,
67
- switchIdx: 0
68
- };
69
- const [response, status] = await this.callApi('/cloud/v2/deviceManaged/bypassV2', 'post', body, head);
70
- const success = this.checkResponse([response, status], 'toggleSwitch');
71
- if (success) {
72
- this.deviceStatus = toggle ? 'on' : 'off';
73
- }
74
- else {
75
- logger_1.logger.debug('Error toggling tower fan power');
76
- }
77
- return success;
78
- }
79
- /**
80
- * Turn device on
81
- */
82
- async turnOn() {
83
- logger_1.logger.debug(`Turning on device: ${this.deviceName}`);
84
- return this.toggleSwitch(true);
85
- }
86
- /**
87
- * Turn device off
88
- */
89
- async turnOff() {
90
- logger_1.logger.debug(`Turning off device: ${this.deviceName}`);
91
- return this.toggleSwitch(false);
53
+ if (!await this.request('setSwitch', { powerSwitch: Number(toggle), switchIdx: 0 }))
54
+ return false;
55
+ this.deviceStatus = toggle ? 'on' : 'off';
56
+ this.details.powerSwitch = Number(toggle);
57
+ return true;
92
58
  }
93
- /**
94
- * Set tower fan mode
95
- */
59
+ async turnOn() { return this.toggleSwitch(true); }
60
+ async turnOff() { return this.toggleSwitch(false); }
96
61
  async mode_toggle(mode) {
97
- if (!this.towerModes.includes(mode)) {
98
- logger_1.logger.debug(`Invalid mode: ${mode}. Must be one of: ${this.towerModes.join(', ')}`);
62
+ const apiMode = mode === 'sleep' ? 'advancedSleep' : mode === 'manual' ? 'normal' : mode;
63
+ if (!this.towerModes.includes(apiMode))
99
64
  return false;
100
- }
101
- if (mode === 'off') {
65
+ if (apiMode === 'off')
102
66
  return this.turnOff();
103
- }
104
- const [head, body] = this.buildApiDict('setTowerFanMode');
105
- body.deviceId = this.cid;
106
- body.payload.data = {
107
- workMode: mode
108
- };
109
- const [response, status] = await this.callApi('/cloud/v2/deviceManaged/bypassV2', 'post', body, head);
110
- const success = this.checkResponse([response, status], 'mode_toggle');
111
- if (success) {
112
- this.details.workMode = mode;
113
- }
114
- else {
115
- logger_1.logger.error(`Failed to set ${mode} mode for device: ${this.deviceName}`);
116
- }
117
- return success;
67
+ if (!await this.request('setTowerFanMode', { workMode: apiMode }))
68
+ return false;
69
+ this.mode = apiMode;
70
+ return true;
118
71
  }
119
- /**
120
- * Change fan speed
121
- */
72
+ async setMode(mode) { return this.mode_toggle(mode); }
73
+ async normal_mode() { return this.setMode('normal'); }
74
+ async manual_mode() { return this.normal_mode(); }
75
+ async advanced_sleep_mode() { return this.setMode('sleep'); }
76
+ async sleep_mode() { return this.advanced_sleep_mode(); }
122
77
  async changeFanSpeed(speed) {
123
- // const speeds = this.config.levels ?? [];
124
- // if (!speeds.includes(speed)) {
125
- // logger.debug(`Invalid speed: ${speed}. Must be one of: ${speeds.join(', ')}`);
126
- // return false;
127
- // }
128
- const [head, body] = this.buildApiDict('setLevel');
129
- body.payload.data = {
130
- levelIdx: 0,
131
- levelType: 'wind',
132
- manualSpeedLevel: speed
133
- };
134
- const [response, status] = await this.callApi('/cloud/v2/deviceManaged/bypassV2', 'post', body, head);
135
- const success = this.checkResponse([response, status], 'changeFanSpeed');
136
- if (success) {
137
- this.details.manualSpeedLevel = speed;
138
- }
139
- else {
140
- logger_1.logger.debug('Error setting fan speed');
141
- }
142
- return success;
143
- }
144
- /**
145
- * Set normal mode
146
- */
147
- async normal_mode() {
148
- logger_1.logger.debug(`Setting normal mode for device: ${this.deviceName}`);
149
- return this.mode_toggle('normal');
150
- }
151
- /**
152
- * Set manual mode - adapter to set mode to normal
153
- */
154
- async manual_mode() {
155
- logger_1.logger.debug(`Setting manual mode for device: ${this.deviceName}`);
156
- return this.normal_mode();
157
- }
158
- /**
159
- * Set advanced sleep mode
160
- */
161
- async advanced_sleep_mode() {
162
- logger_1.logger.debug(`Setting advanced sleep mode for device: ${this.deviceName}`);
163
- return this.mode_toggle('advancedSleep');
164
- }
165
- /**
166
- * Set sleep mode - adapter for advanced sleep mode
167
- */
168
- async sleep_mode() {
169
- logger_1.logger.debug(`Setting sleep mode for device: ${this.deviceName}`);
170
- return this.advanced_sleep_mode();
171
- }
172
- /**
173
- * Override setMode to handle tower fan specific modes
174
- */
175
- async setMode(mode) {
176
- return this.mode_toggle(mode);
177
- }
178
- /**
179
- * Get current speed
180
- */
181
- get speed() {
182
- return this.details.manualSpeedLevel || 0;
183
- }
184
- /**
185
- * Get display status
186
- */
187
- get displayState() {
188
- return Boolean(this.details.screenState);
189
- }
190
- /**
191
- * Get oscillation status
192
- */
193
- get oscillationState() {
194
- return Boolean(this.details.oscillationState);
195
- }
196
- /**
197
- * Get mute status
198
- */
199
- get muteState() {
200
- return Boolean(this.details.muteState);
201
- }
202
- /**
203
- * Get temperature (in tenths of a degree)
204
- */
205
- get temperature() {
206
- return this.details.temperature || 0;
207
- }
208
- /**
209
- * Get humidity percentage
210
- */
211
- get humidity() {
212
- return this.details.humidity || 0;
78
+ var _a;
79
+ if (!Number.isInteger(speed) || !((_a = this.config.levels) !== null && _a !== void 0 ? _a : []).includes(speed))
80
+ return false;
81
+ if (!await this.request('setLevel', { levelIdx: 0, levelType: 'wind', manualSpeedLevel: speed }))
82
+ return false;
83
+ this.details.manualSpeedLevel = speed;
84
+ return true;
213
85
  }
214
- /**
215
- * Get thermal comfort level
216
- */
217
- get thermalComfort() {
218
- return this.details.thermalComfort || 0;
86
+ async setOscillation(toggle) {
87
+ if (typeof toggle !== 'boolean')
88
+ return false;
89
+ if (!await this.request('setOscillationSwitch', { oscillationSwitch: Number(toggle) }))
90
+ return false;
91
+ this.details.oscillationSwitch = Number(toggle);
92
+ this.details.oscillationState = Number(toggle);
93
+ return true;
219
94
  }
220
- /**
221
- * Get sleep preferences
222
- */
223
- get sleepPreference() {
224
- return this.details.sleepPreference || {};
95
+ async setOscillationOn() { return this.setOscillation(true); }
96
+ async setOscillationOff() { return this.setOscillation(false); }
97
+ async setDisplay(enabled) {
98
+ if (typeof enabled !== 'boolean')
99
+ return false;
100
+ if (!await this.request('setDisplay', { screenSwitch: Number(enabled) }))
101
+ return false;
102
+ this.details.screenSwitch = Number(enabled);
103
+ this.details.screenState = Number(enabled);
104
+ return true;
225
105
  }
226
- /**
227
- * Get schedule count
228
- */
229
- get scheduleCount() {
230
- return this.details.scheduleCount || 0;
106
+ async setMute(enabled) {
107
+ if (typeof enabled !== 'boolean')
108
+ return false;
109
+ if (!await this.request('setMuteSwitch', { muteSwitch: Number(enabled) }))
110
+ return false;
111
+ this.details.muteSwitch = Number(enabled);
112
+ this.details.muteState = Number(enabled);
113
+ return true;
231
114
  }
232
- /**
233
- * Get error code
234
- */
235
- get errorCode() {
236
- return this.details.errorCode || 0;
115
+ /** Public timer duration remains hours, like the other tsvesync devices. */
116
+ async setTimer(hours, action = this.deviceStatus === 'on' ? 'off' : 'on') {
117
+ const seconds = Math.round(hours * 3600);
118
+ if (!Number.isFinite(hours) || hours <= 0 || hours > 12 || seconds < 1 || !['on', 'off'].includes(action))
119
+ return false;
120
+ const result = await this.request('setTimer', { action, total: seconds });
121
+ if (!result || !Number.isInteger(result.id))
122
+ return false;
123
+ this.timerId = result.id;
124
+ this.updateTimerState(seconds, action);
125
+ return true;
237
126
  }
238
- /**
239
- * Turn oscillation on
240
- */
241
- async setOscillationOn() {
242
- return this.setOscillation(true);
127
+ async getTimer() {
128
+ const result = await this.request('getTimer');
129
+ if (!result || !('timers' in result) || (result.timers !== null && !Array.isArray(result.timers)))
130
+ return false;
131
+ if (result.timers === null || result.timers.length === 0) {
132
+ this.updateTimerState(0, 'off');
133
+ return true;
134
+ }
135
+ const timer = result.timers[0];
136
+ if (!Number.isInteger(timer.id) || !Number.isFinite(timer.remain) || timer.remain < 0 || !['on', 'off'].includes(timer.action))
137
+ return false;
138
+ this.timerId = timer.id;
139
+ this.updateTimerState(timer.remain, timer.action);
140
+ return true;
243
141
  }
244
- /**
245
- * Turn oscillation off
246
- */
247
- async setOscillationOff() {
248
- return this.setOscillation(false);
142
+ async clearTimer() {
143
+ // Status responses include the remaining duration but not the timer id.
144
+ if (this.timerRemaining > 0 && this.timerId === undefined && !await this.getTimer())
145
+ return false;
146
+ if (this.timerId === undefined)
147
+ return this.timerRemaining === 0;
148
+ if (!await this.request('clearTimer', { id: this.timerId }))
149
+ return false;
150
+ this.updateTimerState(0, 'off');
151
+ return true;
249
152
  }
250
- /**
251
- * Display device info
252
- */
153
+ updateTimerState(seconds, action) {
154
+ const remaining = Number.isFinite(seconds) ? Math.max(0, seconds) : 0;
155
+ this.details.timerRemain = remaining;
156
+ this._timer = remaining > 0 ? { duration: remaining, action } : null;
157
+ this.timerDeadline = remaining > 0 ? Date.now() + remaining * 1000 : undefined;
158
+ if (remaining === 0)
159
+ this.timerId = undefined;
160
+ }
161
+ get timerRemaining() {
162
+ return this.timerDeadline === undefined ? 0 : Math.max(0, Math.ceil((this.timerDeadline - Date.now()) / 1000));
163
+ }
164
+ get mode() { var _a; return this.details.workMode === 'advancedSleep' ? 'sleep' : (_a = this.details.workMode) !== null && _a !== void 0 ? _a : 'normal'; }
165
+ set mode(value) {
166
+ this.details.workMode = value === 'sleep' ? 'advancedSleep' : value === 'manual' ? 'normal' : value;
167
+ this.details.mode = this.mode;
168
+ }
169
+ get speed() { var _a, _b; return (_b = (_a = this.details.manualSpeedLevel) !== null && _a !== void 0 ? _a : this.details.fanSpeedLevel) !== null && _b !== void 0 ? _b : 0; }
170
+ set speed(value) { this.details.manualSpeedLevel = value; }
171
+ get displayState() { return this.details.screenState === 1; }
172
+ get displayEnabled() { return this.details.screenSwitch === 1; }
173
+ get screenStatus() { return this.displayEnabled ? 'on' : 'off'; }
174
+ get oscillationState() { return this.details.oscillationState === 1; }
175
+ get oscillationEnabled() { return this.details.oscillationSwitch === 1; }
176
+ get muteState() { return this.details.muteState === 1; }
177
+ /** Raw API temperature is Fahrenheit times ten, retained for compatibility. */
178
+ get temperature() { var _a; return (_a = this.details.temperature) !== null && _a !== void 0 ? _a : 0; }
179
+ get temperatureCelsius() {
180
+ const raw = this.details.temperature;
181
+ return typeof raw === 'number' && Number.isFinite(raw) ? (raw / 10 - 32) * 5 / 9 : undefined;
182
+ }
183
+ get humidity() { var _a; return (_a = this.details.humidity) !== null && _a !== void 0 ? _a : 0; }
184
+ get thermalComfort() { var _a; return (_a = this.details.thermalComfort) !== null && _a !== void 0 ? _a : 0; }
185
+ get sleepPreference() { var _a; return (_a = this.details.sleepPreference) !== null && _a !== void 0 ? _a : {}; }
186
+ get scheduleCount() { var _a; return (_a = this.details.scheduleCount) !== null && _a !== void 0 ? _a : 0; }
187
+ get errorCode() { var _a; return (_a = this.details.errorCode) !== null && _a !== void 0 ? _a : 0; }
253
188
  display() {
189
+ var _a;
254
190
  super.display();
255
191
  const info = [
256
192
  ['Mode: ', this.details.workMode || ''],
@@ -263,7 +199,7 @@ class VeSyncTowerFan extends airBaseV2_1.VeSyncAirBaseV2 {
263
199
  ['Mute: ', this.muteState],
264
200
  ['Mute Switch: ', this.details.muteSwitch],
265
201
  ['Timer: ', this.timer],
266
- ['Temperature: ', this.temperature / 10, '°C'],
202
+ ['Temperature: ', (_a = this.temperatureCelsius) !== null && _a !== void 0 ? _a : 'unknown', '°C'],
267
203
  ['Humidity: ', this.humidity, '%'],
268
204
  ['Thermal Comfort: ', this.thermalComfort],
269
205
  ['Sleep Preference: ', JSON.stringify(this.sleepPreference)],
@@ -229,25 +229,35 @@ exports.fanConfig = {
229
229
  'LTF-F422S-KEU': {
230
230
  module: 'VeSyncTowerFan',
231
231
  // No child_lock: the LTF-F422S API neither reports nor accepts it (pyvesync parity)
232
- features: ['display', 'night_light', 'timer', 'fan_speeds', 'tower_modes'],
232
+ features: ['display', 'timer', 'fan_speeds', 'tower_modes', 'oscillation', 'temperature', 'mute'],
233
+ modes: ['normal', 'auto', 'sleep', 'turbo'],
233
234
  levels: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
234
235
  },
235
236
  'LTF-F422S-WUSR': {
236
237
  module: 'VeSyncTowerFan',
237
238
  // No child_lock: the LTF-F422S API neither reports nor accepts it (pyvesync parity)
238
- features: ['display', 'night_light', 'timer', 'fan_speeds', 'tower_modes'],
239
+ features: ['display', 'timer', 'fan_speeds', 'tower_modes', 'oscillation', 'temperature', 'mute'],
240
+ modes: ['normal', 'auto', 'sleep', 'turbo'],
241
+ levels: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
242
+ },
243
+ 'LTF-F422S-WJP': {
244
+ module: 'VeSyncTowerFan',
245
+ features: ['display', 'timer', 'fan_speeds', 'tower_modes', 'oscillation', 'temperature', 'mute'],
246
+ modes: ['normal', 'auto', 'sleep', 'turbo'],
239
247
  levels: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
240
248
  },
241
249
  'LTF-F422_WJP': {
242
250
  module: 'VeSyncTowerFan',
243
251
  // No child_lock: the LTF-F422S API neither reports nor accepts it (pyvesync parity)
244
- features: ['display', 'night_light', 'timer', 'fan_speeds', 'tower_modes'],
252
+ features: ['display', 'timer', 'fan_speeds', 'tower_modes', 'oscillation', 'temperature', 'mute'],
253
+ modes: ['normal', 'auto', 'sleep', 'turbo'],
245
254
  levels: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
246
255
  },
247
256
  'LTF-F422S-WUS': {
248
257
  module: 'VeSyncTowerFan',
249
258
  // No child_lock: the LTF-F422S API neither reports nor accepts it (pyvesync parity)
250
- features: ['display', 'night_light', 'timer', 'fan_speeds', 'tower_modes'],
259
+ features: ['display', 'timer', 'fan_speeds', 'tower_modes', 'oscillation', 'temperature', 'mute'],
260
+ modes: ['normal', 'auto', 'sleep', 'turbo'],
251
261
  levels: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
252
262
  },
253
263
  // Classic Series
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tsvesync",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "A TypeScript library for interacting with VeSync smart home devices",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -12,7 +12,8 @@
12
12
  "prepare": "npm run build",
13
13
  "version": "git add -A src",
14
14
  "postversion": "git push && git push --tags",
15
- "test:token-refresh": "ts-node test/token-refresh-retry.test.ts"
15
+ "test:token-refresh": "ts-node test/token-refresh-retry.test.ts",
16
+ "test:tower-fan": "ts-node test/tower-fan-parity.test.ts"
16
17
  },
17
18
  "keywords": [
18
19
  "vesync",
package/src/index.ts CHANGED
@@ -7,6 +7,9 @@ export { VeSyncOutlet7A, VeSyncOutlet10A, VeSyncOutlet15A, VeSyncOutdoorPlug } f
7
7
  export { VeSyncBulb } from './lib/vesyncBulb';
8
8
  export { VeSyncWallSwitch, VeSyncDimmerSwitch } from './lib/vesyncSwitchImpl';
9
9
 
10
+ // Tower fan API
11
+ export { VeSyncTowerFan } from './lib/fans/towerFan';
12
+
10
13
  // Export base device class
11
14
  export { VeSyncBaseDevice } from './lib/vesyncBaseDevice';
12
15
 
@@ -69,6 +69,7 @@ export const fanModules: Record<string, new (details: Record<string, any>, manag
69
69
  // LTF Series
70
70
  'LTF-F422S-KEU': VeSyncTowerFan,
71
71
  'LTF-F422S-WUSR': VeSyncTowerFan,
72
+ 'LTF-F422S-WJP': VeSyncTowerFan,
72
73
  'LTF-F422_WJP': VeSyncTowerFan,
73
74
  'LTF-F422S-WUS': VeSyncTowerFan,
74
75