homebridge-melcloud-control 4.9.2-beta.2 → 4.9.2-beta.4

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
@@ -112,6 +112,7 @@ class MelCloudPlatform {
112
112
  }
113
113
  if (logLevel.debug) log.info(melCloudDevicesData.Status);
114
114
 
115
+
115
116
  //filter configured devices
116
117
  const devicesIds = (melCloudDevicesData.Devices ?? []).map(d => String(d.DeviceID));
117
118
  const ataDevices = (account.ataDevices || []).filter(d => (d.displayType ?? 0) > 0 && devicesIds.includes(d.id));
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "displayName": "MELCloud Control",
3
3
  "name": "homebridge-melcloud-control",
4
- "version": "4.9.2-beta.2",
4
+ "version": "4.9.2-beta.4",
5
5
  "description": "Homebridge plugin to control Mitsubishi Air Conditioner, Heat Pump and Energy Recovery Ventilation.",
6
6
  "license": "MIT",
7
7
  "author": "grzegorz914",
@@ -40,6 +40,15 @@ class MelCloudHome extends EventEmitter {
40
40
  // Flaga zapobiegająca wielokrotnemu dodaniu interceptorów
41
41
  this.interceptorsAttached = false;
42
42
 
43
+ // WebSocket state
44
+ this.socket = null;
45
+ this.socketConnected = false;
46
+ this.connecting = false;
47
+ this.heartbeat = null;
48
+ this.reconnectTimer = null;
49
+ this.reconnectDelay = 5_000; // ms, rośnie wykładniczo do reconnectDelayMax
50
+ this.reconnectDelayMax = 300_000; // 5 minut
51
+
43
52
  if (pluginStart) {
44
53
  this.impulseGenerator = new ImpulseGenerator()
45
54
  .on('checkDevicesList', async () => {
@@ -51,6 +60,116 @@ class MelCloudHome extends EventEmitter {
51
60
  }
52
61
  }
53
62
 
63
+ // ── WebSocket ─────────────────────────────────────────────────────────────
64
+
65
+ cleanupSocket() {
66
+ if (this.heartbeat) {
67
+ clearInterval(this.heartbeat);
68
+ this.heartbeat = null;
69
+ }
70
+ this.socketConnected = false;
71
+ this.connecting = false;
72
+ this.socket = null;
73
+ }
74
+
75
+ // Łączy się z WebSocket. Wywoływane po udanym connect() lub po reconnect.
76
+ async connectSocket() {
77
+ if (this.connecting || this.socketConnected) return;
78
+ this.connecting = true;
79
+
80
+ let hash;
81
+ try {
82
+ const resp = await this.client.get(ApiUrls.Home.Get.Context);
83
+ hash = resp.data.id ?? null;
84
+ } catch (err) {
85
+ if (this.logError) this.emit('error', `connectSocket: cannot get WS hash: ${err.message}`);
86
+ this.connecting = false;
87
+ this.scheduleReconnect();
88
+ return;
89
+ }
90
+
91
+ const url = `${ApiUrls.Home.WebSocket}${hash}`;
92
+ const headers = {
93
+ Origin: ApiUrls.Home.Base,
94
+ Pragma: 'no-cache',
95
+ 'Cache-Control': 'no-cache',
96
+ };
97
+
98
+ if (this.logDebug) this.emit('debug', `Connecting WebSocket: ${url.slice(0, 60)}...`);
99
+
100
+ try {
101
+ const ws = new WebSocket(url, { headers });
102
+ this.socket = ws;
103
+
104
+ ws.on('error', (error) => {
105
+ if (this.logError) this.emit('error', `Web socket error: ${error.message}`);
106
+ try { ws.close(); } catch { /* ignoruj */ }
107
+ });
108
+
109
+ ws.on('close', () => {
110
+ if (this.logDebug) this.emit('debug', 'Web socket closed');
111
+ this.cleanupSocket();
112
+ this.scheduleReconnect();
113
+ });
114
+
115
+ ws.on('open', () => {
116
+ this.socketConnected = true;
117
+ this.connecting = false;
118
+ this.reconnectDelay = 5_000; // reset backoff po udanym połączeniu
119
+ if (this.reconnectTimer) {
120
+ clearTimeout(this.reconnectTimer);
121
+ this.reconnectTimer = null;
122
+ }
123
+ if (this.logDebug) this.emit('debug', 'Web Socket Connected');
124
+
125
+ // Heartbeat co 30s
126
+ this.heartbeat = setInterval(() => {
127
+ if (ws.readyState === WebSocket.OPEN) {
128
+ if (this.logDebug) this.emit('debug', 'Web socket send heartbeat');
129
+ ws.ping();
130
+ }
131
+ }, 30_000);
132
+ });
133
+
134
+ ws.on('pong', () => {
135
+ if (this.logDebug) this.emit('debug', 'Web socket received heartbeat');
136
+ });
137
+
138
+ ws.on('message', (message) => {
139
+ try {
140
+ const parsedMessage = JSON.parse(message);
141
+ if (this.logDebug) this.emit('debug', `Web socket incoming message: ${JSON.stringify(parsedMessage, null, 2)}`);
142
+
143
+ // Format: array, pierwszy element ma Data.id
144
+ const messageData = parsedMessage?.[0]?.Data;
145
+ if (!messageData || parsedMessage.message === 'Forbidden') return;
146
+
147
+ this.emit(messageData.id, 'ws', parsedMessage[0]);
148
+ } catch (err) {
149
+ if (this.logError) this.emit('error', `Web socket message parse error: ${err.message}`);
150
+ }
151
+ });
152
+ } catch (error) {
153
+ if (this.logError) this.emit('error', `Web socket connection failed: ${error.message}`);
154
+ this.cleanupSocket();
155
+ this.scheduleReconnect();
156
+ }
157
+ }
158
+
159
+ // Wykładniczy backoff: 5s → 10s → 20s → ... → max 5 minut
160
+ scheduleReconnect() {
161
+ if (this.reconnectTimer) return; // już zaplanowany
162
+
163
+ if (this.logDebug) this.emit('debug', `Web socket reconnecting in ${this.reconnectDelay / 1000}s...`);
164
+
165
+ this.reconnectTimer = setTimeout(async () => {
166
+ this.reconnectTimer = null;
167
+ await this.connectSocket();
168
+ }, this.reconnectDelay);
169
+
170
+ this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.reconnectDelayMax);
171
+ }
172
+
54
173
  // ── Utils ─────────────────────────────────────────────────────────────────
55
174
 
56
175
  capitalizeKeysDeep(obj) {
@@ -301,17 +420,20 @@ class MelCloudHome extends EventEmitter {
301
420
 
302
421
  // ── Buduje connectInfo po udanym token exchange ───────────────────────────
303
422
 
304
- buildConnectInfo(connectInfo, exchangeRes) {
423
+ async buildConnectInfo(connectInfo, exchangeRes) {
305
424
  if (exchangeRes) {
306
425
  // ensureClient() tworzy client jeśli nie istnieje.
307
426
  // attachTokenInterceptors() dodaje interceptory tylko przy pierwszym wywołaniu.
308
427
  this.ensureClient();
309
428
  this.attachTokenInterceptors();
310
429
  this.emit('client', this.client);
430
+ await this.connectSocket().catch(err => {
431
+ if (this.logError) this.emit('error', `Initial WebSocket connect failed: ${err.message}`);
432
+ });
311
433
  }
312
434
 
313
435
  connectInfo.State = exchangeRes;
314
- connectInfo.Status = exchangeRes ? 'Connect Success' : 'Connect Failed at token exchange';
436
+ connectInfo.Status = `Connect Success${this.socketConnected ? ', Web Socket Connected' : ''}`;
315
437
 
316
438
  return connectInfo;
317
439
  }
@@ -517,7 +639,7 @@ class MelCloudHome extends EventEmitter {
517
639
  if (authCode) {
518
640
  if (this.logDebug) this.emit('debug', 'Re-login with existing session (skipping credentials)');
519
641
  const exchangeRes = await this.exchangeCodeForTokens(client, authCode, codeVerifier);
520
- return this.buildConnectInfo(connectInfo, exchangeRes);
642
+ return await this.buildConnectInfo(connectInfo, exchangeRes);
521
643
  }
522
644
 
523
645
  // ── Step 3: Wyślij dane logowania do Cognito ──────────────────────
@@ -650,7 +772,7 @@ class MelCloudHome extends EventEmitter {
650
772
 
651
773
  // ── Step 6: Wymień kod na tokeny ──────────────────────────────────
652
774
  const exchangeRes = await this.exchangeCodeForTokens(client, authCode, codeVerifier);
653
- return this.buildConnectInfo(connectInfo, exchangeRes);
775
+ return await this.buildConnectInfo(connectInfo, exchangeRes);
654
776
 
655
777
  } catch (error) {
656
778
  throw new Error(`Connect error: ${error.message}`);