12 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
7 changed files with 845 additions and 1684 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 # 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) [![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) [![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) [![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)
@@ -229,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 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 [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. 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

@@ -70,7 +70,7 @@ async function main() {
`'${device.Name}' status changed: ${status.temperature}°C, ${status.humidity}%, ${watts ?? 'na'}W` `'${device.Name}' status changed: ${status.temperature}°C, ${status.humidity}%, ${watts ?? 'na'}W`
); );
} catch (error) { } catch (error) {
rootLogger.error(`Error processing status update for device '${status.deviceId}':`, error); rootLogger.error(error, `Error processing status update for device '${status.deviceId}'`);
} }
}); });
@@ -79,7 +79,7 @@ async function main() {
const device = devices.DevicesObj[change.deviceId]; const device = devices.DevicesObj[change.deviceId];
rootLogger.info(`'${device.Name}' setpoint changed from ${change.previousSetPoint} to ${change.newSetPoint}`); rootLogger.info(`'${device.Name}' setpoint changed from ${change.previousSetPoint} to ${change.newSetPoint}`);
} catch (error) { } catch (error) {
rootLogger.error(`Error processing setpoint update for device '${change.deviceId}':`, error); rootLogger.error(error, `Error processing setpoint update for device '${change.deviceId}'`);
} }
}); });
@@ -88,17 +88,19 @@ async function main() {
const device = devices.DevicesObj[change.deviceId]; const device = devices.DevicesObj[change.deviceId];
rootLogger.info(change, `'${device.Name}' state changed.`); rootLogger.info(change, `'${device.Name}' state changed.`);
} catch (error) { } 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)) { await Promise.all(
const serial = await client.getDeviceSerialNumber(device[0]); Object.entries(devices.DevicesObj).map(async ([deviceId, device]) => {
rootLogger.info(`Serial number for device '${device[0]}' (${device[1].Name}): ${serial}`); const serial = await client.getDeviceSerialNumber(deviceId);
rootLogger.info(`Serial number for device '${deviceId}' (${device.Name}): ${serial}`);
await client.startRealtimeUpdates(device[0]); await client.startRealtimeUpdates(deviceId);
} })
);
} }
main().catch((error) => { main().catch((error) => {

2306
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -50,8 +50,8 @@
"brace-expansion": "^2.0.2" "brace-expansion": "^2.0.2"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-iot": "3.835.0", "@aws-sdk/client-iot": "3.901.0",
"@aws-sdk/credential-providers": "3.876.0", "@aws-sdk/credential-providers": "3.901.0",
"amazon-cognito-identity-js": "6.3.15", "amazon-cognito-identity-js": "6.3.15",
"aws-iot-device-sdk-v2": "1.22.0", "aws-iot-device-sdk-v2": "1.22.0",
"dayjs": "1.11.13", "dayjs": "1.11.13",
@@ -62,12 +62,12 @@
"@semantic-release/npm": "12.0.2", "@semantic-release/npm": "12.0.2",
"@types/lodash": "4.17.20", "@types/lodash": "4.17.20",
"@types/node": "24.3.0", "@types/node": "24.3.0",
"conventional-changelog-conventionalcommits": "9.0.0", "conventional-changelog-conventionalcommits": "9.1.0",
"dotenv": "17.2.1", "dotenv": "17.2.1",
"eslint": "9.34.0", "eslint": "9.34.0",
"eslint-plugin-jsdoc": "54.1.1", "eslint-plugin-jsdoc": "54.1.1",
"eslint-plugin-tsdoc": "0.4.0", "eslint-plugin-tsdoc": "0.4.0",
"pino": "9.7.0", "pino": "9.13.0",
"pino-pretty": "13.0.0", "pino-pretty": "13.0.0",
"prettier": "3.6.2", "prettier": "3.6.2",
"prettier-plugin-jsdoc": "1.3.3", "prettier-plugin-jsdoc": "1.3.3",
@@ -75,7 +75,7 @@
"semantic-release": "24.2.7", "semantic-release": "24.2.7",
"tsup": "8.5.0", "tsup": "8.5.0",
"tsx": "4.20.3", "tsx": "4.20.3",
"typedoc": "0.28.11", "typedoc": "0.28.13",
"typedoc-material-theme": "1.4.0", "typedoc-material-theme": "1.4.0",
"typescript": "5.9.2", "typescript": "5.9.2",
"typescript-eslint": "8.41.0" "typescript-eslint": "8.41.0"

View File

@@ -75,8 +75,8 @@ export class MysaApiClient {
/** The fetcher function used by the client. */ /** The fetcher function used by the client. */
private _fetcher: typeof fetch; private _fetcher: typeof fetch;
/** The MQTT connection used for real-time updates. */ /** A promise that resolves to the MQTT connection used for real-time updates. */
private _mqttConnection?: mqtt.MqttClientConnection; private _mqttConnectionPromise?: Promise<mqtt.MqttClientConnection>;
/** The device IDs that are currently being updated in real-time, mapped to their respective timeouts. */ /** The device IDs that are currently being updated in real-time, mapped to their respective timeouts. */
private _realtimeDeviceIds: Map<string, NodeJS.Timeout> = new Map(); 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}'...`); this._logger.debug(`Sending request to set device state for '${deviceId}'...`);
const payload = serializeMqttPayload<ChangeDeviceState>({ const payload = serializeMqttPayload<ChangeDeviceState>({
msg: InMessageType.CHANGE_DEVICE_STATE, msg: InMessageType.CHANGE_DEVICE_STATE,
id: now.unix(), id: now.valueOf(),
time: now.unix(), time: now.unix(),
ver: '1.0', ver: '1.0',
src: { src: {
@@ -543,11 +543,24 @@ export class MysaApiClient {
* @returns A promise that resolves to an active MQTT connection. * @returns A promise that resolves to an active MQTT connection.
* @throws {@link Error} When connection establishment fails. * @throws {@link Error} When connection establishment fails.
*/ */
private async getMqttConnection(): Promise<mqtt.MqttClientConnection> { private getMqttConnection(): Promise<mqtt.MqttClientConnection> {
if (this._mqttConnection) { if (!this._mqttConnectionPromise) {
return this._mqttConnection; 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 session = await this.getFreshSession();
const credentialsProvider = fromCognitoIdentityPool({ const credentialsProvider = fromCognitoIdentityPool({
clientConfig: { clientConfig: {
@@ -572,16 +585,25 @@ export class MysaApiClient {
const config = builder.build(); const config = builder.build();
const client = new mqtt.MqttClient(); 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._logger.info('MQTT connection closed');
this._mqttConnection = undefined; this._mqttConnectionPromise = undefined;
}); });
await this._mqttConnection.connect(); await connection.connect();
return this._mqttConnection; return connection;
} }
/** /**