From da90344ffc1375905770aec77adaec83172d943f Mon Sep 17 00:00:00 2001 From: remiolivier Date: Sun, 2 Nov 2025 07:00:14 -0800 Subject: [PATCH] fix: Unable to change the set point when Home Assistant is configured with Fahrenheit temperature unit (#73) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Fix Temperature Handling in Fahrenheit Mode ## Problem When operating in **Fahrenheit mode**, Mysa still expects temperature values to be provided in **Celsius**. However, Home Assistant sends integer Fahrenheit values (e.g., `72.02°F`), which convert to **non-aligned Celsius values** like `22.22°C`. Mysa’s API only accepts temperature values that are either **whole numbers** or **increments of 0.5°C** (for example: `21.0`, `21.5`, `22.0`). As a result, values such as `22.22°C` or `21.72°C` are considered invalid and are **rejected** by Mysa’s API. --- ## Root Cause - The original code accepted **0.1°C precision** and **0.5°C step size**. - When Home Assistant runs in Fahrenheit, the conversion from °F to °C produces fractional values that are not valid (e.g., 72°F → 22.22°C). - Because Mysa enforces strict 0.5°C increments, these fractional setpoints caused failed updates. --- ## Solution This update ensures valid behavior when using Fahrenheit mode **while keeping the current behavior for Celsius**: - Adds a new environment variable: **`M2M_TEMP_UNIT`** — accepts either: - `C` *(default)* - `F` *(for Fahrenheit operation)* - When running in Fahrenheit mode (`M2M_TEMP_UNIT=F`): - Celsius values are **rounded and clamped to the nearest 0.5°C**. - Temperature step size and precision are adjusted: - Precision → `1°F` - Step size → `1°F` - When running in Celsius mode, existing logic remains unchanged (0.1 precision, 0.5 step). --- ## Technical Summary | Mode | Env Variable | Precision | Step | Conversion Behavior | |------|---------------|------------|------|----------------------| | Celsius | `M2M_TEMP_UNIT=C` (default) | 0.1°C | 0.5°C | Direct pass-through | | Fahrenheit | `M2M_TEMP_UNIT=F` | 1°F | 1°F | Convert °F → °C, snap to 0.5°C | The rounding logic ensures that when a Fahrenheit value (e.g., `72°F`) is converted to Celsius (`21.72°C`), it is adjusted to the nearest valid half-degree (`21.5°C` or `22.0°C`). # Demo https://github.com/user-attachments/assets/bbffe5fe-a3be-43cb-aed0-f63bdfacb1d4 --------- Co-authored-by: Pascal Bourque --- .prettierignore | 1 + README.md | 6 ++++++ src/main.ts | 3 ++- src/options.ts | 7 +++++++ src/thermostat.ts | 25 +++++++++++++++++++------ 5 files changed, 35 insertions(+), 7 deletions(-) create mode 100644 .prettierignore diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..b43bf86 --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +README.md diff --git a/README.md b/README.md index a571d19..59e2074 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,11 @@ For development or custom modifications: The application can be configured using either command-line arguments or environment variables. Environment variables take precedence over command-line defaults. +> [!IMPORTANT] +> The `M2M_TEMPERATURE_UNIT` option must match Home Assistant's unit system (Settings → General → Unit System) +> so setpoints and readings are interpreted correctly. If mismatched, climate entities will show incorrect values (e.g. +> 21°C treated as 21°F) and commands may result in unexpected temperatures. + ### Required Configuration | CLI Option | Environment Variable | Description | @@ -151,6 +156,7 @@ take precedence over command-line defaults. | `-l, --log-level` | `M2M_LOG_LEVEL` | `info` | Log level: `silent`, `fatal`, `error`, `warn`, `info`, `debug`, `trace` | | `-f, --log-format` | `M2M_LOG_FORMAT` | `pretty` | Log format: `pretty`, `json` | | `-s, --mysa-session-file` | `M2M_MYSA_SESSION_FILE` | `session.json` | Path to Mysa session file | +| `-t, --temperature-unit` | `M2M_TEMPERATURE_UNIT` | `C` | Temperature unit (`C` = Celsius, `F` = Fahrenheit) | ## Usage Examples diff --git a/src/main.ts b/src/main.ts index 6e48a23..7ce5d35 100644 --- a/src/main.ts +++ b/src/main.ts @@ -99,7 +99,8 @@ async function main() { mqttSettings, new PinoLogger(rootLogger.child({ module: 'thermostat', deviceId: device.Id })), firmwares.Firmware[device.Id], - serialNumbers.get(device.Id) + serialNumbers.get(device.Id), + options.temperatureUnit ) ); diff --git a/src/options.ts b/src/options.ts index af99ec0..4d2439f 100644 --- a/src/options.ts +++ b/src/options.ts @@ -143,5 +143,12 @@ export const options = new Command('mysa2mqtt') .default('mysa2mqtt') .helpGroup('MQTT') ) + .addOption( + new Option('--temperature-unit ', 'temperature unit (C or F)') + .env('M2M_TEMPERATURE_UNIT') + .choices(['C', 'F']) + .default('C') + .helpGroup('Configuration') + ) .parse() .opts(); diff --git a/src/thermostat.ts b/src/thermostat.ts index 496df2d..229ebe2 100644 --- a/src/thermostat.ts +++ b/src/thermostat.ts @@ -51,8 +51,11 @@ export class Thermostat { private readonly mqttSettings: MqttSettings, private readonly logger: Logger, public readonly mysaDeviceFirmware?: FirmwareDevice, - public readonly mysaDeviceSerialNumber?: string + public readonly mysaDeviceSerialNumber?: string, + public readonly temperatureUnit?: 'C' | 'F' ) { + const is_celsius = (temperatureUnit ?? 'C') === 'C'; + this.mqttDevice = { identifiers: mysaDevice.Id, name: mysaDevice.Name, @@ -81,9 +84,9 @@ export class Thermostat { min_temp: mysaDevice.MinSetpoint, max_temp: mysaDevice.MaxSetpoint, modes: ['off', 'heat'], // TODO: AC - precision: 0.1, - temp_step: 0.5, - temperature_unit: 'C', // TODO: Confirm that Mysa always works in C + precision: is_celsius ? 0.1 : 1.0, + temp_step: is_celsius ? 0.5 : 1.0, + temperature_unit: 'C', optimistic: true } }, @@ -118,7 +121,17 @@ export class Thermostat { if (message === '') { this.mysaApiClient.setDeviceState(this.mysaDevice.Id, undefined, undefined); } else { - this.mysaApiClient.setDeviceState(this.mysaDevice.Id, parseFloat(message), undefined); + let temperature = parseFloat(message); + + if (!is_celsius) { + const snapHalfC = (c: number) => Math.round(c * 2) / 2; + const clamp = (v: number, min: number, max: number) => Math.min(max, Math.max(min, v)); + // Snap to 0.5 °C and clamp to device limits + const setC = snapHalfC(temperature); + temperature = clamp(setC, this.mysaDevice.MinSetpoint ?? 0, this.mysaDevice.MaxSetpoint ?? 100); + } + + this.mysaApiClient.setDeviceState(this.mysaDevice.Id, temperature, undefined); } break; } @@ -137,7 +150,7 @@ export class Thermostat { device_class: 'temperature', state_class: 'measurement', unit_of_measurement: '°C', - suggested_display_precision: 1, + suggested_display_precision: is_celsius ? 0.1 : 0.0, force_update: true } });