homebridge-midea-platform 1.2.5 → 1.2.6-beta.10

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