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.
@@ -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
- const status = await this.kumoAPI.getDeviceStatus(this.deviceSerial);
204
- if (status) {
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
- const status = await this.kumoAPI.getDeviceStatus(this.deviceSerial);
218
- if (status) {
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 target heating cooling state for ${this.accessory.displayName}`);
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
- const status = await this.kumoAPI.getDeviceStatus(this.deviceSerial);
265
- if (status) {
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.lastUpdateSource !== 'none') {
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
- const status = await this.kumoAPI.getDeviceStatus(this.deviceSerial);
286
- if (status) {
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.lastUpdateSource !== 'none') {
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;
@@ -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
- setStreamingHealthConfig(checkInterval: number, staleThreshold: number): void;
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
- this.log.info('Successfully logged in to Kumo Cloud API');
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.log.debug('Access token refreshed successfully');
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
- if (this.debugMode) {
218
- const errorText = await response.text();
219
- this.log.info(` Error response: ${errorText}`);
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
- setStreamingHealthConfig(checkInterval, staleThreshold) {
436
- this.streamingHealthCheckInterval = checkInterval * 1000;
437
- this.streamingStaleThreshold = staleThreshold * 1000;
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;
@@ -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
- const staleThreshold = kumoConfig.streamingStaleThreshold || 60;
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.log.info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
246
- this.log.info(' STREAMING RESUMED');
247
- this.log.info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
248
- this.exitDegradedMode();
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 = 15 * 60 * 1000;
8
+ exports.TOKEN_REFRESH_INTERVAL = 20 * 60 * 1000;
9
9
  exports.POLL_INTERVAL = 30 * 1000;
10
10
  exports.APP_VERSION = '3.2.3';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homebridge-mitsubishi-comfort",
3
- "version": "1.3.0",
3
+ "version": "1.3.3",
4
4
  "description": "Homebridge plugin for Mitsubishi heat pumps using Kumo Cloud v3 API",
5
5
  "author": "burtherman",
6
6
  "main": "dist/index.js",