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

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.30",
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,96 @@ 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
+ const messageData = parsed[0].Data;
118
+ const unitId = messageData.id;
119
+ const stringifyMessage = JSON.stringify(parsedMessage, null, 2);
120
+ if (!this.logDebug) this.emit('warn', `Incoming message: ${stringifyMessage}`);
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); //emit state this.emit('deviceState', deviceData);
134
+ break;
135
+ }
136
+ break;
137
+ }
138
+ });
139
+
140
+ } catch (error) {
141
+ if (this.logError) this.emit('error', `Socket connection failed: ${error}`);
142
+ this.reconnect();
143
+ }
144
+ }
145
+
55
146
  async checkState() {
56
147
  try {
57
148
 
@@ -67,40 +158,16 @@ class MelCloudAta extends EventEmitter {
67
158
  deviceData.Device.ActualFanSpeed = AirConditioner.FanSpeedMapStringToEnum[deviceData.Device.ActualFanSpeed] ?? deviceData.Device.ActualFanSpeed;
68
159
  deviceData.Device.SetFanSpeed = AirConditioner.FanSpeedMapStringToEnum[deviceData.Device.SetFanSpeed] ?? deviceData.Device.SetFanSpeed;
69
160
  deviceData.Device.VaneVerticalDirection = AirConditioner.VaneVerticalDirectionMapStringToEnum[deviceData.Device.VaneVerticalDirection] ?? deviceData.Device.VaneVerticalDirection;
70
- deviceData.Device.VaneHorizontalDirection = AirConditioner.VaneHorizontalDirectionMapStringToEnum[deviceData.Device.VaneHorizontalDirection] ?? deviceData.Device.VaneHorizontalDirection;
161
+ deviceData.Device.VaneHorizontalDirection = AirConditioner.VaneHorizontalDirectionMapStringToEnum[deviceData.Device.VaneHorizontalDirection] ?? deviceData.Device.VaneHorizontalDirection
71
162
 
72
163
  //read default temps
73
164
  const temps = await this.functions.readData(this.defaultTempsFile, true);
74
165
  deviceData.Device.DefaultHeatingSetTemperature = temps?.defaultHeatingSetTemperature ?? 20;
75
166
  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
167
 
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
- });
168
+ await this.connectSocket(deviceData, devicesData.WsHeaders);
102
169
  }
103
-
170
+ if (this.logDebug) this.emit('debug', `Device Data: ${JSON.stringify(deviceData, null, 2)}`);
104
171
 
105
172
  //device
106
173
  const serialNumber = deviceData.SerialNumber || '4.0.0';
@@ -256,6 +323,11 @@ class MelCloudAta extends EventEmitter {
256
323
  this.headers.Referer = ApiUrlsHome.Referers.GetPutScenes;
257
324
  break;
258
325
  default:
326
+ if (!this.socket || this.socket.readyState !== this.socket.OPEN) {
327
+ if (this.logDebug) this.emit('debug', `Socket not open, cannot send`);
328
+ return false;
329
+ }
330
+
259
331
  if (displayType === 1 && deviceData.Device.OperationMode === 8) {
260
332
  deviceData.Device.SetTemperature = (deviceData.Device.DefaultCoolingSetTemperature + deviceData.Device.DefaultHeatingSetTemperature) / 2;
261
333
 
@@ -281,7 +353,14 @@ class MelCloudAta extends EventEmitter {
281
353
  method = 'PUT';
282
354
  path = ApiUrlsHome.PutAta.replace('deviceid', deviceData.DeviceID);
283
355
  this.headers.Referer = ApiUrlsHome.Referers.PutDeviceSettings;
284
- break
356
+
357
+ try {
358
+ this.socket.send(JSON.stringify(payload));
359
+ return true;
360
+ } catch (err) {
361
+ if (this.logDebug) this.emit('debug', `Socket send error: ${err}`);
362
+ return false;
363
+ }
285
364
  }
286
365
 
287
366
  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);