38 Commits

Author SHA1 Message Date
Pascal Bourque
598edf50d9 fix(example): Provide error object as first arg to rootLogger.error() (#143) 2025-10-05 11:04:22 -04:00
Pascal Bourque
ad34fe7486 style: Fixed lint errors introduced by all-contributors bot (#142) 2025-10-05 10:54:48 -04:00
allcontributors[bot]
daed17753e docs: add jagmandan as a contributor for code (#140)
Adds @jagmandan as a contributor for code.

This was requested by bourquep [in this
comment](https://github.com/bourquep/mysa-js-sdk/pull/139#issuecomment-3369095675)

[skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2025-10-05 10:45:23 -04:00
jagmandan
b845fe5a82 fix: Unable to control BB-V2-0 thermostats (#139)
This change was identified while investigating why my BB-V2-0 Mysa would
not respond to device state commands from Home Assistant (using
mysq2mqtt). The id type was previously in unix time (seconds), but
should be in milliseconds per the reference here:
https://github.com/dlenski/mysotherm/blob/main/mysa_messages.md Making
this update corrected the behavior and commands now work successfully.
2025-10-05 10:44:34 -04:00
Pascal Bourque
bb876ef60d ci: Add CodeQL analysis workflow configuration (#141) 2025-10-05 10:41:33 -04:00
dependabot[bot]
2aa7bd1679 chore(deps): Bump @aws-sdk/client-iot from 3.835.0 to 3.901.0 (#136) 2025-10-05 14:20:52 +00:00
dependabot[bot]
df16d2553b chore(deps-dev): Bump conventional-changelog-conventionalcommits from 9.0.0 to 9.1.0 (#98) 2025-10-05 14:18:26 +00:00
dependabot[bot]
55ec9a8fe9 chore(deps): Bump axios from 1.9.0 to 1.12.1 in the npm_and_yarn group across 1 directory (#114) 2025-10-05 14:18:11 +00:00
dependabot[bot]
77e972bde2 chore(deps-dev): Bump typedoc from 0.28.11 to 0.28.13 (#115) 2025-10-05 14:17:59 +00:00
dependabot[bot]
ed8a83f89b chore(deps): Bump @aws-sdk/credential-providers from 3.876.0 to 3.901.0 (#137) 2025-10-05 14:17:33 +00:00
dependabot[bot]
aa6ed44a19 chore(deps-dev): Bump pino from 9.7.0 to 9.13.0 (#138) 2025-10-05 14:17:13 +00:00
Pascal Bourque
98003665b8 fix: Build error after TypeScript update (#95) 2025-08-29 08:14:35 -04:00
dependabot[bot]
7afec1a7a9 chore(deps-dev): Bump the dev-dependencies group across 1 directory with 10 updates (#78) 2025-08-29 12:02:56 +00:00
dependabot[bot]
e6631b0fd8 chore(deps): Bump form-data from 4.0.2 to 4.0.4 in the npm_and_yarn group (#69) 2025-08-29 11:56:32 +00:00
dependabot[bot]
efaf3310d2 chore(deps-dev): Bump dotenv from 16.5.0 to 17.2.1 (#71) 2025-08-29 11:56:16 +00:00
dependabot[bot]
a62b538c42 chore(deps-dev): Bump typedoc from 0.28.5 to 0.28.11 (#91) 2025-08-29 11:55:28 +00:00
dependabot[bot]
2023e8b321 chore(deps-dev): Bump @eslint/js from 9.29.0 to 9.34.0 (#92) 2025-08-29 11:55:17 +00:00
dependabot[bot]
808e8f1037 chore(deps-dev): Bump typescript-eslint from 8.35.0 to 8.41.0 (#93) 2025-08-29 11:55:02 +00:00
dependabot[bot]
f201c7944a chore(deps): Bump @aws-sdk/credential-providers from 3.835.0 to 3.876.0 (#94) 2025-08-29 11:54:39 +00:00
dependabot[bot]
f6c6127dab chore(deps): Bump @aws-sdk/credential-providers from 3.830.0 to 3.835.0 (#42) 2025-06-25 14:44:42 +00:00
dependabot[bot]
73cec9a90e chore(deps): Bump aws-iot-device-sdk-v2 from 1.21.5 to 1.22.0 (#40) 2025-06-25 14:41:32 +00:00
dependabot[bot]
39fc9048df chore(deps-dev): Bump typescript-eslint from 8.34.0 to 8.35.0 (#43) 2025-06-25 14:38:35 +00:00
dependabot[bot]
45d69453df chore(deps): Bump @aws-sdk/client-iot from 3.830.0 to 3.835.0 (#44) 2025-06-25 14:38:21 +00:00
Pascal Bourque
6dc6da2dde chore(deps): Updated brace-expansion to fix CVE-2025-5889 (#37) 2025-06-21 10:58:17 -04:00
Pascal Bourque
0cf7a1756c feat(example): Ability to output raw data from the thermostats (#36) 2025-06-21 10:33:08 -04:00
dependabot[bot]
51b8f64dab chore(deps): Bump @aws-sdk/client-iot from 3.826.0 to 3.830.0 (#33) 2025-06-21 13:36:28 +00:00
dependabot[bot]
14ee1d30eb chore(deps-dev): Bump tsx from 4.19.4 to 4.20.3 (#30) 2025-06-21 13:35:23 +00:00
dependabot[bot]
4680ca2f85 chore(deps-dev): Bump @eslint/js from 9.28.0 to 9.29.0 (#32) 2025-06-21 13:35:06 +00:00
dependabot[bot]
3f020d5dc3 chore(deps): Bump @aws-sdk/credential-providers from 3.826.0 to 3.830.0 (#34) 2025-06-21 13:33:25 +00:00
dependabot[bot]
e93741525c chore(deps-dev): Bump the dev-dependencies group across 1 directory with 4 updates (#35) 2025-06-21 13:32:47 +00:00
dependabot[bot]
9b945869aa chore(deps-dev): Bump @types/node from 22.15.30 to 24.0.0 in the dev-dependencies group (#22) 2025-06-10 11:41:22 +00:00
dependabot[bot]
becafbfacc chore(deps-dev): Bump typescript-eslint from 8.33.1 to 8.34.0 (#23) 2025-06-10 11:41:11 +00:00
dependabot[bot]
c29e750f9c chore(deps): Bump @aws-sdk/client-iot from 3.825.0 to 3.826.0 (#20) 2025-06-09 12:34:40 +00:00
dependabot[bot]
e3c93453ed chore(deps): Bump @aws-sdk/credential-providers from 3.825.0 to 3.826.0 (#21) 2025-06-09 12:31:41 +00:00
Pascal Bourque
17f277e844 docs: Added missing TSDoc comments (#19) 2025-06-07 10:08:05 -04:00
Pascal Bourque
6a88e52702 feat: Added getDeviceSerialNumber() API (#18) 2025-06-07 09:57:28 -04:00
dependabot[bot]
d6971453d2 chore(deps-dev): Bump @types/node from 22.15.29 to 22.15.30 in the dev-dependencies group (#16) 2025-06-06 12:21:38 +00:00
dependabot[bot]
8769928622 chore(deps): Bump @aws-sdk/credential-providers from 3.823.0 to 3.825.0 (#17) 2025-06-06 12:21:26 +00:00
8 changed files with 1474 additions and 1116 deletions

22
.all-contributorsrc Normal file
View File

@@ -0,0 +1,22 @@
{
"files": ["README.md"],
"imageSize": 100,
"commit": false,
"commitType": "docs",
"commitConvention": "angular",
"contributors": [
{
"login": "jagmandan",
"name": "jagmandan",
"avatar_url": "https://avatars.githubusercontent.com/u/227265405?v=4",
"profile": "https://github.com/jagmandan",
"contributions": ["code"]
}
],
"contributorsPerLine": 7,
"skipCi": true,
"repoType": "github",
"repoHost": "https://github.com",
"projectName": "mysa-js-sdk",
"projectOwner": "bourquep"
}

100
.github/workflows/codeql.yml vendored Normal file
View File

@@ -0,0 +1,100 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: 'CodeQL Advanced'
on:
push:
branches: ['main']
pull_request:
branches: ['main']
schedule:
- cron: '44 5 * * 5'
jobs:
analyze:
name: Analyze (${{ matrix.language }})
# Runner size impacts CodeQL analysis time. To learn more, please see:
# - https://gh.io/recommended-hardware-resources-for-running-codeql
# - https://gh.io/supported-runners-and-hardware-resources
# - https://gh.io/using-larger-runners (GitHub.com only)
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
permissions:
# required for all workflows
security-events: write
# required to fetch internal or private CodeQL packs
packages: read
# only required for workflows in private repositories
actions: read
contents: read
strategy:
fail-fast: false
matrix:
include:
- language: actions
build-mode: none
- language: javascript-typescript
build-mode: none
# CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift'
# Use `c-cpp` to analyze code written in C, C++ or both
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Add any setup steps before running the `github/codeql-action/init` action.
# This includes steps like installing compilers or runtimes (`actions/setup-node`
# or others). This is typically only required for manual builds.
# - name: Setup runtime (example)
# uses: actions/setup-example@v1
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# If the analyze step fails for one of the languages you are analyzing with
# "We were unable to automatically build your code", modify the matrix above
# to set the build mode to "manual" for that language. Then modify this step
# to build your code.
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
- if: matrix.build-mode == 'manual'
shell: bash
run: |
echo 'If you are using a "manual" build mode for one or more of the' \
'languages you are analyzing, replace this with the commands to build' \
'your code, for example:'
echo ' make bootstrap'
echo ' make release'
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: '/language:${{matrix.language}}'

View File

@@ -1,5 +1,11 @@
# Mysa Smart Thermostat JavaScript SDK
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
[![All Contributors](https://img.shields.io/badge/all_contributors-1-orange.svg?style=flat-square)](#contributors-)
<!-- ALL-CONTRIBUTORS-BADGE:END -->
[![NPM Version](https://img.shields.io/npm/v/mysa-js-sdk)](https://www.npmjs.com/package/mysa-js-sdk)
[![CodeQL](https://github.com/bourquep/mysa-js-sdk/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/bourquep/mysa-js-sdk/actions/workflows/github-code-scanning/codeql)
[![CI: lint, build and release](https://github.com/bourquep/mysa-js-sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/bourquep/mysa-js-sdk/actions/workflows/ci.yml)
@@ -51,6 +57,12 @@ Then, run the example:
npm run example
```
If you prefer to see the raw data published by your Mysa smart thermostats, run:
```bash
npm run example:raw
```
## Using
The Mysa SDK provides a simple interface to interact with Mysa smart thermostats.
@@ -223,3 +235,26 @@ For general questions and discussions, join our [Discussion Forum](https://githu
This library would not be possible without the amazing work by [@dlenski](https://github.com/dlenski) in his
[mysotherm](https://github.com/dlenski/mysotherm) repository. He's the one who reversed-engineered the Mysa MQTT
protocol which is being used by this library.
## Contributors ✨
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tbody>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jagmandan"><img src="https://avatars.githubusercontent.com/u/227265405?v=4?s=100" width="100px;" alt="jagmandan"/><br /><sub><b>jagmandan</b></sub></a><br /><a href="https://github.com/bourquep/mysa-js-sdk/commits?author=jagmandan" title="Code">💻</a></td>
</tr>
</tbody>
</table>
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification.
Contributions of any kind welcome!

View File

@@ -57,6 +57,11 @@ async function main() {
const devices = await client.getDevices();
if (process.env.MYSA_OUTPUT_RAW_DATA === 'true') {
client.emitter.on('rawRealtimeMessageReceived', (data) => {
rootLogger.info(data, 'Raw message received');
});
} else {
client.emitter.on('statusChanged', (status) => {
try {
const device = devices.DevicesObj[status.deviceId];
@@ -65,7 +70,7 @@ async function main() {
`'${device.Name}' status changed: ${status.temperature}°C, ${status.humidity}%, ${watts ?? 'na'}W`
);
} catch (error) {
rootLogger.error(`Error processing status update for device '${status.deviceId}':`, error);
rootLogger.error(error, `Error processing status update for device '${status.deviceId}'`);
}
});
@@ -74,7 +79,7 @@ async function main() {
const device = devices.DevicesObj[change.deviceId];
rootLogger.info(`'${device.Name}' setpoint changed from ${change.previousSetPoint} to ${change.newSetPoint}`);
} catch (error) {
rootLogger.error(`Error processing setpoint update for device '${change.deviceId}':`, error);
rootLogger.error(error, `Error processing setpoint update for device '${change.deviceId}'`);
}
});
@@ -83,11 +88,15 @@ async function main() {
const device = devices.DevicesObj[change.deviceId];
rootLogger.info(change, `'${device.Name}' state changed.`);
} catch (error) {
rootLogger.error(`Error processing setpoint update for device '${change.deviceId}':`, error);
rootLogger.error(error, `Error processing state update for device '${change.deviceId}'`);
}
});
}
for (const device of Object.entries(devices.DevicesObj)) {
const serial = await client.getDeviceSerialNumber(device[0]);
rootLogger.info(`Serial number for device '${device[0]}' (${device[1].Name}): ${serial}`);
await client.startRealtimeUpdates(device[0]);
}
}

2150
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -40,39 +40,44 @@
"browser": false,
"scripts": {
"example": "tsx --watch ./example/main.ts",
"example:raw": "MYSA_OUTPUT_RAW_DATA=true tsx --watch ./example/main.ts",
"lint": "eslint --max-warnings 0 src/**/*.ts",
"style-lint": "prettier -c .",
"build": "tsup",
"build:docs": "typedoc"
},
"overrides": {
"brace-expansion": "^2.0.2"
},
"dependencies": {
"@aws-sdk/credential-providers": "3.823.0",
"@aws-sdk/client-iot": "3.901.0",
"@aws-sdk/credential-providers": "3.901.0",
"amazon-cognito-identity-js": "6.3.15",
"aws-iot-device-sdk-v2": "1.21.5",
"aws-iot-device-sdk-v2": "1.22.0",
"dayjs": "1.11.13",
"lodash": "4.17.21"
},
"devDependencies": {
"@eslint/js": "9.28.0",
"@semantic-release/npm": "12.0.1",
"@types/lodash": "4.17.17",
"@types/node": "22.15.29",
"conventional-changelog-conventionalcommits": "9.0.0",
"dotenv": "16.5.0",
"eslint": "9.28.0",
"eslint-plugin-jsdoc": "50.7.1",
"@eslint/js": "9.34.0",
"@semantic-release/npm": "12.0.2",
"@types/lodash": "4.17.20",
"@types/node": "24.3.0",
"conventional-changelog-conventionalcommits": "9.1.0",
"dotenv": "17.2.1",
"eslint": "9.34.0",
"eslint-plugin-jsdoc": "54.1.1",
"eslint-plugin-tsdoc": "0.4.0",
"pino": "9.7.0",
"pino": "9.13.0",
"pino-pretty": "13.0.0",
"prettier": "3.5.3",
"prettier-plugin-jsdoc": "1.3.2",
"prettier-plugin-organize-imports": "4.1.0",
"semantic-release": "24.2.5",
"prettier": "3.6.2",
"prettier-plugin-jsdoc": "1.3.3",
"prettier-plugin-organize-imports": "4.2.0",
"semantic-release": "24.2.7",
"tsup": "8.5.0",
"tsx": "4.19.4",
"typedoc": "0.28.5",
"tsx": "4.20.3",
"typedoc": "0.28.13",
"typedoc-material-theme": "1.4.0",
"typescript": "5.8.3",
"typescript-eslint": "8.33.1"
"typescript": "5.9.2",
"typescript-eslint": "8.41.0"
}
}

View File

@@ -7,6 +7,7 @@ import { InMessageType } from '@/types/mqtt/in/InMessageType';
import { StartPublishingDeviceStatus } from '@/types/mqtt/in/StartPublishingDeviceStatus';
import { OutMessageType } from '@/types/mqtt/out/OutMessageType';
import { Devices, DeviceStates, Firmwares } from '@/types/rest';
import { DescribeThingCommand, IoTClient } from '@aws-sdk/client-iot';
import { fromCognitoIdentityPool } from '@aws-sdk/credential-providers';
import {
AuthenticationDetails,
@@ -143,8 +144,23 @@ export class MysaApiClient {
/**
* Logs in the user with the given email address and password.
*
* This method authenticates the user with Mysa's Cognito user pool and establishes a session that can be used for
* subsequent API calls. Upon successful login, a 'sessionChanged' event is emitted.
*
* @example
*
* ```typescript
* try {
* await client.login('user@example.com', 'password123');
* console.log('Login successful!');
* } catch (error) {
* console.error('Login failed:', error.message);
* }
* ```
*
* @param emailAddress - The email address of the user.
* @param password - The password of the user.
* @throws {@link Error} When authentication fails due to invalid credentials or network issues.
*/
async login(emailAddress: string, password: string): Promise<void> {
this._cognitoUser = undefined;
@@ -175,7 +191,21 @@ export class MysaApiClient {
/**
* Retrieves the list of devices associated with the user.
*
* This method fetches all Mysa devices linked to the authenticated user's account, including device information such
* as models, locations, and configuration details.
*
* @example
*
* ```typescript
* const devices = await client.getDevices();
* for (const [deviceId, device] of Object.entries(devices.DevicesObj)) {
* console.log(`Device: ${device.DisplayName} (${device.Model})`);
* }
* ```
*
* @returns A promise that resolves to the list of devices.
* @throws {@link MysaApiError} When the API request fails.
* @throws {@link UnauthenticatedError} When the user is not authenticated.
*/
async getDevices(): Promise<Devices> {
this._logger.debug(`Fetching devices...`);
@@ -195,6 +225,70 @@ export class MysaApiClient {
return response.json();
}
/**
* Retrieves the serial number for a specific device.
*
* This method uses AWS IoT's DescribeThing API to fetch the serial number attribute for the specified device. This
* requires additional AWS IoT permissions and may not be available for all devices.
*
* @example
*
* ```typescript
* const serialNumber = await client.getDeviceSerialNumber('device123');
* if (serialNumber) {
* console.log(`Device serial: ${serialNumber}`);
* } else {
* console.log('Serial number not available');
* }
* ```
*
* @param deviceId - The ID of the device to get the serial number for.
* @returns A promise that resolves to the serial number, or undefined if not found.
* @throws {@link UnauthenticatedError} When the user is not authenticated.
*/
async getDeviceSerialNumber(deviceId: string): Promise<string | undefined> {
this._logger.debug(`Fetching serial number for device ${deviceId}...`);
const session = await this.getFreshSession();
// Get AWS credentials for IoT client
const credentialsProvider = fromCognitoIdentityPool({
clientConfig: {
region: AwsRegion
},
identityPoolId: CognitoIdentityPoolId,
logins: {
[CognitoLoginKey]: session.getIdToken().getJwtToken()
}
});
const credentials = await credentialsProvider();
const iotClient = new IoTClient({
region: AwsRegion,
credentials: {
accessKeyId: credentials.accessKeyId,
secretAccessKey: credentials.secretAccessKey,
sessionToken: credentials.sessionToken
}
});
try {
const command = new DescribeThingCommand({ thingName: deviceId });
const response = await iotClient.send(command);
return response.attributes?.['Serial'];
} catch (error) {
this._logger.warn(`Could not get serial number for device ${deviceId}:`, error);
return undefined;
}
}
/**
* Retrieves firmware information for all devices.
*
* @returns A promise that resolves to the firmware information for all devices.
* @throws {@link MysaApiError} When the API request fails.
* @throws {@link UnauthenticatedError} When the user is not authenticated.
*/
async getDeviceFirmwares(): Promise<Firmwares> {
this._logger.debug(`Fetching device firmwares...`);
@@ -213,6 +307,13 @@ export class MysaApiClient {
return response.json();
}
/**
* Retrieves the current state information for all devices.
*
* @returns A promise that resolves to the current state of all devices.
* @throws {@link MysaApiError} When the API request fails.
* @throws {@link UnauthenticatedError} When the user is not authenticated.
*/
async getDeviceStates(): Promise<DeviceStates> {
this._logger.debug(`Fetching device states...`);
@@ -231,6 +332,31 @@ export class MysaApiClient {
return response.json();
}
/**
* Sets the state of a specific device by sending commands via MQTT.
*
* This method allows you to change the temperature set point and/or operating mode of a Mysa device. The command is
* sent through the MQTT connection for real-time device control.
*
* @example
*
* ```typescript
* // Set temperature to 22°C
* await client.setDeviceState('device123', 22);
*
* // Turn device off
* await client.setDeviceState('device123', undefined, 'off');
*
* // Set temperature and mode
* await client.setDeviceState('device123', 20, 'heat');
* ```
*
* @param deviceId - The ID of the device to control.
* @param setPoint - The target temperature set point (optional).
* @param mode - The operating mode to set ('off', 'heat', or undefined to leave unchanged).
* @throws {@link UnauthenticatedError} When the user is not authenticated.
* @throws {@link Error} When MQTT connection or command sending fails.
*/
async setDeviceState(deviceId: string, setPoint?: number, mode?: MysaDeviceMode) {
this._logger.debug(`Setting device state for '${deviceId}'`);
@@ -248,7 +374,7 @@ export class MysaApiClient {
this._logger.debug(`Sending request to set device state for '${deviceId}'...`);
const payload = serializeMqttPayload<ChangeDeviceState>({
msg: InMessageType.CHANGE_DEVICE_STATE,
id: now.unix(),
id: now.valueOf(),
time: now.unix(),
ver: '1.0',
src: {
@@ -285,7 +411,23 @@ export class MysaApiClient {
/**
* Starts receiving real-time updates for the specified device.
*
* This method establishes an MQTT subscription to receive live status updates from the device, including temperature,
* humidity, set point changes, and other state information. The client will automatically send keep-alive messages to
* maintain the connection.
*
* @example
*
* ```typescript
* // Start receiving updates and listen for events
* await client.startRealtimeUpdates('device123');
*
* client.emitter.on('statusChanged', (status) => {
* console.log(`Temperature: ${status.temperature}°C`);
* });
* ```
*
* @param deviceId - The ID of the device to start receiving updates for.
* @throws {@link Error} When MQTT connection or subscription fails.
*/
async startRealtimeUpdates(deviceId: string) {
this._logger.info(`Starting real-time updates for device '${deviceId}'`);
@@ -329,7 +471,11 @@ export class MysaApiClient {
/**
* Stops receiving real-time updates for the specified device.
*
* This method unsubscribes from the MQTT topic for the specified device and clears any associated timers to stop the
* keep-alive messages.
*
* @param deviceId - The ID of the device to stop receiving real-time updates for.
* @throws {@link Error} When MQTT unsubscription fails.
*/
async stopRealtimeUpdates(deviceId: string) {
this._logger.info(`Stopping real-time updates for device '${deviceId}'`);
@@ -350,6 +496,15 @@ export class MysaApiClient {
this._realtimeDeviceIds.delete(deviceId);
}
/**
* Ensures a valid, non-expired session is available.
*
* This method checks if the current session is valid and not expired. If the session is expired, it automatically
* refreshes it using the refresh token.
*
* @returns A promise that resolves to a valid CognitoUserSession.
* @throws {@link UnauthenticatedError} When no session exists or refresh fails.
*/
private async getFreshSession(): Promise<CognitoUserSession> {
if (!this._cognitoUser || !this._cognitoUserSession) {
throw new UnauthenticatedError('An attempt was made to access a resource without a valid session.');
@@ -379,6 +534,15 @@ export class MysaApiClient {
});
}
/**
* Establishes and returns an MQTT connection for real-time communication.
*
* This method creates a new MQTT connection if one doesn't exist, using AWS IoT WebSocket connections with Cognito
* credentials. The connection is cached and reused for subsequent calls.
*
* @returns A promise that resolves to an active MQTT connection.
* @throws {@link Error} When connection establishment fails.
*/
private async getMqttConnection(): Promise<mqtt.MqttClientConnection> {
if (this._mqttConnection) {
return this._mqttConnection;
@@ -420,6 +584,15 @@ export class MysaApiClient {
return this._mqttConnection;
}
/**
* Processes incoming MQTT messages and emits appropriate events.
*
* This method parses MQTT payloads and converts them into typed events that can be listened to via the client's event
* emitter. It handles both v1 and v2 device message formats and emits events like 'statusChanged', 'setPointChanged',
* and 'stateChanged'.
*
* @param payload - The raw MQTT message payload to process.
*/
private processMqttMessage(payload: ArrayBuffer) {
try {
const parsedPayload = parseMqttPayload(payload);

View File

@@ -32,7 +32,7 @@ export function parseMqttPayload(payload: ArrayBuffer): OutPayload {
* @param payload - The typed payload object to serialize
* @returns The serialized payload as ArrayBuffer ready for MQTT transmission
*/
export function serializeMqttPayload<T extends InPayload>(payload: T): ArrayBuffer {
export function serializeMqttPayload<T extends InPayload>(payload: T): Uint8Array<ArrayBuffer> {
const jsonString = JSON.stringify(payload);
const encoder = new TextEncoder();
return encoder.encode(jsonString);