mysa2mqtt 3.0.1 → 3.0.3

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 CHANGED
@@ -1,5 +1,51 @@
1
1
  # mysa2mqtt
2
2
 
3
+ ## 3.0.3
4
+
5
+ ### Patch Changes
6
+
7
+ - [#226](https://github.com/bourquep/mysa2mqtt/pull/226) [`a00f308`](https://github.com/bourquep/mysa2mqtt/commit/a00f3083c4d843c312a55e14c7903f0c4f6598cf) Thanks [@souvik101990](https://github.com/souvik101990)! - Fix fan-speed control for AC-V1-0 (CodeNum=1117) thermostats that don't enumerate `SupportedCaps.fanSpeeds`.
8
+
9
+ These devices declare fan-speed support via `SupportedCaps.keys` (the FanSpeed ACState key, `4`) and report a live `FanSpeed` using canonical `fn` values (`1/2/4/6`), but their generic IR code set omits an explicit `SupportedCaps.fanSpeeds` list. As a result the fan was effectively uncontrollable:
10
+
11
+ - `buildFanModes` (mysa2mqtt) treated "no `fanSpeeds`" as "no fan support" and advertised only `['auto']`, hiding low/medium/high in Home Assistant.
12
+ - `buildFanSpeedSendMap` (mysa-js-sdk) fell back to the legacy `1/3/5/7` map, so a `high` command sent `fn: 7` — a value these canonical devices ignore — leaving the fan stuck at its current speed.
13
+
14
+ Now:
15
+
16
+ - `buildFanModes` advertises the canonical AC speeds (`auto/low/medium/high`) when a device omits `fanSpeeds` but declares fan support via `SupportedCaps.keys`.
17
+ - `buildFanSpeedSendMap` uses the canonical `1/2/4/6` map for CodeNum=1117 devices without an explicit `fanSpeeds` list.
18
+
19
+ Verified end-to-end against an AC-V1-0 unit: Home Assistant now advertises `auto/low/medium/high` and every fan command reaches the device (fan speed changes on the thermostat).
20
+
21
+ - [#230](https://github.com/bourquep/mysa2mqtt/pull/230) [`712d4c8`](https://github.com/bourquep/mysa2mqtt/commit/712d4c8b8d7a10056395afdffbacc68faef72e89) Thanks [@bourquep](https://github.com/bourquep)! - Fill in the Docker image metadata Docker Hub shows on the tag's Specifications tab: a full set of OCI labels (title, authors, vendor, url, documentation), a `copyright` label, `NODE_ENV=production`, and the source/revision/created labels and SBOM stamped by the release build.
22
+
23
+ - Updated dependencies [[`a00f308`](https://github.com/bourquep/mysa2mqtt/commit/a00f3083c4d843c312a55e14c7903f0c4f6598cf), [`50d71a3`](https://github.com/bourquep/mysa2mqtt/commit/50d71a3b4bb6810e10ea54e11500905f2157f590)]:
24
+ - mysa-js-sdk@3.1.2
25
+
26
+ ## 3.0.2
27
+
28
+ ### Patch Changes
29
+
30
+ - [#121](https://github.com/bourquep/mysa2mqtt/pull/121) [`8fab111`](https://github.com/bourquep/mysa2mqtt/commit/8fab111568255ea6130123be8dd3fcf5cbb08b5b) Thanks [@souvik101990](https://github.com/souvik101990)! - Derive AC fan modes from `SupportedCaps` and preserve state on fan-mode changes (CodeNum=1117).
31
+
32
+ AC-V1-X thermostats (Mysa for Mini-Split) report their supported fan speeds through `SupportedCaps.fanSpeeds` and use canonical `fn` values (`[1, 2, 4, 6]`) that differ from the legacy universal mapping. `mysa2mqtt` now:
33
+
34
+ - recognizes the canonical `fn=2/4/6` values on the receive path so the current fan speed is reported instead of dropped;
35
+ - derives the advertised `fan_modes` from the device's actual `SupportedCaps` instead of a hardcoded list (devices without fan-speed support advertise only `auto`), deduplicating modes that map from both legacy and canonical raw values;
36
+ - rejects fan-mode commands the device doesn't support instead of silently reapplying the current state; and
37
+ - preserves the current target temperature and climate mode when changing fan mode, and keeps the current fan mode when a state update omits the fan speed.
38
+
39
+ `mysa-js-sdk` adds an optional per-mode `fanSpeeds` field to `SupportedCaps.modes` (the top-level `fanSpeeds` field was already present).
40
+
41
+ - [#129](https://github.com/bourquep/mysa2mqtt/pull/129) [`e81f182`](https://github.com/bourquep/mysa2mqtt/commit/e81f182cbeb84c8a5ca97f954746c7aba2f3a0e2) Thanks [@vavallee](https://github.com/vavallee)! - Retry startup on transient network errors instead of exiting immediately. DNS, TCP or TLS hiccups during the initial Cognito authentication (surfaced as generic `Network error` by `amazon-cognito-identity-js`) are now retried up to 10 times with exponential backoff before the process gives up. Configuration and programming errors still exit immediately.
42
+
43
+ - [#217](https://github.com/bourquep/mysa2mqtt/pull/217) [`0da4a1d`](https://github.com/bourquep/mysa2mqtt/commit/0da4a1d4e95dddf9638ec00747895008a10f7ba8) Thanks [@bourquep](https://github.com/bourquep)! - Updated dependencies to latest versions
44
+
45
+ - Updated dependencies [[`8fab111`](https://github.com/bourquep/mysa2mqtt/commit/8fab111568255ea6130123be8dd3fcf5cbb08b5b), [`169272d`](https://github.com/bourquep/mysa2mqtt/commit/169272d366dc76cb3d07831d752b83a7f9f57733), [`0da4a1d`](https://github.com/bourquep/mysa2mqtt/commit/0da4a1d4e95dddf9638ec00747895008a10f7ba8), [`f0dc8c1`](https://github.com/bourquep/mysa2mqtt/commit/f0dc8c1c812e469cde891b513a812e717d584aff), [`02ad499`](https://github.com/bourquep/mysa2mqtt/commit/02ad49990fd3f3b79f8c7d302fb461d29b5fb542), [`b89b238`](https://github.com/bourquep/mysa2mqtt/commit/b89b238741f94cd9557c2795929f0a7c7a60d341), [`6bd3f92`](https://github.com/bourquep/mysa2mqtt/commit/6bd3f920dc85697c2d9665b31e25b36767884ecd)]:
46
+ - mysa-js-sdk@3.1.1
47
+ - mqtt2ha@5.1.1
48
+
3
49
  ## 3.0.1
4
50
 
5
51
  ### Patch Changes
package/LICENSE.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 Pascal Bourque
3
+ Copyright (C) 2025-2026 Pascal Bourque
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -13,18 +13,19 @@ home automation platforms.
13
13
  - **MQTT Integration**: Exposes Mysa thermostats as MQTT devices compatible with Home Assistant's auto-discovery
14
14
  - **Real-time Updates**: Live temperature, humidity, and power consumption monitoring
15
15
  - **Full Control**: Set temperature, change modes (heat/off), and monitor thermostat status
16
- - **Self-healing Authentication**: Re-authenticates automatically when the Mysa session expires, with no state to persist
16
+ - **Self-healing Authentication**: Re-authenticates automatically when the Mysa session expires, with no state to
17
+ persist
17
18
  - **Configurable Logging**: Support for JSON and pretty-printed log formats with adjustable levels
18
19
 
19
20
  ## Supported hardware
20
21
 
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 measure power, but can report an estimate (see [Power reporting](#power-reporting)) |
22
+ | Model Number | Description | Supported |
23
+ | ------------ | --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
24
+ | `BB-V1-X` | Mysa Smart Thermostat for Electric Baseboard Heaters V1 | ✅ Tested and working |
25
+ | `BB-V2-X` | Mysa Smart Thermostat for Electric Baseboard Heaters V2 | ⚠️ Partially working, in progress |
26
+ | `BB-V2-X-L` | Mysa Smart Thermostat LITE for Electric Baseboard Heaters | ⚠️ Partially working, in progress; does not measure power, but can report an estimate (see [Power reporting](#power-reporting)) |
26
27
  | `INF-V1-0` | Mysa Smart Thermostat for Electric In-Floor Heating | ⚠️ Partially working, in progress; controls and a floor-temperature sensor are supported, and power can be estimated (see [Power reporting](#power-reporting)) |
27
- | `AC-V1-X` | Mysa Smart Thermostat for Mini-Split Heat Pumps & AC | ⚠️ Partially working, in progress; missing swing and position functions |
28
+ | `AC-V1-X` | Mysa Smart Thermostat for Mini-Split Heat Pumps & AC | ⚠️ Partially working, in progress; missing swing and position functions |
28
29
 
29
30
  ## Disclaimer
30
31
 
@@ -59,8 +60,8 @@ npx mysa2mqtt --help
59
60
 
60
61
  ### Option 3: Home Assistant App (Addon)
61
62
 
62
- @itsamenathan made an [app/addon](https://github.com/itsamenathan/mysa2mqtt-hass-addon) for
63
- Home Assistant that wraps this tool.
63
+ @itsamenathan made an [app/addon](https://github.com/itsamenathan/mysa2mqtt-hass-addon) for Home Assistant that wraps
64
+ this tool.
64
65
 
65
66
  ### Option 4: Development Setup
66
67
 
@@ -123,10 +124,9 @@ For development or custom modifications:
123
124
  The application can be configured using either command-line arguments or environment variables. Environment variables
124
125
  take precedence over command-line defaults.
125
126
 
126
- > [!IMPORTANT]
127
- > The `M2M_TEMPERATURE_UNIT` option must match Home Assistant's unit system (Settings General Unit System)
128
- > so setpoints and readings are interpreted correctly. If mismatched, climate entities will show incorrect values (e.g.
129
- > 21°C treated as 21°F) and commands may result in unexpected temperatures.
127
+ > [!IMPORTANT] The `M2M_TEMPERATURE_UNIT` option must match Home Assistant's unit system (Settings → General → Unit
128
+ > System) so setpoints and readings are interpreted correctly. If mismatched, climate entities will show incorrect
129
+ > values (e.g. 21°C treated as 21°F) and commands may result in unexpected temperatures.
130
130
 
131
131
  ### Required Configuration
132
132
 
@@ -150,13 +150,13 @@ take precedence over command-line defaults.
150
150
 
151
151
  #### Application Settings
152
152
 
153
- | CLI Option | Environment Variable | Default | Description |
154
- | ------------------------- | ----------------------- | -------------- | ----------------------------------------------------------------------- |
155
- | `-l, --log-level` | `M2M_LOG_LEVEL` | `info` | Log level: `silent`, `fatal`, `error`, `warn`, `info`, `debug`, `trace` |
156
- | `-f, --log-format` | `M2M_LOG_FORMAT` | `pretty` | Log format: `pretty`, `json` |
157
- | `-t, --temperature-unit` | `M2M_TEMPERATURE_UNIT` | `C` | Temperature unit (`C` = Celsius, `F` = Fahrenheit) |
158
- | `--heater-watts` | `M2M_HEATER_WATTS` | - | Rated wattage of the heaters controlled by each thermostat, as a comma-separated list of `<device>=<watts>` pairs (see [Power reporting](#power-reporting)) |
159
- | `--heartbeat-file` | `M2M_HEARTBEAT_FILE` | - | File touched on every message received from the Mysa cloud, for external liveness checks (e.g. a container liveness probe on its mtime) |
153
+ | CLI Option | Environment Variable | Default | Description |
154
+ | ------------------------ | ---------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
155
+ | `-l, --log-level` | `M2M_LOG_LEVEL` | `info` | Log level: `silent`, `fatal`, `error`, `warn`, `info`, `debug`, `trace` |
156
+ | `-f, --log-format` | `M2M_LOG_FORMAT` | `pretty` | Log format: `pretty`, `json` |
157
+ | `-t, --temperature-unit` | `M2M_TEMPERATURE_UNIT` | `C` | Temperature unit (`C` = Celsius, `F` = Fahrenheit) |
158
+ | `--heater-watts` | `M2M_HEATER_WATTS` | - | Rated wattage of the heaters controlled by each thermostat, as a comma-separated list of `<device>=<watts>` pairs (see [Power reporting](#power-reporting)) |
159
+ | `--heartbeat-file` | `M2M_HEARTBEAT_FILE` | - | File touched on every message received from the Mysa cloud, for external liveness checks (e.g. a container liveness probe on its mtime) |
160
160
 
161
161
  ### Power reporting
162
162
 
@@ -278,12 +278,12 @@ process receives is not the value you typed:
278
278
 
279
279
  Taking `pa$w0rd` and `pa#w0rd` as example passwords:
280
280
 
281
- | Where the password is set | Gotcha | Write it as |
282
- | ----------------------------------------- | ------------------------------------------------------------ | ----------------------------- |
283
- | Shell (`export`, `docker run -e`, `-p …`) | `$` and backticks are expanded inside `"…"` | `-p 'pa$w0rd'` (single quotes) |
284
- | Docker Compose `environment:` | `$` starts an interpolation (`$FOO`, `${FOO}`) | `M2M_MYSA_PASSWORD=pa$$w0rd` |
285
- | Docker Compose `env_file:` | same `$` interpolation as `environment:` | `M2M_MYSA_PASSWORD=pa$$w0rd` |
286
- | `.env` file (read by mysa2mqtt itself) | `$` is safe, but a `#` anywhere in an unquoted value comments the rest out | `M2M_MYSA_PASSWORD="pa#w0rd"` |
281
+ | Where the password is set | Gotcha | Write it as |
282
+ | ----------------------------------------- | -------------------------------------------------------------------------- | ------------------------------ |
283
+ | Shell (`export`, `docker run -e`, `-p …`) | `$` and backticks are expanded inside `"…"` | `-p 'pa$w0rd'` (single quotes) |
284
+ | Docker Compose `environment:` | `$` starts an interpolation (`$FOO`, `${FOO}`) | `M2M_MYSA_PASSWORD=pa$$w0rd` |
285
+ | Docker Compose `env_file:` | same `$` interpolation as `environment:` | `M2M_MYSA_PASSWORD=pa$$w0rd` |
286
+ | `.env` file (read by mysa2mqtt itself) | `$` is safe, but a `#` anywhere in an unquoted value comments the rest out | `M2M_MYSA_PASSWORD="pa#w0rd"` |
287
287
 
288
288
  Docker Compose is the most common culprit, and note that `env_file:` does **not** avoid it: Compose interpolates `$` in
289
289
  those files too, so `pa$w0rd` silently becomes `pa` plus whatever `$w0rd` expands to (usually nothing). Doubling it to
@@ -429,8 +429,7 @@ The `mysa2mqtt-capture` tool gathers exactly that. It logs in to your Mysa accou
429
429
  target device(s), then **passively records every shadow message** to a file until you stop it. It needs no MQTT broker —
430
430
  it only reads from Mysa.
431
431
 
432
- > [!NOTE]
433
- > The capture is passive: a device only publishes to its shadow when something changes it. You must exercise the
432
+ > [!NOTE] The capture is passive: a device only publishes to its shadow when something changes it. You must exercise the
434
433
  > thermostat from the Mysa mobile app while the capture runs, otherwise nothing is recorded.
435
434
 
436
435
  Run it from a clone of the repository (no build step needed):
@@ -462,9 +461,8 @@ While it runs, drive the thermostat from the Mysa app so every interaction is re
462
461
  Pause a few seconds between actions, then press **Ctrl+C**. Attach the resulting file to a
463
462
  [GitHub issue](https://github.com/bourquep/mysa2mqtt/issues).
464
463
 
465
- > [!IMPORTANT]
466
- > The metadata dump can contain account identifiers (`Owner`, `Home`, `AllowedUsers`, `Zone`). Review the file before
467
- > sharing it publicly. Authentication tokens and your password are **never** included.
464
+ > [!IMPORTANT] The metadata dump can contain account identifiers (`Owner`, `Home`, `AllowedUsers`, `Zone`). Review the
465
+ > file before sharing it publicly. Authentication tokens and your password are **never** included.
468
466
 
469
467
  ## Contributing
470
468
 
@@ -479,7 +477,7 @@ copyright notice and license text in any copy of the software or substantial por
479
477
 
480
478
  ## Copyright
481
479
 
482
- © 2025 Pascal Bourque
480
+ © 2025-2026 Pascal Bourque
483
481
 
484
482
  ## Support
485
483
 
@@ -499,6 +497,6 @@ copyright notice and license text in any copy of the software or substantial por
499
497
 
500
498
  ## Contributors ✨
501
499
 
502
- This project follows the [all-contributors](https://github.com/all-contributors/all-contributors)
503
- specification. See the [contributor list](https://github.com/bourquep/mysa2mqtt#contributors-) in the
504
- repository README. Contributions of any kind welcome!
500
+ This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. See the
501
+ [contributor list](https://github.com/bourquep/mysa2mqtt#contributors-) in the repository README. Contributions of any
502
+ kind welcome!
package/dist/capture.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  /*
3
3
  mysa2mqtt
4
- Copyright (C) 2025 Pascal Bourque
4
+ Copyright (C) 2025-2026 Pascal Bourque
5
5
 
6
6
  Permission is hereby granted, free of charge, to any person obtaining a copy
7
7
  of this software and associated documentation files (the "Software"), to deal
@@ -22,6 +22,6 @@ 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{a as C}from"./chunk-QBMDKIY7.js";import{Command as O,InvalidArgumentError as b,Option as l}from"commander";import{configDotenv as M}from"dotenv";import{chmodSync as T,createWriteStream as E,existsSync as A}from"fs";import{MysaApiClient as D,UnauthenticatedError as I}from"mysa-js-sdk";import{pino as $}from"pino";M({path:[".env",".env.local"],override:!0});var t=new O("mysa2mqtt-capture").description("Capture the raw AWS IoT Device Shadow traffic of Mysa central-HVAC (ST-V1) thermostats, to help implement support for them. Logs in to Mysa, dumps the device metadata, then records every shadow message until you press Ctrl+C. Drive the thermostat from the Mysa app while it runs.").addOption(new l("-u, --mysa-username <mysaUsername>","Mysa account username (email)").env("M2M_MYSA_USERNAME").makeOptionMandatory()).addOption(new l("-p, --mysa-password <mysaPassword>","Mysa account password").env("M2M_MYSA_PASSWORD").makeOptionMandatory()).addOption(new l("-d, --device <deviceIdOrName...>","limit capture to these device id(s) or name(s). Repeatable. Defaults to every central-HVAC (ST-*) device")).addOption(new l("--all-devices","capture from every device on the account, not just central-HVAC ones")).addOption(new l("-e, --extra-topic <topicFilter...>","additional raw MQTT topic filter(s) to subscribe to, on top of the per-device shadow topics. Repeatable")).addOption(new l("-o, --output <file>","also write the metadata dump and every captured message to this file")).addOption(new l("--duration <seconds>","stop automatically after this many seconds (default: run until Ctrl+C)").argParser(o=>{let s=parseInt(o,10);if(String(s)!==o.trim()||s<0)throw new b("Must be a whole number of seconds (0 to run until Ctrl+C).");return s}).default(0)).addOption(new l("-l, --log-level <logLevel>","log level").choices(["silent","fatal","error","warn","info","debug","trace"]).env("M2M_LOG_LEVEL").default("info")).parse().opts(),n=$({name:"mysa2mqtt-capture",level:t.logLevel,transport:{target:"pino-pretty",options:{colorize:!0,singleLine:!0,ignore:"hostname,module,pid"}}}),a;function e(o){process.stdout.write(o+`
25
+ import{a as C}from"./chunk-O3OLOFHP.js";import{Command as O,InvalidArgumentError as b,Option as l}from"commander";import{configDotenv as M}from"dotenv";import{chmodSync as T,createWriteStream as E,existsSync as A}from"fs";import{MysaApiClient as D,UnauthenticatedError as I}from"mysa-js-sdk";import{pino as $}from"pino";M({path:[".env",".env.local"],override:!0});var t=new O("mysa2mqtt-capture").description("Capture the raw AWS IoT Device Shadow traffic of Mysa central-HVAC (ST-V1) thermostats, to help implement support for them. Logs in to Mysa, dumps the device metadata, then records every shadow message until you press Ctrl+C. Drive the thermostat from the Mysa app while it runs.").addOption(new l("-u, --mysa-username <mysaUsername>","Mysa account username (email)").env("M2M_MYSA_USERNAME").makeOptionMandatory()).addOption(new l("-p, --mysa-password <mysaPassword>","Mysa account password").env("M2M_MYSA_PASSWORD").makeOptionMandatory()).addOption(new l("-d, --device <deviceIdOrName...>","limit capture to these device id(s) or name(s). Repeatable. Defaults to every central-HVAC (ST-*) device")).addOption(new l("--all-devices","capture from every device on the account, not just central-HVAC ones")).addOption(new l("-e, --extra-topic <topicFilter...>","additional raw MQTT topic filter(s) to subscribe to, on top of the per-device shadow topics. Repeatable")).addOption(new l("-o, --output <file>","also write the metadata dump and every captured message to this file")).addOption(new l("--duration <seconds>","stop automatically after this many seconds (default: run until Ctrl+C)").argParser(o=>{let s=parseInt(o,10);if(String(s)!==o.trim()||s<0)throw new b("Must be a whole number of seconds (0 to run until Ctrl+C).");return s}).default(0)).addOption(new l("-l, --log-level <logLevel>","log level").choices(["silent","fatal","error","warn","info","debug","trace"]).env("M2M_LOG_LEVEL").default("info")).parse().opts(),n=$({name:"mysa2mqtt-capture",level:t.logLevel,transport:{target:"pino-pretty",options:{colorize:!0,singleLine:!0,ignore:"hostname,module,pid"}}}),a;function e(o){process.stdout.write(o+`
26
26
  `),a==null||a.write(o+`
27
27
  `)}async function N(o){try{await o.login()}catch(s){throw s instanceof I?new Error("Mysa rejected the credentials. Verify that the same username and password let you sign in to the Mysa mobile app, and beware of a shell or .env file mangling special characters ($, `) in the password.",{cause:s}):s}}function L(o){let s=Object.entries(o);if(t.device&&t.device.length>0){let r=t.device.map(c=>c.toLowerCase());return s.filter(([c,d])=>r.includes(c.toLowerCase())||d.Name!==void 0&&r.includes(d.Name.toLowerCase())).map(([c])=>c)}return t.allDevices?s.map(([r])=>r):s.filter(([,r])=>{var u;return(u=r.Model)==null?void 0:u.toUpperCase().startsWith("ST-")}).map(([r])=>r)}async function R(){var v,y;t.output&&(A(t.output)&&T(t.output,384),a=E(t.output,{flags:"w",mode:384}),a.on("error",i=>{n.error(i,`Failed to write to output file '${t.output}'`)}),n.info(`Writing metadata and captured messages to '${t.output}'`));let o=new D({username:t.mysaUsername,password:t.mysaPassword},{logger:new C(n.child({module:"mysa-js-sdk"}))});n.info("Logging in to Mysa..."),await N(o),n.info("Fetching devices, firmwares and states...");let[s,r,u]=await Promise.all([o.getDevices(),o.getDeviceFirmwares(),o.getDeviceStates()]),c=L(s.DevicesObj);if(c.length===0){n.error("No matching devices found. If your central-HVAC thermostat did not match the ST-* filter, re-run with `--all-devices` to list every device, or target it explicitly with `--device <id-or-name>`."),e("=== ALL DEVICES (no target matched) ==="),e(JSON.stringify(s.DevicesObj,null,2)),a==null||a.end();return}e("=================================================================="),e(" Mysa central-HVAC capture"),e(` Generated: ${new Date().toISOString()}`),e(" NOTE: this dump may contain account identifiers (Owner, Home,"),e(" AllowedUsers, Zone). Review before sharing publicly. Auth tokens"),e(" and passwords are NEVER included."),e("==================================================================");let d=[];for(let i of c){let p=s.DevicesObj[i],w=(v=r.Firmware)==null?void 0:v[i],S=(y=u.DeviceStatesObj)==null?void 0:y[i];e(""),e("------------------------------------------------------------------"),e(`DEVICE ${p.Name??"(unnamed)"} \u2014 model ${p.Model} \u2014 id ${i}`),e(`Firmware: ${(w==null?void 0:w.InstalledVersion)??"unknown"}`),e("------------------------------------------------------------------"),e("--- device metadata (REST /devices) ---"),e(JSON.stringify(p,null,2)),e("--- device state (REST /states) ---"),e(JSON.stringify(S??null,null,2)),d.push(`$aws/things/${i}/shadow/#`),d.push(`/v1/dev/${i}/#`)}t.extraTopic&&d.push(...t.extraTopic),e(""),e("=== SUBSCRIBING TO TOPIC FILTERS ===");for(let i of d)e(` ${i}`);let h=0,m=new Set;await o.startRawTopicCapture(d,(i,p)=>{h++,m.add(i),e(""),e(`<<< [${new Date().toISOString()}] ${i}`),e(p)}),e(""),e("=================================================================="),e(" Capture is running. Now, in the Mysa mobile app, exercise the"),e(" thermostat so every interaction is recorded:"),e(" 1. Turn the system OFF, then back ON."),e(" 2. Switch modes: Heat, Cool, Auto, Fan-only (whatever it has)."),e(" 3. Change the heat setpoint, then the cool setpoint."),e(" 4. In Auto, change both setpoints (and the deadband if shown)."),e(" 5. Change the fan mode (Auto / On / Circulate)."),e(" 6. Leave it idle a few minutes to catch periodic telemetry."),e(" Pause a few seconds between actions. Press Ctrl+C when done."),e("==================================================================");let g=!1,f=i=>{g||(g=!0,n.info(`Stopping capture (${i}). Captured ${h} message(s) across ${m.size} topic(s).`),m.size===0&&n.warn("No shadow messages were captured. Either the thermostat was not touched during the capture, or the account's AWS IoT policy does not allow subscribing to its shadow topics (check the log above for subscribe failures). Re-run with `--log-level debug` to see the per-filter subscribe results."),e(""),e(`=== END OF CAPTURE \u2014 ${h} message(s), ${m.size} distinct topic(s) ===`),a?a.end(()=>process.exit(0)):process.exit(0))};process.on("SIGINT",()=>f("Ctrl+C")),process.on("SIGTERM",()=>f("SIGTERM")),t.duration&&t.duration>0&&(n.info(`Will stop automatically after ${t.duration}s.`),setTimeout(()=>f(`--duration ${t.duration}s elapsed`),t.duration*1e3))}R().catch(o=>{n.error(o,"Capture failed"),a?a.end(()=>process.exit(1)):process.exit(1)});
@@ -1,6 +1,6 @@
1
1
  /*
2
2
  mysa2mqtt
3
- Copyright (C) 2025 Pascal Bourque
3
+ Copyright (C) 2025-2026 Pascal Bourque
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/dist/main.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  /*
3
3
  mysa2mqtt
4
- Copyright (C) 2025 Pascal Bourque
4
+ Copyright (C) 2025-2026 Pascal Bourque
5
5
 
6
6
  Permission is hereby granted, free of charge, to any person obtaining a copy
7
7
  of this software and associated documentation files (the "Software"), to deal
@@ -22,10 +22,10 @@ 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{a as A}from"./chunk-QBMDKIY7.js";import{writeFile as ie}from"fs/promises";import{MysaApiClient as ae,UnauthenticatedError as re}from"mysa-js-sdk";import{pino as ne}from"pino";import{Command as W,InvalidArgumentError as _,Option as l}from"commander";import{configDotenv as Q}from"dotenv";import{readFileSync as Y}from"fs";import{dirname as B,join as V}from"path";import{fileURLToPath as j}from"url";Q({path:[".env",".env.local"],override:!0});function z(){try{let i=j(import.meta.url),e=B(i),t=V(e,"..","package.json");return JSON.parse(Y(t,"utf-8")).version||"unknown"}catch{return"unknown"}}function E(i){let e=parseInt(i,10);if(isNaN(e))throw new _("Must be a number.");return e}function X(i){let e=E(i);if(String(e)!==i.trim())throw new _("Must be a whole number of seconds.");if(e!==0&&e<30)throw new _("Must be 0 (disabled) or at least 30 seconds.");return e}function J(i){let e=new Map;for(let t of i.split(",")){let a=t.trim();if(a.length===0)continue;let n=a.lastIndexOf("=");if(n<0)throw new _(`'${a}' is not a <device>=<watts> pair.`);let s=a.slice(0,n).trim(),c=Number(a.slice(n+1).trim());if(s.length===0)throw new _(`'${a}' is missing a device id or name.`);if(!Number.isFinite(c)||c<=0)throw new _(`'${a}' must specify a wattage greater than zero.`);e.set(s.toLowerCase(),c)}return e}var R=z(),K=`
26
- Copyright (c) 2025 Pascal Bourque
25
+ import{a as E}from"./chunk-O3OLOFHP.js";import{writeFile as ne}from"fs/promises";import{MysaApiClient as oe,UnauthenticatedError as se}from"mysa-js-sdk";import{pino as me}from"pino";import{Command as Q,InvalidArgumentError as _,Option as l}from"commander";import{configDotenv as Y}from"dotenv";import{readFileSync as B}from"fs";import{dirname as j,join as X}from"path";import{fileURLToPath as V}from"url";Y({path:[".env",".env.local"],override:!0});function z(){try{let i=V(import.meta.url),e=j(i),t=X(e,"..","package.json");return JSON.parse(B(t,"utf-8")).version||"unknown"}catch{return"unknown"}}function D(i){let e=parseInt(i,10);if(isNaN(e))throw new _("Must be a number.");return e}function J(i){let e=D(i);if(String(e)!==i.trim())throw new _("Must be a whole number of seconds.");if(e!==0&&e<30)throw new _("Must be 0 (disabled) or at least 30 seconds.");return e}function K(i){let e=new Map;for(let t of i.split(",")){let a=t.trim();if(a.length===0)continue;let r=a.lastIndexOf("=");if(r<0)throw new _(`'${a}' is not a <device>=<watts> pair.`);let s=a.slice(0,r).trim(),m=Number(a.slice(r+1).trim());if(s.length===0)throw new _(`'${a}' is missing a device id or name.`);if(!Number.isFinite(m)||m<=0)throw new _(`'${a}' must specify a wattage greater than zero.`);e.set(s.toLowerCase(),m)}return e}var R=z(),Z=`
26
+ Copyright (C) 2025-2026 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
- `,m=new W("mysa2mqtt").version(R).description("Expose Mysa smart thermostats to home automation platforms via MQTT.").addHelpText("afterAll",K).addOption(new l("-l, --log-level <logLevel>","log level").choices(["silent","fatal","error","warn","info","debug","trace"]).env("M2M_LOG_LEVEL").default("info").helpGroup("Configuration")).addOption(new l("-f, --log-format <logFormat>","log format").choices(["pretty","json"]).env("M2M_LOG_FORMAT").default("pretty").helpGroup("Configuration")).addOption(new l("-H, --mqtt-host <mqttHost>","hostname of the MQTT broker").env("M2M_MQTT_HOST").makeOptionMandatory().helpGroup("MQTT")).addOption(new l("-P, --mqtt-port <mqttPort>","port of the MQTT broker").env("M2M_MQTT_PORT").argParser(E).default(1883).helpGroup("MQTT")).addOption(new l("-U, --mqtt-username <mqttUsername>","username of the MQTT broker").env("M2M_MQTT_USERNAME").helpGroup("MQTT")).addOption(new l("-B, --mqtt-password <mqttPassword>","password of the MQTT broker").env("M2M_MQTT_PASSWORD").helpGroup("MQTT")).addOption(new l("-u, --mysa-username <mysaUsername>","Mysa account username").env("M2M_MYSA_USERNAME").makeOptionMandatory().helpGroup("Mysa")).addOption(new l("-p, --mysa-password <mysaPassword>","Mysa account password").env("M2M_MYSA_PASSWORD").makeOptionMandatory().helpGroup("Mysa")).addOption(new l("-N, --mqtt-client-name <mqttClientName>","name of the MQTT client").env("M2M_MQTT_CLIENT_NAME").default("mysa2mqtt").helpGroup("MQTT")).addOption(new l("-T, --mqtt-topic-prefix <mqttTopicPrefix>","prefix of the MQTT topic").env("M2M_MQTT_TOPIC_PREFIX").default("mysa2mqtt").helpGroup("MQTT")).addOption(new l("--temperature-unit <temperatureUnit>","temperature unit (C or F)").env("M2M_TEMPERATURE_UNIT").choices(["C","F"]).default("C").helpGroup("Configuration")).addOption(new l("--heater-watts <heaterWatts>",'rated wattage of the heaters controlled by each thermostat, as a comma-separated list of <device>=<watts> pairs, where <device> is a device id or name (e.g. "Kitchen=1500,<device-id>=750"). Required for V2 thermostats to report power, as they do not measure current themselves').env("M2M_HEATER_WATTS").argParser(J).helpGroup("Configuration")).addOption(new l("--poll-interval-seconds <pollIntervalSeconds>","how often, in seconds, to refresh device state from the Mysa REST API. This keeps Home Assistant current even when the real-time connection cannot be established or is unstable. Set to 0 to disable, or to at least 30").env("M2M_POLL_INTERVAL_SECONDS").argParser(X).default(60).helpGroup("Configuration")).addOption(new l("--heartbeat-file <heartbeatFile>","file touched on every message received from the Mysa cloud, for external liveness checks").env("M2M_HEARTBEAT_FILE").helpGroup("Configuration")).parse().opts();import{Climate as Z,Sensor as T}from"mqtt2ha";var I=["off","heat"],D=["off","heat","cool","dry","fan_only","auto"],ee={1:"off",2:"auto",3:"heat",4:"cool",5:"fan_only",6:"dry"},P=["auto","low","medium","high","max"],te={1:"auto",3:"low",5:"medium",7:"high",8:"max"},O=3e4,N=3e5,F=Math.ceil(Math.log2(N/O)),C=class{constructor(e,t,a,n,s,c,h,q){this.mysaApiClient=e;this.mysaDevice=t;this.mqttSettings=a;this.logger=n;this.mysaDeviceFirmware=s;this.mysaDeviceSerialNumber=c;this.temperatureUnit=h;this.heaterWatts=q;let f=(h??"C")==="C";this.mqttDevice={identifiers:t.Id,name:t.Name,manufacturer:"Mysa",model:t.Model,sw_version:s==null?void 0:s.InstalledVersion,serial_number:c},this.mqttOrigin={name:"mysa2mqtt",sw_version:R,support_url:"https://github.com/bourquep/mysa2mqtt"};let u=t.Model.startsWith("AC");this.deviceType=u?"AC":"BB";let w=/-v2-/i.test(t.Model),v=/^INF-/i.test(t.Model),d=!u&&(!(w||v)||q!=null);this.mqttClimate=new Z({mqtt:this.mqttSettings,logger:this.logger,component:{component:"climate",device:this.mqttDevice,origin:this.mqttOrigin,unique_id:`mysa_${t.Id}_climate`,name:"Thermostat",min_temp:t.MinSetpoint,max_temp:t.MaxSetpoint,modes:u?D:I,fan_modes:u?P:void 0,precision:f?.1:1,temp_step:f?.5:1,temperature_unit:"C",optimistic:!0}},u?["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()=>{},u?["mode_command_topic","power_command_topic","temperature_command_topic","fan_mode_command_topic"]:["mode_command_topic","power_command_topic","temperature_command_topic"],async(S,g)=>{switch(S){case"mode_command_topic":{let p=g,M=u?D.includes(p)?p:void 0:I.includes(p)?p:void 0;await this.setDeviceState(void 0,M);break}case"power_command_topic":await this.setDeviceState(void 0,g==="OFF"?"off":g==="ON"&&!u?"heat":void 0);break;case"temperature_command_topic":if(g==="")await this.setDeviceState(void 0,void 0);else{let p=parseFloat(g);if(!f){let M=b=>Math.round(b*2)/2,L=(b,$,G)=>Math.min(G,Math.max($,b)),U=M(p);p=L(U,this.mysaDevice.MinSetpoint??0,this.mysaDevice.MaxSetpoint??100)}await this.setDeviceState(p,void 0)}break;case"fan_mode_command_topic":{let p=g,M=P.includes(p)?p:void 0;await this.setDeviceState(void 0,void 0,M);break}}}),this.mqttTemperature=new T({mqtt:this.mqttSettings,logger:this.logger,component:{component:"sensor",device:this.mqttDevice,origin:this.mqttOrigin,unique_id:`mysa_${t.Id}_temperature`,name:"Current temperature",device_class:"temperature",state_class:"measurement",unit_of_measurement:"\xB0C",suggested_display_precision:f?.1:0,force_update:!0}}),this.mqttFloorTemperature=v?new T({mqtt:this.mqttSettings,logger:this.logger,component:{component:"sensor",device:this.mqttDevice,origin:this.mqttOrigin,unique_id:`mysa_${t.Id}_floor_temperature`,name:"Floor temperature",device_class:"temperature",state_class:"measurement",unit_of_measurement:"\xB0C",suggested_display_precision:f?.1:0,force_update:!0}}):void 0,this.mqttHumidity=new T({mqtt:this.mqttSettings,logger:this.logger,component:{component:"sensor",device:this.mqttDevice,origin:this.mqttOrigin,unique_id:`mysa_${t.Id}_humidity`,name:"Current humidity",device_class:"humidity",state_class:"measurement",unit_of_measurement:"%",suggested_display_precision:0,force_update:!0}});let y=new T({mqtt:this.mqttSettings,logger:this.logger,component:{component:"sensor",device:this.mqttDevice,origin:this.mqttOrigin,unique_id:`mysa_${t.Id}_power`,name:"Current power",device_class:"power",state_class:"measurement",unit_of_measurement:"W",suggested_display_precision:0,force_update:!0}});this.mqttPower=d?y:void 0,this.mqttRetiredPower=d?void 0:y}mysaApiClient;mysaDevice;mqttSettings;logger;mysaDeviceFirmware;mysaDeviceSerialNumber;temperatureUnit;heaterWatts;isStarted=!1;realtimeGeneration=0;realtimeRetryAttempt=0;realtimeRetryTimer;mqttDevice;mqttOrigin;mqttClimate;mqttTemperature;mqttFloorTemperature;mqttHumidity;mqttPower;mqttRetiredPower;mysaStatusUpdateHandler=e=>{this.handleMysaStatusUpdate(e).catch(t=>{this.logger.error("Failed to handle Mysa status update",{error:t,deviceId:this.mysaDevice.Id})})};mysaStateChangeHandler=e=>{this.handleMysaStateChange(e).catch(t=>{this.logger.error("Failed to handle Mysa state change",{error:t,deviceId:this.mysaDevice.Id})})};deviceType;async start(){var e,t;if(!this.isStarted){this.isStarted=!0,this.realtimeGeneration+=1;try{let n=(await this.mysaApiClient.getDeviceStates()).DeviceStatesObj[this.mysaDevice.Id];n!=null&&await this.publishRestState(n),await this.mqttClimate.writeConfig(),await this.mqttTemperature.writeConfig(),await((e=this.mqttFloorTemperature)==null?void 0:e.writeConfig()),await this.mqttHumidity.writeConfig(),this.mqttPower!=null&&(await this.mqttPower.setState("state_topic","None"),await this.mqttPower.writeConfig()),await((t=this.mqttRetiredPower)==null?void 0:t.removeConfig()),this.mysaApiClient.emitter.on("statusChanged",this.mysaStatusUpdateHandler),this.mysaApiClient.emitter.on("stateChanged",this.mysaStateChangeHandler),await this.startRealtimeUpdates()}catch(a){throw this.isStarted=!1,this.realtimeGeneration+=1,this.clearRealtimeRetry(),a}}}async refreshFromRest(e){if(!(!this.isStarted||e==null))try{await this.publishRestState(e)}catch(t){this.logger.error("Failed to apply REST state poll",{error:t,deviceId:this.mysaDevice.Id})}}async publishRestState(e){var t,a,n,s,c,h;this.mqttClimate.currentTemperature=(t=e.CorrectedTemp)==null?void 0:t.v,this.mqttClimate.currentHumidity=(a=e.Humidity)==null?void 0:a.v,this.mqttClimate.currentMode=ee[(n=e.TstatMode)==null?void 0:n.v]??this.mqttClimate.currentMode,this.mqttClimate.currentFanMode=te[(s=e.FanSpeed)==null?void 0:s.v]??this.mqttClimate.currentFanMode,this.mqttClimate.currentAction=this.computeCurrentAction(void 0,(c=e.Duty)==null?void 0:c.v),this.mqttClimate.targetTemperature=this.mqttClimate.currentMode!=="off"?(h=e.SetPoint)==null?void 0:h.v:void 0,await this.mqttTemperature.setState("state_topic",e.CorrectedTemp!=null?e.CorrectedTemp.v.toFixed(2):"None"),await this.mqttHumidity.setState("state_topic",e.Humidity!=null?e.Humidity.v.toFixed(2):"None")}async stop(){var e,t;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((e=this.mqttPower)==null?void 0:e.setState("state_topic","None")),await this.mqttTemperature.setState("state_topic","None"),await((t=this.mqttFloorTemperature)==null?void 0:t.setState("state_topic","None")),await this.mqttHumidity.setState("state_topic","None"))}async setDeviceState(e,t,a){try{await this.mysaApiClient.setDeviceState(this.mysaDevice.Id,e,t,a)}catch(n){this.logger.error("Failed to update Mysa device state",{error:n,deviceId:this.mysaDevice.Id})}}async startRealtimeUpdates(){let e=this.realtimeGeneration;try{if(await this.mysaApiClient.startRealtimeUpdates(this.mysaDevice.Id),!this.isStarted||e!==this.realtimeGeneration){await this.stopRealtimeUpdates();return}this.realtimeRetryAttempt=0,this.logger.info("Started realtime updates",{deviceId:this.mysaDevice.Id})}catch(t){this.isStarted&&e===this.realtimeGeneration&&this.scheduleRealtimeRetry(t)}}async stopRealtimeUpdates(){try{await this.mysaApiClient.stopRealtimeUpdates(this.mysaDevice.Id)}catch(e){this.logger.warn("Failed to stop realtime updates",{error:e,deviceId:this.mysaDevice.Id})}}scheduleRealtimeRetry(e){if(!this.isStarted||this.realtimeRetryTimer!=null)return;let t=Math.min(this.realtimeRetryAttempt,F),a=Math.min(N,O*2**t);this.realtimeRetryAttempt=Math.min(this.realtimeRetryAttempt+1,F),this.logger.error("Failed to start realtime updates; retrying",{error:e,deviceId:this.mysaDevice.Id,retryDelayMs:a}),this.realtimeRetryTimer=setTimeout(()=>{this.realtimeRetryTimer=void 0,this.startRealtimeUpdates()},a)}clearRealtimeRetry(){this.realtimeRetryTimer!=null&&(clearTimeout(this.realtimeRetryTimer),this.realtimeRetryTimer=void 0)}async handleMysaStatusUpdate(e){var t;if(!(!this.isStarted||e.deviceId!==this.mysaDevice.Id)){if(this.mqttClimate.currentAction=this.computeCurrentAction(e.current,e.dutyCycle),this.mqttClimate.currentTemperature=e.temperature,this.mqttClimate.currentHumidity=e.humidity,this.mqttClimate.targetTemperature=this.mqttClimate.currentMode!=="off"?e.setPoint:void 0,this.mqttPower!=null){let a=this.computeWatts(e);this.logger.debug("Computed power draw",{current:e.current,dutyCycle:e.dutyCycle,watts:a}),await this.mqttPower.setState("state_topic",a!=null?a.toFixed(2):"None")}await this.mqttTemperature.setState("state_topic",e.temperature.toFixed(2)),e.floorTemperature!=null&&await((t=this.mqttFloorTemperature)==null?void 0:t.setState("state_topic",e.floorTemperature.toFixed(2))),await this.mqttHumidity.setState("state_topic",e.humidity.toFixed(2))}}async handleMysaStateChange(e){if(!(!this.isStarted||e.deviceId!==this.mysaDevice.Id))switch(e.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=e.mode,this.deviceType==="AC"&&(this.mqttClimate.currentAction=this.computeCurrentAction()),this.mqttClimate.targetTemperature=e.setPoint,this.mqttClimate.currentFanMode=e.fanSpeed;break;case"dry":case"fan_only":this.mqttClimate.currentMode=e.mode,this.mqttClimate.currentAction=this.computeCurrentAction(),this.mqttClimate.currentFanMode=e.fanSpeed;break;default:this.mqttClimate.currentMode!=="off"&&(this.mqttClimate.targetTemperature=e.setPoint),e.fanSpeed!==void 0&&(this.mqttClimate.currentFanMode=e.fanSpeed);break}}computeWatts(e){if(e.current!=null&&this.mysaDevice.Voltage!=null)return this.mysaDevice.Voltage*e.current;if(e.dutyCycle!=null&&this.heaterWatts!=null)return this.heaterWatts*Math.min(Math.max(e.dutyCycle,0),1)}computeCurrentAction(e,t){let a=this.mqttClimate.currentMode;switch(D.includes(a)?a:void 0){case"off":return"off";case"heat":return this.deviceType==="BB"?e!=null?e>0?"heating":"idle":(t??0)>0?"heating":"idle":"heating";case"cool":return"cooling";case"fan_only":return"fan";case"dry":return"drying";default:return"idle"}}};var x=3e4,H=3e5,oe=Math.ceil(Math.log2(H/x)),o=ne({name:"mysa2mqtt",level:m.logLevel,transport:m.logFormat==="pretty"?{target:"pino-pretty",options:{colorize:!0,singleLine:!0,ignore:"hostname,module",messageFormat:"\x1B[33m[{module}]\x1B[39m {msg}"}}:void 0});async function se(){o.info("Starting mysa2mqtt...");let i=new ae({username:m.mysaUsername,password:m.mysaPassword},{logger:new A(o.child({module:"mysa-js-sdk"}))}),e=m.heartbeatFile;if(e){let r=0;i.emitter.on("rawRealtimeMessageReceived",()=>{let d=Date.now();d-r<1e4||(r=d,ie(e,`${new Date(d).toISOString()}
31
- `).catch(y=>{o.warn(y,`Failed to write heartbeat file '${e}'`)}))})}o.info("Logging in..."),await ue(i),o.debug("Fetching devices and firmwares...");let[t,a]=await Promise.all([i.getDevices(),i.getDeviceFirmwares()]);o.debug("Fetching serial numbers...");let n=new Map;for(let[r]of Object.entries(t.DevicesObj))try{let d=await i.getDeviceSerialNumber(r);d&&n.set(r,d)}catch(d){o.error(d,`Failed to retrieve serial number for device ${r}`)}o.debug("Initializing MQTT entities...");let s={host:m.mqttHost,port:m.mqttPort,username:m.mqttUsername,password:m.mqttPassword,client_name:m.mqttClientName,state_prefix:m.mqttTopicPrefix},c=m.heaterWatts??new Map,h=new Set;function q(r){for(let d of[r.Id,r.Name]){let y=d==null?void 0:d.toLowerCase();if(y==null)continue;let S=c.get(y);if(S!=null)return h.add(y),S}}let f=Object.entries(t.DevicesObj).map(([,r])=>new C(i,r,s,new A(o.child({module:"thermostat",deviceId:r.Id})),a.Firmware[r.Id],n.get(r.Id),m.temperatureUnit,q(r))),u=c.size-h.size;u>0&&o.warn(`${u} of the ${c.size} heater wattage entries match no device id or name. Check the configured names and ids against your devices.`);let w=0,v=[];for(let r of f)await pe(r)?w+=1:v.push(r);if(f.length>0&&w===0)throw new Error("Failed to start any thermostats");for(let r of v)k(r);me(i,f)}function me(i,e){let t=m.pollIntervalSeconds;if(t<=0||e.length===0)return;let a=new Map(e.map(s=>[s.mysaDevice.Id,s]));o.info(`Polling device state over REST every ${t}s`);let n=!1;setInterval(()=>{if(n){o.debug("Skipping REST state poll; previous poll still in flight");return}n=!0,(async()=>{try{let s=await i.getDeviceStates();await Promise.all(Array.from(a,([c,h])=>h.refreshFromRest(s.DeviceStatesObj[c])))}catch(s){o.warn(s,"Periodic REST state poll failed")}finally{n=!1}})()},t*1e3)}var de="NotAuthorizedException",ce="UserNotFoundException";function le(i){let e=i.cause;if(typeof e!="object"||e===null)return;let t=("code"in e?e.code:void 0)??("name"in e?e.name:void 0);return typeof t=="string"?t:void 0}async function ue(i){o.debug(`Authenticating with a password of ${m.mysaPassword.length} character(s).`);try{await i.login()}catch(e){let t=e instanceof re?le(e):void 0;throw t===de?new Error("Mysa rejected the credentials. Verify that they let you sign in to the Mysa mobile app, then check that the password reaches mysa2mqtt intact: a shell expands $ and ` inside double quotes, and Docker Compose expands $ in `environment:` entries and in `env_file:` files, where a $ must be written as $$. An `env_file:` entry declared with `format: raw` is the exception -- it takes the password verbatim, so a $ stays a single $ there. Re-run with --log-level debug to log the length of the password that was received and compare it against your actual password.",{cause:e}):t===ce?new Error("Mysa does not recognize that username. Check --mysa-username (M2M_MYSA_USERNAME): it must be the email address you sign in to the Mysa mobile app with.",{cause:e}):e}}async function pe(i){try{return await i.start(),!0}catch(e){return o.error(e,`Failed to start thermostat ${i.mysaDevice.Id}`),!1}}function k(i,e=0){let t=Math.min(e,oe),a=Math.min(H,x*2**t);o.info(`Retrying thermostat ${i.mysaDevice.Id} startup in ${a}ms`),setTimeout(()=>{he(i,e+1)},a)}async function he(i,e){try{await i.start(),o.info(`Started thermostat ${i.mysaDevice.Id} after retry`)}catch(t){o.error(t,`Failed to start thermostat ${i.mysaDevice.Id}`),k(i,e)}}se().catch(i=>{o.fatal(i,"Unexpected error"),process.exit(1)});
30
+ `,d=new Q("mysa2mqtt").version(R).description("Expose Mysa smart thermostats to home automation platforms via MQTT.").addHelpText("afterAll",Z).addOption(new l("-l, --log-level <logLevel>","log level").choices(["silent","fatal","error","warn","info","debug","trace"]).env("M2M_LOG_LEVEL").default("info").helpGroup("Configuration")).addOption(new l("-f, --log-format <logFormat>","log format").choices(["pretty","json"]).env("M2M_LOG_FORMAT").default("pretty").helpGroup("Configuration")).addOption(new l("-H, --mqtt-host <mqttHost>","hostname of the MQTT broker").env("M2M_MQTT_HOST").makeOptionMandatory().helpGroup("MQTT")).addOption(new l("-P, --mqtt-port <mqttPort>","port of the MQTT broker").env("M2M_MQTT_PORT").argParser(D).default(1883).helpGroup("MQTT")).addOption(new l("-U, --mqtt-username <mqttUsername>","username of the MQTT broker").env("M2M_MQTT_USERNAME").helpGroup("MQTT")).addOption(new l("-B, --mqtt-password <mqttPassword>","password of the MQTT broker").env("M2M_MQTT_PASSWORD").helpGroup("MQTT")).addOption(new l("-u, --mysa-username <mysaUsername>","Mysa account username").env("M2M_MYSA_USERNAME").makeOptionMandatory().helpGroup("Mysa")).addOption(new l("-p, --mysa-password <mysaPassword>","Mysa account password").env("M2M_MYSA_PASSWORD").makeOptionMandatory().helpGroup("Mysa")).addOption(new l("-N, --mqtt-client-name <mqttClientName>","name of the MQTT client").env("M2M_MQTT_CLIENT_NAME").default("mysa2mqtt").helpGroup("MQTT")).addOption(new l("-T, --mqtt-topic-prefix <mqttTopicPrefix>","prefix of the MQTT topic").env("M2M_MQTT_TOPIC_PREFIX").default("mysa2mqtt").helpGroup("MQTT")).addOption(new l("--temperature-unit <temperatureUnit>","temperature unit (C or F)").env("M2M_TEMPERATURE_UNIT").choices(["C","F"]).default("C").helpGroup("Configuration")).addOption(new l("--heater-watts <heaterWatts>",'rated wattage of the heaters controlled by each thermostat, as a comma-separated list of <device>=<watts> pairs, where <device> is a device id or name (e.g. "Kitchen=1500,<device-id>=750"). Required for V2 thermostats to report power, as they do not measure current themselves').env("M2M_HEATER_WATTS").argParser(K).helpGroup("Configuration")).addOption(new l("--poll-interval-seconds <pollIntervalSeconds>","how often, in seconds, to refresh device state from the Mysa REST API. This keeps Home Assistant current even when the real-time connection cannot be established or is unstable. Set to 0 to disable, or to at least 30").env("M2M_POLL_INTERVAL_SECONDS").argParser(J).default(60).helpGroup("Configuration")).addOption(new l("--heartbeat-file <heartbeatFile>","file touched on every message received from the Mysa cloud, for external liveness checks").env("M2M_HEARTBEAT_FILE").helpGroup("Configuration")).parse().opts();import{Climate as ee,Sensor as T}from"mqtt2ha";var I=["off","heat"],b=["off","heat","cool","dry","fan_only","auto"],te={1:"off",2:"auto",3:"heat",4:"cool",5:"fan_only",6:"dry"},ie=["auto","low","medium","high","max"],O={1:"auto",2:"low",3:"low",4:"medium",5:"medium",6:"high",7:"high",8:"max"},ae=4,re=["auto","low","medium","high"],N=3e4,k=3e5,F=Math.ceil(Math.log2(k/N));function P(i){var t;if(!(i!=null&&i.modes))return[...ie];let e=new Set;if(i.fanSpeeds)for(let a of i.fanSpeeds)e.add(a);for(let a of Object.values(i.modes))for(let r of a.fanSpeeds??[])e.add(r);return e.size===0?(t=i.keys)!=null&&t.includes(ae)?[...re]:["auto"]:Array.from(new Set(Object.entries(O).filter(([a])=>e.has(Number(a))).map(([,a])=>a)))}var C=class{constructor(e,t,a,r,s,m,u,q){this.mysaApiClient=e;this.mysaDevice=t;this.mqttSettings=a;this.logger=r;this.mysaDeviceFirmware=s;this.mysaDeviceSerialNumber=m;this.temperatureUnit=u;this.heaterWatts=q;let f=(u??"C")==="C";this.mqttDevice={identifiers:t.Id,name:t.Name,manufacturer:"Mysa",model:t.Model,sw_version:s==null?void 0:s.InstalledVersion,serial_number:m},this.mqttOrigin={name:"mysa2mqtt",sw_version:R,support_url:"https://github.com/bourquep/mysa2mqtt"};let p=t.Model.startsWith("AC");this.deviceType=p?"AC":"BB";let v=/-v2-/i.test(t.Model),M=/^INF-/i.test(t.Model),c=!p&&(!(v||M)||q!=null);this.mqttClimate=new ee({mqtt:this.mqttSettings,logger:this.logger,component:{component:"climate",device:this.mqttDevice,origin:this.mqttOrigin,unique_id:`mysa_${t.Id}_climate`,name:"Thermostat",min_temp:t.MinSetpoint,max_temp:t.MaxSetpoint,modes:p?b:I,fan_modes:p?P(t.SupportedCaps):void 0,precision:f?.1:1,temp_step:f?.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(w,g)=>{switch(w){case"mode_command_topic":{let h=g,S=p?b.includes(h)?h:void 0:I.includes(h)?h:void 0;await this.setDeviceState(void 0,S);break}case"power_command_topic":await this.setDeviceState(void 0,g==="OFF"?"off":g==="ON"&&!p?"heat":void 0);break;case"temperature_command_topic":if(g==="")await this.setDeviceState(void 0,void 0);else{let h=parseFloat(g);if(!f){let S=A=>Math.round(A*2)/2,H=(A,G,W)=>Math.min(W,Math.max(G,A)),$=S(h);h=H($,this.mysaDevice.MinSetpoint??0,this.mysaDevice.MaxSetpoint??100)}await this.setDeviceState(h,void 0)}break;case"fan_mode_command_topic":{let h=g;if(!P(this.mysaDevice.SupportedCaps).includes(h))break;await this.setDeviceState(this.mqttClimate.targetTemperature,this.mqttClimate.currentMode,h);break}}}),this.mqttTemperature=new T({mqtt:this.mqttSettings,logger:this.logger,component:{component:"sensor",device:this.mqttDevice,origin:this.mqttOrigin,unique_id:`mysa_${t.Id}_temperature`,name:"Current temperature",device_class:"temperature",state_class:"measurement",unit_of_measurement:"\xB0C",suggested_display_precision:f?.1:0,force_update:!0}}),this.mqttFloorTemperature=M?new T({mqtt:this.mqttSettings,logger:this.logger,component:{component:"sensor",device:this.mqttDevice,origin:this.mqttOrigin,unique_id:`mysa_${t.Id}_floor_temperature`,name:"Floor temperature",device_class:"temperature",state_class:"measurement",unit_of_measurement:"\xB0C",suggested_display_precision:f?.1:0,force_update:!0}}):void 0,this.mqttHumidity=new T({mqtt:this.mqttSettings,logger:this.logger,component:{component:"sensor",device:this.mqttDevice,origin:this.mqttOrigin,unique_id:`mysa_${t.Id}_humidity`,name:"Current humidity",device_class:"humidity",state_class:"measurement",unit_of_measurement:"%",suggested_display_precision:0,force_update:!0}});let y=new T({mqtt:this.mqttSettings,logger:this.logger,component:{component:"sensor",device:this.mqttDevice,origin:this.mqttOrigin,unique_id:`mysa_${t.Id}_power`,name:"Current power",device_class:"power",state_class:"measurement",unit_of_measurement:"W",suggested_display_precision:0,force_update:!0}});this.mqttPower=c?y:void 0,this.mqttRetiredPower=c?void 0:y}mysaApiClient;mysaDevice;mqttSettings;logger;mysaDeviceFirmware;mysaDeviceSerialNumber;temperatureUnit;heaterWatts;isStarted=!1;realtimeGeneration=0;realtimeRetryAttempt=0;realtimeRetryTimer;mqttDevice;mqttOrigin;mqttClimate;mqttTemperature;mqttFloorTemperature;mqttHumidity;mqttPower;mqttRetiredPower;mysaStatusUpdateHandler=e=>{this.handleMysaStatusUpdate(e).catch(t=>{this.logger.error("Failed to handle Mysa status update",{error:t,deviceId:this.mysaDevice.Id})})};mysaStateChangeHandler=e=>{this.handleMysaStateChange(e).catch(t=>{this.logger.error("Failed to handle Mysa state change",{error:t,deviceId:this.mysaDevice.Id})})};deviceType;async start(){var e,t;if(!this.isStarted){this.isStarted=!0,this.realtimeGeneration+=1;try{let r=(await this.mysaApiClient.getDeviceStates()).DeviceStatesObj[this.mysaDevice.Id];r!=null&&await this.publishRestState(r),await this.mqttClimate.writeConfig(),await this.mqttTemperature.writeConfig(),await((e=this.mqttFloorTemperature)==null?void 0:e.writeConfig()),await this.mqttHumidity.writeConfig(),this.mqttPower!=null&&(await this.mqttPower.setState("state_topic","None"),await this.mqttPower.writeConfig()),await((t=this.mqttRetiredPower)==null?void 0:t.removeConfig()),this.mysaApiClient.emitter.on("statusChanged",this.mysaStatusUpdateHandler),this.mysaApiClient.emitter.on("stateChanged",this.mysaStateChangeHandler),await this.startRealtimeUpdates()}catch(a){throw this.isStarted=!1,this.realtimeGeneration+=1,this.clearRealtimeRetry(),a}}}async refreshFromRest(e){if(!(!this.isStarted||e==null))try{await this.publishRestState(e)}catch(t){this.logger.error("Failed to apply REST state poll",{error:t,deviceId:this.mysaDevice.Id})}}async publishRestState(e){var t,a,r,s,m,u;this.mqttClimate.currentTemperature=(t=e.CorrectedTemp)==null?void 0:t.v,this.mqttClimate.currentHumidity=(a=e.Humidity)==null?void 0:a.v,this.mqttClimate.currentMode=te[(r=e.TstatMode)==null?void 0:r.v]??this.mqttClimate.currentMode,this.mqttClimate.currentFanMode=O[(s=e.FanSpeed)==null?void 0:s.v]??this.mqttClimate.currentFanMode,this.mqttClimate.currentAction=this.computeCurrentAction(void 0,(m=e.Duty)==null?void 0:m.v),this.mqttClimate.targetTemperature=this.mqttClimate.currentMode!=="off"?(u=e.SetPoint)==null?void 0:u.v:void 0,await this.mqttTemperature.setState("state_topic",e.CorrectedTemp!=null?e.CorrectedTemp.v.toFixed(2):"None"),await this.mqttHumidity.setState("state_topic",e.Humidity!=null?e.Humidity.v.toFixed(2):"None")}async stop(){var e,t;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((e=this.mqttPower)==null?void 0:e.setState("state_topic","None")),await this.mqttTemperature.setState("state_topic","None"),await((t=this.mqttFloorTemperature)==null?void 0:t.setState("state_topic","None")),await this.mqttHumidity.setState("state_topic","None"))}async setDeviceState(e,t,a){try{await this.mysaApiClient.setDeviceState(this.mysaDevice.Id,e,t,a)}catch(r){this.logger.error("Failed to update Mysa device state",{error:r,deviceId:this.mysaDevice.Id})}}async startRealtimeUpdates(){let e=this.realtimeGeneration;try{if(await this.mysaApiClient.startRealtimeUpdates(this.mysaDevice.Id),!this.isStarted||e!==this.realtimeGeneration){await this.stopRealtimeUpdates();return}this.realtimeRetryAttempt=0,this.logger.info("Started realtime updates",{deviceId:this.mysaDevice.Id})}catch(t){this.isStarted&&e===this.realtimeGeneration&&this.scheduleRealtimeRetry(t)}}async stopRealtimeUpdates(){try{await this.mysaApiClient.stopRealtimeUpdates(this.mysaDevice.Id)}catch(e){this.logger.warn("Failed to stop realtime updates",{error:e,deviceId:this.mysaDevice.Id})}}scheduleRealtimeRetry(e){if(!this.isStarted||this.realtimeRetryTimer!=null)return;let t=Math.min(this.realtimeRetryAttempt,F),a=Math.min(k,N*2**t);this.realtimeRetryAttempt=Math.min(this.realtimeRetryAttempt+1,F),this.logger.error("Failed to start realtime updates; retrying",{error:e,deviceId:this.mysaDevice.Id,retryDelayMs:a}),this.realtimeRetryTimer=setTimeout(()=>{this.realtimeRetryTimer=void 0,this.startRealtimeUpdates()},a)}clearRealtimeRetry(){this.realtimeRetryTimer!=null&&(clearTimeout(this.realtimeRetryTimer),this.realtimeRetryTimer=void 0)}async handleMysaStatusUpdate(e){var t;if(!(!this.isStarted||e.deviceId!==this.mysaDevice.Id)){if(this.mqttClimate.currentAction=this.computeCurrentAction(e.current,e.dutyCycle),this.mqttClimate.currentTemperature=e.temperature,this.mqttClimate.currentHumidity=e.humidity,this.mqttClimate.targetTemperature=this.mqttClimate.currentMode!=="off"?e.setPoint:void 0,this.mqttPower!=null){let a=this.computeWatts(e);this.logger.debug("Computed power draw",{current:e.current,dutyCycle:e.dutyCycle,watts:a}),await this.mqttPower.setState("state_topic",a!=null?a.toFixed(2):"None")}await this.mqttTemperature.setState("state_topic",e.temperature.toFixed(2)),e.floorTemperature!=null&&await((t=this.mqttFloorTemperature)==null?void 0:t.setState("state_topic",e.floorTemperature.toFixed(2))),await this.mqttHumidity.setState("state_topic",e.humidity.toFixed(2))}}async handleMysaStateChange(e){if(!(!this.isStarted||e.deviceId!==this.mysaDevice.Id))switch(e.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=e.mode,this.deviceType==="AC"&&(this.mqttClimate.currentAction=this.computeCurrentAction()),this.mqttClimate.targetTemperature=e.setPoint,e.fanSpeed!==void 0&&(this.mqttClimate.currentFanMode=e.fanSpeed);break;case"dry":case"fan_only":this.mqttClimate.currentMode=e.mode,this.mqttClimate.currentAction=this.computeCurrentAction(),e.fanSpeed!==void 0&&(this.mqttClimate.currentFanMode=e.fanSpeed);break;default:this.mqttClimate.currentMode!=="off"&&(this.mqttClimate.targetTemperature=e.setPoint),e.fanSpeed!==void 0&&(this.mqttClimate.currentFanMode=e.fanSpeed);break}}computeWatts(e){if(e.current!=null&&this.mysaDevice.Voltage!=null)return this.mysaDevice.Voltage*e.current;if(e.dutyCycle!=null&&this.heaterWatts!=null)return this.heaterWatts*Math.min(Math.max(e.dutyCycle,0),1)}computeCurrentAction(e,t){let a=this.mqttClimate.currentMode;switch(b.includes(a)?a:void 0){case"off":return"off";case"heat":return this.deviceType==="BB"?e!=null?e>0?"heating":"idle":(t??0)>0?"heating":"idle":"heating";case"cool":return"cooling";case"fan_only":return"fan";case"dry":return"drying";default:return"idle"}}};var x=3e4,L=3e5,de=Math.ceil(Math.log2(L/x)),o=me({name:"mysa2mqtt",level:d.logLevel,transport:d.logFormat==="pretty"?{target:"pino-pretty",options:{colorize:!0,singleLine:!0,ignore:"hostname,module",messageFormat:"\x1B[33m[{module}]\x1B[39m {msg}"}}:void 0});async function ce(){o.info("Starting mysa2mqtt...");let i=new oe({username:d.mysaUsername,password:d.mysaPassword},{logger:new E(o.child({module:"mysa-js-sdk"}))}),e=d.heartbeatFile;if(e){let n=0;i.emitter.on("rawRealtimeMessageReceived",()=>{let c=Date.now();c-n<1e4||(n=c,ne(e,`${new Date(c).toISOString()}
31
+ `).catch(y=>{o.warn(y,`Failed to write heartbeat file '${e}'`)}))})}o.info("Logging in..."),await fe(i),o.debug("Fetching devices and firmwares...");let[t,a]=await Promise.all([i.getDevices(),i.getDeviceFirmwares()]);o.debug("Fetching serial numbers...");let r=new Map;for(let[n]of Object.entries(t.DevicesObj))try{let c=await i.getDeviceSerialNumber(n);c&&r.set(n,c)}catch(c){o.error(c,`Failed to retrieve serial number for device ${n}`)}o.debug("Initializing MQTT entities...");let s={host:d.mqttHost,port:d.mqttPort,username:d.mqttUsername,password:d.mqttPassword,client_name:d.mqttClientName,state_prefix:d.mqttTopicPrefix},m=d.heaterWatts??new Map,u=new Set;function q(n){for(let c of[n.Id,n.Name]){let y=c==null?void 0:c.toLowerCase();if(y==null)continue;let w=m.get(y);if(w!=null)return u.add(y),w}}let f=Object.entries(t.DevicesObj).map(([,n])=>new C(i,n,s,new E(o.child({module:"thermostat",deviceId:n.Id})),a.Firmware[n.Id],r.get(n.Id),d.temperatureUnit,q(n))),p=m.size-u.size;p>0&&o.warn(`${p} of the ${m.size} heater wattage entries match no device id or name. Check the configured names and ids against your devices.`);let v=0,M=[];for(let n of f)await ye(n)?v+=1:M.push(n);if(f.length>0&&v===0)throw new Error("Failed to start any thermostats");for(let n of M)U(n);le(i,f)}function le(i,e){let t=d.pollIntervalSeconds;if(t<=0||e.length===0)return;let a=new Map(e.map(s=>[s.mysaDevice.Id,s]));o.info(`Polling device state over REST every ${t}s`);let r=!1;setInterval(()=>{if(r){o.debug("Skipping REST state poll; previous poll still in flight");return}r=!0,(async()=>{try{let s=await i.getDeviceStates();await Promise.all(Array.from(a,([m,u])=>u.refreshFromRest(s.DeviceStatesObj[m])))}catch(s){o.warn(s,"Periodic REST state poll failed")}finally{r=!1}})()},t*1e3)}var ue="NotAuthorizedException",pe="UserNotFoundException";function he(i){let e=i.cause;if(typeof e!="object"||e===null)return;let t=("code"in e?e.code:void 0)??("name"in e?e.name:void 0);return typeof t=="string"?t:void 0}async function fe(i){o.debug(`Authenticating with a password of ${d.mysaPassword.length} character(s).`);try{await i.login()}catch(e){let t=e instanceof se?he(e):void 0;throw t===ue?new Error("Mysa rejected the credentials. Verify that they let you sign in to the Mysa mobile app, then check that the password reaches mysa2mqtt intact: a shell expands $ and ` inside double quotes, and Docker Compose expands $ in `environment:` entries and in `env_file:` files, where a $ must be written as $$. An `env_file:` entry declared with `format: raw` is the exception -- it takes the password verbatim, so a $ stays a single $ there. Re-run with --log-level debug to log the length of the password that was received and compare it against your actual password.",{cause:e}):t===pe?new Error("Mysa does not recognize that username. Check --mysa-username (M2M_MYSA_USERNAME): it must be the email address you sign in to the Mysa mobile app with.",{cause:e}):e}}async function ye(i){try{return await i.start(),!0}catch(e){return o.error(e,`Failed to start thermostat ${i.mysaDevice.Id}`),!1}}function U(i,e=0){let t=Math.min(e,de),a=Math.min(L,x*2**t);o.info(`Retrying thermostat ${i.mysaDevice.Id} startup in ${a}ms`),setTimeout(()=>{ge(i,e+1)},a)}async function ge(i,e){try{await i.start(),o.info(`Started thermostat ${i.mysaDevice.Id} after retry`)}catch(t){o.error(t,`Failed to start thermostat ${i.mysaDevice.Id}`),U(i,e)}}function _e(i){let e=i==null?void 0:i.code,t=String((i==null?void 0:i.message)??i??"");if(e&&new Set(["NetworkError","ETIMEDOUT","ECONNRESET","ENOTFOUND","EAI_AGAIN"]).has(e)||/Network error|getaddrinfo|socket hang up/i.test(t))return!0;let r=/connect|connection|socket|network|request|handshake|tls|dns|host/i;return/time(?:d\s*)?out|ETIMEDOUT/i.test(t)&&r.test(t)}async function Me(){let t=5e3;for(let a=0;a<=10;a++)try{await ce();return}catch(r){(!_e(r)||a===10)&&(o.fatal(r,"Unexpected error"),process.exit(1));let m=a+1;o.warn({err:r,retry:m,maxRetries:10,retryInMs:t},`Transient error during startup (retry ${m}/10); retrying in ${t}ms`),await new Promise(u=>setTimeout(u,t)),t=Math.min(t*2,6e4)}}Me();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mysa2mqtt",
3
- "version": "3.0.1",
3
+ "version": "3.0.3",
4
4
  "license": "MIT",
5
5
  "private": false,
6
6
  "description": "Expose Mysa smart thermostats to home automation platforms via MQTT.",
@@ -53,8 +53,8 @@
53
53
  "dependencies": {
54
54
  "commander": "15.0.0",
55
55
  "dotenv": "17.4.2",
56
- "mqtt2ha": "5.1.0",
57
- "mysa-js-sdk": "3.1.0",
56
+ "mqtt2ha": "5.1.1",
57
+ "mysa-js-sdk": "3.1.2",
58
58
  "pino": "10.3.1",
59
59
  "pino-pretty": "13.1.3"
60
60
  },