19 Commits

Author SHA1 Message Date
dependabot[bot]
36539b17b1 chore(deps-dev): Bump brace-expansion from 1.1.11 to 1.1.12 in the npm_and_yarn group (#54) 2025-08-29 12:36:55 +00:00
dependabot[bot]
e1bd2e3a91 chore(deps-dev): Bump conventional-changelog-conventionalcommits from 9.0.0 to 9.1.0 (#38) 2025-08-29 12:21:56 +00:00
dependabot[bot]
be163eddca chore(deps): Bump form-data from 4.0.2 to 4.0.4 in the npm_and_yarn group (#43) 2025-08-29 12:21:44 +00:00
dependabot[bot]
8a8ab7ab07 chore(deps): Bump dotenv from 16.5.0 to 17.2.1 (#45) 2025-08-29 12:21:29 +00:00
dependabot[bot]
a2f47220bd chore(deps-dev): Bump @eslint/js from 9.29.0 to 9.34.0 (#52) 2025-08-29 12:21:07 +00:00
dependabot[bot]
ec166cce61 chore(deps-dev): Bump typescript-eslint from 8.35.0 to 8.41.0 (#53) 2025-08-29 12:20:55 +00:00
Pascal Bourque
d2f7c73d84 docs(readme): Updated compatibility matrix for BB-V2 2025-08-25 11:40:39 -04:00
dependabot[bot]
1bfb7e3add chore(deps-dev): Bump typescript-eslint from 8.34.1 to 8.35.0 (#22) 2025-06-25 14:39:05 +00:00
dependabot[bot]
e9f2335c38 chore(deps): Bump mysa-js-sdk from 1.2.0 to 1.3.0 (#20) 2025-06-25 14:36:27 +00:00
dependabot[bot]
96114d2e91 chore(deps-dev): Bump the dev-dependencies group across 1 directory with 3 updates (#16) 2025-06-21 13:39:21 +00:00
dependabot[bot]
21bc257b22 chore(deps-dev): Bump tsx from 4.19.4 to 4.20.3 (#18) 2025-06-21 13:36:54 +00:00
dependabot[bot]
16a82f93f4 chore(deps-dev): Bump @eslint/js from 9.28.0 to 9.29.0 (#17) 2025-06-21 13:35:55 +00:00
dependabot[bot]
a95aee6c27 chore(deps-dev): Bump typescript-eslint from 8.34.0 to 8.34.1 (#19) 2025-06-21 13:33:57 +00:00
dependabot[bot]
20b2866ee4 chore(deps-dev): Bump @types/node from 22.15.30 to 24.0.0 in the dev-dependencies group (#10) 2025-06-10 11:41:44 +00:00
dependabot[bot]
dd23fca857 chore(deps-dev): Bump typescript-eslint from 8.33.1 to 8.34.0 (#11) 2025-06-10 11:41:35 +00:00
Pascal Bourque
374dae1885 feat: Expose device serial number and origin (#7) 2025-06-07 10:58:41 -04:00
Pascal Bourque
2e2e64d2d0 feat: Temperature and humidity sensors (#6) 2025-06-07 09:35:48 -04:00
Pascal Bourque
57502c5fb7 docs(readme): Added a Docker Hub badge to the readme 2025-06-06 13:04:41 -04:00
Pascal Bourque
4895828426 fix(ci): Properly pass release version output to docker job 2025-06-06 12:54:08 -04:00
7 changed files with 483 additions and 311 deletions

View File

@@ -63,6 +63,8 @@ jobs:
if: github.event_name == 'workflow_dispatch'
needs: build
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
permissions:
contents: write # to be able to publish a GitHub release
issues: write # to be able to comment on released issues

View File

@@ -1,6 +1,7 @@
# mysa2mqtt
[![NPM Version](https://img.shields.io/npm/v/mysa2mqtt)](https://www.npmjs.com/package/mysa2mqtt)
[![Docker Hub](https://img.shields.io/docker/pulls/bourquep/mysa2mqtt)](https://hub.docker.com/r/bourquep/mysa2mqtt)
[![CodeQL](https://github.com/bourquep/mysa2mqtt/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/bourquep/mysa2mqtt/actions/workflows/github-code-scanning/codeql)
[![CI: lint, build and release](https://github.com/bourquep/mysa2mqtt/actions/workflows/ci.yml/badge.svg)](https://github.com/bourquep/mysa2mqtt/actions/workflows/ci.yml)
@@ -17,13 +18,13 @@ home automation platforms.
## Supported hardware
| Model Number | Description | Supported |
| ------------ | --------------------------------------------------------- | ---------------------------------------------------------------- |
| `BB-V1-X` | Mysa Smart Thermostat for Electric Baseboard Heaters V1 | ✅ Tested and working |
| `BB-V2-X` | Mysa Smart Thermostat for Electric Baseboard Heaters V2 | ⚠️ Should work but not tested |
| `BB-V2-X-L` | Mysa Smart Thermostat LITE for Electric Baseboard Heaters | ⚠️ Should work but not tested; does not report power consumption |
| `unknown` | Mysa Smart Thermostat for Electric In-Floor Heating | ⚠️ Should work but not tested |
| `AC-V1-X` | Mysa Smart Thermostat for Mini-Split Heat Pumps & AC | 🚫 Not supported (yet) |
| Model Number | Description | Supported |
| ------------ | --------------------------------------------------------- | -------------------------------------------------------------------- |
| `BB-V1-X` | Mysa Smart Thermostat for Electric Baseboard Heaters V1 | ✅ Tested and working |
| `BB-V2-X` | Mysa Smart Thermostat for Electric Baseboard Heaters V2 | ⚠️ Partially working, in progress |
| `BB-V2-X-L` | Mysa Smart Thermostat LITE for Electric Baseboard Heaters | ⚠️ Partially working, in progress; does not report power consumption |
| `unknown` | Mysa Smart Thermostat for Electric In-Floor Heating | ⚠️ Should work but not tested |
| `AC-V1-X` | Mysa Smart Thermostat for Mini-Split Heat Pumps & AC | 🚫 Not supported (yet) |
## Disclaimer

658
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -47,28 +47,28 @@
},
"dependencies": {
"commander": "14.0.0",
"dotenv": "16.5.0",
"mqtt2ha": "4.0.0",
"mysa-js-sdk": "1.1.2",
"dotenv": "17.2.1",
"mqtt2ha": "4.1.0",
"mysa-js-sdk": "1.3.0",
"pino": "9.7.0",
"pino-pretty": "13.0.0"
},
"devDependencies": {
"@commander-js/extra-typings": "14.0.0",
"@eslint/js": "9.28.0",
"@eslint/js": "9.34.0",
"@semantic-release/npm": "12.0.1",
"@types/node": "22.15.30",
"conventional-changelog-conventionalcommits": "9.0.0",
"eslint": "9.28.0",
"eslint-plugin-jsdoc": "50.7.1",
"@types/node": "24.0.3",
"conventional-changelog-conventionalcommits": "9.1.0",
"eslint": "9.29.0",
"eslint-plugin-jsdoc": "51.1.1",
"eslint-plugin-tsdoc": "0.4.0",
"prettier": "3.5.3",
"prettier-plugin-jsdoc": "1.3.2",
"prettier-plugin-organize-imports": "4.1.0",
"semantic-release": "24.2.5",
"tsup": "8.5.0",
"tsx": "4.19.4",
"tsx": "4.20.3",
"typescript": "5.8.3",
"typescript-eslint": "8.33.1"
"typescript-eslint": "8.41.0"
}
}

View File

@@ -64,8 +64,24 @@ async function main() {
await client.login(options.mysaUsername, options.mysaPassword);
}
rootLogger.debug('Fetching devices and firmwares...');
const [devices, firmwares] = await Promise.all([client.getDevices(), client.getDeviceFirmwares()]);
rootLogger.debug('Fetching serial numbers...');
const serialNumbers = new Map<string, string>();
for (const [deviceId] of Object.entries(devices.DevicesObj)) {
try {
const serial = await client.getDeviceSerialNumber(deviceId);
if (serial) {
serialNumbers.set(deviceId, serial);
}
} catch (error) {
rootLogger.error(`Failed to retrieve serial number for device ${deviceId}`, error);
}
}
rootLogger.debug('Initializing MQTT entities...');
const mqttSettings: MqttSettings = {
host: options.mqttHost,
port: options.mqttPort,
@@ -82,7 +98,8 @@ async function main() {
device,
mqttSettings,
new PinoLogger(rootLogger.child({ module: 'thermostat', deviceId: device.Id })),
firmwares.Firmware[device.Id]
firmwares.Firmware[device.Id],
serialNumbers.get(device.Id)
)
);

View File

@@ -64,6 +64,8 @@ function parseRequiredInt(value: string) {
return parsedValue;
}
export const version = getPackageVersion();
const extraHelpText = `
Copyright (c) 2025 Pascal Bourque
Licensed under the MIT License
@@ -72,7 +74,7 @@ Source code and documentation available at: https://github.com/bourquep/mysa2mqt
`;
export const options = new Command('mysa2mqtt')
.version(getPackageVersion())
.version(version)
.description('Expose Mysa smart thermostats to home automation platforms via MQTT.')
.addHelpText('afterAll', extraHelpText)
.addOption(

View File

@@ -21,13 +21,25 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
import { Climate, ClimateAction, DeviceConfiguration, Logger, MqttSettings, Sensor } from 'mqtt2ha';
import {
Climate,
ClimateAction,
DeviceConfiguration,
Logger,
MqttSettings,
OriginConfiguration,
Sensor
} from 'mqtt2ha';
import { DeviceBase, FirmwareDevice, MysaApiClient, MysaDeviceMode, StateChange, Status } from 'mysa-js-sdk';
import { version } from './options';
export class Thermostat {
private isStarted = false;
private readonly mqttDevice: DeviceConfiguration;
private readonly mqttOrigin: OriginConfiguration;
private readonly mqttClimate: Climate;
private readonly mqttTemperature: Sensor;
private readonly mqttHumidity: Sensor;
private readonly mqttPower: Sensor;
private readonly mysaStatusUpdateHandler = this.handleMysaStatusUpdate.bind(this);
@@ -38,14 +50,22 @@ export class Thermostat {
public readonly mysaDevice: DeviceBase,
private readonly mqttSettings: MqttSettings,
private readonly logger: Logger,
public readonly mysaDeviceFirmware?: FirmwareDevice
public readonly mysaDeviceFirmware?: FirmwareDevice,
public readonly mysaDeviceSerialNumber?: string
) {
this.mqttDevice = {
identifiers: mysaDevice.Id,
name: mysaDevice.Name,
manufacturer: 'Mysa',
model: mysaDevice.Model,
sw_version: mysaDeviceFirmware?.InstalledVersion
sw_version: mysaDeviceFirmware?.InstalledVersion,
serial_number: mysaDeviceSerialNumber
};
this.mqttOrigin = {
name: 'mysa2mqtt',
sw_version: version,
support_url: 'https://github.com/bourquep/mysa2mqtt'
};
this.mqttClimate = new Climate(
@@ -55,6 +75,7 @@ export class Thermostat {
component: {
component: 'climate',
device: this.mqttDevice,
origin: this.mqttOrigin,
unique_id: `mysa_${mysaDevice.Id}_climate`,
name: 'Thermostat',
min_temp: mysaDevice.MinSetpoint,
@@ -104,13 +125,49 @@ export class Thermostat {
}
);
this.mqttTemperature = new Sensor({
mqtt: this.mqttSettings,
logger: this.logger,
component: {
component: 'sensor',
device: this.mqttDevice,
origin: this.mqttOrigin,
unique_id: `mysa_${mysaDevice.Id}_temperature`,
name: 'Current temperature',
device_class: 'temperature',
state_class: 'measurement',
unit_of_measurement: '°C',
suggested_display_precision: 1,
force_update: true
}
});
this.mqttHumidity = new Sensor({
mqtt: this.mqttSettings,
logger: this.logger,
component: {
component: 'sensor',
device: this.mqttDevice,
origin: this.mqttOrigin,
unique_id: `mysa_${mysaDevice.Id}_humidity`,
name: 'Current humidity',
device_class: 'humidity',
state_class: 'measurement',
unit_of_measurement: '%',
suggested_display_precision: 0,
force_update: true
}
});
this.mqttPower = new Sensor({
mqtt: this.mqttSettings,
logger: this.logger,
component: {
component: 'sensor',
device: this.mqttDevice,
origin: this.mqttOrigin,
unique_id: `mysa_${mysaDevice.Id}_power`,
name: 'Current power',
device_class: 'power',
state_class: 'measurement',
unit_of_measurement: 'W',
@@ -136,9 +193,14 @@ export class Thermostat {
this.mqttClimate.currentMode = state.TstatMode.v === 1 ? 'off' : state.TstatMode.v === 3 ? 'heat' : undefined;
this.mqttClimate.currentAction = this.computeCurrentAction(undefined, state.Duty.v);
this.mqttClimate.targetTemperature = this.mqttClimate.currentMode !== 'off' ? state.SetPoint.v : undefined;
await this.mqttClimate.writeConfig();
await this.mqttTemperature.setState('state_topic', state.CorrectedTemp.v.toFixed(2));
await this.mqttTemperature.writeConfig();
await this.mqttHumidity.setState('state_topic', state.Humidity.v.toFixed(2));
await this.mqttHumidity.writeConfig();
// `state.Current.v` always has a non-zero value, even for thermostats that are off, so we can't use it to determine initial power state.
await this.mqttPower.setState('state_topic', 'None');
await this.mqttPower.writeConfig();
@@ -166,6 +228,8 @@ export class Thermostat {
this.mysaApiClient.emitter.off('stateChanged', this.mysaStateChangeHandler);
await this.mqttPower.setState('state_topic', 'None');
await this.mqttTemperature.setState('state_topic', 'None');
await this.mqttHumidity.setState('state_topic', 'None');
}
private async handleMysaStatusUpdate(status: Status) {
@@ -184,6 +248,9 @@ export class Thermostat {
} else {
await this.mqttPower.setState('state_topic', 'None');
}
await this.mqttTemperature.setState('state_topic', status.temperature.toFixed(2));
await this.mqttHumidity.setState('state_topic', status.humidity.toFixed(2));
}
private async handleMysaStateChange(state: StateChange) {
@@ -200,6 +267,7 @@ export class Thermostat {
case 'heat':
this.mqttClimate.currentMode = 'heat';
this.mqttClimate.targetTemperature = state.setPoint;
break;
}
}