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/README.md +4 -3
- package/dist/index.cjs +123 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +63 -2
- package/dist/index.d.ts +63 -2
- package/dist/index.js +124 -18
- package/dist/index.js.map +1 -1
- package/package.json +30 -28
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(
|
|
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
|
-
|
|
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(
|
|
841
|
+
this._logger.debug(`MQTT connect (clientId=${this._mqttClientId})`);
|
|
762
842
|
});
|
|
763
843
|
connection.on("connection_success", () => {
|
|
764
|
-
this._logger.debug(
|
|
844
|
+
this._logger.debug(`MQTT connection_success (clientId=${this._mqttClientId})`);
|
|
765
845
|
});
|
|
766
846
|
connection.on("connection_failure", (e) => {
|
|
767
|
-
this._logger.error(
|
|
847
|
+
this._logger.error(`MQTT connection_failure (clientId=${this._mqttClientId})`, e);
|
|
768
848
|
});
|
|
769
849
|
connection.on("interrupt", async (e) => {
|
|
770
|
-
|
|
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
|
-
|
|
775
|
-
|
|
776
|
-
|
|
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
|
-
|
|
783
|
-
this._logger.
|
|
784
|
-
|
|
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("
|
|
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(
|
|
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(
|
|
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,
|