homebridge-midea-platform 1.2.1 → 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.
- package/homebridge-ui/{server.cjs → server.js} +175 -102
- package/package.json +1 -1
|
@@ -7,41 +7,57 @@
|
|
|
7
7
|
* Based on https://github.com/homebridge/plugin-ui-utils
|
|
8
8
|
*
|
|
9
9
|
*/
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
import {
|
|
11
|
+
HomebridgePluginUiServer,
|
|
12
|
+
RequestError,
|
|
13
|
+
} from "@homebridge/plugin-ui-utils";
|
|
14
|
+
import Discover from "../dist/core/MideaDiscover.js";
|
|
15
|
+
import CloudFactory from "../dist/core/MideaCloud.js";
|
|
16
|
+
import {
|
|
17
|
+
DeviceType,
|
|
18
|
+
TCPMessageType,
|
|
19
|
+
ProtocolVersion,
|
|
20
|
+
Endianness,
|
|
21
|
+
} from "../dist/core/MideaConstants.js";
|
|
22
|
+
import { LocalSecurity, ProxiedSecurity } from "../dist/core/MideaSecurity.js";
|
|
23
|
+
import { PromiseSocket } from "../dist/core/MideaUtils.js";
|
|
24
|
+
import { defaultConfig, defaultDeviceConfig } from "../dist/platformUtils.js";
|
|
25
|
+
import { createRequire } from "node:module";
|
|
26
|
+
const require = createRequire(import.meta.url);
|
|
27
|
+
|
|
28
|
+
import _ from "lodash";
|
|
17
29
|
|
|
18
30
|
const DEFAULT_ACCOUNT = [
|
|
19
|
-
BigInt(
|
|
20
|
-
|
|
21
|
-
|
|
31
|
+
BigInt(
|
|
32
|
+
"39182118275972017797890111985649342047468653967530949796945843010512",
|
|
33
|
+
),
|
|
34
|
+
BigInt(
|
|
35
|
+
"29406100301096535908214728322278519471982973450672552249652548883645",
|
|
36
|
+
),
|
|
37
|
+
BigInt(
|
|
38
|
+
"39182118275972017797890111985649342050088014265865102175083010656997",
|
|
39
|
+
),
|
|
22
40
|
];
|
|
23
41
|
|
|
24
|
-
var _ = require('lodash');
|
|
25
|
-
|
|
26
42
|
/*********************************************************************
|
|
27
43
|
* Logger
|
|
28
44
|
* Lightweight log class to mimic the homebridge log capability
|
|
29
45
|
*/
|
|
30
46
|
class Logger {
|
|
31
47
|
_debug;
|
|
32
|
-
_Reset =
|
|
33
|
-
_Bright =
|
|
34
|
-
_Dim =
|
|
48
|
+
_Reset = "\x1b[0m";
|
|
49
|
+
_Bright = "\x1b[1m";
|
|
50
|
+
_Dim = "\x1b[2m";
|
|
35
51
|
|
|
36
|
-
_FgBlack =
|
|
37
|
-
_FgRed =
|
|
38
|
-
_FgGreen =
|
|
39
|
-
_FgYellow =
|
|
40
|
-
_FgBlue =
|
|
41
|
-
_FgMagenta =
|
|
42
|
-
_FgCyan =
|
|
43
|
-
_FgWhite =
|
|
44
|
-
_FgGray =
|
|
52
|
+
_FgBlack = "\x1b[30m";
|
|
53
|
+
_FgRed = "\x1b[31m";
|
|
54
|
+
_FgGreen = "\x1b[32m";
|
|
55
|
+
_FgYellow = "\x1b[33m";
|
|
56
|
+
_FgBlue = "\x1b[34m";
|
|
57
|
+
_FgMagenta = "\x1b[35m";
|
|
58
|
+
_FgCyan = "\x1b[36m";
|
|
59
|
+
_FgWhite = "\x1b[37m";
|
|
60
|
+
_FgGray = "\x1b[90m";
|
|
45
61
|
|
|
46
62
|
constructor(uiDebug = false) {
|
|
47
63
|
this._debug = uiDebug;
|
|
@@ -79,33 +95,53 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
79
95
|
constructor() {
|
|
80
96
|
super();
|
|
81
97
|
// Obtain the plugin configuration from homebridge config JSON file.
|
|
82
|
-
const config = require(this.homebridgeConfigPath).platforms.find(
|
|
98
|
+
const config = require(this.homebridgeConfigPath).platforms.find(
|
|
99
|
+
(obj) => obj.platform === "midea-platform",
|
|
100
|
+
);
|
|
83
101
|
this.logger = new Logger(config?.uiDebug ?? false);
|
|
84
|
-
this.logger.info(
|
|
102
|
+
this.logger.info("Custom UI created.");
|
|
85
103
|
this.logger.debug(`ENV:\n${JSON.stringify(process.env, null, 2)}`);
|
|
86
104
|
this.security = new LocalSecurity();
|
|
87
|
-
this.promiseSocket = new PromiseSocket(
|
|
105
|
+
this.promiseSocket = new PromiseSocket(
|
|
106
|
+
this.logger,
|
|
107
|
+
config?.uiDebug ?? false,
|
|
108
|
+
);
|
|
88
109
|
|
|
89
|
-
this.onRequest(
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
110
|
+
this.onRequest(
|
|
111
|
+
"/login",
|
|
112
|
+
async ({ username, password, registeredApp, useDefaultProfile }) => {
|
|
113
|
+
try {
|
|
114
|
+
if (useDefaultProfile) {
|
|
115
|
+
this.logger.debug("Using default profile.");
|
|
116
|
+
registeredApp = "Midea SmartHome (MSmartHome)";
|
|
117
|
+
username = Buffer.from(
|
|
118
|
+
(DEFAULT_ACCOUNT[0] ^ DEFAULT_ACCOUNT[1]).toString(16),
|
|
119
|
+
"hex",
|
|
120
|
+
).toString("ascii");
|
|
121
|
+
password = Buffer.from(
|
|
122
|
+
(DEFAULT_ACCOUNT[0] ^ DEFAULT_ACCOUNT[2]).toString(16),
|
|
123
|
+
"hex",
|
|
124
|
+
).toString("ascii");
|
|
125
|
+
}
|
|
126
|
+
this.cloud = CloudFactory.createCloud(
|
|
127
|
+
username,
|
|
128
|
+
password,
|
|
129
|
+
registeredApp,
|
|
130
|
+
);
|
|
131
|
+
if (username && password && registeredApp) {
|
|
132
|
+
await this.cloud.login();
|
|
133
|
+
}
|
|
134
|
+
} catch (e) {
|
|
135
|
+
const msg = e instanceof Error ? e.stack : e;
|
|
136
|
+
this.logger.warn(`Login failed:\n${msg}`);
|
|
137
|
+
throw new RequestError(
|
|
138
|
+
"Login failed! Check the logs for more information.",
|
|
139
|
+
);
|
|
100
140
|
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
this.logger.warn(`Login failed:\n${msg}`);
|
|
104
|
-
throw new RequestError('Login failed! Check the logs for more information.');
|
|
105
|
-
}
|
|
106
|
-
});
|
|
141
|
+
},
|
|
142
|
+
);
|
|
107
143
|
|
|
108
|
-
this.onRequest(
|
|
144
|
+
this.onRequest("/mergeToDefault", async ({ config }) => {
|
|
109
145
|
_.defaultsDeep(config, defaultConfig);
|
|
110
146
|
config.devices.forEach((device) => {
|
|
111
147
|
_.defaultsDeep(device, defaultDeviceConfig);
|
|
@@ -116,42 +152,57 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
116
152
|
return config;
|
|
117
153
|
});
|
|
118
154
|
|
|
119
|
-
this.onRequest(
|
|
155
|
+
this.onRequest("/getDefaults", async () => {
|
|
120
156
|
return {
|
|
121
157
|
defaultConfig,
|
|
122
158
|
defaultDeviceConfig,
|
|
123
159
|
};
|
|
124
160
|
});
|
|
125
161
|
|
|
126
|
-
this.onRequest(
|
|
162
|
+
this.onRequest("/discover", async ({ ip }) => {
|
|
127
163
|
try {
|
|
128
164
|
const devices = await this.blockingDiscover(ip);
|
|
129
165
|
for (const device of devices) {
|
|
130
166
|
if (device.version === ProtocolVersion.V3 && this.cloud.loggedIn) {
|
|
131
167
|
await this.getNewCredentials(device);
|
|
132
168
|
} else {
|
|
133
|
-
device.token =
|
|
134
|
-
device.key =
|
|
169
|
+
device.token = "";
|
|
170
|
+
device.key = "";
|
|
135
171
|
}
|
|
136
172
|
}
|
|
137
173
|
this.logger.debug(`All devices:\n${JSON.stringify(devices, null, 2)}`);
|
|
138
|
-
return devices
|
|
174
|
+
return devices
|
|
175
|
+
.filter((a) => Object.keys(a).length > 0)
|
|
176
|
+
.sort((a, b) => a.ip.localeCompare(b.ip));
|
|
139
177
|
} catch (e) {
|
|
140
178
|
const msg = e instanceof Error ? e.stack : e;
|
|
141
179
|
throw new RequestError(`Device discovery failed:\n${msg}`);
|
|
142
180
|
}
|
|
143
181
|
});
|
|
144
182
|
|
|
145
|
-
this.onRequest(
|
|
183
|
+
this.onRequest("/downloadLua", async ({ deviceType, deviceSn }) => {
|
|
146
184
|
try {
|
|
147
185
|
if (!this.cloud || !(this.cloud instanceof ProxiedSecurity)) {
|
|
148
|
-
this.pushEvent(
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
186
|
+
this.pushEvent("showToast", {
|
|
187
|
+
success: true,
|
|
188
|
+
msg: "Currently used cloud provider doesn't support Lua downloading, using the default profile now...",
|
|
189
|
+
});
|
|
190
|
+
const registeredApp = "Midea SmartHome (MSmartHome)";
|
|
191
|
+
const username = Buffer.from(
|
|
192
|
+
(DEFAULT_ACCOUNT[0] ^ DEFAULT_ACCOUNT[1]).toString(16),
|
|
193
|
+
"hex",
|
|
194
|
+
).toString("ascii");
|
|
195
|
+
const password = Buffer.from(
|
|
196
|
+
(DEFAULT_ACCOUNT[0] ^ DEFAULT_ACCOUNT[2]).toString(16),
|
|
197
|
+
"hex",
|
|
198
|
+
).toString("ascii");
|
|
199
|
+
this.cloud = CloudFactory.createCloud(
|
|
200
|
+
username,
|
|
201
|
+
password,
|
|
202
|
+
registeredApp,
|
|
203
|
+
);
|
|
153
204
|
await this.cloud.login();
|
|
154
|
-
}
|
|
205
|
+
}
|
|
155
206
|
const lua = await this.cloud.getProtocolLua(deviceType, deviceSn);
|
|
156
207
|
return lua;
|
|
157
208
|
} catch (e) {
|
|
@@ -180,20 +231,24 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
180
231
|
const endianess = i === 0 ? Endianness.Little : Endianness.Big;
|
|
181
232
|
try {
|
|
182
233
|
const [token, key] = await this.cloud.getTokenKey(device.id, endianess);
|
|
183
|
-
device.token = token ? token.toString(
|
|
184
|
-
device.key = key ? key.toString(
|
|
234
|
+
device.token = token ? token.toString("hex") : undefined;
|
|
235
|
+
device.key = key ? key.toString("hex") : undefined;
|
|
185
236
|
await this.authenticate(device);
|
|
186
237
|
connected = true;
|
|
187
238
|
} catch (e) {
|
|
188
239
|
//const msg = e instanceof Error ? e.stack : e;
|
|
189
|
-
this.logger.debug(
|
|
240
|
+
this.logger.debug(
|
|
241
|
+
`[${device.name}] Getting token and key with ${endianess}-endian is not successful.\n${e}`,
|
|
242
|
+
);
|
|
190
243
|
// if failed then reset token/key
|
|
191
244
|
device.token = undefined;
|
|
192
245
|
device.key = undefined;
|
|
193
246
|
}
|
|
194
247
|
i++;
|
|
195
248
|
}
|
|
196
|
-
this.logger.debug(
|
|
249
|
+
this.logger.debug(
|
|
250
|
+
`[${device.name}] Token: ${device.token}, Key: ${device.key}`,
|
|
251
|
+
);
|
|
197
252
|
return;
|
|
198
253
|
}
|
|
199
254
|
|
|
@@ -209,7 +264,10 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
209
264
|
// Wrap next block in try/finally so we can destroy the socket if error occurs
|
|
210
265
|
// let thrown errors cascade up.
|
|
211
266
|
try {
|
|
212
|
-
const request = this.security.encode_8370(
|
|
267
|
+
const request = this.security.encode_8370(
|
|
268
|
+
Buffer.from(device.token, "hex"),
|
|
269
|
+
TCPMessageType.HANDSHAKE_REQUEST,
|
|
270
|
+
);
|
|
213
271
|
await this.promiseSocket.write(request);
|
|
214
272
|
const response = await this.promiseSocket.read();
|
|
215
273
|
if (response) {
|
|
@@ -219,12 +277,16 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
219
277
|
response.length
|
|
220
278
|
})\n${JSON.stringify(response)}`,
|
|
221
279
|
);
|
|
222
|
-
throw Error(
|
|
280
|
+
throw Error(
|
|
281
|
+
`[${device.name}] Authenticate error when receiving data from ${device.ip}:${device.port}. (Data length mismatch)`,
|
|
282
|
+
);
|
|
223
283
|
}
|
|
224
284
|
const resp = response.subarray(8, 72);
|
|
225
|
-
this.security.tcp_key_from_resp(resp, Buffer.from(device.key,
|
|
285
|
+
this.security.tcp_key_from_resp(resp, Buffer.from(device.key, "hex"));
|
|
226
286
|
} else {
|
|
227
|
-
throw Error(
|
|
287
|
+
throw Error(
|
|
288
|
+
`[${device.name}] Authenticate error when receiving data from ${device.ip}:${device.port}.`,
|
|
289
|
+
);
|
|
228
290
|
}
|
|
229
291
|
} finally {
|
|
230
292
|
this.promiseSocket.destroy();
|
|
@@ -238,11 +300,16 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
238
300
|
*/
|
|
239
301
|
async blockingDiscover(ipAddrs = undefined) {
|
|
240
302
|
let devices = [];
|
|
241
|
-
this.logger.debug(
|
|
303
|
+
this.logger.debug(
|
|
304
|
+
`[blockingDiscover] IP addresses: ${JSON.stringify(ipAddrs)}`,
|
|
305
|
+
);
|
|
242
306
|
const discover = new Discover(this.logger);
|
|
243
307
|
return new Promise((resolve, reject) => {
|
|
244
|
-
this.logger.info(
|
|
245
|
-
this.pushEvent(
|
|
308
|
+
this.logger.info("Start device discovery...");
|
|
309
|
+
this.pushEvent("showToast", {
|
|
310
|
+
success: true,
|
|
311
|
+
msg: "Start device discovery",
|
|
312
|
+
});
|
|
246
313
|
// If IP addresses provided then probe them directly
|
|
247
314
|
ipAddrs?.forEach((ip) => {
|
|
248
315
|
discover.discoverDeviceByIP(ip);
|
|
@@ -250,49 +317,55 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
250
317
|
// And then send broadcast to network(s)
|
|
251
318
|
discover.startDiscover();
|
|
252
319
|
|
|
253
|
-
discover.on(
|
|
320
|
+
discover.on("device", async (device) => {
|
|
254
321
|
switch (device.type) {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
322
|
+
case DeviceType.AIR_CONDITIONER:
|
|
323
|
+
device.displayName = "Air Conditioner";
|
|
324
|
+
break;
|
|
325
|
+
case DeviceType.DEHUMIDIFIER:
|
|
326
|
+
device.displayName = "Dehumidifier";
|
|
327
|
+
break;
|
|
328
|
+
case DeviceType.HEAT_PUMP_WIFI_CONTROLLER:
|
|
329
|
+
device.displayName = "Heat Pump WiFi Controller";
|
|
330
|
+
case DeviceType.FRONT_LOAD_WASHER:
|
|
331
|
+
device.displayName = "Front Load Washer";
|
|
332
|
+
break;
|
|
333
|
+
case DeviceType.DISHWASHER:
|
|
334
|
+
device.displayName = "Dishwasher";
|
|
335
|
+
break;
|
|
336
|
+
case DeviceType.ELECTRIC_WATER_HEATER:
|
|
337
|
+
device.displayName = "Electric Water Heater";
|
|
338
|
+
break;
|
|
339
|
+
case DeviceType.GAS_WATER_HEATER:
|
|
340
|
+
device.displayName = "Gas Water Heater";
|
|
341
|
+
break;
|
|
342
|
+
case DeviceType.FAN:
|
|
343
|
+
device.displayName = "Fan";
|
|
344
|
+
break;
|
|
345
|
+
case DeviceType.UNKNOWN:
|
|
346
|
+
default:
|
|
347
|
+
device.displayName = "Unknown";
|
|
348
|
+
break;
|
|
282
349
|
}
|
|
283
350
|
devices.push(device);
|
|
284
351
|
// too verbose to post every device as found...
|
|
285
352
|
// this.pushEvent('showToast', { success: true, msg: `Discovered ${device.name} at ${device.ip}`, device: device });
|
|
286
353
|
});
|
|
287
354
|
|
|
288
|
-
discover.on(
|
|
289
|
-
this.logger.info(
|
|
290
|
-
this.pushEvent(
|
|
355
|
+
discover.on("retry", (nTry, nDevices) => {
|
|
356
|
+
this.logger.info("Device discovery complete.");
|
|
357
|
+
this.pushEvent("showToast", {
|
|
358
|
+
success: true,
|
|
359
|
+
msg: `Continuing to search for devices (${nDevices} found)`,
|
|
360
|
+
});
|
|
291
361
|
});
|
|
292
362
|
|
|
293
|
-
discover.on(
|
|
294
|
-
this.logger.info(
|
|
295
|
-
this.pushEvent(
|
|
363
|
+
discover.on("complete", () => {
|
|
364
|
+
this.logger.info("Device discovery complete.");
|
|
365
|
+
this.pushEvent("showToast", {
|
|
366
|
+
success: true,
|
|
367
|
+
msg: "Discovery complete",
|
|
368
|
+
});
|
|
296
369
|
resolve(devices);
|
|
297
370
|
});
|
|
298
371
|
});
|
package/package.json
CHANGED