homebridge-melcloud-control 4.3.0-beta.3 → 4.3.0-beta.31

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/index.js CHANGED
@@ -194,7 +194,7 @@ class MelCloudPlatform {
194
194
 
195
195
  //start impulse generators\
196
196
  await configuredDevice.startStopImpulseGenerator(true, [{ name: 'checkState', sampling: deviceRefreshInterval }]);
197
- const timmers = accountType === 'melcloudhome' ? [{ name: 'connect', sampling: 3000000 }, { name: 'checkDevicesList', sampling: 3000 }] : [{ name: 'checkDevicesList', sampling: refreshInterval }];
197
+ const timmers = accountType === 'melcloudhome' ? [] : [{ name: 'checkDevicesList', sampling: refreshInterval }];
198
198
  await melCloud.impulseGenerator.state(true, timmers, false);
199
199
 
200
200
  //stop impulse generator
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "displayName": "MELCloud Control",
3
3
  "name": "homebridge-melcloud-control",
4
- "version": "4.3.0-beta.3",
4
+ "version": "4.3.0-beta.31",
5
5
  "description": "Homebridge plugin to control Mitsubishi Air Conditioner, Heat Pump and Energy Recovery Ventilation.",
6
6
  "license": "MIT",
7
7
  "author": "grzegorz914",
package/src/constants.js CHANGED
@@ -12,6 +12,7 @@ export const ApiUrls = {
12
12
  SetAta: "/Device/SetAta",
13
13
  SetAtw: "/Device/SetAtw",
14
14
  SetErv: "/Device/SetErv",
15
+ GetRefreshUnit: "/Device/RequestRefresh?id=deviceid",
15
16
  UpdateApplicationOptions: "/User/UpdateApplicationOptions",
16
17
  HolidayModeUpdate: "/HolidayMode/Update",
17
18
  EnergyCostReport: "/EnergyCost/Report",
@@ -42,7 +43,8 @@ export const ApiUrlsHome = {
42
43
  PutDeviceSettings: "https://melcloudhome.com/dashboard",
43
44
  PutScheduleEnabled: "https://melcloudhome.com/ata/deviceid/schedule",
44
45
  },
45
- Origin: "https://melcloudhome.com"
46
+ Origin: "https://melcloudhome.com",
47
+ WebSocketURL: "wss://ws.melcloudhome.com/?hash="
46
48
  };
47
49
 
48
50
  export const DeviceType = [
package/src/deviceata.js CHANGED
@@ -1079,7 +1079,7 @@ class DeviceAta extends EventEmitter {
1079
1079
  //control
1080
1080
  if (button.displayType > 3) {
1081
1081
  if (this.logDebug) this.emit('debug', `Prepare button control ${name} service`);
1082
- const buttonControlService = new serviceType(serviceName, `buttonControlService${deviceId} ${i}`);
1082
+ const buttonControlService = new Service.Switch(serviceName, `buttonControlService${deviceId} ${i}`);
1083
1083
  buttonControlService.addOptionalCharacteristic(Characteristic.ConfiguredName);
1084
1084
  buttonControlService.setCharacteristic(Characteristic.ConfiguredName, serviceName);
1085
1085
  buttonControlService.getCharacteristic(Characteristic.On)
package/src/deviceatw.js CHANGED
@@ -1346,7 +1346,7 @@ class DeviceAtw extends EventEmitter {
1346
1346
  //control
1347
1347
  if (button.displayType > 3) {
1348
1348
  if (this.logDebug) this.emit('debug', `Prepare button control ${name} service`);
1349
- const buttonControlService = new serviceType(serviceName1, `buttonControlService${deviceId} ${i}`);
1349
+ const buttonControlService = new Service.Switch(serviceName1, `buttonControlService${deviceId} ${i}`);
1350
1350
  buttonControlService.addOptionalCharacteristic(Characteristic.ConfiguredName);
1351
1351
  buttonControlService.setCharacteristic(Characteristic.ConfiguredName, serviceName1);
1352
1352
  buttonControlService.getCharacteristic(Characteristic.On)
package/src/deviceerv.js CHANGED
@@ -1006,7 +1006,7 @@ class DeviceErv extends EventEmitter {
1006
1006
  //control
1007
1007
  if (button.displayType > 3) {
1008
1008
  if (this.logDebug) this.emit('debug', `Prepare button control ${name} service`);
1009
- const buttonControlService = new serviceType(serviceName1, `buttonControlService${deviceId} ${i}`);
1009
+ const buttonControlService = new Service.Switch(serviceName1, `buttonControlService${deviceId} ${i}`);
1010
1010
  buttonControlService.addOptionalCharacteristic(Characteristic.ConfiguredName);
1011
1011
  buttonControlService.setCharacteristic(Characteristic.ConfiguredName, serviceName1);
1012
1012
  buttonControlService.getCharacteristic(Characteristic.On)
@@ -26,7 +26,8 @@ class MelCloudAta extends EventEmitter {
26
26
  //set default values
27
27
  this.deviceData = {};
28
28
  this.headers = {};
29
- this.start = true;
29
+ this.socket = null;
30
+ this.socketConnected = false;
30
31
 
31
32
  //lock flag
32
33
  this.locks = false;
@@ -52,6 +53,97 @@ class MelCloudAta extends EventEmitter {
52
53
  }
53
54
  }
54
55
 
56
+ cleanupSocket() {
57
+ if (this.heartbeat) {
58
+ clearInterval(this.heartbeat);
59
+ this.heartbeat = null;
60
+ }
61
+
62
+ if (this.socket) {
63
+ try { this.socket.close(); } catch { }
64
+ this.socket = null;
65
+ }
66
+
67
+ this.socketConnected = false;
68
+ }
69
+
70
+ reconnect() {
71
+ this.cleanupSocket();
72
+
73
+ const delay = Math.min(this.reconnectDelay, 30000); // max 30s
74
+ if (this.logDebug) this.emit('debug', `Reconnect in ${delay}ms`);
75
+
76
+ setTimeout(() => {
77
+ this.reconnectDelay *= 2; // exponential backoff
78
+ this.reconnectDelay = Math.max(1000, this.reconnectDelay);
79
+ this.connectSocket(deviceData);
80
+ }, delay);
81
+ };
82
+
83
+ async connectSocket(deviceData, wsHeaders) {
84
+ if (this.connecting || this.socketConnected) return;
85
+
86
+ this.connecting = true;
87
+ const url = `${ApiUrlsHome.WebSocketURL}${wsHeaders.hash}`;
88
+ try {
89
+ const socket = new WebSocket(url, { headers: wsHeaders.headers })
90
+ .on('error', (error) => {
91
+ if (this.logError) this.emit('error', `Socket error: ${error}`);
92
+ })
93
+ .on('close', () => {
94
+ if (this.logDebug) this.emit('debug', `Socket closed`);
95
+ this.reconnect();
96
+ })
97
+ .on('open', () => {
98
+ this.socket = socket;
99
+ this.socketConnected = true;
100
+ this.connecting = false;
101
+ this.reconnectDelay = 1000;
102
+ this.emit('success', `Socket Connect Success`);
103
+
104
+ // heartbeat
105
+ this.heartbeat = setInterval(() => {
106
+ if (socket.readyState === socket.OPEN) {
107
+ if (!this.logDebug) this.emit('warn', `Socket send heartbeat`);
108
+ socket.ping();
109
+ }
110
+ }, 30000);
111
+ })
112
+ .on('pong', () => {
113
+ if (!this.logDebug) this.emit('warn', `Socket received heartbeat`);
114
+ })
115
+ .on('message', (message) => {
116
+ const parsedMessage = JSON.parse(message);
117
+ if (!this.logDebug) this.emit('warn', `Incoming message: ${JSON.stringify(parsedMessage, null, 2)}`);
118
+
119
+ const messageData = parsedMessage[0].Data;
120
+ const unitId = messageData.id;
121
+
122
+ switch (unitId) {
123
+ case this.deviceId:
124
+ const messageType = parsedMessage[0].messageType;
125
+ switch (messageType) {
126
+ case 'unitStateChanged':
127
+ const settings = Object.fromEntries(messageData.settings.map(s => [s.name, s.value]));
128
+ Object.assign(deviceData.Device, settings);
129
+ this.emit('deviceState', deviceData);
130
+ break;
131
+ case 'unitWiFiChanged':
132
+ const state = Object.fromEntries(messageData.settings.map(s => [s.name, s.value]));
133
+ Object.assign(deviceData.Device, state);
134
+ this.emit('deviceState', deviceData);
135
+ break;
136
+ }
137
+ break;
138
+ }
139
+ });
140
+
141
+ } catch (error) {
142
+ if (this.logError) this.emit('error', `Socket connection failed: ${error}`);
143
+ this.reconnect();
144
+ }
145
+ }
146
+
55
147
  async checkState() {
56
148
  try {
57
149
 
@@ -67,40 +159,16 @@ class MelCloudAta extends EventEmitter {
67
159
  deviceData.Device.ActualFanSpeed = AirConditioner.FanSpeedMapStringToEnum[deviceData.Device.ActualFanSpeed] ?? deviceData.Device.ActualFanSpeed;
68
160
  deviceData.Device.SetFanSpeed = AirConditioner.FanSpeedMapStringToEnum[deviceData.Device.SetFanSpeed] ?? deviceData.Device.SetFanSpeed;
69
161
  deviceData.Device.VaneVerticalDirection = AirConditioner.VaneVerticalDirectionMapStringToEnum[deviceData.Device.VaneVerticalDirection] ?? deviceData.Device.VaneVerticalDirection;
70
- deviceData.Device.VaneHorizontalDirection = AirConditioner.VaneHorizontalDirectionMapStringToEnum[deviceData.Device.VaneHorizontalDirection] ?? deviceData.Device.VaneHorizontalDirection;
162
+ deviceData.Device.VaneHorizontalDirection = AirConditioner.VaneHorizontalDirectionMapStringToEnum[deviceData.Device.VaneHorizontalDirection] ?? deviceData.Device.VaneHorizontalDirection
71
163
 
72
164
  //read default temps
73
165
  const temps = await this.functions.readData(this.defaultTempsFile, true);
74
166
  deviceData.Device.DefaultHeatingSetTemperature = temps?.defaultHeatingSetTemperature ?? 20;
75
167
  deviceData.Device.DefaultCoolingSetTemperature = temps?.defaultCoolingSetTemperature ?? 24;
76
- }
77
- if (this.logDebug) this.emit('debug', `Device Data: ${JSON.stringify(deviceData, null, 2)}`);
78
-
79
- if (this.start) {
80
- const hash = '2db32d6f-c19c-4b1f-a0dd-1915420a5152';
81
168
 
82
- const ws = new WebSocket(`wss://ws.melcloudhome.com/?hash=${hash}`, {
83
- headers: {
84
- 'Origin': 'https://melcloudhome.com',
85
- 'Pragma': 'no-cache',
86
- 'Cache-Control': 'no-cache'
87
- }
88
- });
89
- ws.on('open', () => {
90
- if (!this.logDebug) this.emit('warn', `Connected to MelCloudHome WebSocket`);
91
- this.start = false;
92
- })
93
- .on('message', (data) => {
94
- if (!this.logDebug) this.emit('warn', `Incoming message:', ${data.toString()}`);
95
- })
96
- .on('close', () => {
97
- if (!this.logDebug) this.emit('warn', `Connection closed`);
98
- })
99
- .on('error', (error) => {
100
- if (!this.logDebug) this.emit('warn', `Connected error: ${error}`);
101
- });
169
+ await this.connectSocket(deviceData, devicesData.WsHeaders);
102
170
  }
103
-
171
+ if (this.logDebug) this.emit('debug', `Device Data: ${JSON.stringify(deviceData, null, 2)}`);
104
172
 
105
173
  //device
106
174
  const serialNumber = deviceData.SerialNumber || '4.0.0';
@@ -256,6 +324,11 @@ class MelCloudAta extends EventEmitter {
256
324
  this.headers.Referer = ApiUrlsHome.Referers.GetPutScenes;
257
325
  break;
258
326
  default:
327
+ if (!this.socket || this.socket.readyState !== this.socket.OPEN) {
328
+ if (this.logDebug) this.emit('debug', `Socket not open, cannot send`);
329
+ return false;
330
+ }
331
+
259
332
  if (displayType === 1 && deviceData.Device.OperationMode === 8) {
260
333
  deviceData.Device.SetTemperature = (deviceData.Device.DefaultCoolingSetTemperature + deviceData.Device.DefaultHeatingSetTemperature) / 2;
261
334
 
@@ -281,7 +354,14 @@ class MelCloudAta extends EventEmitter {
281
354
  method = 'PUT';
282
355
  path = ApiUrlsHome.PutAta.replace('deviceid', deviceData.DeviceID);
283
356
  this.headers.Referer = ApiUrlsHome.Referers.PutDeviceSettings;
284
- break
357
+
358
+ try {
359
+ this.socket.send(JSON.stringify(payload));
360
+ return true;
361
+ } catch (err) {
362
+ if (this.logDebug) this.emit('debug', `Socket send error: ${err}`);
363
+ return false;
364
+ }
285
365
  }
286
366
 
287
367
  this.headers['Content-Type'] = 'application/json; charset=utf-8';
@@ -23,6 +23,7 @@ class MelCloudHome extends EventEmitter {
23
23
  this.buildingsFile = buildingsFile;
24
24
  this.devicesFile = devicesFile;
25
25
  this.headers = {};
26
+ this.wsHeaders = {};
26
27
 
27
28
  this.functions = new Functions(this.logWarn, this.logError, this.logDebug)
28
29
  .on('warn', warn => this.emit('warn', warn))
@@ -208,6 +209,7 @@ class MelCloudHome extends EventEmitter {
208
209
  devicesList.Devices = devices;
209
210
  devicesList.Scenes = scenes;
210
211
  devicesList.Headers = this.headers;
212
+ devicesList.WsHeaders = this.wsHeaders;
211
213
 
212
214
  await this.functions.saveData(this.devicesFile, devicesList);
213
215
  if (this.logDebug) this.emit('debug', `${devicesCount} devices saved`);
@@ -276,6 +278,22 @@ class MelCloudHome extends EventEmitter {
276
278
  page.setDefaultTimeout(GLOBAL_TIMEOUT);
277
279
  page.setDefaultNavigationTimeout(GLOBAL_TIMEOUT);
278
280
 
281
+ // === CDP session ===
282
+ let hash = null;
283
+ const client = await page.createCDPSession();
284
+ await client.send('Network.enable')
285
+ client.on('Network.webSocketCreated', ({ url }) => {
286
+ try {
287
+ if (url.startsWith('wss://ws.melcloudhome.com/?hash=')) {
288
+ const params = new URL(url).searchParams;
289
+ hash = params.get('hash');
290
+ if (this.logDebug) this.emit('debug', `MelCloudHome WS hash detected: ${melcloudHash}`);
291
+ }
292
+ } catch (err) {
293
+ this.emit('error', `CDP WebSocketCreated handler error: ${err.message}`);
294
+ }
295
+ });
296
+
279
297
  try {
280
298
  await page.goto(ApiUrlsHome.BaseURL, { waitUntil: ['domcontentloaded', 'networkidle2'], timeout: GLOBAL_TIMEOUT });
281
299
  } catch (error) {
@@ -356,6 +374,16 @@ class MelCloudHome extends EventEmitter {
356
374
  headers: headers
357
375
  })
358
376
 
377
+ this.wsHeaders = {
378
+ hash: hash,
379
+ headers: {
380
+ 'Origin': ApiUrlsHome.BaseURL,
381
+ 'Pragma': 'no-cache',
382
+ 'Cache-Control': 'no-cache'
383
+ }
384
+ };
385
+
386
+
359
387
  accountInfo.State = true;
360
388
  accountInfo.Info = 'Connect to MELCloud Home Success';
361
389
  await this.functions.saveData(this.accountFile, accountInfo);