mysa2mqtt 1.1.3 → 1.2.0

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.
Files changed (3) hide show
  1. package/README.md +42 -7
  2. package/dist/main.js +2 -2
  3. package/package.json +13 -13
package/README.md CHANGED
@@ -1,5 +1,11 @@
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
+
3
9
  [![NPM Version](https://img.shields.io/npm/v/mysa2mqtt)](https://www.npmjs.com/package/mysa2mqtt)
4
10
  [![Docker Hub](https://img.shields.io/docker/pulls/bourquep/mysa2mqtt)](https://hub.docker.com/r/bourquep/mysa2mqtt)
5
11
  [![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)
@@ -18,13 +24,13 @@ home automation platforms.
18
24
 
19
25
  ## Supported hardware
20
26
 
21
- | Model Number | Description | Supported |
22
- | ------------ | --------------------------------------------------------- | -------------------------------------------------------------------- |
23
- | `BB-V1-X` | Mysa Smart Thermostat for Electric Baseboard Heaters V1 | ✅ Tested and working |
24
- | `BB-V2-X` | Mysa Smart Thermostat for Electric Baseboard Heaters V2 | ⚠️ Partially working, in progress |
25
- | `BB-V2-X-L` | Mysa Smart Thermostat LITE for Electric Baseboard Heaters | ⚠️ Partially working, in progress; does not report power consumption |
26
- | `unknown` | Mysa Smart Thermostat for Electric In-Floor Heating | ⚠️ Should work but not tested |
27
- | `AC-V1-X` | Mysa Smart Thermostat for Mini-Split Heat Pumps & AC | 🚫 Not supported (yet) |
27
+ | Model Number | Description | Supported |
28
+ | ------------ | --------------------------------------------------------- | ----------------------------------------------------------------------- |
29
+ | `BB-V1-X` | Mysa Smart Thermostat for Electric Baseboard Heaters V1 | ✅ Tested and working |
30
+ | `BB-V2-X` | Mysa Smart Thermostat for Electric Baseboard Heaters V2 | ⚠️ Partially working, in progress |
31
+ | `BB-V2-X-L` | Mysa Smart Thermostat LITE for Electric Baseboard Heaters | ⚠️ Partially working, in progress; does not report power consumption |
32
+ | `unknown` | Mysa Smart Thermostat for Electric In-Floor Heating | ⚠️ Should work but not tested |
33
+ | `AC-V1-X` | Mysa Smart Thermostat for Mini-Split Heat Pumps & AC | ⚠️ Partially working, in progress; missing swing and position functions |
28
34
 
29
35
  ## Disclaimer
30
36
 
@@ -118,6 +124,11 @@ For development or custom modifications:
118
124
  The application can be configured using either command-line arguments or environment variables. Environment variables
119
125
  take precedence over command-line defaults.
120
126
 
127
+ > [!IMPORTANT]
128
+ > The `M2M_TEMPERATURE_UNIT` option must match Home Assistant's unit system (Settings → General → Unit System)
129
+ > so setpoints and readings are interpreted correctly. If mismatched, climate entities will show incorrect values (e.g.
130
+ > 21°C treated as 21°F) and commands may result in unexpected temperatures.
131
+
121
132
  ### Required Configuration
122
133
 
123
134
  | CLI Option | Environment Variable | Description |
@@ -145,6 +156,7 @@ take precedence over command-line defaults.
145
156
  | `-l, --log-level` | `M2M_LOG_LEVEL` | `info` | Log level: `silent`, `fatal`, `error`, `warn`, `info`, `debug`, `trace` |
146
157
  | `-f, --log-format` | `M2M_LOG_FORMAT` | `pretty` | Log format: `pretty`, `json` |
147
158
  | `-s, --mysa-session-file` | `M2M_MYSA_SESSION_FILE` | `session.json` | Path to Mysa session file |
159
+ | `-t, --temperature-unit` | `M2M_TEMPERATURE_UNIT` | `C` | Temperature unit (`C` = Celsius, `F` = Fahrenheit) |
148
160
 
149
161
  ## Usage Examples
150
162
 
@@ -369,3 +381,26 @@ copyright notice and license text in any copy of the software or substantial por
369
381
  - [mqtt2ha](https://github.com/bourquep/mqtt2ha) - MQTT to Home Assistant bridge library
370
382
  - [Commander.js](https://github.com/tj/commander.js) - Command-line argument parsing
371
383
  - [Pino](https://github.com/pinojs/pino) - Fast JSON logger
384
+
385
+ ## Contributors ✨
386
+
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!
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 x}from"mysa-js-sdk";import{pino as k}from"pino";var p=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 _,InvalidArgumentError as q,Option as s}from"commander";import{configDotenv as v}from"dotenv";import{readFileSync as M}from"fs";import{dirname as w,join as S}from"path";import{fileURLToPath as C}from"url";v({path:[".env",".env.local"],override:!0});function T(){try{let n=C(import.meta.url),t=w(n),e=S(t,"..","package.json");return JSON.parse(M(e,"utf-8")).version||"unknown"}catch{return"unknown"}}function b(n){let t=parseInt(n,10);if(isNaN(t))throw new q("Must be a number.");return t}var h=T(),O=`
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=`
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
- `,a=new _("mysa2mqtt").version(h).description("Expose Mysa smart thermostats to home automation platforms via MQTT.").addHelpText("afterAll",O).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(b).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")).parse().opts();import{readFile as A,rm as I,writeFile as P}from"fs/promises";async function g(n,t){try{t.info("Loading Mysa session...");let e=await A(n,"utf8");return JSON.parse(e)}catch{t.info("No valid Mysa session file found.")}}async function y(n,t,e){if(n)e.info("Saving Mysa session..."),await P(t,JSON.stringify(n));else try{e.debug("Removing Mysa session file..."),await I(t)}catch{}}import{Climate as D,Sensor as f}from"mqtt2ha";var c=class{constructor(t,e,i,u,d,l){this.mysaApiClient=t;this.mysaDevice=e;this.mqttSettings=i;this.logger=u;this.mysaDeviceFirmware=d;this.mysaDeviceSerialNumber=l;this.mqttDevice={identifiers:e.Id,name:e.Name,manufacturer:"Mysa",model:e.Model,sw_version:d==null?void 0:d.InstalledVersion,serial_number:l},this.mqttOrigin={name:"mysa2mqtt",sw_version:h,support_url:"https://github.com/bourquep/mysa2mqtt"},this.mqttClimate=new D({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:["off","heat"],precision:.1,temp_step:.5,temperature_unit:"C",optimistic:!0}},["action_topic","current_humidity_topic","current_temperature_topic","mode_state_topic","temperature_state_topic"],async()=>{},["mode_command_topic","power_command_topic","temperature_command_topic"],async(r,o)=>{switch(r){case"mode_command_topic":this.mysaApiClient.setDeviceState(this.mysaDevice.Id,void 0,o==="off"?"off":o==="heat"?"heat":void 0);break;case"power_command_topic":this.mysaApiClient.setDeviceState(this.mysaDevice.Id,void 0,o==="OFF"?"off":o==="ON"?"heat":void 0);break;case"temperature_command_topic":o===""?this.mysaApiClient.setDeviceState(this.mysaDevice.Id,void 0,void 0):this.mysaApiClient.setDeviceState(this.mysaDevice.Id,parseFloat(o),void 0);break}}),this.mqttTemperature=new f({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:1,force_update:!0}}),this.mqttHumidity=new f({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 f({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);async start(){if(!this.isStarted){this.isStarted=!0;try{let e=(await this.mysaApiClient.getDeviceStates()).DeviceStatesObj[this.mysaDevice.Id];this.mqttClimate.currentTemperature=e.CorrectedTemp.v,this.mqttClimate.currentHumidity=e.Humidity.v,this.mqttClimate.currentMode=e.TstatMode.v===1?"off":e.TstatMode.v===3?"heat":void 0,this.mqttClimate.currentAction=this.computeCurrentAction(void 0,e.Duty.v),this.mqttClimate.targetTemperature=this.mqttClimate.currentMode!=="off"?e.SetPoint.v:void 0,await this.mqttClimate.writeConfig(),await this.mqttTemperature.setState("state_topic",e.CorrectedTemp.v.toFixed(2)),await this.mqttTemperature.writeConfig(),await this.mqttHumidity.setState("state_topic",e.Humidity.v.toFixed(2)),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(t){throw this.isStarted=!1,t}}}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,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;break;case"heat":this.mqttClimate.currentMode="heat",this.mqttClimate.targetTemperature=t.setPoint;break}}computeCurrentAction(t,e){switch(this.mqttClimate.currentMode==="heat"?"heat":this.mqttClimate.currentMode==="off"?"off":void 0){case"off":return"off";case"heat":return t!=null?t>0?"heating":"idle":(e??0)>0?"heating":"idle";default:return"idle"}}};var m=k({name:"mysa2mqtt",level:a.logLevel,transport:a.logFormat==="pretty"?{target:"pino-pretty",options:{colorize:!0,singleLine:!0,ignore:"hostname,module",messageFormat:"\x1B[33m[{module}]\x1B[39m {msg}"}}:void 0});async function F(){m.info("Starting mysa2mqtt...");let n=await g(a.mysaSessionFile,m),t=new x(n,{logger:new p(m.child({module:"mysa-js-sdk"}))});t.emitter.on("sessionChanged",async r=>{await y(r,a.mysaSessionFile,m)}),t.isAuthenticated||(m.info("Logging in..."),await t.login(a.mysaUsername,a.mysaPassword)),m.debug("Fetching devices and firmwares...");let[e,i]=await Promise.all([t.getDevices(),t.getDeviceFirmwares()]);m.debug("Fetching serial numbers...");let u=new Map;for(let[r]of Object.entries(e.DevicesObj))try{let o=await t.getDeviceSerialNumber(r);o&&u.set(r,o)}catch(o){m.error(o,`Failed to retrieve serial number for device ${r}`)}m.debug("Initializing MQTT entities...");let d={host:a.mqttHost,port:a.mqttPort,username:a.mqttUsername,password:a.mqttPassword,client_name:a.mqttClientName,state_prefix:a.mqttTopicPrefix},l=Object.entries(e.DevicesObj).map(([,r])=>new c(t,r,d,new p(m.child({module:"thermostat",deviceId:r.Id})),i.Firmware[r.Id],u.get(r.Id)));for(let r of l)await r.start()}F().catch(n=>{m.fatal(n,"Unexpected error"),process.exit(1)});
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)});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mysa2mqtt",
3
- "version": "1.1.3",
3
+ "version": "1.2.0",
4
4
  "license": "MIT",
5
5
  "private": false,
6
6
  "description": "Expose Mysa smart thermostats to home automation platforms via MQTT.",
@@ -46,29 +46,29 @@
46
46
  "build": "tsup"
47
47
  },
48
48
  "dependencies": {
49
- "commander": "14.0.1",
49
+ "commander": "14.0.2",
50
50
  "dotenv": "17.2.3",
51
51
  "mqtt2ha": "4.1.2",
52
- "mysa-js-sdk": "1.3.3",
53
- "pino": "10.0.0",
54
- "pino-pretty": "13.1.1"
52
+ "mysa-js-sdk": "2.0.1",
53
+ "pino": "10.1.0",
54
+ "pino-pretty": "13.1.2"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@commander-js/extra-typings": "14.0.0",
58
- "@eslint/js": "9.37.0",
59
- "@semantic-release/npm": "12.0.2",
60
- "@types/node": "24.6.2",
58
+ "@eslint/js": "9.39.0",
59
+ "@semantic-release/npm": "13.1.1",
60
+ "@types/node": "24.10.0",
61
61
  "conventional-changelog-conventionalcommits": "9.1.0",
62
- "eslint": "9.37.0",
63
- "eslint-plugin-jsdoc": "60.8.1",
62
+ "eslint": "9.39.0",
63
+ "eslint-plugin-jsdoc": "61.1.11",
64
64
  "eslint-plugin-tsdoc": "0.4.0",
65
65
  "prettier": "3.6.2",
66
- "prettier-plugin-jsdoc": "1.3.3",
66
+ "prettier-plugin-jsdoc": "1.5.0",
67
67
  "prettier-plugin-organize-imports": "4.3.0",
68
- "semantic-release": "24.2.9",
68
+ "semantic-release": "25.0.1",
69
69
  "tsup": "8.5.0",
70
70
  "tsx": "4.20.6",
71
71
  "typescript": "5.9.3",
72
- "typescript-eslint": "8.45.0"
72
+ "typescript-eslint": "8.46.2"
73
73
  }
74
74
  }