homebridge-midea-platform 1.2.0-beta.8 → 1.2.2

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 (167) hide show
  1. package/CHANGELOG.md +5 -2
  2. package/README.md +2 -2
  3. package/dist/accessory/AccessoryFactory.d.ts +14 -15
  4. package/dist/accessory/AccessoryFactory.js +34 -40
  5. package/dist/accessory/AccessoryFactory.js.map +1 -1
  6. package/dist/accessory/AirConditionerAccessory.d.ts +98 -99
  7. package/dist/accessory/AirConditionerAccessory.js +666 -662
  8. package/dist/accessory/AirConditionerAccessory.js.map +1 -1
  9. package/dist/accessory/BaseAccessory.d.ts +11 -12
  10. package/dist/accessory/BaseAccessory.js +21 -21
  11. package/dist/accessory/BaseAccessory.js.map +1 -1
  12. package/dist/accessory/DehumidifierAccessory.d.ts +45 -46
  13. package/dist/accessory/DehumidifierAccessory.js +343 -344
  14. package/dist/accessory/DehumidifierAccessory.js.map +1 -1
  15. package/dist/accessory/DishwasherAccessory.d.ts +30 -31
  16. package/dist/accessory/DishwasherAccessory.js +59 -63
  17. package/dist/accessory/DishwasherAccessory.js.map +1 -1
  18. package/dist/accessory/ElectricWaterHeaterAccessory.d.ts +44 -45
  19. package/dist/accessory/ElectricWaterHeaterAccessory.js +172 -176
  20. package/dist/accessory/ElectricWaterHeaterAccessory.js.map +1 -1
  21. package/dist/accessory/FanAccessory.d.ts +39 -40
  22. package/dist/accessory/FanAccessory.js +120 -123
  23. package/dist/accessory/FanAccessory.js.map +1 -1
  24. package/dist/accessory/FrontLoadWasherAccessory.d.ts +30 -31
  25. package/dist/accessory/FrontLoadWasherAccessory.js +63 -66
  26. package/dist/accessory/FrontLoadWasherAccessory.js.map +1 -1
  27. package/dist/accessory/GasWaterHeaterAccessory.d.ts +51 -52
  28. package/dist/accessory/GasWaterHeaterAccessory.js +217 -216
  29. package/dist/accessory/GasWaterHeaterAccessory.js.map +1 -1
  30. package/dist/accessory/{HeatPumpWifiControllerAccessory.d.ts → HeatPumpWiFiControllerAccessory.d.ts} +34 -35
  31. package/dist/accessory/{HeatPumpWifiControllerAccessory.js → HeatPumpWiFiControllerAccessory.js} +55 -36
  32. package/dist/accessory/HeatPumpWiFiControllerAccessory.js.map +1 -0
  33. package/dist/core/MideaCloud.d.ts +34 -36
  34. package/dist/core/MideaCloud.js +349 -350
  35. package/dist/core/MideaCloud.js.map +1 -1
  36. package/dist/core/MideaConstants.d.ts +51 -52
  37. package/dist/core/MideaConstants.js +56 -59
  38. package/dist/core/MideaConstants.js.map +1 -1
  39. package/dist/core/MideaDevice.d.ts +75 -78
  40. package/dist/core/MideaDevice.js +430 -420
  41. package/dist/core/MideaDevice.js.map +1 -1
  42. package/dist/core/MideaDiscover.d.ts +34 -36
  43. package/dist/core/MideaDiscover.js +208 -212
  44. package/dist/core/MideaDiscover.js.map +1 -1
  45. package/dist/core/MideaMessage.d.ts +75 -76
  46. package/dist/core/MideaMessage.js +185 -184
  47. package/dist/core/MideaMessage.js.map +1 -1
  48. package/dist/core/MideaPacketBuilder.d.ts +9 -11
  49. package/dist/core/MideaPacketBuilder.js +60 -60
  50. package/dist/core/MideaPacketBuilder.js.map +1 -1
  51. package/dist/core/MideaSecurity.d.ts +62 -64
  52. package/dist/core/MideaSecurity.js +241 -251
  53. package/dist/core/MideaSecurity.js.map +1 -1
  54. package/dist/core/MideaUtils.d.ts +31 -33
  55. package/dist/core/MideaUtils.js +178 -181
  56. package/dist/core/MideaUtils.js.map +1 -1
  57. package/dist/devices/DeviceFactory.d.ts +14 -15
  58. package/dist/devices/DeviceFactory.js +34 -40
  59. package/dist/devices/DeviceFactory.js.map +1 -1
  60. package/dist/devices/a1/MideaA1Device.d.ts +75 -77
  61. package/dist/devices/a1/MideaA1Device.js +142 -145
  62. package/dist/devices/a1/MideaA1Device.js.map +1 -1
  63. package/dist/devices/a1/MideaA1Message.d.ts +39 -41
  64. package/dist/devices/a1/MideaA1Message.js +219 -198
  65. package/dist/devices/a1/MideaA1Message.js.map +1 -1
  66. package/dist/devices/ac/MideaACDevice.d.ts +105 -107
  67. package/dist/devices/ac/MideaACDevice.js +417 -400
  68. package/dist/devices/ac/MideaACDevice.js.map +1 -1
  69. package/dist/devices/ac/MideaACMessage.d.ts +97 -96
  70. package/dist/devices/ac/MideaACMessage.js +724 -621
  71. package/dist/devices/ac/MideaACMessage.js.map +1 -1
  72. package/dist/devices/c3/MideaC3Device.d.ts +73 -75
  73. package/dist/devices/c3/MideaC3Device.js +249 -255
  74. package/dist/devices/c3/MideaC3Device.js.map +1 -1
  75. package/dist/devices/c3/MideaC3Message.d.ts +81 -80
  76. package/dist/devices/c3/MideaC3Message.js +190 -152
  77. package/dist/devices/c3/MideaC3Message.js.map +1 -1
  78. package/dist/devices/db/MideaDBDevice.d.ts +28 -30
  79. package/dist/devices/db/MideaDBDevice.js +94 -100
  80. package/dist/devices/db/MideaDBDevice.js.map +1 -1
  81. package/dist/devices/db/MideaDBMessage.d.ts +31 -33
  82. package/dist/devices/db/MideaDBMessage.js +101 -101
  83. package/dist/devices/db/MideaDBMessage.js.map +1 -1
  84. package/dist/devices/e1/MideaE1Device.d.ts +55 -57
  85. package/dist/devices/e1/MideaE1Device.js +122 -128
  86. package/dist/devices/e1/MideaE1Device.js.map +1 -1
  87. package/dist/devices/e1/MideaE1Message.d.ts +27 -29
  88. package/dist/devices/e1/MideaE1Message.js +128 -107
  89. package/dist/devices/e1/MideaE1Message.js.map +1 -1
  90. package/dist/devices/e2/MideaE2Device.d.ts +43 -45
  91. package/dist/devices/e2/MideaE2Device.js +124 -129
  92. package/dist/devices/e2/MideaE2Device.js.map +1 -1
  93. package/dist/devices/e2/MideaE2Message.d.ts +34 -34
  94. package/dist/devices/e2/MideaE2Message.js +143 -132
  95. package/dist/devices/e2/MideaE2Message.js.map +1 -1
  96. package/dist/devices/e3/MideaE3Device.d.ts +42 -44
  97. package/dist/devices/e3/MideaE3Device.js +132 -137
  98. package/dist/devices/e3/MideaE3Device.js.map +1 -1
  99. package/dist/devices/e3/MideaE3Message.d.ts +51 -52
  100. package/dist/devices/e3/MideaE3Message.js +144 -136
  101. package/dist/devices/e3/MideaE3Message.js.map +1 -1
  102. package/dist/devices/fa/MideaFADevice.d.ts +35 -37
  103. package/dist/devices/fa/MideaFADevice.js +102 -106
  104. package/dist/devices/fa/MideaFADevice.js.map +1 -1
  105. package/dist/devices/fa/MideaFAMessage.d.ts +38 -39
  106. package/dist/devices/fa/MideaFAMessage.js +108 -98
  107. package/dist/devices/fa/MideaFAMessage.js.map +1 -1
  108. package/dist/index.d.ts +6 -7
  109. package/dist/index.js +8 -6
  110. package/dist/index.js.map +1 -1
  111. package/dist/platform.d.ts +61 -61
  112. package/dist/platform.js +232 -212
  113. package/dist/platform.js.map +1 -1
  114. package/dist/platformUtils.d.ts +116 -117
  115. package/dist/platformUtils.js +107 -110
  116. package/dist/platformUtils.js.map +1 -1
  117. package/dist/settings.d.ts +8 -9
  118. package/dist/settings.js +8 -11
  119. package/dist/settings.js.map +1 -1
  120. package/docs/download_lua.md +9 -0
  121. package/eslint.config.js +43 -0
  122. package/homebridge-ui/public/js/bootstrap.min.js +1179 -1
  123. package/homebridge-ui/server.js +157 -84
  124. package/package.json +21 -31
  125. package/.eslintignore +0 -3
  126. package/.husky/pre-commit +0 -5
  127. package/.prettierrc +0 -19
  128. package/dist/accessory/AccessoryFactory.d.ts.map +0 -1
  129. package/dist/accessory/AirConditionerAccessory.d.ts.map +0 -1
  130. package/dist/accessory/BaseAccessory.d.ts.map +0 -1
  131. package/dist/accessory/DehumidifierAccessory.d.ts.map +0 -1
  132. package/dist/accessory/DishwasherAccessory.d.ts.map +0 -1
  133. package/dist/accessory/ElectricWaterHeaterAccessory.d.ts.map +0 -1
  134. package/dist/accessory/FanAccessory.d.ts.map +0 -1
  135. package/dist/accessory/FrontLoadWasherAccessory.d.ts.map +0 -1
  136. package/dist/accessory/GasWaterHeaterAccessory.d.ts.map +0 -1
  137. package/dist/accessory/HeatPumpWifiControllerAccessory.d.ts.map +0 -1
  138. package/dist/accessory/HeatPumpWifiControllerAccessory.js.map +0 -1
  139. package/dist/core/MideaCloud.d.ts.map +0 -1
  140. package/dist/core/MideaConstants.d.ts.map +0 -1
  141. package/dist/core/MideaDevice.d.ts.map +0 -1
  142. package/dist/core/MideaDiscover.d.ts.map +0 -1
  143. package/dist/core/MideaMessage.d.ts.map +0 -1
  144. package/dist/core/MideaPacketBuilder.d.ts.map +0 -1
  145. package/dist/core/MideaSecurity.d.ts.map +0 -1
  146. package/dist/core/MideaUtils.d.ts.map +0 -1
  147. package/dist/devices/DeviceFactory.d.ts.map +0 -1
  148. package/dist/devices/a1/MideaA1Device.d.ts.map +0 -1
  149. package/dist/devices/a1/MideaA1Message.d.ts.map +0 -1
  150. package/dist/devices/ac/MideaACDevice.d.ts.map +0 -1
  151. package/dist/devices/ac/MideaACMessage.d.ts.map +0 -1
  152. package/dist/devices/c3/MideaC3Device.d.ts.map +0 -1
  153. package/dist/devices/c3/MideaC3Message.d.ts.map +0 -1
  154. package/dist/devices/db/MideaDBDevice.d.ts.map +0 -1
  155. package/dist/devices/db/MideaDBMessage.d.ts.map +0 -1
  156. package/dist/devices/e1/MideaE1Device.d.ts.map +0 -1
  157. package/dist/devices/e1/MideaE1Message.d.ts.map +0 -1
  158. package/dist/devices/e2/MideaE2Device.d.ts.map +0 -1
  159. package/dist/devices/e2/MideaE2Message.d.ts.map +0 -1
  160. package/dist/devices/e3/MideaE3Device.d.ts.map +0 -1
  161. package/dist/devices/e3/MideaE3Message.d.ts.map +0 -1
  162. package/dist/devices/fa/MideaFADevice.d.ts.map +0 -1
  163. package/dist/devices/fa/MideaFAMessage.d.ts.map +0 -1
  164. package/dist/index.d.ts.map +0 -1
  165. package/dist/platform.d.ts.map +0 -1
  166. package/dist/platformUtils.d.ts.map +0 -1
  167. package/dist/settings.d.ts.map +0 -1
@@ -1,423 +1,433 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- const MideaSecurity_1 = require("./MideaSecurity");
7
- const MideaConstants_1 = require("./MideaConstants");
8
- const MideaMessage_1 = require("./MideaMessage");
9
- const MideaPacketBuilder_1 = __importDefault(require("./MideaPacketBuilder"));
10
- const MideaUtils_1 = require("./MideaUtils");
11
- const events_1 = __importDefault(require("events"));
12
- class MideaDevice extends events_1.default {
13
- constructor(logger, device_info, config, configDev) {
14
- var _a, _b;
15
- super();
16
- this.logger = logger;
17
- this.SOCKET_TIMEOUT = 1000; // milliseconds
18
- this.is_running = false;
19
- this.available = false;
20
- this.unsupported_protocol = [];
21
- this.device_protocol_version = 0;
22
- this.token = undefined;
23
- this.key = undefined;
24
- this.ip = device_info.ip;
25
- this.port = device_info.port;
26
- this.id = device_info.id;
27
- this.model = (_a = device_info.model) !== null && _a !== void 0 ? _a : 'unknown';
28
- this.sn = (_b = device_info.sn) !== null && _b !== void 0 ? _b : 'unknown';
29
- this.name = device_info.name;
30
- this.type = device_info.type;
31
- this.version = device_info.version;
32
- this.verbose = configDev.advanced_options.verbose;
33
- this.logRecoverableErrors = configDev.advanced_options.logRecoverableErrors;
34
- this.logRefreshStatusErrors = configDev.advanced_options.logRefreshStatusErrors;
35
- this.logger.debug(`[${this.name}] Device specific verbose debug logging is set to ${configDev.advanced_options.verbose}`);
36
- this.logger.debug(`[${this.name}] Device specific log recoverable errors is set to ${configDev.advanced_options.logRecoverableErrors}`);
37
- this.refresh_interval = config.refreshInterval * 1000; // convert to miliseconds
38
- this.heartbeat_interval = config.heartbeatInterval * 1000;
39
- this.security = new MideaSecurity_1.LocalSecurity();
40
- this.buffer = Buffer.alloc(0);
41
- this.promiseSocket = new MideaUtils_1.PromiseSocket(this.logger, this.logRecoverableErrors);
42
- }
43
- get sub_type() {
44
- return this._sub_type || 0;
45
- }
46
- setCredentials(token, key) {
47
- this.token = token;
48
- this.key = key;
49
- }
50
- fetch_v2_message(message) {
51
- const result = [];
52
- while (message.length > 0) {
53
- const length = message.length;
54
- if (length < 6) {
55
- break;
56
- }
57
- const alleged_length = message[4] + (message[5] << 8);
58
- if (length >= alleged_length) {
59
- result.push(message.subarray(0, alleged_length));
60
- message = message.subarray(alleged_length, length);
61
- }
62
- else {
63
- break;
64
- }
65
- }
66
- return [result, message];
67
- }
68
- async connect(refresh_status = true) {
69
- try {
70
- this.logger.debug(`Connecting to device ${this.name} (${this.ip}:${this.port})...`);
71
- await this.promiseSocket.connect(this.port, this.ip);
72
- this.promiseSocket.setTimeout(this.SOCKET_TIMEOUT);
73
- if (this.version === MideaConstants_1.ProtocolVersion.V3) {
74
- await this.authenticate();
75
- }
76
- let retries = 0;
77
- if (refresh_status) {
78
- let success = await this.refresh_status(true);
79
- while (!success && retries++ < 3) {
80
- success = await this.refresh_status(true, true);
81
- }
82
- }
83
- if (retries > 3) {
84
- this.logger.debug(`[${this.name}] Error when connecting to device ${this.name} (${this.ip}:${this.port}): Refresh status failed.`);
85
- return false;
86
- }
87
- // Start listening for network traffic
88
- this.open();
89
- return true;
90
- }
91
- catch (err) {
92
- const msg = err instanceof Error ? err.stack : err;
93
- this.logger.debug(`[${this.name}] Error when connecting to device ${this.name} (${this.ip}:${this.port}):\n${msg}`);
94
- // Even though error thrown, it is probably because device is offline. Start listening anyway.
95
- this.open();
96
- return true;
97
- }
98
- }
99
- async authenticate() {
100
- if (!(this.token && this.key)) {
101
- throw new Error('Token or key is missing!');
102
- }
103
- const request = this.security.encode_8370(this.token, MideaConstants_1.TCPMessageType.HANDSHAKE_REQUEST);
104
- await this.promiseSocket.write(request);
105
- const response = await this.promiseSocket.read();
106
- if (response) {
107
- if (response.length < 20) {
108
- throw Error(`Authenticate error when receiving data from ${this.ip}:${this.port}. (Data length mismatch)`);
109
- }
110
- const resp = response.subarray(8, 72);
111
- this.security.tcp_key_from_resp(resp, this.key);
112
- this.logger.debug(`[${this.name}] Authentication success.`);
113
- }
114
- else {
115
- throw Error(`Authenticate error when receiving data from ${this.ip}:${this.port}.`);
116
- }
117
- }
118
- async send_message(data) {
119
- if (this.verbose) {
120
- this.logger.debug(`[${this.name}] Send message:\n${data.toString('hex')}`);
121
- }
122
- if (this.version === MideaConstants_1.ProtocolVersion.V3) {
123
- await this.send_message_v3(data);
124
- }
125
- else {
126
- await this.send_message_v2(data);
127
- }
128
- }
129
- async send_message_v2(data, retries = 3, force_reinit = false) {
130
- if (retries === 0) {
131
- throw new Error(`[${this.name} | send_message] Error when sending data to device.`);
132
- }
133
- if (force_reinit || !this.promiseSocket || this.promiseSocket.destroyed) {
134
- this.promiseSocket = new MideaUtils_1.PromiseSocket(this.logger, this.logRecoverableErrors);
135
- let connected = await this.connect(false);
136
- while (!connected) {
137
- connected = await this.connect(false);
138
- }
139
- }
140
- try {
141
- await this.promiseSocket.write(data);
142
- }
143
- catch (_a) {
144
- this.logger.debug(`[${this.name}] Error when sending data to device, retrying...`);
145
- await this.send_message_v2(data, retries - 1, true);
146
- }
147
- }
148
- async send_message_v3(data, message_type = MideaConstants_1.TCPMessageType.ENCRYPTED_REQUEST) {
149
- const encrypted_data = this.security.encode_8370(data, message_type);
150
- await this.send_message_v2(encrypted_data);
151
- }
152
- async build_send(command) {
153
- const data = command.serialize();
154
- const message = new MideaPacketBuilder_1.default(this.id, data).finalize();
155
- await this.send_message(message);
156
- }
157
- async refresh_status(wait_response = false, ignore_unsupported = false) {
158
- this.logger.debug(`[${this.name}] Refreshing status...`);
159
- const commands = this.build_query();
160
- if (this._sub_type === undefined) {
161
- commands.unshift(new MideaMessage_1.MessageQuerySubtype(this.type));
162
- }
163
- try {
164
- let error_cnt = 0;
165
- for (const cmd of commands) {
166
- if (ignore_unsupported || !this.unsupported_protocol.includes(cmd.constructor.name)) {
167
- await this.build_send(cmd);
168
- if (wait_response) {
169
- try {
170
- // eslint-disable-next-line no-constant-condition
171
- while (true) {
172
- const message = await this.promiseSocket.read();
173
- if (message.length === 0) {
174
- throw new Error(`[${this.name} | refresh_status] Error when receiving data from device.`);
175
- }
176
- const result = this.parse_message(message);
177
- if (result === MideaConstants_1.ParseMessageResult.SUCCESS) {
178
- const cmd_idx = this.unsupported_protocol.indexOf(cmd.constructor.name);
179
- if (cmd_idx !== -1) {
180
- this.unsupported_protocol.splice(cmd_idx, 1);
181
- }
182
- break;
183
- }
184
- else if (result === MideaConstants_1.ParseMessageResult.PADDING) {
185
- continue;
186
- }
187
- else {
188
- throw new Error(`[${this.name} | refresh_status] Error when parsing message.`);
189
- }
190
- }
191
- }
192
- catch (err) {
193
- error_cnt++;
194
- // TODO: handle connection error
195
- // this.unsupported_protocol.push(cmd.constructor.name);
196
- if (this.logRefreshStatusErrors) {
197
- this.logger.warn(`[${this.name}] Does not supports the protocol ${cmd.constructor.name}, ignored, error: ${err}`);
198
- }
199
- else {
200
- this.logger.debug(`[${this.name}] Does not supports the protocol ${cmd.constructor.name}, ignored, error: ${err}`);
201
- }
202
- }
203
- }
204
- }
205
- else {
206
- error_cnt++;
207
- }
208
- }
209
- if (error_cnt === commands.length) {
210
- if (this.logRefreshStatusErrors) {
211
- this.logger.error(`[${this.name}] Refresh failed.`);
212
- }
213
- else {
214
- this.logger.debug(`[${this.name}] Refresh failed.`);
215
- }
216
- return false;
217
- }
218
- }
219
- catch (err) {
220
- const msg = err instanceof Error ? err.stack : err;
221
- if (this.logRecoverableErrors) {
222
- this.logger.warn(`[${this.name} | refresh_status] Recoverable error:\n${msg}`);
223
- }
224
- else {
225
- this.logger.debug(`[${this.name} | refresh_status] Recoverable error:\n${msg}`);
226
- }
227
- return false;
228
- }
229
- return true;
230
- }
231
- preprocess_message(message) {
232
- if (message[9] === MideaMessage_1.MessageType.QUERY_SUBTYPE) {
233
- const msg = new MideaMessage_1.MessageSubtypeResponse(message);
234
- this._sub_type = msg.sub_type;
235
- this.set_subtype();
236
- this.device_protocol_version = msg.device_protocol_version;
237
- this.logger.debug(`[${this.name}] Subtype: ${this._sub_type}, device protocol version: ${this.device_protocol_version}`);
238
- return false;
239
- }
240
- return true;
241
- }
242
- parse_message(message) {
243
- let messages;
244
- if (this.verbose) {
245
- this.logger.debug(`[${this.name}] Raw data to parse:\n${message.toString('hex')}`);
246
- }
247
- if (this.version === MideaConstants_1.ProtocolVersion.V3) {
248
- [messages, this.buffer] = this.security.decode_8370(Buffer.concat([this.buffer, message]));
249
- }
250
- else {
251
- [messages, this.buffer] = this.fetch_v2_message(Buffer.concat([this.buffer, message]));
252
- }
253
- if (message.length === 0) {
254
- return MideaConstants_1.ParseMessageResult.PADDING;
255
- }
256
- for (const msg of messages) {
257
- if (msg.toString('utf8') === 'ERROR') {
258
- return MideaConstants_1.ParseMessageResult.ERROR;
259
- }
260
- const payload_length = msg[4] + (msg[5] << 8) - 56;
261
- const payload_type = msg[2] + (msg[3] << 8);
262
- if (this.verbose) {
263
- this.logger.debug(`[${this.name}] Msg to process. Length: ${payload_length} (0x${payload_length.toString(16)}), Type: ${payload_type} (0x${payload_type.toString(16)})\n${msg.toString('hex')}`);
264
- }
265
- if ([0x1001, 0x0001].includes(payload_type)) {
266
- // Heartbeat
267
- if (this.verbose) {
268
- this.logger.debug(`[${this.name}] Heartbeat:\n${msg.toString('hex')}`);
269
- }
270
- }
271
- else if (msg.length > 56) {
272
- const cryptographic = msg.subarray(40, -16);
273
- if (payload_length % 16 === 0) {
274
- const decrypted = this.security.aes_decrypt(cryptographic);
275
- if (this.preprocess_message(decrypted)) {
276
- if (this.verbose) {
277
- this.logger.debug(`[${this.name}] Decrypted data to parse:\n${decrypted.toString('hex')}`);
278
- }
279
- this.process_message(decrypted);
280
- }
281
- }
282
- else {
283
- if (this.logRecoverableErrors) {
284
- this.logger.warn(`[${this.name}] Invalid payload length: ` + `${payload_length} (0x${payload_length.toString(16)})`);
285
- }
286
- else {
287
- this.logger.debug(`[${this.name}] Invalid payload length: ` + `${payload_length} (0x${payload_length.toString(16)})`);
288
- }
289
- }
290
- }
291
- else {
292
- this.logger.warn(`[${this.name}] Illegal message.`);
293
- }
294
- }
295
- return MideaConstants_1.ParseMessageResult.SUCCESS;
296
- }
297
- async send_command(command_type, command_body) {
298
- const cmd = new MideaMessage_1.MessageQuestCustom(this.type, command_type, command_body);
299
- try {
300
- if (this.verbose) {
301
- this.logger.debug(`[${this.name}] Send command: ${command_body.toString('hex')}`);
302
- }
303
- await this.build_send(cmd);
304
- }
305
- catch (e) {
1
+ import { LocalSecurity } from './MideaSecurity.js';
2
+ import { TCPMessageType, ProtocolVersion, ParseMessageResult } from './MideaConstants.js';
3
+ import { MessageQuerySubtype, MessageQuestCustom, MessageSubtypeResponse, MessageType } from './MideaMessage.js';
4
+ import PacketBuilder from './MideaPacketBuilder.js';
5
+ import EventEmitter from 'events';
6
+ import { PromiseSocket } from './MideaUtils.js';
7
+ export default class MideaDevice extends EventEmitter {
8
+ logger;
9
+ SOCKET_TIMEOUT = 1000; // milliseconds
10
+ ip;
11
+ port;
12
+ id;
13
+ model;
14
+ sn;
15
+ name;
16
+ type;
17
+ version;
18
+ is_running = false;
19
+ available = false;
20
+ unsupported_protocol = [];
21
+ device_protocol_version = 0;
22
+ refresh_interval;
23
+ heartbeat_interval;
24
+ verbose;
25
+ logRecoverableErrors;
26
+ logRefreshStatusErrors;
27
+ _sub_type;
28
+ token = undefined;
29
+ key = undefined;
30
+ security;
31
+ buffer;
32
+ promiseSocket;
33
+ constructor(logger, device_info, config, configDev) {
34
+ super();
35
+ this.logger = logger;
36
+ this.ip = device_info.ip;
37
+ this.port = device_info.port;
38
+ this.id = device_info.id;
39
+ this.model = device_info.model ?? 'unknown';
40
+ this.sn = device_info.sn ?? 'unknown';
41
+ this.name = device_info.name;
42
+ this.type = device_info.type;
43
+ this.version = device_info.version;
44
+ this.verbose = configDev.advanced_options.verbose;
45
+ this.logRecoverableErrors = configDev.advanced_options.logRecoverableErrors;
46
+ this.logRefreshStatusErrors = configDev.advanced_options.logRefreshStatusErrors;
47
+ this.logger.debug(`[${this.name}] Device specific verbose debug logging is set to ${configDev.advanced_options.verbose}`);
48
+ this.logger.debug(`[${this.name}] Device specific log recoverable errors is set to ${configDev.advanced_options.logRecoverableErrors}`);
49
+ this.refresh_interval = config.refreshInterval * 1000; // convert to miliseconds
50
+ this.heartbeat_interval = config.heartbeatInterval * 1000;
51
+ this.security = new LocalSecurity();
52
+ this.buffer = Buffer.alloc(0);
53
+ this.promiseSocket = new PromiseSocket(this.logger, this.logRecoverableErrors);
54
+ }
55
+ get sub_type() {
56
+ return this._sub_type || 0;
57
+ }
58
+ setCredentials(token, key) {
59
+ this.token = token;
60
+ this.key = key;
61
+ }
62
+ fetch_v2_message(message) {
63
+ const result = [];
64
+ while (message.length > 0) {
65
+ const length = message.length;
66
+ if (length < 6) {
67
+ break;
68
+ }
69
+ const alleged_length = message[4] + (message[5] << 8);
70
+ if (length >= alleged_length) {
71
+ result.push(message.subarray(0, alleged_length));
72
+ message = message.subarray(alleged_length, length);
73
+ }
74
+ else {
75
+ break;
76
+ }
77
+ }
78
+ return [result, message];
79
+ }
80
+ async connect(refresh_status = true) {
81
+ try {
82
+ this.logger.debug(`Connecting to device ${this.name} (${this.ip}:${this.port})...`);
83
+ await this.promiseSocket.connect(this.port, this.ip);
84
+ this.promiseSocket.setTimeout(this.SOCKET_TIMEOUT);
85
+ if (this.version === ProtocolVersion.V3) {
86
+ await this.authenticate();
87
+ }
88
+ let retries = 0;
89
+ if (refresh_status) {
90
+ let success = await this.refresh_status(true);
91
+ while (!success && retries++ < 3) {
92
+ success = await this.refresh_status(true, true);
93
+ }
94
+ }
95
+ if (retries > 3) {
96
+ this.logger.debug(`[${this.name}] Error when connecting to device ${this.name} (${this.ip}:${this.port}): Refresh status failed.`);
97
+ return false;
98
+ }
99
+ // Start listening for network traffic
100
+ this.open();
101
+ return true;
102
+ }
103
+ catch (err) {
104
+ const msg = err instanceof Error ? err.stack : err;
105
+ this.logger.debug(`[${this.name}] Error when connecting to device ${this.name} (${this.ip}:${this.port}):\n${msg}`);
106
+ // Even though error thrown, it is probably because device is offline. Start listening anyway.
107
+ this.open();
108
+ return true;
109
+ }
110
+ }
111
+ async authenticate() {
112
+ if (!(this.token && this.key)) {
113
+ throw new Error('Token or key is missing!');
114
+ }
115
+ const request = this.security.encode_8370(this.token, TCPMessageType.HANDSHAKE_REQUEST);
116
+ await this.promiseSocket.write(request);
117
+ const response = await this.promiseSocket.read();
118
+ if (response) {
119
+ if (response.length < 20) {
120
+ throw Error(`Authenticate error when receiving data from ${this.ip}:${this.port}. (Data length mismatch)`);
121
+ }
122
+ const resp = response.subarray(8, 72);
123
+ this.security.tcp_key_from_resp(resp, this.key);
124
+ this.logger.debug(`[${this.name}] Authentication success.`);
125
+ }
126
+ else {
127
+ throw Error(`Authenticate error when receiving data from ${this.ip}:${this.port}.`);
128
+ }
129
+ }
130
+ async send_message(data) {
131
+ if (this.verbose) {
132
+ this.logger.debug(`[${this.name}] Send message:\n${data.toString('hex')}`);
133
+ }
134
+ if (this.version === ProtocolVersion.V3) {
135
+ await this.send_message_v3(data);
136
+ }
137
+ else {
138
+ await this.send_message_v2(data);
139
+ }
140
+ }
141
+ async send_message_v2(data, retries = 3, force_reinit = false) {
142
+ if (retries === 0) {
143
+ throw new Error(`[${this.name} | send_message] Error when sending data to device.`);
144
+ }
145
+ if (force_reinit || !this.promiseSocket || this.promiseSocket.destroyed) {
146
+ this.promiseSocket = new PromiseSocket(this.logger, this.logRecoverableErrors);
147
+ let connected = await this.connect(false);
148
+ while (!connected) {
149
+ connected = await this.connect(false);
150
+ }
151
+ }
152
+ try {
153
+ await this.promiseSocket.write(data);
154
+ }
155
+ catch {
156
+ this.logger.debug(`[${this.name}] Error when sending data to device, retrying...`);
157
+ await this.send_message_v2(data, retries - 1, true);
158
+ }
159
+ }
160
+ async send_message_v3(data, message_type = TCPMessageType.ENCRYPTED_REQUEST) {
161
+ const encrypted_data = this.security.encode_8370(data, message_type);
162
+ await this.send_message_v2(encrypted_data);
163
+ }
164
+ async build_send(command) {
165
+ const data = command.serialize();
166
+ const message = new PacketBuilder(this.id, data).finalize();
167
+ await this.send_message(message);
168
+ }
169
+ async refresh_status(wait_response = false, ignore_unsupported = false) {
170
+ this.logger.debug(`[${this.name}] Refreshing status...`);
171
+ const commands = this.build_query();
172
+ if (this._sub_type === undefined) {
173
+ commands.unshift(new MessageQuerySubtype(this.type));
174
+ }
175
+ try {
176
+ let error_cnt = 0;
177
+ for (const cmd of commands) {
178
+ if (ignore_unsupported || !this.unsupported_protocol.includes(cmd.constructor.name)) {
179
+ await this.build_send(cmd);
180
+ if (wait_response) {
181
+ try {
182
+ while (true) {
183
+ const message = await this.promiseSocket.read();
184
+ if (message.length === 0) {
185
+ throw new Error(`[${this.name} | refresh_status] Error when receiving data from device.`);
186
+ }
187
+ const result = this.parse_message(message);
188
+ if (result === ParseMessageResult.SUCCESS) {
189
+ const cmd_idx = this.unsupported_protocol.indexOf(cmd.constructor.name);
190
+ if (cmd_idx !== -1) {
191
+ this.unsupported_protocol.splice(cmd_idx, 1);
192
+ }
193
+ break;
194
+ }
195
+ else if (result === ParseMessageResult.PADDING) {
196
+ continue;
197
+ }
198
+ else {
199
+ throw new Error(`[${this.name} | refresh_status] Error when parsing message.`);
200
+ }
201
+ }
202
+ }
203
+ catch (err) {
204
+ error_cnt++;
205
+ // TODO: handle connection error
206
+ // this.unsupported_protocol.push(cmd.constructor.name);
207
+ if (this.logRefreshStatusErrors) {
208
+ this.logger.warn(`[${this.name}] Does not supports the protocol ${cmd.constructor.name}, ignored, error: ${err}`);
209
+ }
210
+ else {
211
+ this.logger.debug(`[${this.name}] Does not supports the protocol ${cmd.constructor.name}, ignored, error: ${err}`);
212
+ }
213
+ }
214
+ }
215
+ }
216
+ else {
217
+ error_cnt++;
218
+ }
219
+ }
220
+ if (error_cnt === commands.length) {
221
+ if (this.logRefreshStatusErrors) {
222
+ this.logger.error(`[${this.name}] Refresh failed.`);
223
+ }
224
+ else {
225
+ this.logger.debug(`[${this.name}] Refresh failed.`);
226
+ }
227
+ return false;
228
+ }
229
+ }
230
+ catch (err) {
231
+ const msg = err instanceof Error ? err.stack : err;
232
+ if (this.logRecoverableErrors) {
233
+ this.logger.warn(`[${this.name} | refresh_status] Recoverable error:\n${msg}`);
234
+ }
235
+ else {
236
+ this.logger.debug(`[${this.name} | refresh_status] Recoverable error:\n${msg}`);
237
+ }
238
+ return false;
239
+ }
240
+ return true;
241
+ }
242
+ preprocess_message(message) {
243
+ if (message[9] === MessageType.QUERY_SUBTYPE) {
244
+ const msg = new MessageSubtypeResponse(message);
245
+ this._sub_type = msg.sub_type;
246
+ this.set_subtype();
247
+ this.device_protocol_version = msg.device_protocol_version;
248
+ this.logger.debug(`[${this.name}] Subtype: ${this._sub_type}, device protocol version: ${this.device_protocol_version}`);
249
+ return false;
250
+ }
251
+ return true;
252
+ }
253
+ parse_message(message) {
254
+ let messages;
255
+ if (this.verbose) {
256
+ this.logger.debug(`[${this.name}] Raw data to parse:\n${message.toString('hex')}`);
257
+ }
258
+ if (this.version === ProtocolVersion.V3) {
259
+ [messages, this.buffer] = this.security.decode_8370(Buffer.concat([this.buffer, message]));
260
+ }
261
+ else {
262
+ [messages, this.buffer] = this.fetch_v2_message(Buffer.concat([this.buffer, message]));
263
+ }
264
+ if (message.length === 0) {
265
+ return ParseMessageResult.PADDING;
266
+ }
267
+ for (const msg of messages) {
268
+ if (msg.toString('utf8') === 'ERROR') {
269
+ return ParseMessageResult.ERROR;
270
+ }
271
+ const payload_length = msg[4] + (msg[5] << 8) - 56;
272
+ const payload_type = msg[2] + (msg[3] << 8);
273
+ if (this.verbose) {
274
+ this.logger.debug(`[${this.name}] Msg to process. Length: ${payload_length} (0x${payload_length.toString(16)}), Type: ${payload_type} (0x${payload_type.toString(16)})\n${msg.toString('hex')}`);
275
+ }
276
+ if ([0x1001, 0x0001].includes(payload_type)) {
277
+ // Heartbeat
278
+ if (this.verbose) {
279
+ this.logger.debug(`[${this.name}] Heartbeat:\n${msg.toString('hex')}`);
280
+ }
281
+ }
282
+ else if (msg.length > 56) {
283
+ const cryptographic = msg.subarray(40, -16);
284
+ if (payload_length % 16 === 0) {
285
+ const decrypted = this.security.aes_decrypt(cryptographic);
286
+ if (this.preprocess_message(decrypted)) {
287
+ if (this.verbose) {
288
+ this.logger.debug(`[${this.name}] Decrypted data to parse:\n${decrypted.toString('hex')}`);
289
+ }
290
+ this.process_message(decrypted);
291
+ }
292
+ }
293
+ else {
294
+ if (this.logRecoverableErrors) {
295
+ this.logger.warn(`[${this.name}] Invalid payload length: ` + `${payload_length} (0x${payload_length.toString(16)})`);
296
+ }
297
+ else {
298
+ this.logger.debug(`[${this.name}] Invalid payload length: ` + `${payload_length} (0x${payload_length.toString(16)})`);
299
+ }
300
+ }
301
+ }
302
+ else {
303
+ this.logger.warn(`[${this.name}] Illegal message.`);
304
+ }
305
+ }
306
+ return ParseMessageResult.SUCCESS;
307
+ }
308
+ async send_command(command_type, command_body) {
309
+ const cmd = new MessageQuestCustom(this.type, command_type, command_body);
310
+ try {
311
+ if (this.verbose) {
312
+ this.logger.debug(`[${this.name}] Send command: ${command_body.toString('hex')}`);
313
+ }
314
+ await this.build_send(cmd);
315
+ }
316
+ catch (e) {
306
317
  this.logger.debug(`[${this.name}] Interface send_command failure: ${e},
307
318
  cmd_type: ${command_type},
308
- cmd_body: ${command_body.toString('hex')}`);
309
- }
310
- }
311
- async send_heartbeat() {
312
- const message = new MideaPacketBuilder_1.default(this.id, Buffer.alloc(0)).finalize(0);
313
- await this.send_message(message);
314
- }
315
- async update(values) {
316
- if (this.verbose) {
317
- this.logger.info(`[${this.name}] Status change: ${JSON.stringify(values)}`);
318
- }
319
- this.emit('update', values);
320
- }
321
- open() {
322
- if (!this.is_running) {
323
- this.is_running = true;
324
- this.run();
325
- }
326
- }
327
- close() {
328
- if (this.is_running) {
329
- this.is_running = false;
330
- this.close_socket();
331
- }
332
- }
333
- close_socket() {
334
- this.unsupported_protocol = [];
335
- this.buffer = Buffer.alloc(0);
336
- if (this.promiseSocket) {
337
- this.promiseSocket.destroy();
338
- }
339
- }
340
- /*********************************************************************
341
- * run
342
- * Continuous loop that runs listening for network traffic from the device
343
- * and proceses each message as received.
344
- */
345
- async run() {
346
- this.logger.info(`[${this.name}] Starting network listener.`);
347
- while (this.is_running) {
348
- while (this.promiseSocket.destroyed) {
349
- if (this.logRecoverableErrors) {
350
- this.logger.info(`[${this.name}] Create new socket, reconnect`);
351
- }
352
- else {
353
- this.logger.debug(`[${this.name}] Create new socket, reconnect`);
354
- }
355
- this.promiseSocket = new MideaUtils_1.PromiseSocket(this.logger, this.logRecoverableErrors);
356
- await this.connect(true); // need to refresh_status on connect as we reset start time below.
357
- const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
358
- await sleep(5000);
359
- }
360
- let timeout_counter = 0;
361
- const start = Date.now(); // milliseconds
362
- let previous_refresh = start;
363
- let previous_heartbeat = start;
364
- while (!this.promiseSocket.destroyed) {
365
- try {
366
- const now = Date.now();
367
- if (0 < this.refresh_interval && this.refresh_interval <= now - previous_refresh) {
368
- this.refresh_status();
369
- previous_refresh = now;
370
- }
371
- else if (now - previous_heartbeat >= this.heartbeat_interval) {
372
- this.send_heartbeat();
373
- previous_heartbeat = now;
374
- }
375
- // We wait up to one second for a message, in effect we cause the while loop
376
- // we are in to itterate once a second... allowing us to check for heartbeat
377
- // and refresh intervals (above).
378
- this.promiseSocket.setTimeout(this.SOCKET_TIMEOUT);
379
- const msg = await this.promiseSocket.read();
380
- if (msg.length > 0) {
381
- const result = this.parse_message(msg);
382
- if (result === MideaConstants_1.ParseMessageResult.ERROR) {
383
- this.logger.debug(`[${this.name} | run] Error return from ParseMessageResult.`);
384
- break;
385
- }
386
- else if (result === MideaConstants_1.ParseMessageResult.SUCCESS) {
387
- timeout_counter = 0;
388
- }
389
- }
390
- else {
391
- timeout_counter++;
392
- if (timeout_counter > 120 / (this.SOCKET_TIMEOUT / 1000)) {
393
- // we've looped for ~two minutes and not received a successful response
394
- // to heartbeat or status refresh. Therefore something must be broken.
395
- if (this.logRecoverableErrors) {
396
- this.logger.warn(`[${this.name} | run] Heartbeat timeout, closing.`);
397
- }
398
- else {
399
- this.logger.debug(`[${this.name} | run] Heartbeat timeout, closing.`);
400
- }
401
- this.close_socket();
402
- // We break out of inner loop, but within outer loop we will attempt to
403
- // reopen the socket and continue.
404
- break;
405
- }
406
- }
407
- }
408
- catch (e) {
409
- const msg = e instanceof Error ? e.stack : e;
410
- if (this.logRecoverableErrors) {
411
- this.logger.warn(`[${this.name} | run] Error reading from socket:\n${msg}`);
412
- }
413
- else {
414
- this.logger.debug(`[${this.name} | run] Error reading from socket:\n${msg}`);
415
- }
416
- }
417
- }
418
- }
419
- this.logger.info(`[${this.name}] Stopping network listener.`);
420
- }
421
- }
422
- exports.default = MideaDevice;
319
+ cmd_body: ${command_body.toString('hex')}`);
320
+ }
321
+ }
322
+ async send_heartbeat() {
323
+ const message = new PacketBuilder(this.id, Buffer.alloc(0)).finalize(0);
324
+ await this.send_message(message);
325
+ }
326
+ async update(values) {
327
+ if (this.verbose) {
328
+ this.logger.info(`[${this.name}] Status change: ${JSON.stringify(values)}`);
329
+ }
330
+ this.emit('update', values);
331
+ }
332
+ open() {
333
+ if (!this.is_running) {
334
+ this.is_running = true;
335
+ this.run();
336
+ }
337
+ }
338
+ close() {
339
+ if (this.is_running) {
340
+ this.is_running = false;
341
+ this.close_socket();
342
+ }
343
+ }
344
+ close_socket() {
345
+ this.unsupported_protocol = [];
346
+ this.buffer = Buffer.alloc(0);
347
+ if (this.promiseSocket) {
348
+ this.promiseSocket.destroy();
349
+ }
350
+ }
351
+ /*********************************************************************
352
+ * run
353
+ * Continuous loop that runs listening for network traffic from the device
354
+ * and proceses each message as received.
355
+ */
356
+ async run() {
357
+ this.logger.info(`[${this.name}] Starting network listener.`);
358
+ while (this.is_running) {
359
+ while (this.promiseSocket.destroyed) {
360
+ if (this.logRecoverableErrors) {
361
+ this.logger.info(`[${this.name}] Create new socket, reconnect`);
362
+ }
363
+ else {
364
+ this.logger.debug(`[${this.name}] Create new socket, reconnect`);
365
+ }
366
+ this.promiseSocket = new PromiseSocket(this.logger, this.logRecoverableErrors);
367
+ await this.connect(true); // need to refresh_status on connect as we reset start time below.
368
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
369
+ await sleep(5000);
370
+ }
371
+ let timeout_counter = 0;
372
+ const start = Date.now(); // milliseconds
373
+ let previous_refresh = start;
374
+ let previous_heartbeat = start;
375
+ while (!this.promiseSocket.destroyed) {
376
+ try {
377
+ const now = Date.now();
378
+ if (0 < this.refresh_interval && this.refresh_interval <= now - previous_refresh) {
379
+ this.refresh_status();
380
+ previous_refresh = now;
381
+ }
382
+ else if (now - previous_heartbeat >= this.heartbeat_interval) {
383
+ this.send_heartbeat();
384
+ previous_heartbeat = now;
385
+ }
386
+ // We wait up to one second for a message, in effect we cause the while loop
387
+ // we are in to itterate once a second... allowing us to check for heartbeat
388
+ // and refresh intervals (above).
389
+ this.promiseSocket.setTimeout(this.SOCKET_TIMEOUT);
390
+ const msg = await this.promiseSocket.read();
391
+ if (msg.length > 0) {
392
+ const result = this.parse_message(msg);
393
+ if (result === ParseMessageResult.ERROR) {
394
+ this.logger.debug(`[${this.name} | run] Error return from ParseMessageResult.`);
395
+ break;
396
+ }
397
+ else if (result === ParseMessageResult.SUCCESS) {
398
+ timeout_counter = 0;
399
+ }
400
+ }
401
+ else {
402
+ timeout_counter++;
403
+ if (timeout_counter > 120 / (this.SOCKET_TIMEOUT / 1000)) {
404
+ // we've looped for ~two minutes and not received a successful response
405
+ // to heartbeat or status refresh. Therefore something must be broken.
406
+ if (this.logRecoverableErrors) {
407
+ this.logger.warn(`[${this.name} | run] Heartbeat timeout, closing.`);
408
+ }
409
+ else {
410
+ this.logger.debug(`[${this.name} | run] Heartbeat timeout, closing.`);
411
+ }
412
+ this.close_socket();
413
+ // We break out of inner loop, but within outer loop we will attempt to
414
+ // reopen the socket and continue.
415
+ break;
416
+ }
417
+ }
418
+ }
419
+ catch (e) {
420
+ const msg = e instanceof Error ? e.stack : e;
421
+ if (this.logRecoverableErrors) {
422
+ this.logger.warn(`[${this.name} | run] Error reading from socket:\n${msg}`);
423
+ }
424
+ else {
425
+ this.logger.debug(`[${this.name} | run] Error reading from socket:\n${msg}`);
426
+ }
427
+ }
428
+ }
429
+ }
430
+ this.logger.info(`[${this.name}] Stopping network listener.`);
431
+ }
432
+ }
423
433
  //# sourceMappingURL=MideaDevice.js.map