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.js
CHANGED
|
@@ -24,14 +24,24 @@ SOFTWARE.
|
|
|
24
24
|
|
|
25
25
|
// src/api/Errors.ts
|
|
26
26
|
var UnauthenticatedError = class extends Error {
|
|
27
|
+
/**
|
|
28
|
+
* The underlying failure that prevented authentication.
|
|
29
|
+
*
|
|
30
|
+
* Callers need it to tell a credential rejection apart from a transport or service failure, since both surface as
|
|
31
|
+
* this error. Declared explicitly rather than passed to `super` so the SDK keeps compiling against its current lib,
|
|
32
|
+
* which predates the two-argument `Error` constructor.
|
|
33
|
+
*/
|
|
34
|
+
cause;
|
|
27
35
|
/**
|
|
28
36
|
* Creates a new UnauthenticatedError instance.
|
|
29
37
|
*
|
|
30
38
|
* @param message - The error message
|
|
39
|
+
* @param cause - The underlying failure that prevented authentication, if any.
|
|
31
40
|
*/
|
|
32
|
-
constructor(message) {
|
|
41
|
+
constructor(message, cause) {
|
|
33
42
|
super(message);
|
|
34
43
|
this.name = "UnauthenticatedError";
|
|
44
|
+
this.cause = cause;
|
|
35
45
|
}
|
|
36
46
|
};
|
|
37
47
|
var MysaApiError = class extends Error {
|
|
@@ -53,6 +63,40 @@ var MysaApiError = class extends Error {
|
|
|
53
63
|
this.statusText = apiResponse.statusText;
|
|
54
64
|
}
|
|
55
65
|
};
|
|
66
|
+
var UnknownDeviceError = class extends Error {
|
|
67
|
+
/**
|
|
68
|
+
* Creates a new UnknownDeviceError instance.
|
|
69
|
+
*
|
|
70
|
+
* @param deviceId - The device id that could not be resolved
|
|
71
|
+
*/
|
|
72
|
+
constructor(deviceId) {
|
|
73
|
+
super(`Unknown device id '${deviceId}': no such device on this account.`);
|
|
74
|
+
this.deviceId = deviceId;
|
|
75
|
+
this.name = "UnknownDeviceError";
|
|
76
|
+
}
|
|
77
|
+
deviceId;
|
|
78
|
+
};
|
|
79
|
+
var UnsupportedFanSpeedError = class extends Error {
|
|
80
|
+
/**
|
|
81
|
+
* Creates a new UnsupportedFanSpeedError instance.
|
|
82
|
+
*
|
|
83
|
+
* @param deviceId - The id of the device the command was aimed at.
|
|
84
|
+
* @param fanSpeed - The requested fan speed that the device does not support.
|
|
85
|
+
* @param supportedFanSpeeds - The fan speeds the device does support.
|
|
86
|
+
*/
|
|
87
|
+
constructor(deviceId, fanSpeed, supportedFanSpeeds) {
|
|
88
|
+
super(
|
|
89
|
+
`Device '${deviceId}' does not support the '${fanSpeed}' fan speed. Supported fan speeds: ${supportedFanSpeeds.join(", ") || "(none)"}.`
|
|
90
|
+
);
|
|
91
|
+
this.deviceId = deviceId;
|
|
92
|
+
this.fanSpeed = fanSpeed;
|
|
93
|
+
this.supportedFanSpeeds = supportedFanSpeeds;
|
|
94
|
+
this.name = "UnsupportedFanSpeedError";
|
|
95
|
+
}
|
|
96
|
+
deviceId;
|
|
97
|
+
fanSpeed;
|
|
98
|
+
supportedFanSpeeds;
|
|
99
|
+
};
|
|
56
100
|
var MqttPublishError = class extends Error {
|
|
57
101
|
/**
|
|
58
102
|
* Creates a new MqttPublishError instance.
|
|
@@ -150,8 +194,9 @@ function parseMqttPayload(payload) {
|
|
|
150
194
|
const jsonString = decoder.decode(payload);
|
|
151
195
|
return JSON.parse(jsonString);
|
|
152
196
|
} catch (error) {
|
|
153
|
-
|
|
154
|
-
|
|
197
|
+
const parseError = new Error("Failed to parse MQTT payload");
|
|
198
|
+
parseError.cause = error;
|
|
199
|
+
throw parseError;
|
|
155
200
|
}
|
|
156
201
|
}
|
|
157
202
|
function serializeMqttPayload(payload) {
|
|
@@ -191,15 +236,7 @@ var OutMessageType = /* @__PURE__ */ ((OutMessageType2) => {
|
|
|
191
236
|
// src/api/MysaApiClient.ts
|
|
192
237
|
import { DescribeThingCommand, IoTClient } from "@aws-sdk/client-iot";
|
|
193
238
|
import { fromCognitoIdentityPool } from "@aws-sdk/credential-providers";
|
|
194
|
-
import {
|
|
195
|
-
AuthenticationDetails,
|
|
196
|
-
CognitoAccessToken,
|
|
197
|
-
CognitoIdToken,
|
|
198
|
-
CognitoRefreshToken,
|
|
199
|
-
CognitoUser,
|
|
200
|
-
CognitoUserPool,
|
|
201
|
-
CognitoUserSession
|
|
202
|
-
} from "amazon-cognito-identity-js";
|
|
239
|
+
import { AuthenticationDetails, CognitoUser, CognitoUserPool } from "amazon-cognito-identity-js";
|
|
203
240
|
import { auth, iot, mqtt } from "aws-iot-device-sdk-v2";
|
|
204
241
|
import { hash } from "crypto";
|
|
205
242
|
import dayjs from "dayjs";
|
|
@@ -216,11 +253,52 @@ var MqttEndpoint = "a3q27gia9qg3zy-ats.iot.us-east-1.amazonaws.com";
|
|
|
216
253
|
var MysaApiBaseUrl = "https://app-prod.mysa.cloud";
|
|
217
254
|
var RealtimeKeepAliveInterval = dayjs.duration(5, "minutes");
|
|
218
255
|
var PublishAckTimeout = dayjs.duration(30, "seconds");
|
|
256
|
+
var MqttInterruptWindow = dayjs.duration(30, "seconds");
|
|
257
|
+
var MqttInterruptThreshold = 3;
|
|
258
|
+
var MqttResetBaseDelay = dayjs.duration(1, "second");
|
|
259
|
+
var MqttResetMaxDelay = dayjs.duration(30, "seconds");
|
|
260
|
+
var MqttStabilityWindow = dayjs.duration(60, "seconds");
|
|
261
|
+
var CanonicalFanSpeedOrder = ["auto", "low", "medium", "high", "max"];
|
|
262
|
+
var LegacyFanSpeedSendMap = { auto: 1, low: 3, medium: 5, high: 7, max: 8 };
|
|
263
|
+
var FanSpeedReceiveMap = {
|
|
264
|
+
1: "auto",
|
|
265
|
+
2: "low",
|
|
266
|
+
// CodeNum=1117 canonical low
|
|
267
|
+
3: "low",
|
|
268
|
+
// legacy
|
|
269
|
+
4: "medium",
|
|
270
|
+
// CodeNum=1117 canonical medium
|
|
271
|
+
5: "medium",
|
|
272
|
+
// legacy
|
|
273
|
+
6: "high",
|
|
274
|
+
// CodeNum=1117 canonical high
|
|
275
|
+
7: "high",
|
|
276
|
+
// legacy
|
|
277
|
+
8: "max"
|
|
278
|
+
};
|
|
279
|
+
function buildFanSpeedSendMap(device) {
|
|
280
|
+
var _a;
|
|
281
|
+
const fanSpeeds = (_a = device.SupportedCaps) == null ? void 0 : _a.fanSpeeds;
|
|
282
|
+
if (!fanSpeeds || fanSpeeds.length === 0) {
|
|
283
|
+
return LegacyFanSpeedSendMap;
|
|
284
|
+
}
|
|
285
|
+
const map = {};
|
|
286
|
+
CanonicalFanSpeedOrder.forEach((name, index) => {
|
|
287
|
+
if (index < fanSpeeds.length) {
|
|
288
|
+
map[name] = fanSpeeds[index];
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
return map;
|
|
292
|
+
}
|
|
219
293
|
var MysaApiClient = class {
|
|
294
|
+
/** The credentials of the Mysa account this client authenticates as. */
|
|
295
|
+
_credentials;
|
|
220
296
|
/** The current session object, if any. */
|
|
221
297
|
_cognitoUserSession;
|
|
222
298
|
/** The current user object, if any. */
|
|
223
299
|
_cognitoUser;
|
|
300
|
+
/** The in-flight session acquisition, if any, so that concurrent callers share a single refresh or login. */
|
|
301
|
+
_freshSessionPromise;
|
|
224
302
|
/** The logger instance used by the client. */
|
|
225
303
|
_logger;
|
|
226
304
|
/** The fetcher function used by the client. */
|
|
@@ -235,8 +313,21 @@ var MysaApiClient = class {
|
|
|
235
313
|
_mqttInterrupts = [];
|
|
236
314
|
/** Whether a forced MQTT reset is currently in progress (guards against re-entrancy). */
|
|
237
315
|
_mqttResetInProgress = false;
|
|
316
|
+
/** Monotonic id of the current MQTT connection; used to ignore events from discarded connections. */
|
|
317
|
+
_mqttGeneration = 0;
|
|
318
|
+
/** Consecutive forced resets without an intervening stable period; drives reset backoff. */
|
|
319
|
+
_mqttConsecutiveResets = 0;
|
|
320
|
+
/** Timestamp (ms) of the most recent successful MQTT (re)connect, for interrupt dwell-time diagnostics. */
|
|
321
|
+
_mqttLastConnectionSuccessAt;
|
|
322
|
+
/** Timer that clears the consecutive-reset counter once a connection stays healthy. */
|
|
323
|
+
_mqttStabilityTimer;
|
|
238
324
|
/** The device IDs that are currently being updated in real-time, mapped to their respective timeouts. */
|
|
239
325
|
_realtimeDeviceIds = /* @__PURE__ */ new Map();
|
|
326
|
+
/**
|
|
327
|
+
* Raw topic filters registered via {@link startRawTopicCapture}, mapped to their message handlers. Re-subscribed on
|
|
328
|
+
* every reconnect so a debug capture survives connection resets.
|
|
329
|
+
*/
|
|
330
|
+
_rawTopicCaptures = /* @__PURE__ */ new Map();
|
|
240
331
|
/** The cached devices object, if any. */
|
|
241
332
|
_cachedDevices;
|
|
242
333
|
/**
|
|
@@ -245,89 +336,63 @@ var MysaApiClient = class {
|
|
|
245
336
|
* @see {@link MysaApiClientEventTypes} for the possible events and their payloads.
|
|
246
337
|
*/
|
|
247
338
|
emitter = new EventEmitter();
|
|
248
|
-
/**
|
|
249
|
-
* Gets the persistable session object.
|
|
250
|
-
*
|
|
251
|
-
* @returns The current persistable session object, if any.
|
|
252
|
-
*/
|
|
253
|
-
get session() {
|
|
254
|
-
if (!this._cognitoUserSession || !this._cognitoUser) {
|
|
255
|
-
return void 0;
|
|
256
|
-
}
|
|
257
|
-
return {
|
|
258
|
-
username: this._cognitoUser.getUsername(),
|
|
259
|
-
idToken: this._cognitoUserSession.getIdToken().getJwtToken(),
|
|
260
|
-
accessToken: this._cognitoUserSession.getAccessToken().getJwtToken(),
|
|
261
|
-
refreshToken: this._cognitoUserSession.getRefreshToken().getToken()
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
|
-
/**
|
|
265
|
-
* Returns whether the client currently has an active session.
|
|
266
|
-
*
|
|
267
|
-
* @returns True if the client has an active session, false otherwise.
|
|
268
|
-
*/
|
|
269
|
-
get isAuthenticated() {
|
|
270
|
-
return !!this.session;
|
|
271
|
-
}
|
|
272
339
|
/**
|
|
273
340
|
* Constructs a new instance of the MysaApiClient.
|
|
274
341
|
*
|
|
275
|
-
* @param
|
|
342
|
+
* @param credentials - The credentials of the Mysa account to authenticate as.
|
|
276
343
|
* @param options - The options for the client.
|
|
277
344
|
*/
|
|
278
|
-
constructor(
|
|
345
|
+
constructor(credentials, options) {
|
|
346
|
+
this._credentials = credentials;
|
|
279
347
|
this._logger = (options == null ? void 0 : options.logger) || new VoidLogger();
|
|
280
348
|
this._fetcher = (options == null ? void 0 : options.fetcher) || fetch;
|
|
281
|
-
if (session) {
|
|
282
|
-
this._cognitoUser = new CognitoUser({
|
|
283
|
-
Username: session.username,
|
|
284
|
-
Pool: new CognitoUserPool({ UserPoolId: CognitoUserPoolId, ClientId: CognitoClientId })
|
|
285
|
-
});
|
|
286
|
-
this._cognitoUserSession = new CognitoUserSession({
|
|
287
|
-
IdToken: new CognitoIdToken({ IdToken: session.idToken }),
|
|
288
|
-
AccessToken: new CognitoAccessToken({ AccessToken: session.accessToken }),
|
|
289
|
-
RefreshToken: new CognitoRefreshToken({ RefreshToken: session.refreshToken })
|
|
290
|
-
});
|
|
291
|
-
}
|
|
292
349
|
}
|
|
293
350
|
/**
|
|
294
|
-
*
|
|
351
|
+
* Ensures the client has a usable session, logging in with the credentials it was constructed with if needed.
|
|
295
352
|
*
|
|
296
|
-
*
|
|
297
|
-
*
|
|
353
|
+
* Calling this method is optional: the client authenticates on demand before its first API call, and re-authenticates
|
|
354
|
+
* on its own whenever its session can no longer be refreshed. Call it explicitly at startup to fail fast on invalid
|
|
355
|
+
* credentials instead of on the first API call. It is a no-op when the current session is still usable.
|
|
298
356
|
*
|
|
299
357
|
* @example
|
|
300
358
|
*
|
|
301
359
|
* ```typescript
|
|
302
360
|
* try {
|
|
303
|
-
* await client.login(
|
|
361
|
+
* await client.login();
|
|
304
362
|
* console.log('Login successful!');
|
|
305
363
|
* } catch (error) {
|
|
306
364
|
* console.error('Login failed:', error.message);
|
|
307
365
|
* }
|
|
308
366
|
* ```
|
|
309
367
|
*
|
|
310
|
-
* @
|
|
311
|
-
|
|
368
|
+
* @throws {@link UnauthenticatedError} When authentication fails due to invalid credentials or network issues.
|
|
369
|
+
*/
|
|
370
|
+
async login() {
|
|
371
|
+
await this._getFreshSession();
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Authenticates with Mysa's Cognito user pool and replaces the current session, if any.
|
|
375
|
+
*
|
|
376
|
+
* @returns A promise that resolves to the newly established session.
|
|
312
377
|
* @throws {@link Error} When authentication fails due to invalid credentials or network issues.
|
|
313
378
|
*/
|
|
314
|
-
|
|
379
|
+
_login() {
|
|
315
380
|
this._cognitoUser = void 0;
|
|
316
381
|
this._cognitoUserSession = void 0;
|
|
317
382
|
this._mqttClientId = void 0;
|
|
318
383
|
this._mqttInterrupts = [];
|
|
319
|
-
this.
|
|
384
|
+
this._mqttConsecutiveResets = 0;
|
|
385
|
+
const { username, password } = this._credentials;
|
|
320
386
|
return new Promise((resolve, reject) => {
|
|
321
387
|
const user = new CognitoUser({
|
|
322
|
-
Username:
|
|
388
|
+
Username: username,
|
|
323
389
|
Pool: new CognitoUserPool({ UserPoolId: CognitoUserPoolId, ClientId: CognitoClientId })
|
|
324
390
|
});
|
|
325
|
-
user.authenticateUser(new AuthenticationDetails({ Username:
|
|
391
|
+
user.authenticateUser(new AuthenticationDetails({ Username: username, Password: password }), {
|
|
326
392
|
onSuccess: (session) => {
|
|
327
393
|
this._cognitoUser = user;
|
|
328
394
|
this._cognitoUserSession = session;
|
|
329
|
-
|
|
330
|
-
resolve();
|
|
395
|
+
resolve(session);
|
|
331
396
|
},
|
|
332
397
|
onFailure: (err) => {
|
|
333
398
|
reject(err);
|
|
@@ -352,7 +417,7 @@ var MysaApiClient = class {
|
|
|
352
417
|
*
|
|
353
418
|
* @returns A promise that resolves to the list of devices.
|
|
354
419
|
* @throws {@link MysaApiError} When the API request fails.
|
|
355
|
-
* @throws {@link UnauthenticatedError} When the
|
|
420
|
+
* @throws {@link UnauthenticatedError} When the client cannot authenticate with its credentials.
|
|
356
421
|
*/
|
|
357
422
|
async getDevices() {
|
|
358
423
|
this._logger.debug(`Fetching devices...`);
|
|
@@ -386,7 +451,7 @@ var MysaApiClient = class {
|
|
|
386
451
|
*
|
|
387
452
|
* @param deviceId - The ID of the device to get the serial number for.
|
|
388
453
|
* @returns A promise that resolves to the serial number, or undefined if not found.
|
|
389
|
-
* @throws {@link UnauthenticatedError} When the
|
|
454
|
+
* @throws {@link UnauthenticatedError} When the client cannot authenticate with its credentials.
|
|
390
455
|
*/
|
|
391
456
|
async getDeviceSerialNumber(deviceId) {
|
|
392
457
|
var _a;
|
|
@@ -424,7 +489,7 @@ var MysaApiClient = class {
|
|
|
424
489
|
*
|
|
425
490
|
* @returns A promise that resolves to the firmware information for all devices.
|
|
426
491
|
* @throws {@link MysaApiError} When the API request fails.
|
|
427
|
-
* @throws {@link UnauthenticatedError} When the
|
|
492
|
+
* @throws {@link UnauthenticatedError} When the client cannot authenticate with its credentials.
|
|
428
493
|
*/
|
|
429
494
|
async getDeviceFirmwares() {
|
|
430
495
|
this._logger.debug(`Fetching device firmwares...`);
|
|
@@ -444,7 +509,7 @@ var MysaApiClient = class {
|
|
|
444
509
|
*
|
|
445
510
|
* @returns A promise that resolves to the current state of all devices.
|
|
446
511
|
* @throws {@link MysaApiError} When the API request fails.
|
|
447
|
-
* @throws {@link UnauthenticatedError} When the
|
|
512
|
+
* @throws {@link UnauthenticatedError} When the client cannot authenticate with its credentials.
|
|
448
513
|
*/
|
|
449
514
|
async getDeviceStates() {
|
|
450
515
|
this._logger.debug(`Fetching device states...`);
|
|
@@ -464,7 +529,7 @@ var MysaApiClient = class {
|
|
|
464
529
|
*
|
|
465
530
|
* @returns A promise that resolves to the homes information.
|
|
466
531
|
* @throws {@link MysaApiError} When the API request fails.
|
|
467
|
-
* @throws {@link UnauthenticatedError} When the
|
|
532
|
+
* @throws {@link UnauthenticatedError} When the client cannot authenticate with its credentials.
|
|
468
533
|
*/
|
|
469
534
|
async getHomes() {
|
|
470
535
|
this._logger.debug(`Fetching homes...`);
|
|
@@ -506,29 +571,37 @@ var MysaApiClient = class {
|
|
|
506
571
|
* @param mode - The operating mode to set (one of MysaDeviceMode values, or undefined to leave unchanged).
|
|
507
572
|
* @param fanSpeed - The fan speed mode to set ('low', 'medium', 'high', 'max', 'auto', or undefined to leave
|
|
508
573
|
* unchanged).
|
|
509
|
-
* @throws {@link UnauthenticatedError} When the
|
|
574
|
+
* @throws {@link UnauthenticatedError} When the client cannot authenticate with its credentials.
|
|
575
|
+
* @throws {@link UnknownDeviceError} When the device id does not match any device on the account.
|
|
576
|
+
* @throws {@link UnsupportedFanSpeedError} When the requested fan speed is not supported by the device.
|
|
510
577
|
* @throws {@link Error} When MQTT connection or command sending fails.
|
|
511
578
|
*/
|
|
512
579
|
async setDeviceState(deviceId, setPoint, mode, fanSpeed) {
|
|
513
|
-
var _a;
|
|
514
580
|
this._logger.debug(`Setting device state for '${deviceId}'`);
|
|
515
581
|
if (!this._cachedDevices) {
|
|
516
582
|
this._cachedDevices = await this.getDevices();
|
|
517
583
|
}
|
|
584
|
+
if (!Object.prototype.hasOwnProperty.call(this._cachedDevices.DevicesObj, deviceId)) {
|
|
585
|
+
throw new UnknownDeviceError(deviceId);
|
|
586
|
+
}
|
|
518
587
|
const device = this._cachedDevices.DevicesObj[deviceId];
|
|
588
|
+
await this._getFreshSession();
|
|
519
589
|
this._logger.debug(`Initializing MQTT connection...`);
|
|
520
590
|
const mqttConnection = await this._getMqttConnection();
|
|
521
591
|
const now = dayjs();
|
|
522
592
|
this._logger.debug(`Sending request to set device state for '${deviceId}'...`);
|
|
523
593
|
const modeMap = { off: 1, auto: 2, heat: 3, cool: 4, fan_only: 5, dry: 6 };
|
|
524
|
-
const fanSpeedMap =
|
|
594
|
+
const fanSpeedMap = buildFanSpeedSendMap(device);
|
|
595
|
+
if (fanSpeed !== void 0 && fanSpeedMap[fanSpeed] === void 0) {
|
|
596
|
+
throw new UnsupportedFanSpeedError(deviceId, fanSpeed, Object.keys(fanSpeedMap));
|
|
597
|
+
}
|
|
525
598
|
const payload = serializeMqttPayload({
|
|
526
599
|
msg: 44 /* CHANGE_DEVICE_STATE */,
|
|
527
600
|
id: now.valueOf(),
|
|
528
601
|
time: now.unix(),
|
|
529
602
|
ver: "1.0",
|
|
530
603
|
src: {
|
|
531
|
-
ref:
|
|
604
|
+
ref: this._cognitoUser.getUsername(),
|
|
532
605
|
type: 100
|
|
533
606
|
},
|
|
534
607
|
dest: {
|
|
@@ -538,7 +611,7 @@ var MysaApiClient = class {
|
|
|
538
611
|
resp: 2,
|
|
539
612
|
body: {
|
|
540
613
|
ver: 1,
|
|
541
|
-
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,
|
|
614
|
+
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,
|
|
542
615
|
cmd: [
|
|
543
616
|
{
|
|
544
617
|
tm: -1,
|
|
@@ -642,37 +715,102 @@ var MysaApiClient = class {
|
|
|
642
715
|
clearInterval(timer);
|
|
643
716
|
this._realtimeDeviceIds.delete(deviceId);
|
|
644
717
|
}
|
|
718
|
+
/**
|
|
719
|
+
* Subscribes to raw MQTT topic filters and relays every message verbatim.
|
|
720
|
+
*
|
|
721
|
+
* Unlike {@link startRealtimeUpdates}, this performs no parsing, emits no typed events and sends no "start publishing"
|
|
722
|
+
* request to the device — it simply forwards the full message topic and the decoded UTF-8 payload of everything that
|
|
723
|
+
* arrives on the given filters. It exists to reverse-engineer device families the SDK does not model yet, most
|
|
724
|
+
* notably the AWS IoT Device Shadow protocol used by the central-HVAC ST-V1 thermostats, where both the topic (which
|
|
725
|
+
* shadow, and `accepted`/`rejected`/`delta`/`documents`) and the raw JSON body carry the information a new
|
|
726
|
+
* implementation needs.
|
|
727
|
+
*
|
|
728
|
+
* The capture is passive: the device only publishes to its shadow topics when something drives a change (the Mysa
|
|
729
|
+
* mobile app, a schedule, or the device itself), so exercise the thermostat while a capture is running.
|
|
730
|
+
*
|
|
731
|
+
* Registered filters are re-subscribed automatically after a reconnect.
|
|
732
|
+
*
|
|
733
|
+
* @param topicFilters - MQTT topic filters to subscribe to. Wildcards (`+`, `#`) are allowed, subject to the AWS IoT
|
|
734
|
+
* policy attached to the account's Cognito identity — a filter the policy forbids resolves with a non-zero
|
|
735
|
+
* `error_code`, which is logged rather than thrown so the remaining filters still subscribe.
|
|
736
|
+
* @param handler - Invoked with the full message topic and the decoded UTF-8 payload for every message received.
|
|
737
|
+
* @throws {@link Error} When the MQTT connection cannot be established.
|
|
738
|
+
*/
|
|
739
|
+
async startRawTopicCapture(topicFilters, handler) {
|
|
740
|
+
this._logger.info(`Starting raw topic capture for ${topicFilters.length} filter(s)`);
|
|
741
|
+
const connection = await this._getMqttConnection();
|
|
742
|
+
const decoder = new TextDecoder("utf-8");
|
|
743
|
+
for (const filter of topicFilters) {
|
|
744
|
+
this._rawTopicCaptures.set(filter, handler);
|
|
745
|
+
this._logger.debug(`Subscribing to raw topic filter '${filter}'...`);
|
|
746
|
+
try {
|
|
747
|
+
const result = await connection.subscribe(filter, mqtt.QoS.AtLeastOnce, (topic, payload) => {
|
|
748
|
+
handler(topic, decoder.decode(payload));
|
|
749
|
+
});
|
|
750
|
+
this._logger.debug(
|
|
751
|
+
`Raw subscribe to '${filter}' granted (topic='${result.topic}', qos=${result.qos}, error_code=${result.error_code ?? 0})`
|
|
752
|
+
);
|
|
753
|
+
} catch (error) {
|
|
754
|
+
this._logger.warn(`Failed to subscribe to raw topic filter '${filter}'`, error);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
645
758
|
/**
|
|
646
759
|
* Ensures a valid, non-expired session is available.
|
|
647
760
|
*
|
|
648
761
|
* This method checks if the current session is valid and not expired. If the session is expired, it automatically
|
|
649
|
-
* refreshes it using the refresh token.
|
|
762
|
+
* refreshes it using the refresh token. If there is no session yet, or if the refresh token has itself expired or
|
|
763
|
+
* been revoked, the client logs back in with its credentials.
|
|
764
|
+
*
|
|
765
|
+
* Concurrent callers share a single in-flight acquisition, so a burst of API calls never triggers more than one
|
|
766
|
+
* refresh or login.
|
|
650
767
|
*
|
|
651
768
|
* @returns A promise that resolves to a valid CognitoUserSession.
|
|
652
|
-
* @throws {@link UnauthenticatedError} When
|
|
769
|
+
* @throws {@link UnauthenticatedError} When neither refreshing nor logging back in succeeds.
|
|
653
770
|
*/
|
|
654
771
|
async _getFreshSession() {
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
}
|
|
658
|
-
if (this._cognitoUserSession.isValid() && dayjs.unix(this._cognitoUserSession.getIdToken().getExpiration()).isAfter()) {
|
|
772
|
+
var _a;
|
|
773
|
+
if (this._cognitoUser && ((_a = this._cognitoUserSession) == null ? void 0 : _a.isValid()) && dayjs.unix(this._cognitoUserSession.getIdToken().getExpiration()).isAfter()) {
|
|
659
774
|
this._logger.debug("Session is valid, no need to refresh");
|
|
660
|
-
return
|
|
775
|
+
return this._cognitoUserSession;
|
|
661
776
|
}
|
|
662
|
-
this.
|
|
663
|
-
|
|
664
|
-
this._cognitoUser.refreshSession(this._cognitoUserSession.getRefreshToken(), (error, session) => {
|
|
665
|
-
if (error) {
|
|
666
|
-
this._logger.error("Failed to refresh session:", error);
|
|
667
|
-
reject(new UnauthenticatedError("Unable to refresh the authentication session."));
|
|
668
|
-
} else {
|
|
669
|
-
this._logger.debug("Session refreshed successfully");
|
|
670
|
-
this._cognitoUserSession = session;
|
|
671
|
-
this.emitter.emit("sessionChanged", this.session);
|
|
672
|
-
resolve(session);
|
|
673
|
-
}
|
|
674
|
-
});
|
|
777
|
+
this._freshSessionPromise ??= this._acquireFreshSession().finally(() => {
|
|
778
|
+
this._freshSessionPromise = void 0;
|
|
675
779
|
});
|
|
780
|
+
return this._freshSessionPromise;
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* Refreshes the current session, falling back to a full login when it cannot be refreshed.
|
|
784
|
+
*
|
|
785
|
+
* @returns A promise that resolves to a valid CognitoUserSession.
|
|
786
|
+
* @throws {@link UnauthenticatedError} When logging back in fails.
|
|
787
|
+
*/
|
|
788
|
+
async _acquireFreshSession() {
|
|
789
|
+
if (this._cognitoUser && this._cognitoUserSession) {
|
|
790
|
+
this._logger.debug("Session is not valid or expired, refreshing...");
|
|
791
|
+
try {
|
|
792
|
+
return await new Promise((resolve, reject) => {
|
|
793
|
+
this._cognitoUser.refreshSession(this._cognitoUserSession.getRefreshToken(), (error, session) => {
|
|
794
|
+
if (error) {
|
|
795
|
+
reject(error);
|
|
796
|
+
} else {
|
|
797
|
+
this._logger.debug("Session refreshed successfully");
|
|
798
|
+
this._cognitoUserSession = session;
|
|
799
|
+
resolve(session);
|
|
800
|
+
}
|
|
801
|
+
});
|
|
802
|
+
});
|
|
803
|
+
} catch (error) {
|
|
804
|
+
this._logger.warn("Failed to refresh session, logging back in:", error);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
try {
|
|
808
|
+
this._logger.info("Logging in...");
|
|
809
|
+
return await this._login();
|
|
810
|
+
} catch (error) {
|
|
811
|
+
this._logger.error("Failed to log in:", error);
|
|
812
|
+
throw new UnauthenticatedError("Unable to establish an authentication session.", error);
|
|
813
|
+
}
|
|
676
814
|
}
|
|
677
815
|
/**
|
|
678
816
|
* Establishes and returns an MQTT connection for real-time communication.
|
|
@@ -814,11 +952,11 @@ var MysaApiClient = class {
|
|
|
814
952
|
throw new Error("MQTT credentials are already expired.");
|
|
815
953
|
}
|
|
816
954
|
if (!this._mqttClientId) {
|
|
817
|
-
const username = ((_a = this.
|
|
955
|
+
const username = ((_a = this._cognitoUser) == null ? void 0 : _a.getUsername()) ?? "anon";
|
|
818
956
|
const usernameHash = hash("sha1", username);
|
|
819
957
|
this._mqttClientId = `mysa-js-sdk-${usernameHash}-${process.pid}-${getRandomClientId()}`;
|
|
820
958
|
}
|
|
821
|
-
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(
|
|
959
|
+
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(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);
|
|
822
960
|
const config = builder.build();
|
|
823
961
|
config.websocket_handshake_transform = async (request, done) => {
|
|
824
962
|
try {
|
|
@@ -861,89 +999,174 @@ var MysaApiClient = class {
|
|
|
861
999
|
};
|
|
862
1000
|
const client = new mqtt.MqttClient();
|
|
863
1001
|
const connection = client.new_connection(config);
|
|
864
|
-
|
|
865
|
-
|
|
1002
|
+
const generation = ++this._mqttGeneration;
|
|
1003
|
+
connection.on("connect", (sessionPresent) => {
|
|
1004
|
+
if (generation !== this._mqttGeneration) return;
|
|
1005
|
+
this._logger.debug(`MQTT connect (clientId=${this._mqttClientId}, sessionPresent=${sessionPresent})`);
|
|
866
1006
|
});
|
|
867
|
-
connection.on("connection_success", () => {
|
|
868
|
-
|
|
1007
|
+
connection.on("connection_success", (result) => {
|
|
1008
|
+
if (generation !== this._mqttGeneration) return;
|
|
1009
|
+
this._mqttLastConnectionSuccessAt = Date.now();
|
|
1010
|
+
this._logger.debug(
|
|
1011
|
+
`MQTT connection_success (clientId=${this._mqttClientId}, sessionPresent=${result == null ? void 0 : result.session_present}, reasonCode=${result == null ? void 0 : result.reason_code})`
|
|
1012
|
+
);
|
|
1013
|
+
this._armMqttStabilityTimer(generation);
|
|
869
1014
|
});
|
|
870
1015
|
connection.on("connection_failure", (e) => {
|
|
1016
|
+
if (generation !== this._mqttGeneration) return;
|
|
871
1017
|
this._logger.error(`MQTT connection_failure (clientId=${this._mqttClientId})`, e);
|
|
872
1018
|
});
|
|
873
|
-
connection.on("interrupt",
|
|
1019
|
+
connection.on("interrupt", (e) => {
|
|
874
1020
|
var _a2;
|
|
875
|
-
|
|
1021
|
+
if (generation !== this._mqttGeneration) return;
|
|
1022
|
+
const dwellMs = this._mqttLastConnectionSuccessAt !== void 0 ? Date.now() - this._mqttLastConnectionSuccessAt : void 0;
|
|
1023
|
+
this._logger.warn(
|
|
1024
|
+
`MQTT interrupt (clientId=${this._mqttClientId}, dwellMs=${dwellMs ?? "n/a"}, generation=${generation})`,
|
|
1025
|
+
e
|
|
1026
|
+
);
|
|
1027
|
+
this._clearMqttStabilityTimer();
|
|
876
1028
|
const now = Date.now();
|
|
877
|
-
this._mqttInterrupts = this._mqttInterrupts.filter((t) => now - t <
|
|
1029
|
+
this._mqttInterrupts = this._mqttInterrupts.filter((t) => now - t < MqttInterruptWindow.asMilliseconds());
|
|
878
1030
|
this._mqttInterrupts.push(now);
|
|
879
1031
|
const areCredentialsExpired = !(((_a2 = this._mqttCredentialsExpiration) == null ? void 0 : _a2.isAfter(dayjs())) ?? false);
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
`High interrupt rate (${this._mqttInterrupts.length}/60s). Possible clientId collision. Regenerating clientId and resetting connection...`
|
|
885
|
-
);
|
|
886
|
-
} else {
|
|
887
|
-
this._logger.warn(`Credentials expired. Regenerating clientId and resetting connection...`);
|
|
888
|
-
}
|
|
889
|
-
this._mqttClientId = void 0;
|
|
890
|
-
this._mqttCredentialsExpiration = void 0;
|
|
891
|
-
this._mqttInterrupts = [];
|
|
892
|
-
this._mqttConnectionPromise = void 0;
|
|
893
|
-
try {
|
|
894
|
-
connection.removeAllListeners();
|
|
895
|
-
await connection.disconnect();
|
|
896
|
-
try {
|
|
897
|
-
this._logger.debug("Old MQTT connection disconnected; establishing new connection...");
|
|
898
|
-
const newConnection = await this._getMqttConnection();
|
|
899
|
-
for (const deviceId of Array.from(this._realtimeDeviceIds.keys())) {
|
|
900
|
-
const topic = `/v1/dev/${deviceId}/out`;
|
|
901
|
-
this._logger.debug(`Re-subscribing to ${topic}`);
|
|
902
|
-
await newConnection.subscribe(topic, mqtt.QoS.AtLeastOnce, (_topic, payload) => {
|
|
903
|
-
this._processMqttMessage(payload);
|
|
904
|
-
});
|
|
905
|
-
}
|
|
906
|
-
this._logger.info("MQTT connection rebuilt successfully after interrupt storm or credentials expiration");
|
|
907
|
-
} catch (err) {
|
|
908
|
-
this._logger.error("Failed to re-subscribe after interrupt storm or credentials expiration", err);
|
|
909
|
-
}
|
|
910
|
-
} catch (error) {
|
|
911
|
-
this._logger.error("Error during MQTT reset", error);
|
|
912
|
-
} finally {
|
|
913
|
-
this._mqttResetInProgress = false;
|
|
914
|
-
}
|
|
1032
|
+
const isStorm = this._mqttInterrupts.length >= MqttInterruptThreshold;
|
|
1033
|
+
if (isStorm || areCredentialsExpired) {
|
|
1034
|
+
const reason = isStorm ? `High interrupt rate (${this._mqttInterrupts.length} in ${MqttInterruptWindow.asSeconds()}s)` : "Credentials expired";
|
|
1035
|
+
void this._resetMqttConnection(reason);
|
|
915
1036
|
}
|
|
916
1037
|
});
|
|
917
1038
|
connection.on("resume", async (returnCode, sessionPresent) => {
|
|
1039
|
+
if (generation !== this._mqttGeneration) return;
|
|
918
1040
|
this._logger.info(
|
|
919
1041
|
`MQTT resume returnCode=${returnCode} sessionPresent=${sessionPresent} clientId=${this._mqttClientId}`
|
|
920
1042
|
);
|
|
921
1043
|
if (!sessionPresent) {
|
|
922
|
-
this._logger.info("No session present, re-subscribing each device");
|
|
923
1044
|
try {
|
|
924
|
-
|
|
925
|
-
const topic = `/v1/dev/${deviceId}/out`;
|
|
926
|
-
this._logger.debug(`Re-subscribing to ${topic}`);
|
|
927
|
-
await connection.subscribe(topic, mqtt.QoS.AtLeastOnce, (_topic, payload) => {
|
|
928
|
-
this._processMqttMessage(payload);
|
|
929
|
-
});
|
|
930
|
-
}
|
|
1045
|
+
await this._resubscribeAll(connection);
|
|
931
1046
|
} catch (err) {
|
|
932
1047
|
this._logger.error("Failed to re-subscribe after resume", err);
|
|
933
1048
|
}
|
|
934
1049
|
}
|
|
935
1050
|
});
|
|
936
1051
|
connection.on("error", (e) => {
|
|
1052
|
+
if (generation !== this._mqttGeneration) return;
|
|
937
1053
|
this._logger.error(`MQTT error (clientId=${this._mqttClientId})`, e);
|
|
938
1054
|
});
|
|
939
1055
|
connection.on("closed", () => {
|
|
1056
|
+
if (generation !== this._mqttGeneration) return;
|
|
940
1057
|
this._logger.info("MQTT connection closed");
|
|
1058
|
+
this._clearMqttStabilityTimer();
|
|
941
1059
|
this._mqttConnectionPromise = void 0;
|
|
942
1060
|
this._mqttCredentialsExpiration = void 0;
|
|
943
1061
|
});
|
|
944
1062
|
await connection.connect();
|
|
945
1063
|
return connection;
|
|
946
1064
|
}
|
|
1065
|
+
/**
|
|
1066
|
+
* Re-subscribes every device currently receiving real-time updates on the given connection.
|
|
1067
|
+
*
|
|
1068
|
+
* Safe to call on every (re)connect: aws-crt replaces the per-topic callback on a duplicate subscribe, so this never
|
|
1069
|
+
* registers a message handler more than once.
|
|
1070
|
+
*
|
|
1071
|
+
* @param connection - The connection to (re-)establish subscriptions on.
|
|
1072
|
+
*/
|
|
1073
|
+
async _resubscribeAll(connection) {
|
|
1074
|
+
for (const deviceId of Array.from(this._realtimeDeviceIds.keys())) {
|
|
1075
|
+
const topic = `/v1/dev/${deviceId}/out`;
|
|
1076
|
+
this._logger.debug(`Re-subscribing to ${topic}`);
|
|
1077
|
+
await connection.subscribe(topic, mqtt.QoS.AtLeastOnce, (_topic, payload) => {
|
|
1078
|
+
this._processMqttMessage(payload);
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
const decoder = new TextDecoder("utf-8");
|
|
1082
|
+
for (const [filter, handler] of Array.from(this._rawTopicCaptures.entries())) {
|
|
1083
|
+
this._logger.debug(`Re-subscribing to raw topic filter '${filter}'`);
|
|
1084
|
+
try {
|
|
1085
|
+
await connection.subscribe(filter, mqtt.QoS.AtLeastOnce, (topic, payload) => {
|
|
1086
|
+
handler(topic, decoder.decode(payload));
|
|
1087
|
+
});
|
|
1088
|
+
} catch (error) {
|
|
1089
|
+
this._logger.warn(`Failed to re-subscribe to raw topic filter '${filter}'`, error);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
/**
|
|
1094
|
+
* Arms (or re-arms) the stability timer. When a connection stays interrupt-free for {@link MqttStabilityWindow}, the
|
|
1095
|
+
* consecutive-reset backoff counter is cleared so a future isolated storm still gets a fast first reset.
|
|
1096
|
+
*
|
|
1097
|
+
* @param generation - The connection generation that armed the timer; the callback is a no-op if the connection has
|
|
1098
|
+
* since been replaced.
|
|
1099
|
+
*/
|
|
1100
|
+
_armMqttStabilityTimer(generation) {
|
|
1101
|
+
var _a, _b;
|
|
1102
|
+
this._clearMqttStabilityTimer();
|
|
1103
|
+
this._mqttStabilityTimer = setTimeout(() => {
|
|
1104
|
+
if (generation !== this._mqttGeneration) return;
|
|
1105
|
+
if (this._mqttConsecutiveResets > 0) {
|
|
1106
|
+
this._logger.debug("MQTT connection stable; clearing consecutive-reset counter");
|
|
1107
|
+
this._mqttConsecutiveResets = 0;
|
|
1108
|
+
}
|
|
1109
|
+
}, MqttStabilityWindow.asMilliseconds());
|
|
1110
|
+
(_b = (_a = this._mqttStabilityTimer).unref) == null ? void 0 : _b.call(_a);
|
|
1111
|
+
}
|
|
1112
|
+
/** Clears the stability timer, if armed. */
|
|
1113
|
+
_clearMqttStabilityTimer() {
|
|
1114
|
+
if (this._mqttStabilityTimer) {
|
|
1115
|
+
clearTimeout(this._mqttStabilityTimer);
|
|
1116
|
+
this._mqttStabilityTimer = void 0;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
/**
|
|
1120
|
+
* Forcefully tears down the current MQTT connection and rebuilds it with a fresh client id and fresh credentials,
|
|
1121
|
+
* escaping interrupt storms that a plain native reconnect cannot.
|
|
1122
|
+
*
|
|
1123
|
+
* Repeatable and self-contained: only one reset runs at a time, consecutive resets without an intervening stable
|
|
1124
|
+
* period back off exponentially (up to {@link MqttResetMaxDelay}), and it never rejects — so it is safe to invoke
|
|
1125
|
+
* fire-and-forget from an event handler.
|
|
1126
|
+
*
|
|
1127
|
+
* @param reason - Human-readable reason for the reset, used in logs.
|
|
1128
|
+
*/
|
|
1129
|
+
async _resetMqttConnection(reason) {
|
|
1130
|
+
if (this._mqttResetInProgress) {
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
this._mqttResetInProgress = true;
|
|
1134
|
+
const connectionToClose = this._mqttConnectionPromise;
|
|
1135
|
+
try {
|
|
1136
|
+
this._mqttConsecutiveResets++;
|
|
1137
|
+
const delayMs = Math.min(
|
|
1138
|
+
MqttResetBaseDelay.asMilliseconds() * Math.pow(2, this._mqttConsecutiveResets - 1),
|
|
1139
|
+
MqttResetMaxDelay.asMilliseconds()
|
|
1140
|
+
);
|
|
1141
|
+
this._logger.warn(
|
|
1142
|
+
`${reason}. Forcing MQTT reset #${this._mqttConsecutiveResets} (new clientId, fresh credentials) after ${Math.round(delayMs)}ms...`
|
|
1143
|
+
);
|
|
1144
|
+
this._mqttClientId = void 0;
|
|
1145
|
+
this._mqttCredentialsExpiration = void 0;
|
|
1146
|
+
this._mqttInterrupts = [];
|
|
1147
|
+
this._clearMqttStabilityTimer();
|
|
1148
|
+
this._mqttConnectionPromise = void 0;
|
|
1149
|
+
if (connectionToClose) {
|
|
1150
|
+
try {
|
|
1151
|
+
const connection = await connectionToClose;
|
|
1152
|
+
connection.removeAllListeners();
|
|
1153
|
+
await connection.disconnect();
|
|
1154
|
+
} catch (err) {
|
|
1155
|
+
this._logger.debug("Error tearing down old MQTT connection during reset (ignored)", err);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
if (delayMs > 0) {
|
|
1159
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
1160
|
+
}
|
|
1161
|
+
const newConnection = await this._getMqttConnection();
|
|
1162
|
+
await this._resubscribeAll(newConnection);
|
|
1163
|
+
this._logger.info(`MQTT connection rebuilt successfully after reset #${this._mqttConsecutiveResets} (${reason})`);
|
|
1164
|
+
} catch (err) {
|
|
1165
|
+
this._logger.error("Failed to rebuild MQTT connection after reset", err);
|
|
1166
|
+
} finally {
|
|
1167
|
+
this._mqttResetInProgress = false;
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
947
1170
|
/**
|
|
948
1171
|
* Processes incoming MQTT messages and emits appropriate events.
|
|
949
1172
|
*
|
|
@@ -979,7 +1202,6 @@ var MysaApiClient = class {
|
|
|
979
1202
|
} else if (isMsgOutPayload(parsedPayload)) {
|
|
980
1203
|
switch (parsedPayload.msg) {
|
|
981
1204
|
case 30 /* DEVICE_AC_STATUS */:
|
|
982
|
-
case 40 /* DEVICE_V2_STATUS */:
|
|
983
1205
|
this.emitter.emit("statusChanged", {
|
|
984
1206
|
deviceId: parsedPayload.src.ref,
|
|
985
1207
|
temperature: parsedPayload.body.ambTemp,
|
|
@@ -988,6 +1210,16 @@ var MysaApiClient = class {
|
|
|
988
1210
|
dutyCycle: parsedPayload.body.dtyCycle
|
|
989
1211
|
});
|
|
990
1212
|
break;
|
|
1213
|
+
case 40 /* DEVICE_V2_STATUS */:
|
|
1214
|
+
this.emitter.emit("statusChanged", {
|
|
1215
|
+
deviceId: parsedPayload.src.ref,
|
|
1216
|
+
temperature: parsedPayload.body.ambTemp,
|
|
1217
|
+
humidity: parsedPayload.body.hum,
|
|
1218
|
+
setPoint: parsedPayload.body.stpt,
|
|
1219
|
+
dutyCycle: parsedPayload.body.dtyCycle ?? parsedPayload.body.heatStat,
|
|
1220
|
+
floorTemperature: parsedPayload.body.flrSnsrTemp
|
|
1221
|
+
});
|
|
1222
|
+
break;
|
|
991
1223
|
case 44 /* DEVICE_STATE_CHANGE */: {
|
|
992
1224
|
const modeMap = {
|
|
993
1225
|
1: "off",
|
|
@@ -997,18 +1229,11 @@ var MysaApiClient = class {
|
|
|
997
1229
|
5: "fan_only",
|
|
998
1230
|
6: "dry"
|
|
999
1231
|
};
|
|
1000
|
-
const fanSpeedMap = {
|
|
1001
|
-
1: "auto",
|
|
1002
|
-
3: "low",
|
|
1003
|
-
5: "medium",
|
|
1004
|
-
7: "high",
|
|
1005
|
-
8: "max"
|
|
1006
|
-
};
|
|
1007
1232
|
this.emitter.emit("stateChanged", {
|
|
1008
1233
|
deviceId: parsedPayload.src.ref,
|
|
1009
1234
|
mode: parsedPayload.body.state.md ? modeMap[parsedPayload.body.state.md] : void 0,
|
|
1010
1235
|
setPoint: parsedPayload.body.state.sp,
|
|
1011
|
-
fanSpeed: parsedPayload.body.state.fn !== void 0 ?
|
|
1236
|
+
fanSpeed: parsedPayload.body.state.fn !== void 0 ? FanSpeedReceiveMap[parsedPayload.body.state.fn] : void 0
|
|
1012
1237
|
});
|
|
1013
1238
|
break;
|
|
1014
1239
|
}
|
|
@@ -1026,6 +1251,8 @@ export {
|
|
|
1026
1251
|
MysaApiError,
|
|
1027
1252
|
OutMessageType,
|
|
1028
1253
|
UnauthenticatedError,
|
|
1254
|
+
UnknownDeviceError,
|
|
1255
|
+
UnsupportedFanSpeedError,
|
|
1029
1256
|
VoidLogger
|
|
1030
1257
|
};
|
|
1031
1258
|
//# sourceMappingURL=index.js.map
|