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 +1 -0
- package/package.json +1 -1
- package/src/melcloudhome.js +126 -4
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.
|
|
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",
|
package/src/melcloudhome.js
CHANGED
|
@@ -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 =
|
|
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}`);
|