mysa2mqtt 1.2.2 → 1.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,26 @@
1
+ # mysa2mqtt
2
+
3
+ ## 1.2.4
4
+
5
+ ### Patch Changes
6
+
7
+ - [#149](https://github.com/bourquep/mysa2mqtt/pull/149)
8
+ [`89e2950`](https://github.com/bourquep/mysa2mqtt/commit/89e2950c4874db14ea9b682380c63984aaf7a9f4) Thanks
9
+ [@bourquep](https://github.com/bourquep)! - Moved development into the
10
+ [mysa2mqtt monorepo](https://github.com/bourquep/mysa2mqtt).
11
+
12
+ There are no functional changes in this release. The package's repository and homepage links now point at the
13
+ monorepo, and issues for all three packages are tracked at https://github.com/bourquep/mysa2mqtt/issues.
14
+
15
+ - Updated dependencies
16
+ [[`89e2950`](https://github.com/bourquep/mysa2mqtt/commit/89e2950c4874db14ea9b682380c63984aaf7a9f4)]:
17
+ - mysa-js-sdk@2.1.1
18
+ - mqtt2ha@4.1.4
19
+
20
+ ## Releases prior to 1.2.3
21
+
22
+ This package previously lived in a standalone repository and used semantic-release, which published its release notes to
23
+ GitHub Releases rather than to a changelog file.
24
+
25
+ See the [release history](https://github.com/bourquep/mysa2mqtt/releases) for notes on versions up to and including
26
+ 1.2.3. Those releases are tagged `v1.2.3`; releases from the monorepo onwards are tagged `mysa2mqtt@<version>`.
package/README.md CHANGED
@@ -1,11 +1,5 @@
1
1
  # mysa2mqtt
2
2
 
3
- <!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
4
-
5
- [![All Contributors](https://img.shields.io/badge/all_contributors-1-orange.svg?style=flat-square)](#contributors-)
6
-
7
- <!-- ALL-CONTRIBUTORS-BADGE:END -->
8
-
9
3
  [![NPM Version](https://img.shields.io/npm/v/mysa2mqtt)](https://www.npmjs.com/package/mysa2mqtt)
10
4
  [![Docker Hub](https://img.shields.io/docker/pulls/bourquep/mysa2mqtt)](https://hub.docker.com/r/bourquep/mysa2mqtt)
11
5
  [![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)
@@ -40,7 +34,7 @@ permanently.
40
34
 
41
35
  ## Prerequisites
42
36
 
43
- - Node.js 22+
37
+ - Node.js 24+
44
38
  - A Mysa account with configured thermostats
45
39
  - An MQTT broker (like Mosquitto)
46
40
  - Optional: Home Assistant for auto-discovery
@@ -63,7 +57,12 @@ Run directly without installing:
63
57
  npx mysa2mqtt --help
64
58
  ```
65
59
 
66
- ### Option 3: Development Setup
60
+ ### Option 3: Home Assistant App (Addon)
61
+
62
+ @itsamenathan made an [app/addon](https://github.com/itsamenathan/mysa2mqtt-hass-addon) for
63
+ Home Assistant that wraps this tool.
64
+
65
+ ### Option 4: Development Setup
67
66
 
68
67
  For development or custom modifications:
69
68
 
@@ -292,7 +291,7 @@ docker run -d --name mysa2mqtt \
292
291
  If you prefer to build your own image, create a `Dockerfile`:
293
292
 
294
293
  ```dockerfile
295
- FROM node:22-alpine
294
+ FROM node:24-alpine
296
295
 
297
296
  WORKDIR /app
298
297
 
@@ -322,7 +321,7 @@ docker run -d --name mysa2mqtt \
322
321
  -e M2M_MQTT_HOST=your-mqtt-broker \
323
322
  -e M2M_MYSA_USERNAME=your-email \
324
323
  -e M2M_MYSA_PASSWORD=your-password \
325
- node:22-alpine \
324
+ node:24-alpine \
326
325
  sh -c "npm install -g mysa2mqtt && mysa2mqtt"
327
326
  ```
328
327
 
@@ -353,7 +352,7 @@ docker-compose up -d
353
352
 
354
353
  ## Contributing
355
354
 
356
- If you want to contribute to this project, please read the [CONTRIBUTING.md](CONTRIBUTING.md) file for guidelines.
355
+ If you want to contribute to this project, please read the [CONTRIBUTING.md](../../CONTRIBUTING.md) file for guidelines.
357
356
 
358
357
  ## License
359
358
 
@@ -373,34 +372,17 @@ copyright notice and license text in any copy of the software or substantial por
373
372
 
374
373
  ## Acknowledgments
375
374
 
376
- - [mysa-js-sdk](https://github.com/bourquep/mysa-js-sdk) - Mysa API client library
375
+ - [mysa-js-sdk](https://github.com/bourquep/mysa2mqtt/tree/main/packages/mysa-js-sdk) - Mysa API client library
377
376
  - This library would not be possible without the amazing work by [@dlenski](https://github.com/dlenski) in his
378
377
  [mysotherm](https://github.com/dlenski/mysotherm) repository. He's the one who reversed-engineered the Mysa MQTT
379
378
  protocol which is being used by this library.
380
379
 
381
- - [mqtt2ha](https://github.com/bourquep/mqtt2ha) - MQTT to Home Assistant bridge library
380
+ - [mqtt2ha](https://github.com/bourquep/mysa2mqtt/tree/main/packages/mqtt2ha) - MQTT to Home Assistant bridge library
382
381
  - [Commander.js](https://github.com/tj/commander.js) - Command-line argument parsing
383
382
  - [Pino](https://github.com/pinojs/pino) - Fast JSON logger
384
383
 
385
384
  ## Contributors ✨
386
385
 
387
- Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
388
-
389
- <!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
390
- <!-- prettier-ignore-start -->
391
- <!-- markdownlint-disable -->
392
- <table>
393
- <tbody>
394
- <tr>
395
- <td align="center" valign="top" width="14.28%"><a href="https://github.com/remiolivier"><img src="https://avatars.githubusercontent.com/u/1379047?v=4?s=100" width="100px;" alt="remiolivier"/><br /><sub><b>remiolivier</b></sub></a><br /><a href="https://github.com/bourquep/mysa2mqtt/commits?author=remiolivier" title="Code">💻</a></td>
396
- </tr>
397
- </tbody>
398
- </table>
399
-
400
- <!-- markdownlint-restore -->
401
- <!-- prettier-ignore-end -->
402
-
403
- <!-- ALL-CONTRIBUTORS-LIST:END -->
404
-
405
- This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification.
406
- Contributions of any kind welcome!
386
+ This project follows the [all-contributors](https://github.com/all-contributors/all-contributors)
387
+ specification. See the [contributor list](https://github.com/bourquep/mysa2mqtt#contributors-) in the
388
+ repository README. Contributions of any kind welcome!
package/dist/main.js CHANGED
@@ -22,9 +22,9 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
22
  SOFTWARE.
23
23
  */
24
24
 
25
- import{MysaApiClient as W}from"mysa-js-sdk";import{pino as Y}from"pino";var g=class{constructor(t){this.logger=t}debug(t,...e){let i=e.at(0);i?this.logger.debug(i,t,...e):this.logger.debug(null,t,...e)}info(t,...e){let i=e.at(0);i?this.logger.info(i,t,...e):this.logger.info(null,t,...e)}warn(t,...e){let i=e.at(0);i?this.logger.warn(i,t,...e):this.logger.warn(null,t,...e)}error(t,...e){let i=e.at(0);i?this.logger.error(i,t,...e):this.logger.error(null,t,...e)}};import{Command as F,InvalidArgumentError as I,Option as s}from"commander";import{configDotenv as E}from"dotenv";import{readFileSync as x}from"fs";import{dirname as N,join as H}from"path";import{fileURLToPath as k}from"url";E({path:[".env",".env.local"],override:!0});function L(){try{let o=k(import.meta.url),t=N(o),e=H(t,"..","package.json");return JSON.parse(x(e,"utf-8")).version||"unknown"}catch{return"unknown"}}function Q(o){let t=parseInt(o,10);if(isNaN(t))throw new I("Must be a number.");return t}var M=L(),U=`
25
+ import{MysaApiClient as Z}from"mysa-js-sdk";import{pino as tt}from"pino";var g=class{constructor(t){this.logger=t}logger;debug(t,...e){let i=e.at(0);i?this.logger.debug(i,t,...e):this.logger.debug(null,t,...e)}info(t,...e){let i=e.at(0);i?this.logger.info(i,t,...e):this.logger.info(null,t,...e)}warn(t,...e){let i=e.at(0);i?this.logger.warn(i,t,...e):this.logger.warn(null,t,...e)}error(t,...e){let i=e.at(0);i?this.logger.error(i,t,...e):this.logger.error(null,t,...e)}};import{Command as L,InvalidArgumentError as k,Option as m}from"commander";import{configDotenv as H}from"dotenv";import{readFileSync as U}from"fs";import{dirname as G,join as Q}from"path";import{fileURLToPath as Y}from"url";H({path:[".env",".env.local"],override:!0});function j(){try{let a=Y(import.meta.url),t=G(a),e=Q(t,"..","package.json");return JSON.parse(U(e,"utf-8")).version||"unknown"}catch{return"unknown"}}function B(a){let t=parseInt(a,10);if(isNaN(t))throw new k("Must be a number.");return t}var v=j(),$=`
26
26
  Copyright (c) 2025 Pascal Bourque
27
27
  Licensed under the MIT License
28
28
 
29
29
  Source code and documentation available at: https://github.com/bourquep/mysa2mqtt
30
- `,r=new F("mysa2mqtt").version(M).description("Expose Mysa smart thermostats to home automation platforms via MQTT.").addHelpText("afterAll",U).addOption(new s("-l, --log-level <logLevel>","log level").choices(["silent","fatal","error","warn","info","debug","trace"]).env("M2M_LOG_LEVEL").default("info").helpGroup("Configuration")).addOption(new s("-f, --log-format <logFormat>","log format").choices(["pretty","json"]).env("M2M_LOG_FORMAT").default("pretty").helpGroup("Configuration")).addOption(new s("-H, --mqtt-host <mqttHost>","hostname of the MQTT broker").env("M2M_MQTT_HOST").makeOptionMandatory().helpGroup("MQTT")).addOption(new s("-P, --mqtt-port <mqttPort>","port of the MQTT broker").env("M2M_MQTT_PORT").argParser(Q).default(1883).helpGroup("MQTT")).addOption(new s("-U, --mqtt-username <mqttUsername>","username of the MQTT broker").env("M2M_MQTT_USERNAME").helpGroup("MQTT")).addOption(new s("-B, --mqtt-password <mqttPassword>","password of the MQTT broker").env("M2M_MQTT_PASSWORD").helpGroup("MQTT")).addOption(new s("-u, --mysa-username <mysaUsername>","Mysa account username").env("M2M_MYSA_USERNAME").makeOptionMandatory().helpGroup("Mysa")).addOption(new s("-p, --mysa-password <mysaPassword>","Mysa account password").env("M2M_MYSA_PASSWORD").makeOptionMandatory().helpGroup("Mysa")).addOption(new s("-s, --mysa-session-file <mysaSessionFile>","Mysa session file").env("M2M_MYSA_SESSION_FILE").default("session.json").helpGroup("Configuration")).addOption(new s("-N, --mqtt-client-name <mqttClientName>","name of the MQTT client").env("M2M_MQTT_CLIENT_NAME").default("mysa2mqtt").helpGroup("MQTT")).addOption(new s("-T, --mqtt-topic-prefix <mqttTopicPrefix>","prefix of the MQTT topic").env("M2M_MQTT_TOPIC_PREFIX").default("mysa2mqtt").helpGroup("MQTT")).addOption(new s("--temperature-unit <temperatureUnit>","temperature unit (C or F)").env("M2M_TEMPERATURE_UNIT").choices(["C","F"]).default("C").helpGroup("Configuration")).parse().opts();import{readFile as R,rm as j,writeFile as G}from"fs/promises";async function C(o,t){try{t.info("Loading Mysa session...");let e=await R(o,"utf8");return JSON.parse(e)}catch{t.info("No valid Mysa session file found.")}}async function S(o,t,e){if(o)e.info("Saving Mysa session..."),await G(t,JSON.stringify(o));else try{e.debug("Removing Mysa session file..."),await j(t)}catch{}}import{Climate as B,Sensor as v}from"mqtt2ha";var w=["off","heat"],q=["off","heat","cool","dry","fan_only","auto"],J={1:"off",2:"auto",3:"heat",4:"cool",5:"fan_only",6:"dry"},T=["auto","low","medium","high","max"],V={1:"auto",3:"low",5:"medium",7:"high",8:"max"},y=class{constructor(t,e,i,u,c,l,a){this.mysaApiClient=t;this.mysaDevice=e;this.mqttSettings=i;this.logger=u;this.mysaDeviceFirmware=c;this.mysaDeviceSerialNumber=l;this.temperatureUnit=a;let n=(a??"C")==="C";this.mqttDevice={identifiers:e.Id,name:e.Name,manufacturer:"Mysa",model:e.Model,sw_version:c==null?void 0:c.InstalledVersion,serial_number:l},this.mqttOrigin={name:"mysa2mqtt",sw_version:M,support_url:"https://github.com/bourquep/mysa2mqtt"};let p=e.Model.startsWith("AC");this.deviceType=p?"AC":"BB",this.mqttClimate=new B({mqtt:this.mqttSettings,logger:this.logger,component:{component:"climate",device:this.mqttDevice,origin:this.mqttOrigin,unique_id:`mysa_${e.Id}_climate`,name:"Thermostat",min_temp:e.MinSetpoint,max_temp:e.MaxSetpoint,modes:p?q:w,fan_modes:p?T:void 0,precision:n?.1:1,temp_step:n?.5:1,temperature_unit:"C",optimistic:!0}},p?["action_topic","current_humidity_topic","current_temperature_topic","mode_state_topic","temperature_state_topic","fan_mode_state_topic"]:["action_topic","current_humidity_topic","current_temperature_topic","mode_state_topic","temperature_state_topic"],async()=>{},p?["mode_command_topic","power_command_topic","temperature_command_topic","fan_mode_command_topic"]:["mode_command_topic","power_command_topic","temperature_command_topic"],async(b,h)=>{switch(b){case"mode_command_topic":{let m=h,f=p?q.includes(m)?m:void 0:w.includes(m)?m:void 0;this.mysaApiClient.setDeviceState(this.mysaDevice.Id,void 0,f);break}case"power_command_topic":this.mysaApiClient.setDeviceState(this.mysaDevice.Id,void 0,h==="OFF"?"off":h==="ON"&&!p?"heat":void 0);break;case"temperature_command_topic":if(h==="")this.mysaApiClient.setDeviceState(this.mysaDevice.Id,void 0,void 0);else{let m=parseFloat(h);if(!n){let f=_=>Math.round(_*2)/2,A=(_,D,P)=>Math.min(P,Math.max(D,_)),O=f(m);m=A(O,this.mysaDevice.MinSetpoint??0,this.mysaDevice.MaxSetpoint??100)}this.mysaApiClient.setDeviceState(this.mysaDevice.Id,m,void 0)}break;case"fan_mode_command_topic":{let m=h,f=T.includes(m)?m:void 0;this.mysaApiClient.setDeviceState(this.mysaDevice.Id,void 0,void 0,f);break}}}),this.mqttTemperature=new v({mqtt:this.mqttSettings,logger:this.logger,component:{component:"sensor",device:this.mqttDevice,origin:this.mqttOrigin,unique_id:`mysa_${e.Id}_temperature`,name:"Current temperature",device_class:"temperature",state_class:"measurement",unit_of_measurement:"\xB0C",suggested_display_precision:n?.1:0,force_update:!0}}),this.mqttHumidity=new v({mqtt:this.mqttSettings,logger:this.logger,component:{component:"sensor",device:this.mqttDevice,origin:this.mqttOrigin,unique_id:`mysa_${e.Id}_humidity`,name:"Current humidity",device_class:"humidity",state_class:"measurement",unit_of_measurement:"%",suggested_display_precision:0,force_update:!0}}),this.mqttPower=new v({mqtt:this.mqttSettings,logger:this.logger,component:{component:"sensor",device:this.mqttDevice,origin:this.mqttOrigin,unique_id:`mysa_${e.Id}_power`,name:"Current power",device_class:"power",state_class:"measurement",unit_of_measurement:"W",suggested_display_precision:0,force_update:!0}})}isStarted=!1;mqttDevice;mqttOrigin;mqttClimate;mqttTemperature;mqttHumidity;mqttPower;mysaStatusUpdateHandler=this.handleMysaStatusUpdate.bind(this);mysaStateChangeHandler=this.handleMysaStateChange.bind(this);deviceType;async start(){var t,e,i,u,c,l;if(!this.isStarted){this.isStarted=!0;try{let n=(await this.mysaApiClient.getDeviceStates()).DeviceStatesObj[this.mysaDevice.Id];this.mqttClimate.currentTemperature=(t=n.CorrectedTemp)==null?void 0:t.v,this.mqttClimate.currentHumidity=(e=n.Humidity)==null?void 0:e.v,this.mqttClimate.currentMode=J[(i=n.TstatMode)==null?void 0:i.v]??this.mqttClimate.currentMode,this.mqttClimate.currentFanMode=V[(u=n.FanSpeed)==null?void 0:u.v]??this.mqttClimate.currentFanMode,this.mqttClimate.currentAction=this.computeCurrentAction(void 0,(c=n.Duty)==null?void 0:c.v),this.mqttClimate.targetTemperature=this.mqttClimate.currentMode!=="off"?(l=n.SetPoint)==null?void 0:l.v:void 0,await this.mqttClimate.writeConfig(),await this.mqttTemperature.setState("state_topic",n.CorrectedTemp!=null?n.CorrectedTemp.v.toFixed(2):"None"),await this.mqttTemperature.writeConfig(),await this.mqttHumidity.setState("state_topic",n.Humidity!=null?n.Humidity.v.toFixed(2):"None"),await this.mqttHumidity.writeConfig(),await this.mqttPower.setState("state_topic","None"),await this.mqttPower.writeConfig(),this.mysaApiClient.emitter.on("statusChanged",this.mysaStatusUpdateHandler),this.mysaApiClient.emitter.on("stateChanged",this.mysaStateChangeHandler),await this.mysaApiClient.startRealtimeUpdates(this.mysaDevice.Id)}catch(a){throw this.isStarted=!1,a}}}async stop(){this.isStarted&&(this.isStarted=!1,await this.mysaApiClient.stopRealtimeUpdates(this.mysaDevice.Id),this.mysaApiClient.emitter.off("statusChanged",this.mysaStatusUpdateHandler),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"))}async handleMysaStatusUpdate(t){if(!(!this.isStarted||t.deviceId!==this.mysaDevice.Id)){if(this.mqttClimate.currentAction=this.computeCurrentAction(t.current,t.dutyCycle),this.mqttClimate.currentTemperature=t.temperature,this.mqttClimate.currentHumidity=t.humidity,this.mqttClimate.targetTemperature=this.mqttClimate.currentMode!=="off"?t.setPoint:void 0,this.mysaDevice.Voltage!=null&&t.current!=null){let e=this.mysaDevice.Voltage*t.current;await this.mqttPower.setState("state_topic",e.toFixed(2))}else await this.mqttPower.setState("state_topic","None");await this.mqttTemperature.setState("state_topic",t.temperature.toFixed(2)),await this.mqttHumidity.setState("state_topic",t.humidity.toFixed(2))}}async handleMysaStateChange(t){if(!(!this.isStarted||t.deviceId!==this.mysaDevice.Id))switch(t.mode){case"off":this.mqttClimate.currentMode="off",this.mqttClimate.currentAction="off",this.mqttClimate.targetTemperature=void 0,this.mqttClimate.currentFanMode=void 0;break;case"heat":case"cool":case"auto":this.mqttClimate.currentMode=t.mode,this.deviceType==="AC"&&(this.mqttClimate.currentAction=this.computeCurrentAction()),this.mqttClimate.targetTemperature=t.setPoint,this.mqttClimate.currentFanMode=t.fanSpeed;break;case"dry":case"fan_only":this.mqttClimate.currentMode=t.mode,this.mqttClimate.currentAction=this.computeCurrentAction(),this.mqttClimate.currentFanMode=t.fanSpeed;break}}computeCurrentAction(t,e){let i=this.mqttClimate.currentMode;switch(q.includes(i)?i:void 0){case"off":return"off";case"heat":switch(this.deviceType){case"BB":return t!=null?t>0?"heating":"idle":(e??0)>0?"heating":"idle";default:return"heating"}case"cool":return"cooling";case"fan_only":return"fan";case"dry":return"drying";default:return"idle"}}};var d=Y({name:"mysa2mqtt",level:r.logLevel,transport:r.logFormat==="pretty"?{target:"pino-pretty",options:{colorize:!0,singleLine:!0,ignore:"hostname,module",messageFormat:"\x1B[33m[{module}]\x1B[39m {msg}"}}:void 0});async function $(){d.info("Starting mysa2mqtt...");let o=await C(r.mysaSessionFile,d),t=new W(o,{logger:new g(d.child({module:"mysa-js-sdk"}))});t.emitter.on("sessionChanged",async a=>{await S(a,r.mysaSessionFile,d)}),t.isAuthenticated||(d.info("Logging in..."),await t.login(r.mysaUsername,r.mysaPassword)),d.debug("Fetching devices and firmwares...");let[e,i]=await Promise.all([t.getDevices(),t.getDeviceFirmwares()]);d.debug("Fetching serial numbers...");let u=new Map;for(let[a]of Object.entries(e.DevicesObj))try{let n=await t.getDeviceSerialNumber(a);n&&u.set(a,n)}catch(n){d.error(n,`Failed to retrieve serial number for device ${a}`)}d.debug("Initializing MQTT entities...");let c={host:r.mqttHost,port:r.mqttPort,username:r.mqttUsername,password:r.mqttPassword,client_name:r.mqttClientName,state_prefix:r.mqttTopicPrefix},l=Object.entries(e.DevicesObj).map(([,a])=>new y(t,a,c,new g(d.child({module:"thermostat",deviceId:a.Id})),i.Firmware[a.Id],u.get(a.Id),r.temperatureUnit));for(let a of l)await a.start()}$().catch(o=>{d.fatal(o,"Unexpected error"),process.exit(1)});
30
+ `,s=new L("mysa2mqtt").version(v).description("Expose Mysa smart thermostats to home automation platforms via MQTT.").addHelpText("afterAll",$).addOption(new m("-l, --log-level <logLevel>","log level").choices(["silent","fatal","error","warn","info","debug","trace"]).env("M2M_LOG_LEVEL").default("info").helpGroup("Configuration")).addOption(new m("-f, --log-format <logFormat>","log format").choices(["pretty","json"]).env("M2M_LOG_FORMAT").default("pretty").helpGroup("Configuration")).addOption(new m("-H, --mqtt-host <mqttHost>","hostname of the MQTT broker").env("M2M_MQTT_HOST").makeOptionMandatory().helpGroup("MQTT")).addOption(new m("-P, --mqtt-port <mqttPort>","port of the MQTT broker").env("M2M_MQTT_PORT").argParser(B).default(1883).helpGroup("MQTT")).addOption(new m("-U, --mqtt-username <mqttUsername>","username of the MQTT broker").env("M2M_MQTT_USERNAME").helpGroup("MQTT")).addOption(new m("-B, --mqtt-password <mqttPassword>","password of the MQTT broker").env("M2M_MQTT_PASSWORD").helpGroup("MQTT")).addOption(new m("-u, --mysa-username <mysaUsername>","Mysa account username").env("M2M_MYSA_USERNAME").makeOptionMandatory().helpGroup("Mysa")).addOption(new m("-p, --mysa-password <mysaPassword>","Mysa account password").env("M2M_MYSA_PASSWORD").makeOptionMandatory().helpGroup("Mysa")).addOption(new m("-s, --mysa-session-file <mysaSessionFile>","Mysa session file").env("M2M_MYSA_SESSION_FILE").default("session.json").helpGroup("Configuration")).addOption(new m("-N, --mqtt-client-name <mqttClientName>","name of the MQTT client").env("M2M_MQTT_CLIENT_NAME").default("mysa2mqtt").helpGroup("MQTT")).addOption(new m("-T, --mqtt-topic-prefix <mqttTopicPrefix>","prefix of the MQTT topic").env("M2M_MQTT_TOPIC_PREFIX").default("mysa2mqtt").helpGroup("MQTT")).addOption(new m("--temperature-unit <temperatureUnit>","temperature unit (C or F)").env("M2M_TEMPERATURE_UNIT").choices(["C","F"]).default("C").helpGroup("Configuration")).parse().opts();import{readFile as J,rm as V,writeFile as X}from"fs/promises";async function w(a,t){try{t.info("Loading Mysa session...");let e=await J(a,"utf8");return JSON.parse(e)}catch{t.info("No valid Mysa session file found.")}}async function q(a,t,e){if(a)e.info("Saving Mysa session..."),await X(t,JSON.stringify(a));else try{e.debug("Removing Mysa session file..."),await V(t)}catch{}}import{Climate as W,Sensor as S}from"mqtt2ha";var C=["off","heat"],T=["off","heat","cool","dry","fan_only","auto"],z={1:"off",2:"auto",3:"heat",4:"cool",5:"fan_only",6:"dry"},A=["auto","low","medium","high","max"],K={1:"auto",3:"low",5:"medium",7:"high",8:"max"},D=3e4,b=3e5,R=Math.ceil(Math.log2(b/D)),_=class{constructor(t,e,i,c,l,u,p){this.mysaApiClient=t;this.mysaDevice=e;this.mqttSettings=i;this.logger=c;this.mysaDeviceFirmware=l;this.mysaDeviceSerialNumber=u;this.temperatureUnit=p;let o=(p??"C")==="C";this.mqttDevice={identifiers:e.Id,name:e.Name,manufacturer:"Mysa",model:e.Model,sw_version:l==null?void 0:l.InstalledVersion,serial_number:u},this.mqttOrigin={name:"mysa2mqtt",sw_version:v,support_url:"https://github.com/bourquep/mysa2mqtt"};let r=e.Model.startsWith("AC");this.deviceType=r?"AC":"BB",this.mqttClimate=new W({mqtt:this.mqttSettings,logger:this.logger,component:{component:"climate",device:this.mqttDevice,origin:this.mqttOrigin,unique_id:`mysa_${e.Id}_climate`,name:"Thermostat",min_temp:e.MinSetpoint,max_temp:e.MaxSetpoint,modes:r?T:C,fan_modes:r?A:void 0,precision:o?.1:1,temp_step:o?.5:1,temperature_unit:"C",optimistic:!0}},r?["action_topic","current_humidity_topic","current_temperature_topic","mode_state_topic","temperature_state_topic","fan_mode_state_topic"]:["action_topic","current_humidity_topic","current_temperature_topic","mode_state_topic","temperature_state_topic"],async()=>{},r?["mode_command_topic","power_command_topic","temperature_command_topic","fan_mode_command_topic"]:["mode_command_topic","power_command_topic","temperature_command_topic"],async(h,y)=>{switch(h){case"mode_command_topic":{let d=y,f=r?T.includes(d)?d:void 0:C.includes(d)?d:void 0;await this.setDeviceState(void 0,f);break}case"power_command_topic":await this.setDeviceState(void 0,y==="OFF"?"off":y==="ON"&&!r?"heat":void 0);break;case"temperature_command_topic":if(y==="")await this.setDeviceState(void 0,void 0);else{let d=parseFloat(y);if(!o){let f=M=>Math.round(M*2)/2,F=(M,N,x)=>Math.min(x,Math.max(N,M)),P=f(d);d=F(P,this.mysaDevice.MinSetpoint??0,this.mysaDevice.MaxSetpoint??100)}await this.setDeviceState(d,void 0)}break;case"fan_mode_command_topic":{let d=y,f=A.includes(d)?d:void 0;await this.setDeviceState(void 0,void 0,f);break}}}),this.mqttTemperature=new S({mqtt:this.mqttSettings,logger:this.logger,component:{component:"sensor",device:this.mqttDevice,origin:this.mqttOrigin,unique_id:`mysa_${e.Id}_temperature`,name:"Current temperature",device_class:"temperature",state_class:"measurement",unit_of_measurement:"\xB0C",suggested_display_precision:o?.1:0,force_update:!0}}),this.mqttHumidity=new S({mqtt:this.mqttSettings,logger:this.logger,component:{component:"sensor",device:this.mqttDevice,origin:this.mqttOrigin,unique_id:`mysa_${e.Id}_humidity`,name:"Current humidity",device_class:"humidity",state_class:"measurement",unit_of_measurement:"%",suggested_display_precision:0,force_update:!0}}),this.mqttPower=new S({mqtt:this.mqttSettings,logger:this.logger,component:{component:"sensor",device:this.mqttDevice,origin:this.mqttOrigin,unique_id:`mysa_${e.Id}_power`,name:"Current power",device_class:"power",state_class:"measurement",unit_of_measurement:"W",suggested_display_precision:0,force_update:!0}})}mysaApiClient;mysaDevice;mqttSettings;logger;mysaDeviceFirmware;mysaDeviceSerialNumber;temperatureUnit;isStarted=!1;realtimeGeneration=0;realtimeRetryAttempt=0;realtimeRetryTimer;mqttDevice;mqttOrigin;mqttClimate;mqttTemperature;mqttHumidity;mqttPower;mysaStatusUpdateHandler=t=>{this.handleMysaStatusUpdate(t).catch(e=>{this.logger.error("Failed to handle Mysa status update",{error:e,deviceId:this.mysaDevice.Id})})};mysaStateChangeHandler=t=>{this.handleMysaStateChange(t).catch(e=>{this.logger.error("Failed to handle Mysa state change",{error:e,deviceId:this.mysaDevice.Id})})};deviceType;async start(){var t,e,i,c,l,u;if(!this.isStarted){this.isStarted=!0,this.realtimeGeneration+=1;try{let o=(await this.mysaApiClient.getDeviceStates()).DeviceStatesObj[this.mysaDevice.Id];this.mqttClimate.currentTemperature=(t=o.CorrectedTemp)==null?void 0:t.v,this.mqttClimate.currentHumidity=(e=o.Humidity)==null?void 0:e.v,this.mqttClimate.currentMode=z[(i=o.TstatMode)==null?void 0:i.v]??this.mqttClimate.currentMode,this.mqttClimate.currentFanMode=K[(c=o.FanSpeed)==null?void 0:c.v]??this.mqttClimate.currentFanMode,this.mqttClimate.currentAction=this.computeCurrentAction(void 0,(l=o.Duty)==null?void 0:l.v),this.mqttClimate.targetTemperature=this.mqttClimate.currentMode!=="off"?(u=o.SetPoint)==null?void 0:u.v:void 0,await this.mqttClimate.writeConfig(),await this.mqttTemperature.setState("state_topic",o.CorrectedTemp!=null?o.CorrectedTemp.v.toFixed(2):"None"),await this.mqttTemperature.writeConfig(),await this.mqttHumidity.setState("state_topic",o.Humidity!=null?o.Humidity.v.toFixed(2):"None"),await this.mqttHumidity.writeConfig(),await this.mqttPower.setState("state_topic","None"),await this.mqttPower.writeConfig(),this.mysaApiClient.emitter.on("statusChanged",this.mysaStatusUpdateHandler),this.mysaApiClient.emitter.on("stateChanged",this.mysaStateChangeHandler),await this.startRealtimeUpdates()}catch(p){throw this.isStarted=!1,this.realtimeGeneration+=1,this.clearRealtimeRetry(),p}}}async stop(){this.isStarted&&(this.isStarted=!1,this.realtimeGeneration+=1,this.clearRealtimeRetry(),await this.stopRealtimeUpdates(),this.mysaApiClient.emitter.off("statusChanged",this.mysaStatusUpdateHandler),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"))}async setDeviceState(t,e,i){try{await this.mysaApiClient.setDeviceState(this.mysaDevice.Id,t,e,i)}catch(c){this.logger.error("Failed to update Mysa device state",{error:c,deviceId:this.mysaDevice.Id})}}async startRealtimeUpdates(){let t=this.realtimeGeneration;try{if(await this.mysaApiClient.startRealtimeUpdates(this.mysaDevice.Id),!this.isStarted||t!==this.realtimeGeneration){await this.stopRealtimeUpdates();return}this.realtimeRetryAttempt=0,this.logger.info("Started realtime updates",{deviceId:this.mysaDevice.Id})}catch(e){this.isStarted&&t===this.realtimeGeneration&&this.scheduleRealtimeRetry(e)}}async stopRealtimeUpdates(){try{await this.mysaApiClient.stopRealtimeUpdates(this.mysaDevice.Id)}catch(t){this.logger.warn("Failed to stop realtime updates",{error:t,deviceId:this.mysaDevice.Id})}}scheduleRealtimeRetry(t){if(!this.isStarted||this.realtimeRetryTimer!=null)return;let e=Math.min(this.realtimeRetryAttempt,R),i=Math.min(b,D*2**e);this.realtimeRetryAttempt=Math.min(this.realtimeRetryAttempt+1,R),this.logger.error("Failed to start realtime updates; retrying",{error:t,deviceId:this.mysaDevice.Id,retryDelayMs:i}),this.realtimeRetryTimer=setTimeout(()=>{this.realtimeRetryTimer=void 0,this.startRealtimeUpdates()},i)}clearRealtimeRetry(){this.realtimeRetryTimer!=null&&(clearTimeout(this.realtimeRetryTimer),this.realtimeRetryTimer=void 0)}async handleMysaStatusUpdate(t){if(!(!this.isStarted||t.deviceId!==this.mysaDevice.Id)){if(this.mqttClimate.currentAction=this.computeCurrentAction(t.current,t.dutyCycle),this.mqttClimate.currentTemperature=t.temperature,this.mqttClimate.currentHumidity=t.humidity,this.mqttClimate.targetTemperature=this.mqttClimate.currentMode!=="off"?t.setPoint:void 0,this.mysaDevice.Voltage!=null&&t.current!=null){let e=this.mysaDevice.Voltage*t.current;await this.mqttPower.setState("state_topic",e.toFixed(2))}else await this.mqttPower.setState("state_topic","None");await this.mqttTemperature.setState("state_topic",t.temperature.toFixed(2)),await this.mqttHumidity.setState("state_topic",t.humidity.toFixed(2))}}async handleMysaStateChange(t){if(!(!this.isStarted||t.deviceId!==this.mysaDevice.Id))switch(t.mode){case"off":this.mqttClimate.currentMode="off",this.mqttClimate.currentAction="off",this.mqttClimate.targetTemperature=void 0,this.mqttClimate.currentFanMode=void 0;break;case"heat":case"cool":case"auto":this.mqttClimate.currentMode=t.mode,this.deviceType==="AC"&&(this.mqttClimate.currentAction=this.computeCurrentAction()),this.mqttClimate.targetTemperature=t.setPoint,this.mqttClimate.currentFanMode=t.fanSpeed;break;case"dry":case"fan_only":this.mqttClimate.currentMode=t.mode,this.mqttClimate.currentAction=this.computeCurrentAction(),this.mqttClimate.currentFanMode=t.fanSpeed;break}}computeCurrentAction(t,e){let i=this.mqttClimate.currentMode;switch(T.includes(i)?i:void 0){case"off":return"off";case"heat":return this.deviceType==="BB"?t!=null?t>0?"heating":"idle":(e??0)>0?"heating":"idle":"heating";case"cool":return"cooling";case"fan_only":return"fan";case"dry":return"drying";default:return"idle"}}};var I=3e4,E=3e5,et=Math.ceil(Math.log2(E/I)),n=tt({name:"mysa2mqtt",level:s.logLevel,transport:s.logFormat==="pretty"?{target:"pino-pretty",options:{colorize:!0,singleLine:!0,ignore:"hostname,module",messageFormat:"\x1B[33m[{module}]\x1B[39m {msg}"}}:void 0});async function it(){n.info("Starting mysa2mqtt...");let a=await w(s.mysaSessionFile,n),t=new Z(a,{logger:new g(n.child({module:"mysa-js-sdk"}))});t.emitter.on("sessionChanged",async r=>{await q(r,s.mysaSessionFile,n)}),t.isAuthenticated||(n.info("Logging in..."),await t.login(s.mysaUsername,s.mysaPassword)),n.debug("Fetching devices and firmwares...");let[e,i]=await Promise.all([t.getDevices(),t.getDeviceFirmwares()]);n.debug("Fetching serial numbers...");let c=new Map;for(let[r]of Object.entries(e.DevicesObj))try{let h=await t.getDeviceSerialNumber(r);h&&c.set(r,h)}catch(h){n.error(h,`Failed to retrieve serial number for device ${r}`)}n.debug("Initializing MQTT entities...");let l={host:s.mqttHost,port:s.mqttPort,username:s.mqttUsername,password:s.mqttPassword,client_name:s.mqttClientName,state_prefix:s.mqttTopicPrefix},u=Object.entries(e.DevicesObj).map(([,r])=>new _(t,r,l,new g(n.child({module:"thermostat",deviceId:r.Id})),i.Firmware[r.Id],c.get(r.Id),s.temperatureUnit)),p=0,o=[];for(let r of u)await at(r)?p+=1:o.push(r);if(u.length>0&&p===0)throw new Error("Failed to start any thermostats");for(let r of o)O(r)}async function at(a){try{return await a.start(),!0}catch(t){return n.error(t,`Failed to start thermostat ${a.mysaDevice.Id}`),!1}}function O(a,t=0){let e=Math.min(t,et),i=Math.min(E,I*2**e);n.info(`Retrying thermostat ${a.mysaDevice.Id} startup in ${i}ms`),setTimeout(()=>{rt(a,t+1)},i)}async function rt(a,t){try{await a.start(),n.info(`Started thermostat ${a.mysaDevice.Id} after retry`)}catch(e){n.error(e,`Failed to start thermostat ${a.mysaDevice.Id}`),O(a,t)}}it().catch(a=>{n.fatal(a,"Unexpected error"),process.exit(1)});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mysa2mqtt",
3
- "version": "1.2.2",
3
+ "version": "1.2.4",
4
4
  "license": "MIT",
5
5
  "private": false,
6
6
  "description": "Expose Mysa smart thermostats to home automation platforms via MQTT.",
@@ -12,7 +12,8 @@
12
12
  "home-assistant"
13
13
  ],
14
14
  "publishConfig": {
15
- "provenance": true
15
+ "provenance": true,
16
+ "access": "public"
16
17
  },
17
18
  "author": {
18
19
  "name": "Pascal Bourque",
@@ -21,13 +22,15 @@
21
22
  "bugs": {
22
23
  "url": "https://github.com/bourquep/mysa2mqtt/issues"
23
24
  },
24
- "homepage": "https://github.com/bourquep/mysa2mqtt",
25
+ "homepage": "https://github.com/bourquep/mysa2mqtt/tree/main/packages/mysa2mqtt",
25
26
  "repository": {
26
27
  "type": "git",
27
- "url": "git+https://github.com/bourquep/mysa2mqtt.git"
28
+ "url": "git+https://github.com/bourquep/mysa2mqtt.git",
29
+ "directory": "packages/mysa2mqtt"
28
30
  },
29
31
  "files": [
30
32
  "README.md",
33
+ "CHANGELOG.md",
31
34
  "LICENSE.txt",
32
35
  "dist"
33
36
  ],
@@ -36,39 +39,24 @@
36
39
  "mysa2mqtt": "dist/main.js"
37
40
  },
38
41
  "engines": {
39
- "node": ">=22.4.0"
42
+ "node": ">=24.15.0"
40
43
  },
41
44
  "browser": false,
42
45
  "scripts": {
43
46
  "dev": "tsx src/main.ts",
44
47
  "lint": "eslint --max-warnings 0 src/**/*.ts",
45
- "style-lint": "prettier -c .",
48
+ "typecheck": "tsc --noEmit -p tsconfig.json",
46
49
  "build": "tsup"
47
50
  },
48
51
  "dependencies": {
49
- "commander": "14.0.2",
50
- "dotenv": "17.2.3",
51
- "mqtt2ha": "4.1.2",
52
- "mysa-js-sdk": "2.0.3",
53
- "pino": "10.1.0",
54
- "pino-pretty": "13.1.2"
52
+ "commander": "15.0.0",
53
+ "dotenv": "17.4.2",
54
+ "mqtt2ha": "4.1.4",
55
+ "mysa-js-sdk": "2.1.1",
56
+ "pino": "10.3.1",
57
+ "pino-pretty": "13.1.3"
55
58
  },
56
59
  "devDependencies": {
57
- "@commander-js/extra-typings": "14.0.0",
58
- "@eslint/js": "9.39.1",
59
- "@semantic-release/npm": "13.1.2",
60
- "@types/node": "24.10.1",
61
- "conventional-changelog-conventionalcommits": "9.1.0",
62
- "eslint": "9.39.1",
63
- "eslint-plugin-jsdoc": "61.2.1",
64
- "eslint-plugin-tsdoc": "0.5.0",
65
- "prettier": "3.6.2",
66
- "prettier-plugin-jsdoc": "1.5.0",
67
- "prettier-plugin-organize-imports": "4.3.0",
68
- "semantic-release": "25.0.2",
69
- "tsup": "8.5.1",
70
- "tsx": "4.20.6",
71
- "typescript": "5.9.3",
72
- "typescript-eslint": "8.46.4"
60
+ "@commander-js/extra-typings": "15.0.0"
73
61
  }
74
62
  }