35 Commits

Author SHA1 Message Date
Pascal Bourque
d813c4f9a9 fix: Race condition when initializing the MqttClientConnection (#144)
Fixes https://github.com/bourquep/mysa2mqtt/issues/41
2025-10-05 14:53:17 -04:00
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
8 changed files with 1264 additions and 1181 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,16 +88,19 @@ 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]);
}
await Promise.all(
Object.entries(devices.DevicesObj).map(async ([deviceId, device]) => {
const serial = await client.getDeviceSerialNumber(deviceId);
rootLogger.info(`Serial number for device '${deviceId}' (${device.Name}): ${serial}`);
await client.startRealtimeUpdates(deviceId);
})
);
}
main().catch((error) => {

2124
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -40,40 +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/client-iot": "3.825.0",
"@aws-sdk/credential-providers": "3.825.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.30",
"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

@@ -75,8 +75,8 @@ export class MysaApiClient {
/** The fetcher function used by the client. */
private _fetcher: typeof fetch;
/** The MQTT connection used for real-time updates. */
private _mqttConnection?: mqtt.MqttClientConnection;
/** A promise that resolves to the MQTT connection used for real-time updates. */
private _mqttConnectionPromise?: Promise<mqtt.MqttClientConnection>;
/** The device IDs that are currently being updated in real-time, mapped to their respective timeouts. */
private _realtimeDeviceIds: Map<string, NodeJS.Timeout> = new Map();
@@ -374,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: {
@@ -543,11 +543,24 @@ export class MysaApiClient {
* @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;
private getMqttConnection(): Promise<mqtt.MqttClientConnection> {
if (!this._mqttConnectionPromise) {
this._mqttConnectionPromise = this.createMqttConnection().catch((err) => {
this._mqttConnectionPromise = undefined;
throw err;
});
}
return this._mqttConnectionPromise;
}
/**
* Creates a new MQTT connection using AWS IoT WebSocket connections with Cognito credentials.
*
* @returns A promise that resolves to an active MQTT connection.
* @throws {@link Error} When connection establishment fails.
*/
private async createMqttConnection(): Promise<mqtt.MqttClientConnection> {
const session = await this.getFreshSession();
const credentialsProvider = fromCognitoIdentityPool({
clientConfig: {
@@ -572,16 +585,25 @@ export class MysaApiClient {
const config = builder.build();
const client = new mqtt.MqttClient();
this._mqttConnection = client.new_connection(config);
const connection = client.new_connection(config);
this._mqttConnection.on('closed', () => {
connection.on('connect', () => this._logger.debug('MQTT connect'));
connection.on('connection_success', () => this._logger.debug('MQTT connection_success'));
connection.on('connection_failure', (e) => this._logger.error('MQTT connection_failure', e));
connection.on('interrupt', (e) => this._logger.warn('MQTT interrupt', e));
connection.on('resume', (returnCode, sessionPresent) =>
this._logger.info(`MQTT resume returnCode=${returnCode} sessionPresent=${sessionPresent}`)
);
connection.on('error', (e) => this._logger.error('MQTT error', e));
connection.on('closed', () => {
this._logger.info('MQTT connection closed');
this._mqttConnection = undefined;
this._mqttConnectionPromise = undefined;
});
await this._mqttConnection.connect();
await connection.connect();
return this._mqttConnection;
return connection;
}
/**

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);