homebridge-mitsubishi-comfort 1.3.0 → 1.3.3
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/dist/accessory.d.ts +1 -0
- package/dist/accessory.js +20 -33
- package/dist/kumo-api.d.ts +9 -6
- package/dist/kumo-api.js +100 -29
- package/dist/platform.d.ts +3 -0
- package/dist/platform.js +33 -8
- package/dist/settings.js +1 -1
- package/package.json +1 -1
package/dist/accessory.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export declare class KumoThermostatAccessory {
|
|
|
15
15
|
private hasHumiditySensor;
|
|
16
16
|
private lastUpdateTimestamp;
|
|
17
17
|
private lastUpdateSource;
|
|
18
|
+
private hasReceivedValidUpdate;
|
|
18
19
|
constructor(platform: KumoV3Platform, accessory: PlatformAccessory, kumoAPI: KumoAPI, pollIntervalSeconds?: number);
|
|
19
20
|
private handleStreamingUpdate;
|
|
20
21
|
getSiteId(): string;
|
package/dist/accessory.js
CHANGED
|
@@ -12,6 +12,7 @@ class KumoThermostatAccessory {
|
|
|
12
12
|
this.hasHumiditySensor = false;
|
|
13
13
|
this.lastUpdateTimestamp = 0;
|
|
14
14
|
this.lastUpdateSource = 'none';
|
|
15
|
+
this.hasReceivedValidUpdate = false;
|
|
15
16
|
this.deviceSerial = this.accessory.context.device.deviceSerial;
|
|
16
17
|
this.siteId = this.accessory.context.device.siteId;
|
|
17
18
|
this.pollIntervalMs = (pollIntervalSeconds || settings_1.POLL_INTERVAL / 1000) * 1000;
|
|
@@ -125,6 +126,7 @@ class KumoThermostatAccessory {
|
|
|
125
126
|
spAuto: zone.adapter.spAuto,
|
|
126
127
|
};
|
|
127
128
|
this.currentStatus = status;
|
|
129
|
+
this.hasReceivedValidUpdate = true;
|
|
128
130
|
this.platform.log.debug(`${this.accessory.displayName}: ${status.roomTemp}°C (target: ${this.getTargetTempFromStatus(status)}°C, mode: ${status.operationMode})`);
|
|
129
131
|
this.service.updateCharacteristic(this.platform.Characteristic.CurrentHeatingCoolingState, this.mapToCurrentHeatingCoolingState(status));
|
|
130
132
|
this.service.updateCharacteristic(this.platform.Characteristic.TargetHeatingCoolingState, this.mapToTargetHeatingCoolingState(status));
|
|
@@ -200,13 +202,8 @@ class KumoThermostatAccessory {
|
|
|
200
202
|
}
|
|
201
203
|
async getCurrentHeatingCoolingState() {
|
|
202
204
|
if (!this.currentStatus) {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
this.currentStatus = status;
|
|
206
|
-
}
|
|
207
|
-
else {
|
|
208
|
-
return this.platform.Characteristic.CurrentHeatingCoolingState.OFF;
|
|
209
|
-
}
|
|
205
|
+
this.platform.log.debug('No status available yet for getCurrentHeatingCoolingState, returning OFF');
|
|
206
|
+
return this.platform.Characteristic.CurrentHeatingCoolingState.OFF;
|
|
210
207
|
}
|
|
211
208
|
const state = this.mapToCurrentHeatingCoolingState(this.currentStatus);
|
|
212
209
|
this.platform.log.debug('Get CurrentHeatingCoolingState:', state);
|
|
@@ -214,13 +211,8 @@ class KumoThermostatAccessory {
|
|
|
214
211
|
}
|
|
215
212
|
async getTargetHeatingCoolingState() {
|
|
216
213
|
if (!this.currentStatus) {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
this.currentStatus = status;
|
|
220
|
-
}
|
|
221
|
-
else {
|
|
222
|
-
return this.platform.Characteristic.TargetHeatingCoolingState.OFF;
|
|
223
|
-
}
|
|
214
|
+
this.platform.log.debug('No status available yet for getTargetHeatingCoolingState, returning OFF');
|
|
215
|
+
return this.platform.Characteristic.TargetHeatingCoolingState.OFF;
|
|
224
216
|
}
|
|
225
217
|
const state = this.mapToTargetHeatingCoolingState(this.currentStatus);
|
|
226
218
|
this.platform.log.debug('Get TargetHeatingCoolingState:', state);
|
|
@@ -229,50 +221,51 @@ class KumoThermostatAccessory {
|
|
|
229
221
|
async setTargetHeatingCoolingState(value) {
|
|
230
222
|
this.platform.log.debug('Set TargetHeatingCoolingState:', value);
|
|
231
223
|
let operationMode;
|
|
224
|
+
let modeName;
|
|
232
225
|
switch (value) {
|
|
233
226
|
case this.platform.Characteristic.TargetHeatingCoolingState.OFF:
|
|
234
227
|
operationMode = 'off';
|
|
228
|
+
modeName = 'OFF';
|
|
235
229
|
break;
|
|
236
230
|
case this.platform.Characteristic.TargetHeatingCoolingState.HEAT:
|
|
237
231
|
operationMode = 'heat';
|
|
232
|
+
modeName = 'HEAT';
|
|
238
233
|
break;
|
|
239
234
|
case this.platform.Characteristic.TargetHeatingCoolingState.COOL:
|
|
240
235
|
operationMode = 'cool';
|
|
236
|
+
modeName = 'COOL';
|
|
241
237
|
break;
|
|
242
238
|
case this.platform.Characteristic.TargetHeatingCoolingState.AUTO:
|
|
243
239
|
operationMode = 'auto';
|
|
240
|
+
modeName = 'AUTO';
|
|
244
241
|
break;
|
|
245
242
|
default:
|
|
246
243
|
this.platform.log.error('Unknown target heating cooling state:', value);
|
|
247
244
|
return;
|
|
248
245
|
}
|
|
246
|
+
this.platform.log.info(`[MODE CHANGE] ${this.accessory.displayName}: HomeKit sent ${modeName} mode`);
|
|
249
247
|
const success = await this.kumoAPI.sendCommand(this.deviceSerial, {
|
|
250
248
|
operationMode,
|
|
251
249
|
});
|
|
252
250
|
if (success) {
|
|
251
|
+
this.platform.log.info(`[MODE CHANGE] ${this.accessory.displayName}: Command accepted by API`);
|
|
253
252
|
if (this.currentStatus) {
|
|
254
253
|
this.currentStatus.operationMode = operationMode;
|
|
255
254
|
this.currentStatus.power = operationMode === 'off' ? 0 : 1;
|
|
256
255
|
}
|
|
257
256
|
}
|
|
258
257
|
else {
|
|
259
|
-
this.platform.log.error(`Failed to set
|
|
258
|
+
this.platform.log.error(`[MODE CHANGE] ${this.accessory.displayName}: Failed to set mode to ${modeName}`);
|
|
260
259
|
}
|
|
261
260
|
}
|
|
262
261
|
async getCurrentTemperature() {
|
|
263
262
|
if (!this.currentStatus) {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
this.currentStatus = status;
|
|
267
|
-
}
|
|
268
|
-
else {
|
|
269
|
-
this.platform.log.debug('No status available yet for getCurrentTemperature, returning default');
|
|
270
|
-
return 20;
|
|
271
|
-
}
|
|
263
|
+
this.platform.log.debug('No status available yet for getCurrentTemperature, returning default');
|
|
264
|
+
return 20;
|
|
272
265
|
}
|
|
273
266
|
const temp = this.currentStatus.roomTemp;
|
|
274
267
|
if (temp === undefined || temp === null || isNaN(temp)) {
|
|
275
|
-
if (this.
|
|
268
|
+
if (this.hasReceivedValidUpdate) {
|
|
276
269
|
this.platform.log.warn(`Invalid roomTemp value for ${this.accessory.displayName}:`, temp);
|
|
277
270
|
}
|
|
278
271
|
return 20;
|
|
@@ -282,18 +275,12 @@ class KumoThermostatAccessory {
|
|
|
282
275
|
}
|
|
283
276
|
async getTargetTemperature() {
|
|
284
277
|
if (!this.currentStatus) {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
this.currentStatus = status;
|
|
288
|
-
}
|
|
289
|
-
else {
|
|
290
|
-
this.platform.log.debug('No status available yet for getTargetTemperature, returning default');
|
|
291
|
-
return 20;
|
|
292
|
-
}
|
|
278
|
+
this.platform.log.debug('No status available yet for getTargetTemperature, returning default');
|
|
279
|
+
return 20;
|
|
293
280
|
}
|
|
294
281
|
const temp = this.getTargetTempFromStatus(this.currentStatus);
|
|
295
282
|
if (temp === undefined || temp === null || isNaN(temp)) {
|
|
296
|
-
if (this.
|
|
283
|
+
if (this.hasReceivedValidUpdate) {
|
|
297
284
|
this.platform.log.warn(`Invalid target temperature value for ${this.accessory.displayName}:`, temp);
|
|
298
285
|
}
|
|
299
286
|
return 20;
|
package/dist/kumo-api.d.ts
CHANGED
|
@@ -14,14 +14,17 @@ export declare class KumoAPI {
|
|
|
14
14
|
private socket;
|
|
15
15
|
private streamingEnabled;
|
|
16
16
|
private deviceUpdateCallbacks;
|
|
17
|
-
private reconnectAttempts;
|
|
18
|
-
private maxReconnectAttempts;
|
|
19
|
-
private lastStreamingUpdate;
|
|
20
17
|
private streamingHealthCallbacks;
|
|
21
18
|
private healthCheckTimer;
|
|
22
19
|
private streamingHealthCheckInterval;
|
|
23
|
-
private streamingStaleThreshold;
|
|
24
20
|
private isStreamingHealthy;
|
|
21
|
+
private refreshRetryCount;
|
|
22
|
+
private lastRefreshAttempt;
|
|
23
|
+
private loginRetryCount;
|
|
24
|
+
private lastLoginAttempt;
|
|
25
|
+
private readonly maxRetryAttempts;
|
|
26
|
+
private readonly baseRetryDelay;
|
|
27
|
+
private readonly minLoginInterval;
|
|
25
28
|
constructor(username: string, password: string, log: Logger, debug?: boolean, enableStreaming?: boolean);
|
|
26
29
|
private maskToken;
|
|
27
30
|
login(): Promise<boolean>;
|
|
@@ -38,13 +41,13 @@ export declare class KumoAPI {
|
|
|
38
41
|
subscribeToDevice(deviceSerial: string, callback: DeviceUpdateCallback): void;
|
|
39
42
|
unsubscribeFromDevice(deviceSerial: string): void;
|
|
40
43
|
isStreamingConnected(): boolean;
|
|
41
|
-
|
|
44
|
+
setStreamingHealthCheckInterval(checkIntervalSec: number): void;
|
|
42
45
|
onStreamingHealthChange(callback: (isHealthy: boolean) => void): void;
|
|
43
46
|
getStreamingHealth(): boolean;
|
|
44
|
-
private updateStreamingTimestamp;
|
|
45
47
|
private checkStreamingHealth;
|
|
46
48
|
private notifyHealthChange;
|
|
47
49
|
private startHealthChecks;
|
|
48
50
|
private stopHealthChecks;
|
|
49
51
|
destroy(): void;
|
|
52
|
+
reconnectStreaming(): Promise<void>;
|
|
50
53
|
}
|
package/dist/kumo-api.js
CHANGED
|
@@ -21,14 +21,17 @@ class KumoAPI {
|
|
|
21
21
|
this.socket = null;
|
|
22
22
|
this.streamingEnabled = true;
|
|
23
23
|
this.deviceUpdateCallbacks = new Map();
|
|
24
|
-
this.reconnectAttempts = 0;
|
|
25
|
-
this.maxReconnectAttempts = 5;
|
|
26
|
-
this.lastStreamingUpdate = new Map();
|
|
27
24
|
this.streamingHealthCallbacks = new Set();
|
|
28
25
|
this.healthCheckTimer = null;
|
|
29
26
|
this.streamingHealthCheckInterval = 30000;
|
|
30
|
-
this.streamingStaleThreshold = 60000;
|
|
31
27
|
this.isStreamingHealthy = false;
|
|
28
|
+
this.refreshRetryCount = 0;
|
|
29
|
+
this.lastRefreshAttempt = 0;
|
|
30
|
+
this.loginRetryCount = 0;
|
|
31
|
+
this.lastLoginAttempt = 0;
|
|
32
|
+
this.maxRetryAttempts = 5;
|
|
33
|
+
this.baseRetryDelay = 5000;
|
|
34
|
+
this.minLoginInterval = 10000;
|
|
32
35
|
this.debugMode = debug;
|
|
33
36
|
this.streamingEnabled = enableStreaming;
|
|
34
37
|
if (this.debugMode) {
|
|
@@ -49,6 +52,14 @@ class KumoAPI {
|
|
|
49
52
|
return `${token.substring(0, 4)}...${token.substring(token.length - 4)}`;
|
|
50
53
|
}
|
|
51
54
|
async login() {
|
|
55
|
+
var _a;
|
|
56
|
+
const timeSinceLastLogin = Date.now() - this.lastLoginAttempt;
|
|
57
|
+
if (this.lastLoginAttempt > 0 && timeSinceLastLogin < this.minLoginInterval) {
|
|
58
|
+
const waitTime = this.minLoginInterval - timeSinceLastLogin;
|
|
59
|
+
this.log.warn(`Rate limit protection: waiting ${Math.round(waitTime / 1000)}s before login attempt`);
|
|
60
|
+
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
61
|
+
}
|
|
62
|
+
this.lastLoginAttempt = Date.now();
|
|
52
63
|
try {
|
|
53
64
|
this.log.debug('Attempting to login to Kumo Cloud API');
|
|
54
65
|
const response = await (0, node_fetch_1.default)(`${settings_1.API_BASE_URL}/login`, {
|
|
@@ -66,17 +77,42 @@ class KumoAPI {
|
|
|
66
77
|
});
|
|
67
78
|
if (!response.ok) {
|
|
68
79
|
const errorText = await response.text();
|
|
80
|
+
if (response.status === 429) {
|
|
81
|
+
this.loginRetryCount++;
|
|
82
|
+
this.log.error(`Login rate limited (429). Retry count: ${this.loginRetryCount}`);
|
|
83
|
+
if (this.loginRetryCount >= this.maxRetryAttempts) {
|
|
84
|
+
this.log.error(`Login retry limit reached (${this.maxRetryAttempts} attempts). Giving up.`);
|
|
85
|
+
this.loginRetryCount = 0;
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
const backoffDelay = Math.min(this.baseRetryDelay * Math.pow(2, this.loginRetryCount), 120000);
|
|
89
|
+
this.log.warn(`Retrying login in ${Math.round(backoffDelay / 1000)}s...`);
|
|
90
|
+
await new Promise(resolve => setTimeout(resolve, backoffDelay));
|
|
91
|
+
return await this.login();
|
|
92
|
+
}
|
|
69
93
|
this.log.error(`Login failed with status: ${response.status}`);
|
|
70
94
|
if (this.debugMode && errorText) {
|
|
71
95
|
this.log.debug(`Login error response: ${errorText}`);
|
|
72
96
|
}
|
|
97
|
+
this.loginRetryCount = 0;
|
|
73
98
|
return false;
|
|
74
99
|
}
|
|
75
100
|
const data = await response.json();
|
|
76
101
|
this.accessToken = data.token.access;
|
|
77
102
|
this.refreshToken = data.token.refresh;
|
|
103
|
+
const wasRecovery = this.loginRetryCount > 0 || this.refreshRetryCount > 0 || ((_a = this.socket) === null || _a === void 0 ? void 0 : _a.connected);
|
|
104
|
+
if (this.loginRetryCount > 0) {
|
|
105
|
+
this.log.info(`Login recovered after ${this.loginRetryCount} retry attempt(s)`);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
this.log.info('Successfully logged in to Kumo Cloud API');
|
|
109
|
+
}
|
|
110
|
+
this.loginRetryCount = 0;
|
|
111
|
+
this.refreshRetryCount = 0;
|
|
78
112
|
this.tokenExpiresAt = Date.now() + settings_1.TOKEN_REFRESH_INTERVAL;
|
|
79
|
-
|
|
113
|
+
if (wasRecovery) {
|
|
114
|
+
await this.reconnectStreaming();
|
|
115
|
+
}
|
|
80
116
|
this.scheduleTokenRefresh();
|
|
81
117
|
return true;
|
|
82
118
|
}
|
|
@@ -90,6 +126,7 @@ class KumoAPI {
|
|
|
90
126
|
else {
|
|
91
127
|
this.log.error('Login error: Unknown error occurred');
|
|
92
128
|
}
|
|
129
|
+
this.loginRetryCount = 0;
|
|
93
130
|
return false;
|
|
94
131
|
}
|
|
95
132
|
}
|
|
@@ -108,6 +145,16 @@ class KumoAPI {
|
|
|
108
145
|
this.log.error('No refresh token available, need to login again');
|
|
109
146
|
return await this.login();
|
|
110
147
|
}
|
|
148
|
+
const timeSinceLastAttempt = Date.now() - this.lastRefreshAttempt;
|
|
149
|
+
if (this.refreshRetryCount > 0) {
|
|
150
|
+
const backoffDelay = Math.min(this.baseRetryDelay * Math.pow(2, this.refreshRetryCount - 1), 60000);
|
|
151
|
+
if (timeSinceLastAttempt < backoffDelay) {
|
|
152
|
+
const waitTime = backoffDelay - timeSinceLastAttempt;
|
|
153
|
+
this.log.warn(`Rate limit backoff: waiting ${Math.round(waitTime / 1000)}s before retry attempt ${this.refreshRetryCount + 1}/${this.maxRetryAttempts}`);
|
|
154
|
+
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
this.lastRefreshAttempt = Date.now();
|
|
111
158
|
try {
|
|
112
159
|
this.log.debug('Refreshing access token');
|
|
113
160
|
const response = await (0, node_fetch_1.default)(`${settings_1.API_BASE_URL}/refresh`, {
|
|
@@ -124,14 +171,32 @@ class KumoAPI {
|
|
|
124
171
|
if (!response.ok) {
|
|
125
172
|
const errorText = await response.text();
|
|
126
173
|
this.log.warn(`Token refresh failed (${response.status}): ${errorText}`);
|
|
174
|
+
if (response.status === 429) {
|
|
175
|
+
this.refreshRetryCount++;
|
|
176
|
+
if (this.refreshRetryCount >= this.maxRetryAttempts) {
|
|
177
|
+
this.log.error(`Rate limit retry limit reached (${this.maxRetryAttempts} attempts). Falling back to full login.`);
|
|
178
|
+
this.refreshRetryCount = 0;
|
|
179
|
+
return await this.login();
|
|
180
|
+
}
|
|
181
|
+
this.log.warn(`Rate limited. Will retry with exponential backoff (attempt ${this.refreshRetryCount}/${this.maxRetryAttempts})`);
|
|
182
|
+
return await this.refreshAccessToken();
|
|
183
|
+
}
|
|
127
184
|
this.log.warn('Attempting full login');
|
|
185
|
+
this.refreshRetryCount = 0;
|
|
128
186
|
return await this.login();
|
|
129
187
|
}
|
|
130
188
|
const data = await response.json();
|
|
131
189
|
this.accessToken = data.access;
|
|
132
190
|
this.refreshToken = data.refresh;
|
|
133
191
|
this.tokenExpiresAt = Date.now() + settings_1.TOKEN_REFRESH_INTERVAL;
|
|
134
|
-
this.
|
|
192
|
+
if (this.refreshRetryCount > 0) {
|
|
193
|
+
this.log.info(`Token refresh recovered after ${this.refreshRetryCount} retry attempt(s)`);
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
this.log.debug('Access token refreshed successfully');
|
|
197
|
+
}
|
|
198
|
+
this.refreshRetryCount = 0;
|
|
199
|
+
await this.reconnectStreaming();
|
|
135
200
|
this.scheduleTokenRefresh();
|
|
136
201
|
return true;
|
|
137
202
|
}
|
|
@@ -142,6 +207,7 @@ class KumoAPI {
|
|
|
142
207
|
else {
|
|
143
208
|
this.log.error('Token refresh error: Unknown error occurred');
|
|
144
209
|
}
|
|
210
|
+
this.refreshRetryCount = 0;
|
|
145
211
|
return await this.login();
|
|
146
212
|
}
|
|
147
213
|
}
|
|
@@ -214,9 +280,9 @@ class KumoAPI {
|
|
|
214
280
|
}
|
|
215
281
|
if (!response.ok) {
|
|
216
282
|
this.log.error(`Request failed with status: ${response.status}`);
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
this.log.
|
|
283
|
+
const errorText = await response.text();
|
|
284
|
+
if (this.debugMode || response.status === 400) {
|
|
285
|
+
this.log.error(` Error response: ${errorText}`);
|
|
220
286
|
}
|
|
221
287
|
return null;
|
|
222
288
|
}
|
|
@@ -352,6 +418,7 @@ class KumoAPI {
|
|
|
352
418
|
this.log.info('Starting streaming connection...');
|
|
353
419
|
this.socket = (0, socket_io_client_1.io)(settings_1.SOCKET_BASE_URL, {
|
|
354
420
|
transports: ['polling', 'websocket'],
|
|
421
|
+
timeout: 20000,
|
|
355
422
|
extraHeaders: {
|
|
356
423
|
'Authorization': `Bearer ${this.accessToken}`,
|
|
357
424
|
'Accept': '*/*',
|
|
@@ -361,14 +428,14 @@ class KumoAPI {
|
|
|
361
428
|
this.socket.on('connect', () => {
|
|
362
429
|
var _a, _b;
|
|
363
430
|
this.log.info(`✓ Streaming connected (ID: ${(_a = this.socket) === null || _a === void 0 ? void 0 : _a.id})`);
|
|
364
|
-
this.reconnectAttempts = 0;
|
|
365
431
|
for (const deviceSerial of deviceSerials) {
|
|
432
|
+
if (!deviceSerial || typeof deviceSerial !== 'string' || deviceSerial.trim().length === 0) {
|
|
433
|
+
this.log.warn(`Skipping invalid device serial: ${deviceSerial}`);
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
366
436
|
this.log.debug(`Subscribing to device: ${deviceSerial}`);
|
|
367
437
|
(_b = this.socket) === null || _b === void 0 ? void 0 : _b.emit('subscribe', deviceSerial);
|
|
368
438
|
}
|
|
369
|
-
for (const deviceSerial of deviceSerials) {
|
|
370
|
-
this.lastStreamingUpdate.set(deviceSerial, Date.now());
|
|
371
|
-
}
|
|
372
439
|
this.isStreamingHealthy = true;
|
|
373
440
|
this.notifyHealthChange(false, true);
|
|
374
441
|
this.startHealthChecks();
|
|
@@ -380,7 +447,6 @@ class KumoAPI {
|
|
|
380
447
|
if (!deviceSerial) {
|
|
381
448
|
return;
|
|
382
449
|
}
|
|
383
|
-
this.updateStreamingTimestamp(deviceSerial);
|
|
384
450
|
if (this.debugMode) {
|
|
385
451
|
this.log.debug(`Stream update for ${deviceSerial}: temp=${data.roomTemp}°C, mode=${data.operationMode}, power=${data.power}`);
|
|
386
452
|
this.log.info(` RAW Streaming Update JSON for ${deviceSerial}:`);
|
|
@@ -397,13 +463,6 @@ class KumoAPI {
|
|
|
397
463
|
this.isStreamingHealthy = false;
|
|
398
464
|
this.notifyHealthChange(wasHealthy, false);
|
|
399
465
|
this.stopHealthChecks();
|
|
400
|
-
if (reason !== 'io client disconnect' && this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
401
|
-
this.reconnectAttempts++;
|
|
402
|
-
this.log.info(`Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`);
|
|
403
|
-
}
|
|
404
|
-
else if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
405
|
-
this.log.error('Max reconnect attempts reached - polling will handle updates');
|
|
406
|
-
}
|
|
407
466
|
});
|
|
408
467
|
this.socket.on('connect_error', (error) => {
|
|
409
468
|
this.log.error(`Streaming connection error: ${error.message}`);
|
|
@@ -432,10 +491,9 @@ class KumoAPI {
|
|
|
432
491
|
var _a;
|
|
433
492
|
return ((_a = this.socket) === null || _a === void 0 ? void 0 : _a.connected) || false;
|
|
434
493
|
}
|
|
435
|
-
|
|
436
|
-
this.streamingHealthCheckInterval =
|
|
437
|
-
this.
|
|
438
|
-
this.log.debug(`Streaming health config: check every ${checkInterval}s, stale after ${staleThreshold}s`);
|
|
494
|
+
setStreamingHealthCheckInterval(checkIntervalSec) {
|
|
495
|
+
this.streamingHealthCheckInterval = checkIntervalSec * 1000;
|
|
496
|
+
this.log.debug(`Streaming health check interval: ${checkIntervalSec}s`);
|
|
439
497
|
}
|
|
440
498
|
onStreamingHealthChange(callback) {
|
|
441
499
|
this.streamingHealthCallbacks.add(callback);
|
|
@@ -443,9 +501,6 @@ class KumoAPI {
|
|
|
443
501
|
getStreamingHealth() {
|
|
444
502
|
return this.isStreamingHealthy;
|
|
445
503
|
}
|
|
446
|
-
updateStreamingTimestamp(deviceSerial) {
|
|
447
|
-
this.lastStreamingUpdate.set(deviceSerial, Date.now());
|
|
448
|
-
}
|
|
449
504
|
checkStreamingHealth() {
|
|
450
505
|
const wasHealthy = this.isStreamingHealthy;
|
|
451
506
|
this.isStreamingHealthy = this.isStreamingConnected();
|
|
@@ -481,7 +536,6 @@ class KumoAPI {
|
|
|
481
536
|
}
|
|
482
537
|
this.stopHealthChecks();
|
|
483
538
|
this.streamingHealthCallbacks.clear();
|
|
484
|
-
this.lastStreamingUpdate.clear();
|
|
485
539
|
this.log.debug('Streaming health monitoring stopped');
|
|
486
540
|
if (this.socket) {
|
|
487
541
|
this.log.debug('Disconnecting streaming connection');
|
|
@@ -489,5 +543,22 @@ class KumoAPI {
|
|
|
489
543
|
this.socket = null;
|
|
490
544
|
}
|
|
491
545
|
}
|
|
546
|
+
async reconnectStreaming() {
|
|
547
|
+
if (!this.streamingEnabled) {
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
const deviceSerials = Array.from(this.deviceUpdateCallbacks.keys());
|
|
551
|
+
if (deviceSerials.length === 0) {
|
|
552
|
+
this.log.debug('No devices subscribed, skipping streaming reconnect');
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
this.log.debug('Reconnecting streaming with refreshed token...');
|
|
556
|
+
if (this.socket) {
|
|
557
|
+
this.socket.removeAllListeners('disconnect');
|
|
558
|
+
this.socket.disconnect();
|
|
559
|
+
this.socket = null;
|
|
560
|
+
}
|
|
561
|
+
await this.startStreaming(deviceSerials);
|
|
562
|
+
}
|
|
492
563
|
}
|
|
493
564
|
exports.KumoAPI = KumoAPI;
|
package/dist/platform.d.ts
CHANGED
|
@@ -14,6 +14,9 @@ export declare class KumoV3Platform implements DynamicPlatformPlugin {
|
|
|
14
14
|
private readonly degradedPollInterval;
|
|
15
15
|
private isStreamingHealthy;
|
|
16
16
|
private isDegradedMode;
|
|
17
|
+
private readonly modeChangeHysteresisMs;
|
|
18
|
+
private pendingModeChange;
|
|
19
|
+
private pendingModeHealthy;
|
|
17
20
|
constructor(log: Logger, config: PlatformConfig, api: API);
|
|
18
21
|
private cleanup;
|
|
19
22
|
configureAccessory(accessory: PlatformAccessory): void;
|
package/dist/platform.js
CHANGED
|
@@ -17,6 +17,9 @@ class KumoV3Platform {
|
|
|
17
17
|
this.siteAccessories = new Map();
|
|
18
18
|
this.isStreamingHealthy = false;
|
|
19
19
|
this.isDegradedMode = false;
|
|
20
|
+
this.modeChangeHysteresisMs = 10000;
|
|
21
|
+
this.pendingModeChange = null;
|
|
22
|
+
this.pendingModeHealthy = null;
|
|
20
23
|
this.kumoConfig = config;
|
|
21
24
|
this.log.debug('Initializing platform:', this.config.name);
|
|
22
25
|
const kumoConfig = this.kumoConfig;
|
|
@@ -42,8 +45,7 @@ class KumoV3Platform {
|
|
|
42
45
|
this.log.debug(`Degraded polling interval: ${this.degradedPollInterval / 1000}s`);
|
|
43
46
|
this.kumoAPI = new kumo_api_1.KumoAPI(kumoConfig.username, kumoConfig.password, this.log, kumoConfig.debug || false);
|
|
44
47
|
const healthCheckInterval = kumoConfig.streamingHealthCheckInterval || 30;
|
|
45
|
-
|
|
46
|
-
this.kumoAPI.setStreamingHealthConfig(healthCheckInterval, staleThreshold);
|
|
48
|
+
this.kumoAPI.setStreamingHealthCheckInterval(healthCheckInterval);
|
|
47
49
|
this.kumoAPI.onStreamingHealthChange((isHealthy) => {
|
|
48
50
|
this.handleStreamingHealthChange(isHealthy);
|
|
49
51
|
});
|
|
@@ -156,7 +158,6 @@ class KumoV3Platform {
|
|
|
156
158
|
this.log.warn('Streaming failed to start - falling back to polling');
|
|
157
159
|
}
|
|
158
160
|
const healthCheckInterval = this.kumoConfig.streamingHealthCheckInterval || 30;
|
|
159
|
-
const staleThreshold = this.kumoConfig.streamingStaleThreshold || 60;
|
|
160
161
|
this.log.info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
161
162
|
this.log.info('Mitsubishi Comfort Plugin Configuration');
|
|
162
163
|
this.log.info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
@@ -165,7 +166,6 @@ class KumoV3Platform {
|
|
|
165
166
|
this.log.info(`Normal poll interval: ${(this.kumoConfig.pollInterval || 30)}s`);
|
|
166
167
|
this.log.info(`Degraded poll interval: ${this.degradedPollInterval / 1000}s`);
|
|
167
168
|
this.log.info(`Health check interval: ${healthCheckInterval}s`);
|
|
168
|
-
this.log.info(`Stale threshold: ${staleThreshold}s`);
|
|
169
169
|
this.log.info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
170
170
|
if (streamingStarted) {
|
|
171
171
|
if (this.kumoConfig.disablePolling) {
|
|
@@ -236,16 +236,41 @@ class KumoV3Platform {
|
|
|
236
236
|
const wasHealthy = this.isStreamingHealthy;
|
|
237
237
|
this.isStreamingHealthy = isHealthy;
|
|
238
238
|
if (wasHealthy && !isHealthy) {
|
|
239
|
+
if (this.pendingModeChange) {
|
|
240
|
+
clearTimeout(this.pendingModeChange);
|
|
241
|
+
this.pendingModeChange = null;
|
|
242
|
+
this.pendingModeHealthy = null;
|
|
243
|
+
this.log.debug('Cancelled pending mode change due to new disconnect');
|
|
244
|
+
}
|
|
239
245
|
this.log.warn('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
240
246
|
this.log.warn('⚠ STREAMING INTERRUPTED');
|
|
241
247
|
this.log.warn('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
242
248
|
this.enterDegradedMode();
|
|
243
249
|
}
|
|
244
250
|
if (!wasHealthy && isHealthy) {
|
|
245
|
-
this.
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
251
|
+
if (this.pendingModeHealthy === true) {
|
|
252
|
+
this.log.debug('Mode change to healthy already pending, waiting for stability...');
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (this.pendingModeChange) {
|
|
256
|
+
clearTimeout(this.pendingModeChange);
|
|
257
|
+
}
|
|
258
|
+
this.pendingModeHealthy = true;
|
|
259
|
+
const hysteresisSec = this.modeChangeHysteresisMs / 1000;
|
|
260
|
+
this.log.info(`Streaming reconnected - waiting ${hysteresisSec}s for stable connection...`);
|
|
261
|
+
this.pendingModeChange = setTimeout(() => {
|
|
262
|
+
this.pendingModeChange = null;
|
|
263
|
+
this.pendingModeHealthy = null;
|
|
264
|
+
if (this.isStreamingHealthy) {
|
|
265
|
+
this.log.info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
266
|
+
this.log.info('✓ STREAMING RESUMED (stable)');
|
|
267
|
+
this.log.info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
268
|
+
this.exitDegradedMode();
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
this.log.warn('Streaming became unhealthy during stability check, staying in degraded mode');
|
|
272
|
+
}
|
|
273
|
+
}, this.modeChangeHysteresisMs);
|
|
249
274
|
}
|
|
250
275
|
}
|
|
251
276
|
enterDegradedMode() {
|
package/dist/settings.js
CHANGED
|
@@ -5,6 +5,6 @@ exports.PLATFORM_NAME = 'KumoV3';
|
|
|
5
5
|
exports.PLUGIN_NAME = 'homebridge-mitsubishi-comfort';
|
|
6
6
|
exports.API_BASE_URL = 'https://app-prod.kumocloud.com/v3';
|
|
7
7
|
exports.SOCKET_BASE_URL = 'https://socket-prod.kumocloud.com';
|
|
8
|
-
exports.TOKEN_REFRESH_INTERVAL =
|
|
8
|
+
exports.TOKEN_REFRESH_INTERVAL = 20 * 60 * 1000;
|
|
9
9
|
exports.POLL_INTERVAL = 30 * 1000;
|
|
10
10
|
exports.APP_VERSION = '3.2.3';
|