mysa-js-sdk 2.1.2 → 3.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/CHANGELOG.md +49 -0
- package/README.md +18 -34
- package/dist/index.cjs +380 -143
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +177 -55
- package/dist/index.d.ts +177 -55
- package/dist/index.js +379 -152
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -59,20 +59,32 @@ __export(index_exports, {
|
|
|
59
59
|
MysaApiError: () => MysaApiError,
|
|
60
60
|
OutMessageType: () => OutMessageType,
|
|
61
61
|
UnauthenticatedError: () => UnauthenticatedError,
|
|
62
|
+
UnknownDeviceError: () => UnknownDeviceError,
|
|
63
|
+
UnsupportedFanSpeedError: () => UnsupportedFanSpeedError,
|
|
62
64
|
VoidLogger: () => VoidLogger
|
|
63
65
|
});
|
|
64
66
|
module.exports = __toCommonJS(index_exports);
|
|
65
67
|
|
|
66
68
|
// src/api/Errors.ts
|
|
67
69
|
var UnauthenticatedError = class extends Error {
|
|
70
|
+
/**
|
|
71
|
+
* The underlying failure that prevented authentication.
|
|
72
|
+
*
|
|
73
|
+
* Callers need it to tell a credential rejection apart from a transport or service failure, since both surface as
|
|
74
|
+
* this error. Declared explicitly rather than passed to `super` so the SDK keeps compiling against its current lib,
|
|
75
|
+
* which predates the two-argument `Error` constructor.
|
|
76
|
+
*/
|
|
77
|
+
cause;
|
|
68
78
|
/**
|
|
69
79
|
* Creates a new UnauthenticatedError instance.
|
|
70
80
|
*
|
|
71
81
|
* @param message - The error message
|
|
82
|
+
* @param cause - The underlying failure that prevented authentication, if any.
|
|
72
83
|
*/
|
|
73
|
-
constructor(message) {
|
|
84
|
+
constructor(message, cause) {
|
|
74
85
|
super(message);
|
|
75
86
|
this.name = "UnauthenticatedError";
|
|
87
|
+
this.cause = cause;
|
|
76
88
|
}
|
|
77
89
|
};
|
|
78
90
|
var MysaApiError = class extends Error {
|
|
@@ -94,6 +106,40 @@ var MysaApiError = class extends Error {
|
|
|
94
106
|
this.statusText = apiResponse.statusText;
|
|
95
107
|
}
|
|
96
108
|
};
|
|
109
|
+
var UnknownDeviceError = class extends Error {
|
|
110
|
+
/**
|
|
111
|
+
* Creates a new UnknownDeviceError instance.
|
|
112
|
+
*
|
|
113
|
+
* @param deviceId - The device id that could not be resolved
|
|
114
|
+
*/
|
|
115
|
+
constructor(deviceId) {
|
|
116
|
+
super(`Unknown device id '${deviceId}': no such device on this account.`);
|
|
117
|
+
this.deviceId = deviceId;
|
|
118
|
+
this.name = "UnknownDeviceError";
|
|
119
|
+
}
|
|
120
|
+
deviceId;
|
|
121
|
+
};
|
|
122
|
+
var UnsupportedFanSpeedError = class extends Error {
|
|
123
|
+
/**
|
|
124
|
+
* Creates a new UnsupportedFanSpeedError instance.
|
|
125
|
+
*
|
|
126
|
+
* @param deviceId - The id of the device the command was aimed at.
|
|
127
|
+
* @param fanSpeed - The requested fan speed that the device does not support.
|
|
128
|
+
* @param supportedFanSpeeds - The fan speeds the device does support.
|
|
129
|
+
*/
|
|
130
|
+
constructor(deviceId, fanSpeed, supportedFanSpeeds) {
|
|
131
|
+
super(
|
|
132
|
+
`Device '${deviceId}' does not support the '${fanSpeed}' fan speed. Supported fan speeds: ${supportedFanSpeeds.join(", ") || "(none)"}.`
|
|
133
|
+
);
|
|
134
|
+
this.deviceId = deviceId;
|
|
135
|
+
this.fanSpeed = fanSpeed;
|
|
136
|
+
this.supportedFanSpeeds = supportedFanSpeeds;
|
|
137
|
+
this.name = "UnsupportedFanSpeedError";
|
|
138
|
+
}
|
|
139
|
+
deviceId;
|
|
140
|
+
fanSpeed;
|
|
141
|
+
supportedFanSpeeds;
|
|
142
|
+
};
|
|
97
143
|
var MqttPublishError = class extends Error {
|
|
98
144
|
/**
|
|
99
145
|
* Creates a new MqttPublishError instance.
|
|
@@ -191,8 +237,9 @@ function parseMqttPayload(payload) {
|
|
|
191
237
|
const jsonString = decoder.decode(payload);
|
|
192
238
|
return JSON.parse(jsonString);
|
|
193
239
|
} catch (error) {
|
|
194
|
-
|
|
195
|
-
|
|
240
|
+
const parseError = new Error("Failed to parse MQTT payload");
|
|
241
|
+
parseError.cause = error;
|
|
242
|
+
throw parseError;
|
|
196
243
|
}
|
|
197
244
|
}
|
|
198
245
|
function serializeMqttPayload(payload) {
|
|
@@ -249,11 +296,52 @@ var MqttEndpoint = "a3q27gia9qg3zy-ats.iot.us-east-1.amazonaws.com";
|
|
|
249
296
|
var MysaApiBaseUrl = "https://app-prod.mysa.cloud";
|
|
250
297
|
var RealtimeKeepAliveInterval = import_dayjs.default.duration(5, "minutes");
|
|
251
298
|
var PublishAckTimeout = import_dayjs.default.duration(30, "seconds");
|
|
299
|
+
var MqttInterruptWindow = import_dayjs.default.duration(30, "seconds");
|
|
300
|
+
var MqttInterruptThreshold = 3;
|
|
301
|
+
var MqttResetBaseDelay = import_dayjs.default.duration(1, "second");
|
|
302
|
+
var MqttResetMaxDelay = import_dayjs.default.duration(30, "seconds");
|
|
303
|
+
var MqttStabilityWindow = import_dayjs.default.duration(60, "seconds");
|
|
304
|
+
var CanonicalFanSpeedOrder = ["auto", "low", "medium", "high", "max"];
|
|
305
|
+
var LegacyFanSpeedSendMap = { auto: 1, low: 3, medium: 5, high: 7, max: 8 };
|
|
306
|
+
var FanSpeedReceiveMap = {
|
|
307
|
+
1: "auto",
|
|
308
|
+
2: "low",
|
|
309
|
+
// CodeNum=1117 canonical low
|
|
310
|
+
3: "low",
|
|
311
|
+
// legacy
|
|
312
|
+
4: "medium",
|
|
313
|
+
// CodeNum=1117 canonical medium
|
|
314
|
+
5: "medium",
|
|
315
|
+
// legacy
|
|
316
|
+
6: "high",
|
|
317
|
+
// CodeNum=1117 canonical high
|
|
318
|
+
7: "high",
|
|
319
|
+
// legacy
|
|
320
|
+
8: "max"
|
|
321
|
+
};
|
|
322
|
+
function buildFanSpeedSendMap(device) {
|
|
323
|
+
var _a;
|
|
324
|
+
const fanSpeeds = (_a = device.SupportedCaps) == null ? void 0 : _a.fanSpeeds;
|
|
325
|
+
if (!fanSpeeds || fanSpeeds.length === 0) {
|
|
326
|
+
return LegacyFanSpeedSendMap;
|
|
327
|
+
}
|
|
328
|
+
const map = {};
|
|
329
|
+
CanonicalFanSpeedOrder.forEach((name, index) => {
|
|
330
|
+
if (index < fanSpeeds.length) {
|
|
331
|
+
map[name] = fanSpeeds[index];
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
return map;
|
|
335
|
+
}
|
|
252
336
|
var MysaApiClient = class {
|
|
337
|
+
/** The credentials of the Mysa account this client authenticates as. */
|
|
338
|
+
_credentials;
|
|
253
339
|
/** The current session object, if any. */
|
|
254
340
|
_cognitoUserSession;
|
|
255
341
|
/** The current user object, if any. */
|
|
256
342
|
_cognitoUser;
|
|
343
|
+
/** The in-flight session acquisition, if any, so that concurrent callers share a single refresh or login. */
|
|
344
|
+
_freshSessionPromise;
|
|
257
345
|
/** The logger instance used by the client. */
|
|
258
346
|
_logger;
|
|
259
347
|
/** The fetcher function used by the client. */
|
|
@@ -268,8 +356,21 @@ var MysaApiClient = class {
|
|
|
268
356
|
_mqttInterrupts = [];
|
|
269
357
|
/** Whether a forced MQTT reset is currently in progress (guards against re-entrancy). */
|
|
270
358
|
_mqttResetInProgress = false;
|
|
359
|
+
/** Monotonic id of the current MQTT connection; used to ignore events from discarded connections. */
|
|
360
|
+
_mqttGeneration = 0;
|
|
361
|
+
/** Consecutive forced resets without an intervening stable period; drives reset backoff. */
|
|
362
|
+
_mqttConsecutiveResets = 0;
|
|
363
|
+
/** Timestamp (ms) of the most recent successful MQTT (re)connect, for interrupt dwell-time diagnostics. */
|
|
364
|
+
_mqttLastConnectionSuccessAt;
|
|
365
|
+
/** Timer that clears the consecutive-reset counter once a connection stays healthy. */
|
|
366
|
+
_mqttStabilityTimer;
|
|
271
367
|
/** The device IDs that are currently being updated in real-time, mapped to their respective timeouts. */
|
|
272
368
|
_realtimeDeviceIds = /* @__PURE__ */ new Map();
|
|
369
|
+
/**
|
|
370
|
+
* Raw topic filters registered via {@link startRawTopicCapture}, mapped to their message handlers. Re-subscribed on
|
|
371
|
+
* every reconnect so a debug capture survives connection resets.
|
|
372
|
+
*/
|
|
373
|
+
_rawTopicCaptures = /* @__PURE__ */ new Map();
|
|
273
374
|
/** The cached devices object, if any. */
|
|
274
375
|
_cachedDevices;
|
|
275
376
|
/**
|
|
@@ -278,89 +379,63 @@ var MysaApiClient = class {
|
|
|
278
379
|
* @see {@link MysaApiClientEventTypes} for the possible events and their payloads.
|
|
279
380
|
*/
|
|
280
381
|
emitter = new EventEmitter();
|
|
281
|
-
/**
|
|
282
|
-
* Gets the persistable session object.
|
|
283
|
-
*
|
|
284
|
-
* @returns The current persistable session object, if any.
|
|
285
|
-
*/
|
|
286
|
-
get session() {
|
|
287
|
-
if (!this._cognitoUserSession || !this._cognitoUser) {
|
|
288
|
-
return void 0;
|
|
289
|
-
}
|
|
290
|
-
return {
|
|
291
|
-
username: this._cognitoUser.getUsername(),
|
|
292
|
-
idToken: this._cognitoUserSession.getIdToken().getJwtToken(),
|
|
293
|
-
accessToken: this._cognitoUserSession.getAccessToken().getJwtToken(),
|
|
294
|
-
refreshToken: this._cognitoUserSession.getRefreshToken().getToken()
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
/**
|
|
298
|
-
* Returns whether the client currently has an active session.
|
|
299
|
-
*
|
|
300
|
-
* @returns True if the client has an active session, false otherwise.
|
|
301
|
-
*/
|
|
302
|
-
get isAuthenticated() {
|
|
303
|
-
return !!this.session;
|
|
304
|
-
}
|
|
305
382
|
/**
|
|
306
383
|
* Constructs a new instance of the MysaApiClient.
|
|
307
384
|
*
|
|
308
|
-
* @param
|
|
385
|
+
* @param credentials - The credentials of the Mysa account to authenticate as.
|
|
309
386
|
* @param options - The options for the client.
|
|
310
387
|
*/
|
|
311
|
-
constructor(
|
|
388
|
+
constructor(credentials, options) {
|
|
389
|
+
this._credentials = credentials;
|
|
312
390
|
this._logger = (options == null ? void 0 : options.logger) || new VoidLogger();
|
|
313
391
|
this._fetcher = (options == null ? void 0 : options.fetcher) || fetch;
|
|
314
|
-
if (session) {
|
|
315
|
-
this._cognitoUser = new import_amazon_cognito_identity_js.CognitoUser({
|
|
316
|
-
Username: session.username,
|
|
317
|
-
Pool: new import_amazon_cognito_identity_js.CognitoUserPool({ UserPoolId: CognitoUserPoolId, ClientId: CognitoClientId })
|
|
318
|
-
});
|
|
319
|
-
this._cognitoUserSession = new import_amazon_cognito_identity_js.CognitoUserSession({
|
|
320
|
-
IdToken: new import_amazon_cognito_identity_js.CognitoIdToken({ IdToken: session.idToken }),
|
|
321
|
-
AccessToken: new import_amazon_cognito_identity_js.CognitoAccessToken({ AccessToken: session.accessToken }),
|
|
322
|
-
RefreshToken: new import_amazon_cognito_identity_js.CognitoRefreshToken({ RefreshToken: session.refreshToken })
|
|
323
|
-
});
|
|
324
|
-
}
|
|
325
392
|
}
|
|
326
393
|
/**
|
|
327
|
-
*
|
|
394
|
+
* Ensures the client has a usable session, logging in with the credentials it was constructed with if needed.
|
|
328
395
|
*
|
|
329
|
-
*
|
|
330
|
-
*
|
|
396
|
+
* Calling this method is optional: the client authenticates on demand before its first API call, and re-authenticates
|
|
397
|
+
* on its own whenever its session can no longer be refreshed. Call it explicitly at startup to fail fast on invalid
|
|
398
|
+
* credentials instead of on the first API call. It is a no-op when the current session is still usable.
|
|
331
399
|
*
|
|
332
400
|
* @example
|
|
333
401
|
*
|
|
334
402
|
* ```typescript
|
|
335
403
|
* try {
|
|
336
|
-
* await client.login(
|
|
404
|
+
* await client.login();
|
|
337
405
|
* console.log('Login successful!');
|
|
338
406
|
* } catch (error) {
|
|
339
407
|
* console.error('Login failed:', error.message);
|
|
340
408
|
* }
|
|
341
409
|
* ```
|
|
342
410
|
*
|
|
343
|
-
* @
|
|
344
|
-
|
|
411
|
+
* @throws {@link UnauthenticatedError} When authentication fails due to invalid credentials or network issues.
|
|
412
|
+
*/
|
|
413
|
+
async login() {
|
|
414
|
+
await this._getFreshSession();
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Authenticates with Mysa's Cognito user pool and replaces the current session, if any.
|
|
418
|
+
*
|
|
419
|
+
* @returns A promise that resolves to the newly established session.
|
|
345
420
|
* @throws {@link Error} When authentication fails due to invalid credentials or network issues.
|
|
346
421
|
*/
|
|
347
|
-
|
|
422
|
+
_login() {
|
|
348
423
|
this._cognitoUser = void 0;
|
|
349
424
|
this._cognitoUserSession = void 0;
|
|
350
425
|
this._mqttClientId = void 0;
|
|
351
426
|
this._mqttInterrupts = [];
|
|
352
|
-
this.
|
|
427
|
+
this._mqttConsecutiveResets = 0;
|
|
428
|
+
const { username, password } = this._credentials;
|
|
353
429
|
return new Promise((resolve, reject) => {
|
|
354
430
|
const user = new import_amazon_cognito_identity_js.CognitoUser({
|
|
355
|
-
Username:
|
|
431
|
+
Username: username,
|
|
356
432
|
Pool: new import_amazon_cognito_identity_js.CognitoUserPool({ UserPoolId: CognitoUserPoolId, ClientId: CognitoClientId })
|
|
357
433
|
});
|
|
358
|
-
user.authenticateUser(new import_amazon_cognito_identity_js.AuthenticationDetails({ Username:
|
|
434
|
+
user.authenticateUser(new import_amazon_cognito_identity_js.AuthenticationDetails({ Username: username, Password: password }), {
|
|
359
435
|
onSuccess: (session) => {
|
|
360
436
|
this._cognitoUser = user;
|
|
361
437
|
this._cognitoUserSession = session;
|
|
362
|
-
|
|
363
|
-
resolve();
|
|
438
|
+
resolve(session);
|
|
364
439
|
},
|
|
365
440
|
onFailure: (err) => {
|
|
366
441
|
reject(err);
|
|
@@ -385,7 +460,7 @@ var MysaApiClient = class {
|
|
|
385
460
|
*
|
|
386
461
|
* @returns A promise that resolves to the list of devices.
|
|
387
462
|
* @throws {@link MysaApiError} When the API request fails.
|
|
388
|
-
* @throws {@link UnauthenticatedError} When the
|
|
463
|
+
* @throws {@link UnauthenticatedError} When the client cannot authenticate with its credentials.
|
|
389
464
|
*/
|
|
390
465
|
async getDevices() {
|
|
391
466
|
this._logger.debug(`Fetching devices...`);
|
|
@@ -419,7 +494,7 @@ var MysaApiClient = class {
|
|
|
419
494
|
*
|
|
420
495
|
* @param deviceId - The ID of the device to get the serial number for.
|
|
421
496
|
* @returns A promise that resolves to the serial number, or undefined if not found.
|
|
422
|
-
* @throws {@link UnauthenticatedError} When the
|
|
497
|
+
* @throws {@link UnauthenticatedError} When the client cannot authenticate with its credentials.
|
|
423
498
|
*/
|
|
424
499
|
async getDeviceSerialNumber(deviceId) {
|
|
425
500
|
var _a;
|
|
@@ -457,7 +532,7 @@ var MysaApiClient = class {
|
|
|
457
532
|
*
|
|
458
533
|
* @returns A promise that resolves to the firmware information for all devices.
|
|
459
534
|
* @throws {@link MysaApiError} When the API request fails.
|
|
460
|
-
* @throws {@link UnauthenticatedError} When the
|
|
535
|
+
* @throws {@link UnauthenticatedError} When the client cannot authenticate with its credentials.
|
|
461
536
|
*/
|
|
462
537
|
async getDeviceFirmwares() {
|
|
463
538
|
this._logger.debug(`Fetching device firmwares...`);
|
|
@@ -477,7 +552,7 @@ var MysaApiClient = class {
|
|
|
477
552
|
*
|
|
478
553
|
* @returns A promise that resolves to the current state of all devices.
|
|
479
554
|
* @throws {@link MysaApiError} When the API request fails.
|
|
480
|
-
* @throws {@link UnauthenticatedError} When the
|
|
555
|
+
* @throws {@link UnauthenticatedError} When the client cannot authenticate with its credentials.
|
|
481
556
|
*/
|
|
482
557
|
async getDeviceStates() {
|
|
483
558
|
this._logger.debug(`Fetching device states...`);
|
|
@@ -497,7 +572,7 @@ var MysaApiClient = class {
|
|
|
497
572
|
*
|
|
498
573
|
* @returns A promise that resolves to the homes information.
|
|
499
574
|
* @throws {@link MysaApiError} When the API request fails.
|
|
500
|
-
* @throws {@link UnauthenticatedError} When the
|
|
575
|
+
* @throws {@link UnauthenticatedError} When the client cannot authenticate with its credentials.
|
|
501
576
|
*/
|
|
502
577
|
async getHomes() {
|
|
503
578
|
this._logger.debug(`Fetching homes...`);
|
|
@@ -539,29 +614,37 @@ var MysaApiClient = class {
|
|
|
539
614
|
* @param mode - The operating mode to set (one of MysaDeviceMode values, or undefined to leave unchanged).
|
|
540
615
|
* @param fanSpeed - The fan speed mode to set ('low', 'medium', 'high', 'max', 'auto', or undefined to leave
|
|
541
616
|
* unchanged).
|
|
542
|
-
* @throws {@link UnauthenticatedError} When the
|
|
617
|
+
* @throws {@link UnauthenticatedError} When the client cannot authenticate with its credentials.
|
|
618
|
+
* @throws {@link UnknownDeviceError} When the device id does not match any device on the account.
|
|
619
|
+
* @throws {@link UnsupportedFanSpeedError} When the requested fan speed is not supported by the device.
|
|
543
620
|
* @throws {@link Error} When MQTT connection or command sending fails.
|
|
544
621
|
*/
|
|
545
622
|
async setDeviceState(deviceId, setPoint, mode, fanSpeed) {
|
|
546
|
-
var _a;
|
|
547
623
|
this._logger.debug(`Setting device state for '${deviceId}'`);
|
|
548
624
|
if (!this._cachedDevices) {
|
|
549
625
|
this._cachedDevices = await this.getDevices();
|
|
550
626
|
}
|
|
627
|
+
if (!Object.prototype.hasOwnProperty.call(this._cachedDevices.DevicesObj, deviceId)) {
|
|
628
|
+
throw new UnknownDeviceError(deviceId);
|
|
629
|
+
}
|
|
551
630
|
const device = this._cachedDevices.DevicesObj[deviceId];
|
|
631
|
+
await this._getFreshSession();
|
|
552
632
|
this._logger.debug(`Initializing MQTT connection...`);
|
|
553
633
|
const mqttConnection = await this._getMqttConnection();
|
|
554
634
|
const now = (0, import_dayjs.default)();
|
|
555
635
|
this._logger.debug(`Sending request to set device state for '${deviceId}'...`);
|
|
556
636
|
const modeMap = { off: 1, auto: 2, heat: 3, cool: 4, fan_only: 5, dry: 6 };
|
|
557
|
-
const fanSpeedMap =
|
|
637
|
+
const fanSpeedMap = buildFanSpeedSendMap(device);
|
|
638
|
+
if (fanSpeed !== void 0 && fanSpeedMap[fanSpeed] === void 0) {
|
|
639
|
+
throw new UnsupportedFanSpeedError(deviceId, fanSpeed, Object.keys(fanSpeedMap));
|
|
640
|
+
}
|
|
558
641
|
const payload = serializeMqttPayload({
|
|
559
642
|
msg: 44 /* CHANGE_DEVICE_STATE */,
|
|
560
643
|
id: now.valueOf(),
|
|
561
644
|
time: now.unix(),
|
|
562
645
|
ver: "1.0",
|
|
563
646
|
src: {
|
|
564
|
-
ref:
|
|
647
|
+
ref: this._cognitoUser.getUsername(),
|
|
565
648
|
type: 100
|
|
566
649
|
},
|
|
567
650
|
dest: {
|
|
@@ -571,7 +654,7 @@ var MysaApiClient = class {
|
|
|
571
654
|
resp: 2,
|
|
572
655
|
body: {
|
|
573
656
|
ver: 1,
|
|
574
|
-
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,
|
|
657
|
+
type: device.Model.startsWith("BB-V1") || device.Model.startsWith("v1") ? 1 : device.Model.startsWith("AC-V1") ? 2 : device.Model.startsWith("INF-V1") ? 3 : device.Model.startsWith("BB-V2") ? device.Model.endsWith("-L") ? 5 : 4 : 0,
|
|
575
658
|
cmd: [
|
|
576
659
|
{
|
|
577
660
|
tm: -1,
|
|
@@ -675,37 +758,102 @@ var MysaApiClient = class {
|
|
|
675
758
|
clearInterval(timer);
|
|
676
759
|
this._realtimeDeviceIds.delete(deviceId);
|
|
677
760
|
}
|
|
761
|
+
/**
|
|
762
|
+
* Subscribes to raw MQTT topic filters and relays every message verbatim.
|
|
763
|
+
*
|
|
764
|
+
* Unlike {@link startRealtimeUpdates}, this performs no parsing, emits no typed events and sends no "start publishing"
|
|
765
|
+
* request to the device — it simply forwards the full message topic and the decoded UTF-8 payload of everything that
|
|
766
|
+
* arrives on the given filters. It exists to reverse-engineer device families the SDK does not model yet, most
|
|
767
|
+
* notably the AWS IoT Device Shadow protocol used by the central-HVAC ST-V1 thermostats, where both the topic (which
|
|
768
|
+
* shadow, and `accepted`/`rejected`/`delta`/`documents`) and the raw JSON body carry the information a new
|
|
769
|
+
* implementation needs.
|
|
770
|
+
*
|
|
771
|
+
* The capture is passive: the device only publishes to its shadow topics when something drives a change (the Mysa
|
|
772
|
+
* mobile app, a schedule, or the device itself), so exercise the thermostat while a capture is running.
|
|
773
|
+
*
|
|
774
|
+
* Registered filters are re-subscribed automatically after a reconnect.
|
|
775
|
+
*
|
|
776
|
+
* @param topicFilters - MQTT topic filters to subscribe to. Wildcards (`+`, `#`) are allowed, subject to the AWS IoT
|
|
777
|
+
* policy attached to the account's Cognito identity — a filter the policy forbids resolves with a non-zero
|
|
778
|
+
* `error_code`, which is logged rather than thrown so the remaining filters still subscribe.
|
|
779
|
+
* @param handler - Invoked with the full message topic and the decoded UTF-8 payload for every message received.
|
|
780
|
+
* @throws {@link Error} When the MQTT connection cannot be established.
|
|
781
|
+
*/
|
|
782
|
+
async startRawTopicCapture(topicFilters, handler) {
|
|
783
|
+
this._logger.info(`Starting raw topic capture for ${topicFilters.length} filter(s)`);
|
|
784
|
+
const connection = await this._getMqttConnection();
|
|
785
|
+
const decoder = new TextDecoder("utf-8");
|
|
786
|
+
for (const filter of topicFilters) {
|
|
787
|
+
this._rawTopicCaptures.set(filter, handler);
|
|
788
|
+
this._logger.debug(`Subscribing to raw topic filter '${filter}'...`);
|
|
789
|
+
try {
|
|
790
|
+
const result = await connection.subscribe(filter, import_aws_iot_device_sdk_v2.mqtt.QoS.AtLeastOnce, (topic, payload) => {
|
|
791
|
+
handler(topic, decoder.decode(payload));
|
|
792
|
+
});
|
|
793
|
+
this._logger.debug(
|
|
794
|
+
`Raw subscribe to '${filter}' granted (topic='${result.topic}', qos=${result.qos}, error_code=${result.error_code ?? 0})`
|
|
795
|
+
);
|
|
796
|
+
} catch (error) {
|
|
797
|
+
this._logger.warn(`Failed to subscribe to raw topic filter '${filter}'`, error);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
}
|
|
678
801
|
/**
|
|
679
802
|
* Ensures a valid, non-expired session is available.
|
|
680
803
|
*
|
|
681
804
|
* This method checks if the current session is valid and not expired. If the session is expired, it automatically
|
|
682
|
-
* refreshes it using the refresh token.
|
|
805
|
+
* refreshes it using the refresh token. If there is no session yet, or if the refresh token has itself expired or
|
|
806
|
+
* been revoked, the client logs back in with its credentials.
|
|
807
|
+
*
|
|
808
|
+
* Concurrent callers share a single in-flight acquisition, so a burst of API calls never triggers more than one
|
|
809
|
+
* refresh or login.
|
|
683
810
|
*
|
|
684
811
|
* @returns A promise that resolves to a valid CognitoUserSession.
|
|
685
|
-
* @throws {@link UnauthenticatedError} When
|
|
812
|
+
* @throws {@link UnauthenticatedError} When neither refreshing nor logging back in succeeds.
|
|
686
813
|
*/
|
|
687
814
|
async _getFreshSession() {
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
}
|
|
691
|
-
if (this._cognitoUserSession.isValid() && import_dayjs.default.unix(this._cognitoUserSession.getIdToken().getExpiration()).isAfter()) {
|
|
815
|
+
var _a;
|
|
816
|
+
if (this._cognitoUser && ((_a = this._cognitoUserSession) == null ? void 0 : _a.isValid()) && import_dayjs.default.unix(this._cognitoUserSession.getIdToken().getExpiration()).isAfter()) {
|
|
692
817
|
this._logger.debug("Session is valid, no need to refresh");
|
|
693
|
-
return
|
|
818
|
+
return this._cognitoUserSession;
|
|
694
819
|
}
|
|
695
|
-
this.
|
|
696
|
-
|
|
697
|
-
this._cognitoUser.refreshSession(this._cognitoUserSession.getRefreshToken(), (error, session) => {
|
|
698
|
-
if (error) {
|
|
699
|
-
this._logger.error("Failed to refresh session:", error);
|
|
700
|
-
reject(new UnauthenticatedError("Unable to refresh the authentication session."));
|
|
701
|
-
} else {
|
|
702
|
-
this._logger.debug("Session refreshed successfully");
|
|
703
|
-
this._cognitoUserSession = session;
|
|
704
|
-
this.emitter.emit("sessionChanged", this.session);
|
|
705
|
-
resolve(session);
|
|
706
|
-
}
|
|
707
|
-
});
|
|
820
|
+
this._freshSessionPromise ??= this._acquireFreshSession().finally(() => {
|
|
821
|
+
this._freshSessionPromise = void 0;
|
|
708
822
|
});
|
|
823
|
+
return this._freshSessionPromise;
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Refreshes the current session, falling back to a full login when it cannot be refreshed.
|
|
827
|
+
*
|
|
828
|
+
* @returns A promise that resolves to a valid CognitoUserSession.
|
|
829
|
+
* @throws {@link UnauthenticatedError} When logging back in fails.
|
|
830
|
+
*/
|
|
831
|
+
async _acquireFreshSession() {
|
|
832
|
+
if (this._cognitoUser && this._cognitoUserSession) {
|
|
833
|
+
this._logger.debug("Session is not valid or expired, refreshing...");
|
|
834
|
+
try {
|
|
835
|
+
return await new Promise((resolve, reject) => {
|
|
836
|
+
this._cognitoUser.refreshSession(this._cognitoUserSession.getRefreshToken(), (error, session) => {
|
|
837
|
+
if (error) {
|
|
838
|
+
reject(error);
|
|
839
|
+
} else {
|
|
840
|
+
this._logger.debug("Session refreshed successfully");
|
|
841
|
+
this._cognitoUserSession = session;
|
|
842
|
+
resolve(session);
|
|
843
|
+
}
|
|
844
|
+
});
|
|
845
|
+
});
|
|
846
|
+
} catch (error) {
|
|
847
|
+
this._logger.warn("Failed to refresh session, logging back in:", error);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
try {
|
|
851
|
+
this._logger.info("Logging in...");
|
|
852
|
+
return await this._login();
|
|
853
|
+
} catch (error) {
|
|
854
|
+
this._logger.error("Failed to log in:", error);
|
|
855
|
+
throw new UnauthenticatedError("Unable to establish an authentication session.", error);
|
|
856
|
+
}
|
|
709
857
|
}
|
|
710
858
|
/**
|
|
711
859
|
* Establishes and returns an MQTT connection for real-time communication.
|
|
@@ -847,11 +995,11 @@ var MysaApiClient = class {
|
|
|
847
995
|
throw new Error("MQTT credentials are already expired.");
|
|
848
996
|
}
|
|
849
997
|
if (!this._mqttClientId) {
|
|
850
|
-
const username = ((_a = this.
|
|
998
|
+
const username = ((_a = this._cognitoUser) == null ? void 0 : _a.getUsername()) ?? "anon";
|
|
851
999
|
const usernameHash = (0, import_crypto.hash)("sha1", username);
|
|
852
1000
|
this._mqttClientId = `mysa-js-sdk-${usernameHash}-${process.pid}-${getRandomClientId()}`;
|
|
853
1001
|
}
|
|
854
|
-
const builder = import_aws_iot_device_sdk_v2.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(
|
|
1002
|
+
const builder = import_aws_iot_device_sdk_v2.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(true).with_keep_alive_seconds(30).with_ping_timeout_ms(3e3).with_protocol_operation_timeout_ms(6e4).with_reconnect_min_sec(3).with_reconnect_max_sec(30);
|
|
855
1003
|
const config = builder.build();
|
|
856
1004
|
config.websocket_handshake_transform = async (request, done) => {
|
|
857
1005
|
try {
|
|
@@ -894,89 +1042,174 @@ var MysaApiClient = class {
|
|
|
894
1042
|
};
|
|
895
1043
|
const client = new import_aws_iot_device_sdk_v2.mqtt.MqttClient();
|
|
896
1044
|
const connection = client.new_connection(config);
|
|
897
|
-
|
|
898
|
-
|
|
1045
|
+
const generation = ++this._mqttGeneration;
|
|
1046
|
+
connection.on("connect", (sessionPresent) => {
|
|
1047
|
+
if (generation !== this._mqttGeneration) return;
|
|
1048
|
+
this._logger.debug(`MQTT connect (clientId=${this._mqttClientId}, sessionPresent=${sessionPresent})`);
|
|
899
1049
|
});
|
|
900
|
-
connection.on("connection_success", () => {
|
|
901
|
-
|
|
1050
|
+
connection.on("connection_success", (result) => {
|
|
1051
|
+
if (generation !== this._mqttGeneration) return;
|
|
1052
|
+
this._mqttLastConnectionSuccessAt = Date.now();
|
|
1053
|
+
this._logger.debug(
|
|
1054
|
+
`MQTT connection_success (clientId=${this._mqttClientId}, sessionPresent=${result == null ? void 0 : result.session_present}, reasonCode=${result == null ? void 0 : result.reason_code})`
|
|
1055
|
+
);
|
|
1056
|
+
this._armMqttStabilityTimer(generation);
|
|
902
1057
|
});
|
|
903
1058
|
connection.on("connection_failure", (e) => {
|
|
1059
|
+
if (generation !== this._mqttGeneration) return;
|
|
904
1060
|
this._logger.error(`MQTT connection_failure (clientId=${this._mqttClientId})`, e);
|
|
905
1061
|
});
|
|
906
|
-
connection.on("interrupt",
|
|
1062
|
+
connection.on("interrupt", (e) => {
|
|
907
1063
|
var _a2;
|
|
908
|
-
|
|
1064
|
+
if (generation !== this._mqttGeneration) return;
|
|
1065
|
+
const dwellMs = this._mqttLastConnectionSuccessAt !== void 0 ? Date.now() - this._mqttLastConnectionSuccessAt : void 0;
|
|
1066
|
+
this._logger.warn(
|
|
1067
|
+
`MQTT interrupt (clientId=${this._mqttClientId}, dwellMs=${dwellMs ?? "n/a"}, generation=${generation})`,
|
|
1068
|
+
e
|
|
1069
|
+
);
|
|
1070
|
+
this._clearMqttStabilityTimer();
|
|
909
1071
|
const now = Date.now();
|
|
910
|
-
this._mqttInterrupts = this._mqttInterrupts.filter((t) => now - t <
|
|
1072
|
+
this._mqttInterrupts = this._mqttInterrupts.filter((t) => now - t < MqttInterruptWindow.asMilliseconds());
|
|
911
1073
|
this._mqttInterrupts.push(now);
|
|
912
1074
|
const areCredentialsExpired = !(((_a2 = this._mqttCredentialsExpiration) == null ? void 0 : _a2.isAfter((0, import_dayjs.default)())) ?? false);
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
`High interrupt rate (${this._mqttInterrupts.length}/60s). Possible clientId collision. Regenerating clientId and resetting connection...`
|
|
918
|
-
);
|
|
919
|
-
} else {
|
|
920
|
-
this._logger.warn(`Credentials expired. Regenerating clientId and resetting connection...`);
|
|
921
|
-
}
|
|
922
|
-
this._mqttClientId = void 0;
|
|
923
|
-
this._mqttCredentialsExpiration = void 0;
|
|
924
|
-
this._mqttInterrupts = [];
|
|
925
|
-
this._mqttConnectionPromise = void 0;
|
|
926
|
-
try {
|
|
927
|
-
connection.removeAllListeners();
|
|
928
|
-
await connection.disconnect();
|
|
929
|
-
try {
|
|
930
|
-
this._logger.debug("Old MQTT connection disconnected; establishing new connection...");
|
|
931
|
-
const newConnection = await this._getMqttConnection();
|
|
932
|
-
for (const deviceId of Array.from(this._realtimeDeviceIds.keys())) {
|
|
933
|
-
const topic = `/v1/dev/${deviceId}/out`;
|
|
934
|
-
this._logger.debug(`Re-subscribing to ${topic}`);
|
|
935
|
-
await newConnection.subscribe(topic, import_aws_iot_device_sdk_v2.mqtt.QoS.AtLeastOnce, (_topic, payload) => {
|
|
936
|
-
this._processMqttMessage(payload);
|
|
937
|
-
});
|
|
938
|
-
}
|
|
939
|
-
this._logger.info("MQTT connection rebuilt successfully after interrupt storm or credentials expiration");
|
|
940
|
-
} catch (err) {
|
|
941
|
-
this._logger.error("Failed to re-subscribe after interrupt storm or credentials expiration", err);
|
|
942
|
-
}
|
|
943
|
-
} catch (error) {
|
|
944
|
-
this._logger.error("Error during MQTT reset", error);
|
|
945
|
-
} finally {
|
|
946
|
-
this._mqttResetInProgress = false;
|
|
947
|
-
}
|
|
1075
|
+
const isStorm = this._mqttInterrupts.length >= MqttInterruptThreshold;
|
|
1076
|
+
if (isStorm || areCredentialsExpired) {
|
|
1077
|
+
const reason = isStorm ? `High interrupt rate (${this._mqttInterrupts.length} in ${MqttInterruptWindow.asSeconds()}s)` : "Credentials expired";
|
|
1078
|
+
void this._resetMqttConnection(reason);
|
|
948
1079
|
}
|
|
949
1080
|
});
|
|
950
1081
|
connection.on("resume", async (returnCode, sessionPresent) => {
|
|
1082
|
+
if (generation !== this._mqttGeneration) return;
|
|
951
1083
|
this._logger.info(
|
|
952
1084
|
`MQTT resume returnCode=${returnCode} sessionPresent=${sessionPresent} clientId=${this._mqttClientId}`
|
|
953
1085
|
);
|
|
954
1086
|
if (!sessionPresent) {
|
|
955
|
-
this._logger.info("No session present, re-subscribing each device");
|
|
956
1087
|
try {
|
|
957
|
-
|
|
958
|
-
const topic = `/v1/dev/${deviceId}/out`;
|
|
959
|
-
this._logger.debug(`Re-subscribing to ${topic}`);
|
|
960
|
-
await connection.subscribe(topic, import_aws_iot_device_sdk_v2.mqtt.QoS.AtLeastOnce, (_topic, payload) => {
|
|
961
|
-
this._processMqttMessage(payload);
|
|
962
|
-
});
|
|
963
|
-
}
|
|
1088
|
+
await this._resubscribeAll(connection);
|
|
964
1089
|
} catch (err) {
|
|
965
1090
|
this._logger.error("Failed to re-subscribe after resume", err);
|
|
966
1091
|
}
|
|
967
1092
|
}
|
|
968
1093
|
});
|
|
969
1094
|
connection.on("error", (e) => {
|
|
1095
|
+
if (generation !== this._mqttGeneration) return;
|
|
970
1096
|
this._logger.error(`MQTT error (clientId=${this._mqttClientId})`, e);
|
|
971
1097
|
});
|
|
972
1098
|
connection.on("closed", () => {
|
|
1099
|
+
if (generation !== this._mqttGeneration) return;
|
|
973
1100
|
this._logger.info("MQTT connection closed");
|
|
1101
|
+
this._clearMqttStabilityTimer();
|
|
974
1102
|
this._mqttConnectionPromise = void 0;
|
|
975
1103
|
this._mqttCredentialsExpiration = void 0;
|
|
976
1104
|
});
|
|
977
1105
|
await connection.connect();
|
|
978
1106
|
return connection;
|
|
979
1107
|
}
|
|
1108
|
+
/**
|
|
1109
|
+
* Re-subscribes every device currently receiving real-time updates on the given connection.
|
|
1110
|
+
*
|
|
1111
|
+
* Safe to call on every (re)connect: aws-crt replaces the per-topic callback on a duplicate subscribe, so this never
|
|
1112
|
+
* registers a message handler more than once.
|
|
1113
|
+
*
|
|
1114
|
+
* @param connection - The connection to (re-)establish subscriptions on.
|
|
1115
|
+
*/
|
|
1116
|
+
async _resubscribeAll(connection) {
|
|
1117
|
+
for (const deviceId of Array.from(this._realtimeDeviceIds.keys())) {
|
|
1118
|
+
const topic = `/v1/dev/${deviceId}/out`;
|
|
1119
|
+
this._logger.debug(`Re-subscribing to ${topic}`);
|
|
1120
|
+
await connection.subscribe(topic, import_aws_iot_device_sdk_v2.mqtt.QoS.AtLeastOnce, (_topic, payload) => {
|
|
1121
|
+
this._processMqttMessage(payload);
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
const decoder = new TextDecoder("utf-8");
|
|
1125
|
+
for (const [filter, handler] of Array.from(this._rawTopicCaptures.entries())) {
|
|
1126
|
+
this._logger.debug(`Re-subscribing to raw topic filter '${filter}'`);
|
|
1127
|
+
try {
|
|
1128
|
+
await connection.subscribe(filter, import_aws_iot_device_sdk_v2.mqtt.QoS.AtLeastOnce, (topic, payload) => {
|
|
1129
|
+
handler(topic, decoder.decode(payload));
|
|
1130
|
+
});
|
|
1131
|
+
} catch (error) {
|
|
1132
|
+
this._logger.warn(`Failed to re-subscribe to raw topic filter '${filter}'`, error);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
/**
|
|
1137
|
+
* Arms (or re-arms) the stability timer. When a connection stays interrupt-free for {@link MqttStabilityWindow}, the
|
|
1138
|
+
* consecutive-reset backoff counter is cleared so a future isolated storm still gets a fast first reset.
|
|
1139
|
+
*
|
|
1140
|
+
* @param generation - The connection generation that armed the timer; the callback is a no-op if the connection has
|
|
1141
|
+
* since been replaced.
|
|
1142
|
+
*/
|
|
1143
|
+
_armMqttStabilityTimer(generation) {
|
|
1144
|
+
var _a, _b;
|
|
1145
|
+
this._clearMqttStabilityTimer();
|
|
1146
|
+
this._mqttStabilityTimer = setTimeout(() => {
|
|
1147
|
+
if (generation !== this._mqttGeneration) return;
|
|
1148
|
+
if (this._mqttConsecutiveResets > 0) {
|
|
1149
|
+
this._logger.debug("MQTT connection stable; clearing consecutive-reset counter");
|
|
1150
|
+
this._mqttConsecutiveResets = 0;
|
|
1151
|
+
}
|
|
1152
|
+
}, MqttStabilityWindow.asMilliseconds());
|
|
1153
|
+
(_b = (_a = this._mqttStabilityTimer).unref) == null ? void 0 : _b.call(_a);
|
|
1154
|
+
}
|
|
1155
|
+
/** Clears the stability timer, if armed. */
|
|
1156
|
+
_clearMqttStabilityTimer() {
|
|
1157
|
+
if (this._mqttStabilityTimer) {
|
|
1158
|
+
clearTimeout(this._mqttStabilityTimer);
|
|
1159
|
+
this._mqttStabilityTimer = void 0;
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
/**
|
|
1163
|
+
* Forcefully tears down the current MQTT connection and rebuilds it with a fresh client id and fresh credentials,
|
|
1164
|
+
* escaping interrupt storms that a plain native reconnect cannot.
|
|
1165
|
+
*
|
|
1166
|
+
* Repeatable and self-contained: only one reset runs at a time, consecutive resets without an intervening stable
|
|
1167
|
+
* period back off exponentially (up to {@link MqttResetMaxDelay}), and it never rejects — so it is safe to invoke
|
|
1168
|
+
* fire-and-forget from an event handler.
|
|
1169
|
+
*
|
|
1170
|
+
* @param reason - Human-readable reason for the reset, used in logs.
|
|
1171
|
+
*/
|
|
1172
|
+
async _resetMqttConnection(reason) {
|
|
1173
|
+
if (this._mqttResetInProgress) {
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
this._mqttResetInProgress = true;
|
|
1177
|
+
const connectionToClose = this._mqttConnectionPromise;
|
|
1178
|
+
try {
|
|
1179
|
+
this._mqttConsecutiveResets++;
|
|
1180
|
+
const delayMs = Math.min(
|
|
1181
|
+
MqttResetBaseDelay.asMilliseconds() * Math.pow(2, this._mqttConsecutiveResets - 1),
|
|
1182
|
+
MqttResetMaxDelay.asMilliseconds()
|
|
1183
|
+
);
|
|
1184
|
+
this._logger.warn(
|
|
1185
|
+
`${reason}. Forcing MQTT reset #${this._mqttConsecutiveResets} (new clientId, fresh credentials) after ${Math.round(delayMs)}ms...`
|
|
1186
|
+
);
|
|
1187
|
+
this._mqttClientId = void 0;
|
|
1188
|
+
this._mqttCredentialsExpiration = void 0;
|
|
1189
|
+
this._mqttInterrupts = [];
|
|
1190
|
+
this._clearMqttStabilityTimer();
|
|
1191
|
+
this._mqttConnectionPromise = void 0;
|
|
1192
|
+
if (connectionToClose) {
|
|
1193
|
+
try {
|
|
1194
|
+
const connection = await connectionToClose;
|
|
1195
|
+
connection.removeAllListeners();
|
|
1196
|
+
await connection.disconnect();
|
|
1197
|
+
} catch (err) {
|
|
1198
|
+
this._logger.debug("Error tearing down old MQTT connection during reset (ignored)", err);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
if (delayMs > 0) {
|
|
1202
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
1203
|
+
}
|
|
1204
|
+
const newConnection = await this._getMqttConnection();
|
|
1205
|
+
await this._resubscribeAll(newConnection);
|
|
1206
|
+
this._logger.info(`MQTT connection rebuilt successfully after reset #${this._mqttConsecutiveResets} (${reason})`);
|
|
1207
|
+
} catch (err) {
|
|
1208
|
+
this._logger.error("Failed to rebuild MQTT connection after reset", err);
|
|
1209
|
+
} finally {
|
|
1210
|
+
this._mqttResetInProgress = false;
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
980
1213
|
/**
|
|
981
1214
|
* Processes incoming MQTT messages and emits appropriate events.
|
|
982
1215
|
*
|
|
@@ -1012,7 +1245,6 @@ var MysaApiClient = class {
|
|
|
1012
1245
|
} else if (isMsgOutPayload(parsedPayload)) {
|
|
1013
1246
|
switch (parsedPayload.msg) {
|
|
1014
1247
|
case 30 /* DEVICE_AC_STATUS */:
|
|
1015
|
-
case 40 /* DEVICE_V2_STATUS */:
|
|
1016
1248
|
this.emitter.emit("statusChanged", {
|
|
1017
1249
|
deviceId: parsedPayload.src.ref,
|
|
1018
1250
|
temperature: parsedPayload.body.ambTemp,
|
|
@@ -1021,6 +1253,16 @@ var MysaApiClient = class {
|
|
|
1021
1253
|
dutyCycle: parsedPayload.body.dtyCycle
|
|
1022
1254
|
});
|
|
1023
1255
|
break;
|
|
1256
|
+
case 40 /* DEVICE_V2_STATUS */:
|
|
1257
|
+
this.emitter.emit("statusChanged", {
|
|
1258
|
+
deviceId: parsedPayload.src.ref,
|
|
1259
|
+
temperature: parsedPayload.body.ambTemp,
|
|
1260
|
+
humidity: parsedPayload.body.hum,
|
|
1261
|
+
setPoint: parsedPayload.body.stpt,
|
|
1262
|
+
dutyCycle: parsedPayload.body.dtyCycle ?? parsedPayload.body.heatStat,
|
|
1263
|
+
floorTemperature: parsedPayload.body.flrSnsrTemp
|
|
1264
|
+
});
|
|
1265
|
+
break;
|
|
1024
1266
|
case 44 /* DEVICE_STATE_CHANGE */: {
|
|
1025
1267
|
const modeMap = {
|
|
1026
1268
|
1: "off",
|
|
@@ -1030,18 +1272,11 @@ var MysaApiClient = class {
|
|
|
1030
1272
|
5: "fan_only",
|
|
1031
1273
|
6: "dry"
|
|
1032
1274
|
};
|
|
1033
|
-
const fanSpeedMap = {
|
|
1034
|
-
1: "auto",
|
|
1035
|
-
3: "low",
|
|
1036
|
-
5: "medium",
|
|
1037
|
-
7: "high",
|
|
1038
|
-
8: "max"
|
|
1039
|
-
};
|
|
1040
1275
|
this.emitter.emit("stateChanged", {
|
|
1041
1276
|
deviceId: parsedPayload.src.ref,
|
|
1042
1277
|
mode: parsedPayload.body.state.md ? modeMap[parsedPayload.body.state.md] : void 0,
|
|
1043
1278
|
setPoint: parsedPayload.body.state.sp,
|
|
1044
|
-
fanSpeed: parsedPayload.body.state.fn !== void 0 ?
|
|
1279
|
+
fanSpeed: parsedPayload.body.state.fn !== void 0 ? FanSpeedReceiveMap[parsedPayload.body.state.fn] : void 0
|
|
1045
1280
|
});
|
|
1046
1281
|
break;
|
|
1047
1282
|
}
|
|
@@ -1060,6 +1295,8 @@ var MysaApiClient = class {
|
|
|
1060
1295
|
MysaApiError,
|
|
1061
1296
|
OutMessageType,
|
|
1062
1297
|
UnauthenticatedError,
|
|
1298
|
+
UnknownDeviceError,
|
|
1299
|
+
UnsupportedFanSpeedError,
|
|
1063
1300
|
VoidLogger
|
|
1064
1301
|
});
|
|
1065
1302
|
//# sourceMappingURL=index.cjs.map
|