mysa-js-sdk 2.0.2 → 2.1.0

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/index.d.cts CHANGED
@@ -327,6 +327,24 @@ interface Firmwares {
327
327
  Firmware: Record<string, FirmwareDevice>;
328
328
  }
329
329
 
330
+ /** Home information object. */
331
+ interface HomeBase {
332
+ /** Unique home identifier */
333
+ Id: string;
334
+ /** User-assigned home name */
335
+ Name?: string;
336
+ }
337
+ /**
338
+ * Top-level interface for the homes REST API response.
339
+ *
340
+ * Contains the complete collection of homes associated with a user account, typically returned from API endpoints that
341
+ * fetch home information.
342
+ */
343
+ interface Homes {
344
+ /** Collection of all homes */
345
+ Homes: HomeBase[];
346
+ }
347
+
330
348
  /** Represents a timestamped value with metadata */
331
349
  interface TimestampedValue<T = number> {
332
350
  /** Timestamp when the value was recorded */
@@ -441,10 +459,41 @@ declare enum OutMessageType {
441
459
  DEVICE_POST_BOOT = 10,
442
460
  /** Version 2 device status report with enhanced device information */
443
461
  DEVICE_V2_STATUS = 40,
462
+ /** AC device status report with temperature, humidity, setpoint, and AC-specific fields */
463
+ DEVICE_AC_STATUS = 30,
444
464
  /** Notification that a device's operational state has changed */
445
465
  DEVICE_STATE_CHANGE = 44
446
466
  }
447
467
 
468
+ /**
469
+ * Interface representing a status report from a Mysa AC device.
470
+ *
471
+ * AC-V1-0 devices send this message (type 30) with environmental readings and AC-specific operational parameters. This
472
+ * is the AC equivalent of {@link DeviceV2Status} (type 40) used by baseboard devices.
473
+ */
474
+ interface DeviceAcStatus extends MsgPayload<OutMessageType.DEVICE_AC_STATUS> {
475
+ /** Source information identifying the device sending the status */
476
+ src: {
477
+ /** Reference identifier for the device */
478
+ ref: string;
479
+ /** Type identifier for the source device */
480
+ type: number;
481
+ };
482
+ /** Status data payload containing current device measurements and settings */
483
+ body: {
484
+ /** Ambient temperature reading from the device sensor (°C) */
485
+ ambTemp: number;
486
+ /** Relative humidity percentage */
487
+ hum: number;
488
+ /** Current temperature setpoint (°C) */
489
+ stpt: number;
490
+ /** Not present on AC devices (included for compatibility with DEVICE_V2_STATUS handler) */
491
+ dtyCycle?: number;
492
+ /** Operating mode (1=off, 2=auto, 3=heat, 4=cool, 5=fan_only, 6=dry) */
493
+ mode?: number;
494
+ };
495
+ }
496
+
448
497
  /**
449
498
  * Interface representing a device state change notification from a Mysa device.
450
499
  *
@@ -519,7 +568,7 @@ interface DeviceV2Status extends MsgPayload<OutMessageType.DEVICE_V2_STATUS> {
519
568
  * This type encompasses payloads where the message type is specified in the `msg` field rather than the `MsgType`
520
569
  * field. Includes device status reports and state change notifications.
521
570
  */
522
- type MsgOutPayload = DeviceV2Status | DeviceStateChange;
571
+ type MsgOutPayload = DeviceAcStatus | DeviceV2Status | DeviceStateChange;
523
572
 
524
573
  /**
525
574
  * Base interface for MQTT message payloads that use the MsgType field.
@@ -739,8 +788,12 @@ declare class MysaApiClient {
739
788
  private _mqttConnectionPromise?;
740
789
  /** Stable per-process MQTT client id (prevents collisions between multiple processes). */
741
790
  private _mqttClientId?;
791
+ /** Expiration time of the credentials currently in use by the MQTT client. */
792
+ private _mqttCredentialsExpiration?;
742
793
  /** Interrupt timestamps for storm / collision detection. */
743
794
  private _mqttInterrupts;
795
+ /** Whether a forced MQTT reset is currently in progress (guards against re-entrancy). */
796
+ private _mqttResetInProgress;
744
797
  /** The device IDs that are currently being updated in real-time, mapped to their respective timeouts. */
745
798
  private _realtimeDeviceIds;
746
799
  /** The cached devices object, if any. */
@@ -850,6 +903,14 @@ declare class MysaApiClient {
850
903
  * @throws {@link UnauthenticatedError} When the user is not authenticated.
851
904
  */
852
905
  getDeviceStates(): Promise<DeviceStates>;
906
+ /**
907
+ * Retrieves information about all homes associated with the user.
908
+ *
909
+ * @returns A promise that resolves to the homes information.
910
+ * @throws {@link MysaApiError} When the API request fails.
911
+ * @throws {@link UnauthenticatedError} When the user is not authenticated.
912
+ */
913
+ getHomes(): Promise<Homes>;
853
914
  /**
854
915
  * Sets the state of a specific device by sending commands via MQTT.
855
916
  *
@@ -1097,4 +1158,4 @@ type MsgTypeInPayload = CheckDeviceSettings | StartPublishingDeviceStatus;
1097
1158
  */
1098
1159
  type InPayload = MsgTypeInPayload | MsgInPayload;
1099
1160
 
1100
- export { type BrandInfo, type ChangeDeviceState, type CheckDeviceSettings, type DeviceBase, type DeviceLog, type DevicePostBoot, type DeviceSetpointChange, type DeviceState, type DeviceStateChange, type DeviceStates, type DeviceStatesObj, type DeviceV1Status, type DeviceV2Status, type Devices, type DevicesObj, type FirmwareDevice, type Firmwares, InMessageType, type InPayload, type Logger, type ModeObj, MqttPublishError, type MqttPublishOptions, type MsgBasePayload, type MsgInPayload, type MsgOutPayload, type MsgPayload, type MsgTypeBasePayload, type MsgTypeInPayload, type MsgTypeOutPayload, type MsgTypePayload, MysaApiClient, type MysaApiClientEventTypes, type MysaApiClientOptions, MysaApiError, type MysaDeviceMode, type MysaFanSpeedMode, type MysaSession, OutMessageType, type OutPayload, type SetPointChange, type StartPublishingDeviceStatus, type StateChange, type Status, type SupportedCaps, type TimestampedValue, UnauthenticatedError, VoidLogger };
1161
+ export { type BrandInfo, type ChangeDeviceState, type CheckDeviceSettings, type DeviceAcStatus, type DeviceBase, type DeviceLog, type DevicePostBoot, type DeviceSetpointChange, type DeviceState, type DeviceStateChange, type DeviceStates, type DeviceStatesObj, type DeviceV1Status, type DeviceV2Status, type Devices, type DevicesObj, type FirmwareDevice, type Firmwares, type HomeBase, type Homes, InMessageType, type InPayload, type Logger, type ModeObj, MqttPublishError, type MqttPublishOptions, type MsgBasePayload, type MsgInPayload, type MsgOutPayload, type MsgPayload, type MsgTypeBasePayload, type MsgTypeInPayload, type MsgTypeOutPayload, type MsgTypePayload, MysaApiClient, type MysaApiClientEventTypes, type MysaApiClientOptions, MysaApiError, type MysaDeviceMode, type MysaFanSpeedMode, type MysaSession, OutMessageType, type OutPayload, type SetPointChange, type StartPublishingDeviceStatus, type StateChange, type Status, type SupportedCaps, type TimestampedValue, UnauthenticatedError, VoidLogger };
package/dist/index.d.ts CHANGED
@@ -327,6 +327,24 @@ interface Firmwares {
327
327
  Firmware: Record<string, FirmwareDevice>;
328
328
  }
329
329
 
330
+ /** Home information object. */
331
+ interface HomeBase {
332
+ /** Unique home identifier */
333
+ Id: string;
334
+ /** User-assigned home name */
335
+ Name?: string;
336
+ }
337
+ /**
338
+ * Top-level interface for the homes REST API response.
339
+ *
340
+ * Contains the complete collection of homes associated with a user account, typically returned from API endpoints that
341
+ * fetch home information.
342
+ */
343
+ interface Homes {
344
+ /** Collection of all homes */
345
+ Homes: HomeBase[];
346
+ }
347
+
330
348
  /** Represents a timestamped value with metadata */
331
349
  interface TimestampedValue<T = number> {
332
350
  /** Timestamp when the value was recorded */
@@ -441,10 +459,41 @@ declare enum OutMessageType {
441
459
  DEVICE_POST_BOOT = 10,
442
460
  /** Version 2 device status report with enhanced device information */
443
461
  DEVICE_V2_STATUS = 40,
462
+ /** AC device status report with temperature, humidity, setpoint, and AC-specific fields */
463
+ DEVICE_AC_STATUS = 30,
444
464
  /** Notification that a device's operational state has changed */
445
465
  DEVICE_STATE_CHANGE = 44
446
466
  }
447
467
 
468
+ /**
469
+ * Interface representing a status report from a Mysa AC device.
470
+ *
471
+ * AC-V1-0 devices send this message (type 30) with environmental readings and AC-specific operational parameters. This
472
+ * is the AC equivalent of {@link DeviceV2Status} (type 40) used by baseboard devices.
473
+ */
474
+ interface DeviceAcStatus extends MsgPayload<OutMessageType.DEVICE_AC_STATUS> {
475
+ /** Source information identifying the device sending the status */
476
+ src: {
477
+ /** Reference identifier for the device */
478
+ ref: string;
479
+ /** Type identifier for the source device */
480
+ type: number;
481
+ };
482
+ /** Status data payload containing current device measurements and settings */
483
+ body: {
484
+ /** Ambient temperature reading from the device sensor (°C) */
485
+ ambTemp: number;
486
+ /** Relative humidity percentage */
487
+ hum: number;
488
+ /** Current temperature setpoint (°C) */
489
+ stpt: number;
490
+ /** Not present on AC devices (included for compatibility with DEVICE_V2_STATUS handler) */
491
+ dtyCycle?: number;
492
+ /** Operating mode (1=off, 2=auto, 3=heat, 4=cool, 5=fan_only, 6=dry) */
493
+ mode?: number;
494
+ };
495
+ }
496
+
448
497
  /**
449
498
  * Interface representing a device state change notification from a Mysa device.
450
499
  *
@@ -519,7 +568,7 @@ interface DeviceV2Status extends MsgPayload<OutMessageType.DEVICE_V2_STATUS> {
519
568
  * This type encompasses payloads where the message type is specified in the `msg` field rather than the `MsgType`
520
569
  * field. Includes device status reports and state change notifications.
521
570
  */
522
- type MsgOutPayload = DeviceV2Status | DeviceStateChange;
571
+ type MsgOutPayload = DeviceAcStatus | DeviceV2Status | DeviceStateChange;
523
572
 
524
573
  /**
525
574
  * Base interface for MQTT message payloads that use the MsgType field.
@@ -739,8 +788,12 @@ declare class MysaApiClient {
739
788
  private _mqttConnectionPromise?;
740
789
  /** Stable per-process MQTT client id (prevents collisions between multiple processes). */
741
790
  private _mqttClientId?;
791
+ /** Expiration time of the credentials currently in use by the MQTT client. */
792
+ private _mqttCredentialsExpiration?;
742
793
  /** Interrupt timestamps for storm / collision detection. */
743
794
  private _mqttInterrupts;
795
+ /** Whether a forced MQTT reset is currently in progress (guards against re-entrancy). */
796
+ private _mqttResetInProgress;
744
797
  /** The device IDs that are currently being updated in real-time, mapped to their respective timeouts. */
745
798
  private _realtimeDeviceIds;
746
799
  /** The cached devices object, if any. */
@@ -850,6 +903,14 @@ declare class MysaApiClient {
850
903
  * @throws {@link UnauthenticatedError} When the user is not authenticated.
851
904
  */
852
905
  getDeviceStates(): Promise<DeviceStates>;
906
+ /**
907
+ * Retrieves information about all homes associated with the user.
908
+ *
909
+ * @returns A promise that resolves to the homes information.
910
+ * @throws {@link MysaApiError} When the API request fails.
911
+ * @throws {@link UnauthenticatedError} When the user is not authenticated.
912
+ */
913
+ getHomes(): Promise<Homes>;
853
914
  /**
854
915
  * Sets the state of a specific device by sending commands via MQTT.
855
916
  *
@@ -1097,4 +1158,4 @@ type MsgTypeInPayload = CheckDeviceSettings | StartPublishingDeviceStatus;
1097
1158
  */
1098
1159
  type InPayload = MsgTypeInPayload | MsgInPayload;
1099
1160
 
1100
- export { type BrandInfo, type ChangeDeviceState, type CheckDeviceSettings, type DeviceBase, type DeviceLog, type DevicePostBoot, type DeviceSetpointChange, type DeviceState, type DeviceStateChange, type DeviceStates, type DeviceStatesObj, type DeviceV1Status, type DeviceV2Status, type Devices, type DevicesObj, type FirmwareDevice, type Firmwares, InMessageType, type InPayload, type Logger, type ModeObj, MqttPublishError, type MqttPublishOptions, type MsgBasePayload, type MsgInPayload, type MsgOutPayload, type MsgPayload, type MsgTypeBasePayload, type MsgTypeInPayload, type MsgTypeOutPayload, type MsgTypePayload, MysaApiClient, type MysaApiClientEventTypes, type MysaApiClientOptions, MysaApiError, type MysaDeviceMode, type MysaFanSpeedMode, type MysaSession, OutMessageType, type OutPayload, type SetPointChange, type StartPublishingDeviceStatus, type StateChange, type Status, type SupportedCaps, type TimestampedValue, UnauthenticatedError, VoidLogger };
1161
+ export { type BrandInfo, type ChangeDeviceState, type CheckDeviceSettings, type DeviceAcStatus, type DeviceBase, type DeviceLog, type DevicePostBoot, type DeviceSetpointChange, type DeviceState, type DeviceStateChange, type DeviceStates, type DeviceStatesObj, type DeviceV1Status, type DeviceV2Status, type Devices, type DevicesObj, type FirmwareDevice, type Firmwares, type HomeBase, type Homes, InMessageType, type InPayload, type Logger, type ModeObj, MqttPublishError, type MqttPublishOptions, type MsgBasePayload, type MsgInPayload, type MsgOutPayload, type MsgPayload, type MsgTypeBasePayload, type MsgTypeInPayload, type MsgTypeOutPayload, type MsgTypePayload, MysaApiClient, type MysaApiClientEventTypes, type MysaApiClientOptions, MysaApiError, type MysaDeviceMode, type MysaFanSpeedMode, type MysaSession, OutMessageType, type OutPayload, type SetPointChange, type StartPublishingDeviceStatus, type StateChange, type Status, type SupportedCaps, type TimestampedValue, UnauthenticatedError, VoidLogger };
package/dist/index.js CHANGED
@@ -67,6 +67,8 @@ var MqttPublishError = class extends Error {
67
67
  this.original = original;
68
68
  this.name = "MqttPublishError";
69
69
  }
70
+ attempts;
71
+ original;
70
72
  };
71
73
 
72
74
  // src/api/Logger.ts
@@ -181,6 +183,7 @@ var OutMessageType = /* @__PURE__ */ ((OutMessageType2) => {
181
183
  OutMessageType2[OutMessageType2["DEVICE_LOG"] = 4] = "DEVICE_LOG";
182
184
  OutMessageType2[OutMessageType2["DEVICE_POST_BOOT"] = 10] = "DEVICE_POST_BOOT";
183
185
  OutMessageType2[OutMessageType2["DEVICE_V2_STATUS"] = 40] = "DEVICE_V2_STATUS";
186
+ OutMessageType2[OutMessageType2["DEVICE_AC_STATUS"] = 30] = "DEVICE_AC_STATUS";
184
187
  OutMessageType2[OutMessageType2["DEVICE_STATE_CHANGE"] = 44] = "DEVICE_STATE_CHANGE";
185
188
  return OutMessageType2;
186
189
  })(OutMessageType || {});
@@ -197,7 +200,8 @@ import {
197
200
  CognitoUserPool,
198
201
  CognitoUserSession
199
202
  } from "amazon-cognito-identity-js";
200
- import { iot, mqtt } from "aws-iot-device-sdk-v2";
203
+ import { auth, iot, mqtt } from "aws-iot-device-sdk-v2";
204
+ import { hash } from "crypto";
201
205
  import dayjs from "dayjs";
202
206
  import duration from "dayjs/plugin/duration.js";
203
207
  import { customAlphabet } from "nanoid";
@@ -224,8 +228,12 @@ var MysaApiClient = class {
224
228
  _mqttConnectionPromise;
225
229
  /** Stable per-process MQTT client id (prevents collisions between multiple processes). */
226
230
  _mqttClientId;
231
+ /** Expiration time of the credentials currently in use by the MQTT client. */
232
+ _mqttCredentialsExpiration;
227
233
  /** Interrupt timestamps for storm / collision detection. */
228
234
  _mqttInterrupts = [];
235
+ /** Whether a forced MQTT reset is currently in progress (guards against re-entrancy). */
236
+ _mqttResetInProgress = false;
229
237
  /** The device IDs that are currently being updated in real-time, mapped to their respective timeouts. */
230
238
  _realtimeDeviceIds = /* @__PURE__ */ new Map();
231
239
  /** The cached devices object, if any. */
@@ -450,6 +458,26 @@ var MysaApiClient = class {
450
458
  }
451
459
  return response.json();
452
460
  }
461
+ /**
462
+ * Retrieves information about all homes associated with the user.
463
+ *
464
+ * @returns A promise that resolves to the homes information.
465
+ * @throws {@link MysaApiError} When the API request fails.
466
+ * @throws {@link UnauthenticatedError} When the user is not authenticated.
467
+ */
468
+ async getHomes() {
469
+ this._logger.debug(`Fetching homes...`);
470
+ const session = await this._getFreshSession();
471
+ const response = await this._fetcher(`${MysaApiBaseUrl}/homes`, {
472
+ headers: {
473
+ Authorization: `${session.getIdToken().getJwtToken()}`
474
+ }
475
+ });
476
+ if (!response.ok) {
477
+ throw new MysaApiError(response);
478
+ }
479
+ return response.json();
480
+ }
453
481
  /**
454
482
  * Sets the state of a specific device by sending commands via MQTT.
455
483
  *
@@ -509,7 +537,7 @@ var MysaApiClient = class {
509
537
  resp: 2,
510
538
  body: {
511
539
  ver: 1,
512
- type: device.Model.startsWith("BB-V1") ? 1 : device.Model.startsWith("AC-V1") ? 2 : device.Model.startsWith("BB-V2") ? device.Model.endsWith("-L") ? 5 : 4 : 0,
540
+ type: device.Model.startsWith("BB-V1") || device.Model.startsWith("v1") ? 1 : device.Model.startsWith("AC-V1") ? 2 : device.Model.startsWith("BB-V2") ? device.Model.endsWith("-L") ? 5 : 4 : 0,
513
541
  cmd: [
514
542
  {
515
543
  tm: -1,
@@ -571,13 +599,14 @@ var MysaApiClient = class {
571
599
  await this._publishWithRetry(mqttConnection, `/v1/dev/${deviceId}/in`, payload, mqtt.QoS.AtLeastOnce);
572
600
  const timer = setInterval(async () => {
573
601
  this._logger.debug(`Sending request to keep-alive publishing device status for '${deviceId}'...`);
602
+ const connection = await this._getMqttConnection();
574
603
  const payload2 = serializeMqttPayload({
575
604
  Device: deviceId,
576
605
  MsgType: 11 /* START_PUBLISHING_DEVICE_STATUS */,
577
606
  Timestamp: dayjs().unix(),
578
607
  Timeout: RealtimeKeepAliveInterval.asSeconds()
579
608
  });
580
- await this._publishWithRetry(mqttConnection, `/v1/dev/${deviceId}/in`, payload2, mqtt.QoS.AtLeastOnce);
609
+ await this._publishWithRetry(connection, `/v1/dev/${deviceId}/in`, payload2, mqtt.QoS.AtLeastOnce);
581
610
  }, RealtimeKeepAliveInterval.subtract(10, "seconds").asMilliseconds());
582
611
  this._realtimeDeviceIds.set(deviceId, timer);
583
612
  }
@@ -676,6 +705,7 @@ var MysaApiClient = class {
676
705
  "AWS_ERROR_MQTT_NO_CONNECTION",
677
706
  "AWS_ERROR_MQTT_UNEXPECTED_HANGUP",
678
707
  "UNEXPECTED_HANGUP",
708
+ "AWS_ERROR_MQTT_CONNECTION_DESTROYED",
679
709
  "Time limit between request and response",
680
710
  "timeout"
681
711
  ];
@@ -750,46 +780,120 @@ var MysaApiClient = class {
750
780
  logger: this._logger
751
781
  });
752
782
  const credentials = await credentialsProvider();
783
+ if (!credentials.expiration) {
784
+ throw new Error("MQTT credentials do not have an expiration time.");
785
+ }
786
+ this._mqttCredentialsExpiration = dayjs(credentials.expiration);
787
+ this._logger.debug(`MQTT credentials expiration: ${this._mqttCredentialsExpiration.format()}`);
788
+ if (!this._mqttCredentialsExpiration.isAfter(dayjs())) {
789
+ this._mqttCredentialsExpiration = void 0;
790
+ throw new Error("MQTT credentials are already expired.");
791
+ }
753
792
  if (!this._mqttClientId) {
754
- this._mqttClientId = `mysa-js-sdk-${((_a = this.session) == null ? void 0 : _a.username) ?? "anon"}-${getRandomClientId()}`;
793
+ const username = ((_a = this.session) == null ? void 0 : _a.username) ?? "anon";
794
+ const usernameHash = hash("sha1", username);
795
+ this._mqttClientId = `mysa-js-sdk-${usernameHash}-${process.pid}-${getRandomClientId()}`;
755
796
  }
756
797
  const builder = iot.AwsIotMqttConnectionConfigBuilder.new_with_websockets().with_credentials(AwsRegion, credentials.accessKeyId, credentials.secretAccessKey, credentials.sessionToken).with_endpoint(MqttEndpoint).with_client_id(this._mqttClientId).with_clean_session(false).with_keep_alive_seconds(30).with_ping_timeout_ms(3e3).with_protocol_operation_timeout_ms(6e4).with_reconnect_min_sec(1).with_reconnect_max_sec(30);
757
798
  const config = builder.build();
799
+ config.websocket_handshake_transform = async (request, done) => {
800
+ try {
801
+ const freshSession = await this._getFreshSession();
802
+ const freshCredentialsProvider = fromCognitoIdentityPool({
803
+ clientConfig: {
804
+ region: AwsRegion
805
+ },
806
+ identityPoolId: CognitoIdentityPoolId,
807
+ logins: {
808
+ [CognitoLoginKey]: freshSession.getIdToken().getJwtToken()
809
+ },
810
+ logger: this._logger
811
+ });
812
+ const freshCredentials = await freshCredentialsProvider();
813
+ if (freshCredentials.expiration) {
814
+ this._mqttCredentialsExpiration = dayjs(freshCredentials.expiration);
815
+ }
816
+ await auth.aws_sign_request(request, {
817
+ algorithm: auth.AwsSigningAlgorithm.SigV4,
818
+ signature_type: auth.AwsSignatureType.HttpRequestViaQueryParams,
819
+ provider: auth.AwsCredentialsProvider.newStatic(
820
+ freshCredentials.accessKeyId,
821
+ freshCredentials.secretAccessKey,
822
+ freshCredentials.sessionToken
823
+ ),
824
+ region: AwsRegion,
825
+ service: "iotdevicegateway",
826
+ signed_body_value: auth.AwsSignedBodyValue.EmptySha256,
827
+ omit_session_token: true
828
+ });
829
+ done();
830
+ } catch (error) {
831
+ this._logger.error("Failed to sign MQTT websocket handshake with fresh credentials", error);
832
+ done(
833
+ 3
834
+ /* AWS_ERROR_UNKNOWN: fail this attempt; the reconnect loop retries */
835
+ );
836
+ }
837
+ };
758
838
  const client = new mqtt.MqttClient();
759
839
  const connection = client.new_connection(config);
760
840
  connection.on("connect", () => {
761
- this._logger.debug("MQTT connect");
841
+ this._logger.debug(`MQTT connect (clientId=${this._mqttClientId})`);
762
842
  });
763
843
  connection.on("connection_success", () => {
764
- this._logger.debug("MQTT connection_success");
844
+ this._logger.debug(`MQTT connection_success (clientId=${this._mqttClientId})`);
765
845
  });
766
846
  connection.on("connection_failure", (e) => {
767
- this._logger.error("MQTT connection_failure", e);
847
+ this._logger.error(`MQTT connection_failure (clientId=${this._mqttClientId})`, e);
768
848
  });
769
849
  connection.on("interrupt", async (e) => {
770
- this._logger.warn("MQTT interrupt", e);
850
+ var _a2;
851
+ this._logger.warn(`MQTT interrupt (clientId=${this._mqttClientId})`, e);
771
852
  const now = Date.now();
772
853
  this._mqttInterrupts = this._mqttInterrupts.filter((t) => now - t < 6e4);
773
854
  this._mqttInterrupts.push(now);
774
- if (this._mqttInterrupts.length > 5) {
775
- this._logger.warn(
776
- `High interrupt rate (${this._mqttInterrupts.length}/60s). Possible clientId collision. Regenerating clientId and resetting connection...`
777
- );
855
+ const areCredentialsExpired = !(((_a2 = this._mqttCredentialsExpiration) == null ? void 0 : _a2.isAfter(dayjs())) ?? false);
856
+ if ((this._mqttInterrupts.length > 5 || areCredentialsExpired) && !this._mqttResetInProgress) {
857
+ this._mqttResetInProgress = true;
858
+ if (this._mqttInterrupts.length > 5) {
859
+ this._logger.warn(
860
+ `High interrupt rate (${this._mqttInterrupts.length}/60s). Possible clientId collision. Regenerating clientId and resetting connection...`
861
+ );
862
+ } else {
863
+ this._logger.warn(`Credentials expired. Regenerating clientId and resetting connection...`);
864
+ }
778
865
  this._mqttClientId = void 0;
866
+ this._mqttCredentialsExpiration = void 0;
779
867
  this._mqttInterrupts = [];
868
+ this._mqttConnectionPromise = void 0;
780
869
  try {
870
+ connection.removeAllListeners();
781
871
  await connection.disconnect();
782
- if (this._mqttConnectionPromise) {
783
- this._logger.warn("MQTT connection promise still defined after disconnect; expected it to be cleared.");
784
- this._mqttConnectionPromise = void 0;
872
+ try {
873
+ this._logger.debug("Old MQTT connection disconnected; establishing new connection...");
874
+ const newConnection = await this._getMqttConnection();
875
+ for (const deviceId of Array.from(this._realtimeDeviceIds.keys())) {
876
+ const topic = `/v1/dev/${deviceId}/out`;
877
+ this._logger.debug(`Re-subscribing to ${topic}`);
878
+ await newConnection.subscribe(topic, mqtt.QoS.AtLeastOnce, (_topic, payload) => {
879
+ this._processMqttMessage(payload);
880
+ });
881
+ }
882
+ this._logger.info("MQTT connection rebuilt successfully after interrupt storm or credentials expiration");
883
+ } catch (err) {
884
+ this._logger.error("Failed to re-subscribe after interrupt storm or credentials expiration", err);
785
885
  }
786
886
  } catch (error) {
787
- this._logger.error("Failed to disconnect MQTT connection", error);
887
+ this._logger.error("Error during MQTT reset", error);
888
+ } finally {
889
+ this._mqttResetInProgress = false;
788
890
  }
789
891
  }
790
892
  });
791
893
  connection.on("resume", async (returnCode, sessionPresent) => {
792
- this._logger.info(`MQTT resume returnCode=${returnCode} sessionPresent=${sessionPresent}`);
894
+ this._logger.info(
895
+ `MQTT resume returnCode=${returnCode} sessionPresent=${sessionPresent} clientId=${this._mqttClientId}`
896
+ );
793
897
  if (!sessionPresent) {
794
898
  this._logger.info("No session present, re-subscribing each device");
795
899
  try {
@@ -806,11 +910,12 @@ var MysaApiClient = class {
806
910
  }
807
911
  });
808
912
  connection.on("error", (e) => {
809
- this._logger.error("MQTT error", e);
913
+ this._logger.error(`MQTT error (clientId=${this._mqttClientId})`, e);
810
914
  });
811
915
  connection.on("closed", () => {
812
916
  this._logger.info("MQTT connection closed");
813
917
  this._mqttConnectionPromise = void 0;
918
+ this._mqttCredentialsExpiration = void 0;
814
919
  });
815
920
  await connection.connect();
816
921
  return connection;
@@ -849,6 +954,7 @@ var MysaApiClient = class {
849
954
  }
850
955
  } else if (isMsgOutPayload(parsedPayload)) {
851
956
  switch (parsedPayload.msg) {
957
+ case 30 /* DEVICE_AC_STATUS */:
852
958
  case 40 /* DEVICE_V2_STATUS */:
853
959
  this.emitter.emit("statusChanged", {
854
960
  deviceId: parsedPayload.src.ref,