mirror of
				https://github.com/bourquep/mysa2mqtt.git
				synced 2025-10-31 11:49:40 +00:00 
			
		
		
		
	Compare commits
	
		
			25 Commits
		
	
	
		
			v1.0.1
			...
			dependabot
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| ![dependabot[bot]](/assets/img/avatar_default.png)  | bab6589979 | ||
|   | 3b5dafeda9 | ||
|   | 35f2effe9c | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | c29eae97ed | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 6502b76b77 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 39d0e64dc0 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 49c7a0fd8e | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 36539b17b1 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | e1bd2e3a91 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | be163eddca | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 8a8ab7ab07 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | a2f47220bd | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | ec166cce61 | ||
|   | d2f7c73d84 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 1bfb7e3add | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | e9f2335c38 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 96114d2e91 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 21bc257b22 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 16a82f93f4 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | a95aee6c27 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 20b2866ee4 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | dd23fca857 | ||
|   | 374dae1885 | ||
|   | 2e2e64d2d0 | ||
|   | 57502c5fb7 | 
							
								
								
									
										18
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,6 +1,7 @@ | ||||
| # mysa2mqtt | ||||
|  | ||||
| [](https://www.npmjs.com/package/mysa2mqtt) | ||||
| [](https://hub.docker.com/r/bourquep/mysa2mqtt) | ||||
| [](https://github.com/bourquep/mysa2mqtt/actions/workflows/github-code-scanning/codeql) | ||||
| [](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 | ||||
|  | ||||
| @@ -212,13 +213,11 @@ When using Home Assistant, devices will be automatically discovered and appear i | ||||
| ### Common Issues | ||||
|  | ||||
| 1. **Authentication Failures** | ||||
|  | ||||
|    - Verify your Mysa username and password | ||||
|    - Check if session.json exists and is valid | ||||
|    - Try deleting session.json to force re-authentication | ||||
|  | ||||
| 2. **MQTT Connection Issues** | ||||
|  | ||||
|    - Verify MQTT broker hostname and port | ||||
|    - Check MQTT credentials if authentication is required | ||||
|    - Ensure the MQTT broker is accessible from your network | ||||
| @@ -363,7 +362,6 @@ copyright notice and license text in any copy of the software or substantial por | ||||
| ## Acknowledgments | ||||
|  | ||||
| - [mysa-js-sdk](https://github.com/bourquep/mysa-js-sdk) - Mysa API client library | ||||
|  | ||||
|   - 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. | ||||
|   | ||||
							
								
								
									
										3805
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3805
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										38
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										38
									
								
								package.json
									
									
									
									
									
								
							| @@ -46,29 +46,29 @@ | ||||
|     "build": "tsup" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "commander": "14.0.0", | ||||
|     "dotenv": "16.5.0", | ||||
|     "mqtt2ha": "4.0.0", | ||||
|     "mysa-js-sdk": "1.1.2", | ||||
|     "pino": "9.7.0", | ||||
|     "pino-pretty": "13.0.0" | ||||
|     "commander": "14.0.1", | ||||
|     "dotenv": "17.2.3", | ||||
|     "mqtt2ha": "4.1.2", | ||||
|     "mysa-js-sdk": "1.3.3", | ||||
|     "pino": "10.0.0", | ||||
|     "pino-pretty": "13.1.1" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@commander-js/extra-typings": "14.0.0", | ||||
|     "@eslint/js": "9.28.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", | ||||
|     "@eslint/js": "9.37.0", | ||||
|     "@semantic-release/npm": "13.1.1", | ||||
|     "@types/node": "24.9.2", | ||||
|     "conventional-changelog-conventionalcommits": "9.1.0", | ||||
|     "eslint": "9.38.0", | ||||
|     "eslint-plugin-jsdoc": "61.1.11", | ||||
|     "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", | ||||
|     "prettier": "3.6.2", | ||||
|     "prettier-plugin-jsdoc": "1.5.0", | ||||
|     "prettier-plugin-organize-imports": "4.3.0", | ||||
|     "semantic-release": "25.0.1", | ||||
|     "tsup": "8.5.0", | ||||
|     "tsx": "4.19.4", | ||||
|     "typescript": "5.8.3", | ||||
|     "typescript-eslint": "8.33.1" | ||||
|     "tsx": "4.20.6", | ||||
|     "typescript": "5.9.3", | ||||
|     "typescript-eslint": "8.45.0" | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -22,7 +22,7 @@ SOFTWARE. | ||||
| */ | ||||
|  | ||||
| import { Logger } from 'mqtt2ha'; | ||||
| import { pino } from 'pino'; | ||||
| import pino from 'pino'; | ||||
|  | ||||
| export class PinoLogger implements Logger { | ||||
|   constructor(private readonly logger: pino.Logger) {} | ||||
| @@ -32,7 +32,7 @@ export class PinoLogger implements Logger { | ||||
|     if (obj) { | ||||
|       this.logger.debug(obj, message, ...meta); | ||||
|     } else { | ||||
|       this.logger.debug(message, ...meta); | ||||
|       this.logger.debug(null, message, ...meta); | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -41,7 +41,7 @@ export class PinoLogger implements Logger { | ||||
|     if (obj) { | ||||
|       this.logger.info(obj, message, ...meta); | ||||
|     } else { | ||||
|       this.logger.info(message, ...meta); | ||||
|       this.logger.info(null, message, ...meta); | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -50,7 +50,7 @@ export class PinoLogger implements Logger { | ||||
|     if (obj) { | ||||
|       this.logger.warn(obj, message, ...meta); | ||||
|     } else { | ||||
|       this.logger.warn(message, ...meta); | ||||
|       this.logger.warn(null, message, ...meta); | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -59,7 +59,7 @@ export class PinoLogger implements Logger { | ||||
|     if (obj) { | ||||
|       this.logger.error(obj, message, ...meta); | ||||
|     } else { | ||||
|       this.logger.error(message, ...meta); | ||||
|       this.logger.error(null, message, ...meta); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										19
									
								
								src/main.ts
									
									
									
									
									
								
							
							
						
						
									
										19
									
								
								src/main.ts
									
									
									
									
									
								
							| @@ -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(error, `Failed to retrieve serial number for device ${deviceId}`); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   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) | ||||
|       ) | ||||
|   ); | ||||
|  | ||||
|   | ||||
| @@ -54,7 +54,6 @@ function getPackageVersion(): string { | ||||
|  * | ||||
|  * @param value - The value to parse. | ||||
|  * @returns The parsed integer value. | ||||
|  * @throws InvalidArgumentError if the value is not a valid integer. | ||||
|  */ | ||||
| function parseRequiredInt(value: string) { | ||||
|   const parsedValue = parseInt(value, 10); | ||||
| @@ -64,6 +63,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 +73,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( | ||||
|   | ||||
| @@ -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; | ||||
|     } | ||||
|   } | ||||
|   | ||||
| @@ -8,6 +8,7 @@ | ||||
|     "noEmit": true, | ||||
|     "resolveJsonModule": true, | ||||
|     "module": "ESNext", | ||||
|     "lib": ["ES2022"], | ||||
|     "paths": { | ||||
|       "@/*": ["./src/*"] | ||||
|     } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user